Pages Medium
Restaurant Menu — Carta
Browseable restaurant menu (carta) with sticky category nav, search filter, allergen chips, and section anchors. Shared warm restaurant palette.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* ─────────────────────────────────────────────
Restaurant theme — shared warm palette
Used across ops + customer + marketing surfaces.
Per-resource: copy this :root block verbatim.
───────────────────────────────────────────── */
:root {
/* Surface */
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
/* Brand */
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
/* Ink */
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
/* State */
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
/* Type */
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Radius / shadow */
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(44, 26, 14, 0.08), 0 2px 6px rgba(44, 26, 14, 0.06);
--shadow-2: 0 8px 24px rgba(44, 26, 14, 0.12);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
min-height: 100vh;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
/* ── Hero ── */
.hero {
padding: 56px 24px 32px;
text-align: center;
background: linear-gradient(180deg, var(--bone) 0%, var(--cream) 100%);
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
}
.hero-inner {
max-width: 720px;
margin: 0 auto;
}
.kicker {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.22em;
color: var(--terracotta);
font-weight: 600;
margin-bottom: 12px;
}
.brand {
font-family: var(--font-display);
font-size: clamp(2.4rem, 5vw, 3.4rem);
font-weight: 700;
letter-spacing: -0.02em;
color: var(--ink);
margin-bottom: 8px;
}
.tagline {
font-size: 1rem;
color: var(--warm-gray);
font-style: italic;
}
/* ── Sticky category nav ── */
.catnav {
position: sticky;
top: 0;
z-index: 30;
background: var(--bone);
border-bottom: 1px solid rgba(44, 26, 14, 0.1);
box-shadow: var(--shadow-1);
}
.catnav-inner {
max-width: 980px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 4px;
padding: 12px 16px;
flex-wrap: wrap;
}
.cat-tab {
background: transparent;
border: 1px solid transparent;
color: var(--ink-2);
font-family: var(--font-body);
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.04em;
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.cat-tab:hover {
background: var(--cream-2);
color: var(--ink);
}
.cat-tab.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.search {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
background: var(--cream-2);
border-radius: 999px;
padding: 6px 12px;
color: var(--warm-gray);
}
.search input {
background: transparent;
border: none;
outline: none;
font-family: var(--font-body);
font-size: 0.85rem;
color: var(--ink);
width: 180px;
}
.search input::placeholder {
color: var(--warm-gray);
}
/* ── Sections ── */
.carta {
max-width: 980px;
margin: 0 auto;
padding: 32px 24px 80px;
}
.section {
margin-top: 48px;
scroll-margin-top: 72px;
}
.section:first-child {
margin-top: 8px;
}
.section-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(44, 26, 14, 0.12);
}
.section-title {
font-family: var(--font-display);
font-size: 1.7rem;
font-weight: 700;
color: var(--ink);
letter-spacing: -0.01em;
}
.section-note {
font-size: 0.78rem;
color: var(--warm-gray);
font-style: italic;
}
/* ── Grid + item cards ── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 18px;
}
.dish {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 18px 18px 16px;
display: flex;
flex-direction: column;
gap: 10px;
transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s;
cursor: pointer;
}
.dish:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-2);
border-color: rgba(193, 113, 74, 0.4);
}
.dish.is-featured {
background: linear-gradient(180deg, var(--bone) 0%, var(--cream) 100%);
border-color: var(--gold);
}
.dish-row {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: baseline;
}
.dish-name {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 600;
color: var(--ink);
line-height: 1.25;
}
.dish-price {
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: 600;
color: var(--terracotta-d);
white-space: nowrap;
}
.dish-desc {
font-size: 0.88rem;
color: var(--ink-2);
line-height: 1.5;
}
.dish-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.chip {
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 3px 8px;
border-radius: 999px;
background: var(--cream-2);
color: var(--ink-2);
}
.chip[data-tone="veg"] {
background: rgba(79, 122, 58, 0.15);
color: var(--success);
}
.chip[data-tone="hot"] {
background: rgba(179, 67, 42, 0.15);
color: var(--danger);
}
.chip[data-tone="gf"] {
background: rgba(201, 168, 76, 0.18);
color: #8a7325;
}
.chip[data-tone="signature"] {
background: var(--gold);
color: var(--ink);
}
/* ── Empty / footer ── */
.empty {
text-align: center;
padding: 32px 24px;
color: var(--warm-gray);
font-style: italic;
}
.footer {
border-top: 1px solid rgba(44, 26, 14, 0.1);
text-align: center;
padding: 24px;
font-size: 0.78rem;
color: var(--warm-gray);
}
/* ── Hidden state for filter ── */
.dish[hidden],
.section[hidden] {
display: none !important;
}
/* ── Small screens ── */
@media (max-width: 640px) {
.catnav-inner {
padding: 10px 12px;
}
.cat-tab {
padding: 6px 10px;
font-size: 0.78rem;
}
.search {
margin-left: 0;
width: 100%;
}
.search input {
width: 100%;
}
}const MENU = [
{
id: "entradas",
title: "Entradas",
note: "To start · for the table",
items: [
{
name: "Pan de masa madre",
desc: "Sourdough loaf, smoked olive oil, sea salt.",
price: 8,
chips: [{ label: "Veg", tone: "veg" }],
},
{
name: "Burrata de la huerta",
desc: "Heirloom tomato, garden basil, aged balsamic, focaccia crumble.",
price: 16,
chips: [
{ label: "Veg", tone: "veg" },
{ label: "Signature", tone: "signature" },
],
featured: true,
},
{
name: "Pulpo a la brasa",
desc: "Charred octopus, smoked paprika potato, salsa verde.",
price: 19,
chips: [{ label: "GF", tone: "gf" }],
},
{
name: "Croquetas de jamón",
desc: "Iberico ham croquettes, six pieces, lemon aioli.",
price: 14,
chips: [],
},
],
},
{
id: "principales",
title: "Principales",
note: "Mains · stone-fired",
items: [
{
name: "Ribeye 14oz",
desc: "Dry-aged 28 days, bone marrow butter, chimichurri.",
price: 48,
chips: [
{ label: "GF", tone: "gf" },
{ label: "Signature", tone: "signature" },
],
featured: true,
},
{
name: "Branzino entero",
desc: "Whole roasted sea bass, fennel, preserved lemon, herb oil.",
price: 38,
chips: [{ label: "GF", tone: "gf" }],
},
{
name: "Risotto de hongos",
desc: "Carnaroli rice, wild mushrooms, brown butter, parmesan.",
price: 26,
chips: [{ label: "Veg", tone: "veg" }],
},
{
name: "Pollo al carbón",
desc: "Half free-range chicken, charred lemon, garlic confit, herb salad.",
price: 28,
chips: [{ label: "GF", tone: "gf" }],
},
{
name: "Pappardelle al ragú",
desc: "Hand-cut pasta, slow-braised lamb shoulder, gremolata.",
price: 24,
chips: [{ label: "Spicy", tone: "hot" }],
},
],
},
{
id: "postres",
title: "Postres",
note: "Made in-house",
items: [
{
name: "Tarta de queso quemada",
desc: "Basque burnt cheesecake, salted caramel, sea salt.",
price: 11,
chips: [
{ label: "Veg", tone: "veg" },
{ label: "Signature", tone: "signature" },
],
featured: true,
},
{
name: "Olive oil cake",
desc: "Citrus olive oil cake, crème fraîche, candied orange.",
price: 10,
chips: [{ label: "Veg", tone: "veg" }],
},
{
name: "Chocolate ganache",
desc: "Bittersweet dark chocolate, hazelnut praline, espresso ice.",
price: 12,
chips: [
{ label: "Veg", tone: "veg" },
{ label: "GF", tone: "gf" },
],
},
],
},
{
id: "bebidas",
title: "Bebidas",
note: "Natural wine · craft cocktails",
items: [
{
name: "Vermut de la casa",
desc: "House vermouth on tap, orange peel, green olive.",
price: 9,
chips: [],
},
{
name: "Negroni sbagliato",
desc: "Campari, sweet vermouth, sparkling wine.",
price: 14,
chips: [],
},
{
name: "Spritz de naranja sanguina",
desc: "Blood orange, Aperol, prosecco, soda.",
price: 13,
chips: [],
},
{
name: "Tinto natural (copa)",
desc: "Rotating natural red, ask your server.",
price: 12,
chips: [],
},
],
},
];
const carta = document.getElementById("carta");
const search = document.getElementById("search");
const tabs = document.querySelectorAll(".cat-tab");
const empty = document.getElementById("empty");
function render() {
carta.innerHTML = MENU.map(
(section) => `
<section class="section" id="${section.id}" data-section>
<header class="section-head">
<h2 class="section-title">${section.title}</h2>
<p class="section-note">${section.note}</p>
</header>
<div class="grid">
${section.items
.map(
(item) => `
<article class="dish ${item.featured ? "is-featured" : ""}" data-dish
data-name="${item.name.toLowerCase()}"
data-desc="${item.desc.toLowerCase()}">
<div class="dish-row">
<h3 class="dish-name">${item.name}</h3>
<span class="dish-price">$${item.price.toFixed(2)}</span>
</div>
<p class="dish-desc">${item.desc}</p>
${
item.chips.length
? `<div class="dish-meta">${item.chips
.map((c) => `<span class="chip" data-tone="${c.tone}">${c.label}</span>`)
.join("")}</div>`
: ""
}
</article>`
)
.join("")}
</div>
</section>`
).join("");
}
function filter(query) {
const q = query.trim().toLowerCase();
let anyVisible = false;
carta.querySelectorAll("[data-section]").forEach((section) => {
let sectionVisible = false;
section.querySelectorAll("[data-dish]").forEach((d) => {
const match = !q || d.dataset.name.includes(q) || d.dataset.desc.includes(q);
d.hidden = !match;
if (match) sectionVisible = true;
});
section.hidden = !sectionVisible;
if (sectionVisible) anyVisible = true;
});
empty.hidden = anyVisible;
}
function activateTab(id) {
tabs.forEach((t) => t.classList.toggle("is-active", t.dataset.target === id));
}
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
const target = document.getElementById(tab.dataset.target);
if (!target) return;
activateTab(tab.dataset.target);
target.scrollIntoView({ behavior: "smooth", block: "start" });
});
});
search.addEventListener("input", (e) => filter(e.target.value));
// Scroll-spy: highlight the section currently in view.
const spy = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) activateTab(visible.target.id);
},
{ rootMargin: "-80px 0px -60% 0px", threshold: [0.1, 0.5] }
);
render();
carta.querySelectorAll("[data-section]").forEach((s) => spy.observe(s));<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;700&family=Inter:wght@400;500;600;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Carta — Casa Olivar</title>
</head>
<body>
<header class="hero">
<div class="hero-inner">
<p class="kicker">Carta · Dinner</p>
<h1 class="brand">Casa Olivar</h1>
<p class="tagline">Stone-fired cooking, garden produce, natural wine.</p>
</div>
</header>
<nav class="catnav" aria-label="Menu categories">
<div class="catnav-inner">
<button class="cat-tab is-active" data-target="entradas">Entradas</button>
<button class="cat-tab" data-target="principales">Principales</button>
<button class="cat-tab" data-target="postres">Postres</button>
<button class="cat-tab" data-target="bebidas">Bebidas</button>
<div class="search">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
<path d="m20 20-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<input
type="search"
id="search"
placeholder="Search dishes…"
aria-label="Search menu"
/>
</div>
</div>
</nav>
<main class="carta" id="carta">
<!-- sections rendered by script.js -->
</main>
<p class="empty" id="empty" hidden>No dishes match that search.</p>
<footer class="footer">
<p>Prices in USD · Tax not included · Ask your server about allergens & substitutions.</p>
</footer>
<script src="script.js"></script>
</body>
</html>Restaurant Menu — Carta
A full browseable restaurant menu (carta) covering Entradas, Principales, Postres and Bebidas. Sticky category tabs scroll-spy the active section, a search box filters by name or description, and each card shows price plus allergen chips.
This is the foundation resource for the restaurant theme — it establishes the shared warm palette (cream / terracotta / forest / gold) and type system (Playfair Display + Inter) reused across the operations and customer surfaces.