/* Klandestina — style.css
   All visual styling lives here. Nothing in this file decides WHAT content
   appears (that's data/menu.json + script.js) — only how it looks. */

/* One palette, no theme switch — a single dark navy + amber-wood scheme
   taken from the restaurant's actual interior (navy wall, amber/wood
   tones, the neon sign). There used to be a light theme here too (see
   git history before this commit if it's ever needed again); removing
   it is what let every other "theme-aware" rule in this file collapse
   to a single value instead of a dark/light pair. */
:root {
  color-scheme: dark;

  --bg: #161b28;
  --bg-alt: #1f2636;
  --ink: #e8e0d0;
  --accent: #c29a6b;
  --on-accent: #161b28;

  /* The one token a scoped surface (.surface-light/.surface-white, below)
     deliberately never redefines — a few things (dish-card photo-caption
     scrims; see .visual .scrim) need to stay this exact navy regardless
     of which surface they happen to sit on, where the scoped --bg itself
     would turn cream/white and break them. Everything else in this file
     keeps using the scoped --bg/--ink/--accent tokens as normal; --navy
     is only for the handful of places that explicitly can't. */
  --navy: #161b28;

  /* Also deliberately not redefined per surface: --accent-fill is what a
     filled, accent-colored surface (right now, only .btn-primary's own
     background) actually paints. On this dark default it's identical to
     --accent, so nothing changes here — .surface-light/.surface-white
     give it a different value than --accent (a brass fill vs. a navy
     pill-fill elsewhere on light surfaces, see their own comment), which
     is the only reason the two tokens exist separately instead of button
     markup just using --accent directly. */
  --accent-fill: var(--accent);

  /* Derived from the tokens above via color-mix() rather than written out
     as their own literals, so a future palette tweak to --accent-fill/
     --ink/--bg propagates here for free instead of needing a second edit
     — including inside .surface-light/.surface-white below, which only
     redefine the leaf tokens (--ink, --accent-fill, --bg, --bg-alt, ...)
     and get every one of these for free through ordinary custom-property
     inheritance, not by repeating the formulas. --accent-deep is the 3D
     buttons'/active-tab's contact shadow (a darkened accent-fill, not a
     different hue); --line is a plain ink-alpha hairline border. --muted
     (secondary text) is mixed against a warm charcoal instead of
     transparent — an alpha blend over this palette's cool-slate --bg
     read faintly blue, which is exactly what muted text here shouldn't
     do, so it's pinned to a neutral/warm grey regardless of what it sits
     on. (The light surfaces give --muted its own literal instead of this
     formula — a warm-charcoal anchor tuned for blending with a CREAM ink
     makes no sense once --ink itself is navy; see their own comment.) */
  --accent-deep: color-mix(in srgb, var(--accent-fill) 55%, black);
  --line: color-mix(in srgb, var(--ink) 12%, transparent);
  --muted: color-mix(in srgb, var(--ink) 60%, #443e36);

  /* Tones for raised/extruded chrome (ghost buttons, tabs, the floating
     nav) that can't be derived from --ink/--accent by a simple alpha
     blend. --surface reuses --bg-alt outright (same "a shade up from the
     base background" role); --surface-edge is --bg-alt's own contact
     shadow, same "~55% toward black" treatment as --accent-deep so the
     two read as one consistent shadow language rather than two
     unrelated formulas. --glass is --bg itself diluted toward
     transparent (not a separate literal) so it can't drift out of sync
     with --bg again. */
  --surface: var(--bg-alt);
  --surface-edge: color-mix(in srgb, var(--bg-alt) 55%, black);
  --glass: color-mix(in srgb, var(--bg) 65%, transparent);
  --footer-bg: color-mix(in srgb, var(--bg) 75%, black);

  /* Navy-black, not brown-black or flat black — rgb(5,8,18) is roughly
     --bg pushed almost to black, so every drop shadow in the site reads
     as "this navy room, further into shadow" rather than a neutral grey/
     black dropped in from outside the palette. .surface-light/
     .surface-white redefine this to a much softer, navy-tinted shadow —
     this harsher one is specifically a dark-surface treatment, not
     something that should carry over unchanged onto a light section. */
  --card-shadow: 0 24px 48px -16px rgba(5, 8, 18, 0.6);
}

/* ---------- Light surfaces (Story, Menu, Gallery teaser) ---------- */
/* Same architecture as :root's own dark defaults above — applied
   directly to a <section> in index.html (and fi/index.html, generated
   from it — see scripts/prerender.mjs), redefining the same custom
   properties locally so every component inside (buttons, pills, cards,
   headings, hairlines, ...) repaints for a light surface automatically,
   through ordinary CSS custom-property inheritance, not per-component
   overrides.
   Every derived token (--accent-deep, --line, --surface, --surface-edge,
   --glass, --footer-bg) is restated here too, NOT just the leaf ones —
   custom properties don't work like Sass variables: var(--bg-alt) inside
   :root's own --surface-edge formula resolves using --bg-alt AT THE
   ELEMENT --surface-edge IS DECLARED ON, which is :root itself, once,
   permanently, since :root is the only place that declares it. A
   descendant redefining --bg-alt doesn't make an ancestor's already-
   declared --surface-edge recompute — confirmed empirically (a tiny
   standalone test page) after it first shipped this way and the gallery
   mosaic tiles came out navy instead of soft-grey. Repeating each
   formula here, on a rule that targets the section itself (so it shares
   the same element as --bg/--bg-alt's own override below), is what
   actually makes it pick up THIS scope's tokens instead of :root's.
   The two classes share everything except which of --bg/--bg-alt is
   cream and which is white. --accent and --accent-fill happen to be the
   same literal here (unlike dark surfaces, where distinguishing them
   doesn't currently matter either) because this palette doesn't need a
   fill color different from its own text/prices/detail color — the
   token split exists for the one place that DOES differ: pills (see
   .tab.active's own comment below), not for this pair's own sake.
   --on-accent is one fixed cream value on BOTH classes (not "whichever
   of --bg/--bg-alt" — it's chosen for its contrast against the brass
   fill specifically, which is identical on either class), same reason
   --navy above stays fixed rather than tracking the scoped --bg. */
.surface-light, .surface-white {
  color-scheme: light;
  --ink: #161b28;
  --accent: #8c6030;
  --accent-fill: #8c6030;
  --on-accent: #f6f1e7;
  --muted: #575b68;
  --accent-deep: color-mix(in srgb, var(--accent-fill) 55%, black);
  --line: color-mix(in srgb, var(--ink) 12%, transparent);
  --surface: var(--bg-alt);
  --surface-edge: color-mix(in srgb, var(--bg-alt) 55%, black);
  --glass: color-mix(in srgb, var(--bg) 65%, transparent);
  --footer-bg: color-mix(in srgb, var(--bg) 75%, black);
  /* Soft and navy-tinted, not the dark surfaces' near-black rgba(5,8,18)
     — a shadow strong enough to lift a card off a cream/white page would
     otherwise read as a harsh dark halo rather than depth. */
  --card-shadow: 0 16px 32px -14px rgba(22, 27, 40, 0.09);
  background: var(--bg);
  color: var(--ink);
}
.surface-light { --bg: #f6f1e7; --bg-alt: #ffffff; }
.surface-white { --bg: #ffffff; --bg-alt: #f6f1e7; }

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* script.js's language switch does its own explicit scroll correction
   (captureScrollAnchor()/restoreScrollAnchor()) after swapping content —
   the browser's own native scroll anchoring would otherwise sometimes
   also kick in afterward (content shifted above the viewport is exactly
   what it watches for) and double-correct on top of that. One mechanism
   doing this, not two fighting each other. */
html { overflow-anchor: none; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "Space Grotesk", sans-serif;
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
  transition: background 0.25s ease, color 0.25s ease;
  position: relative;
}
/* Global ambient background: a flat var(--bg) reads as harshly flat/dark
   compared to a soft ambient gradient glow behind content. Built from
   var(--accent)/var(--bg) so it re-themes for free rather than needing
   its own treatment — several large, very soft radial blobs at different
   positions for an organic feel, not one obvious spotlight. Kept
   deliberately faint (8-12% accent) now that the palette itself reads
   calmer — anything stronger started glowing orange rather than just
   warming the room — plus one cooler, lighter-slate blob (a fixed
   #2a3350, not derived from a token — it's a step up from --bg-alt
   specifically for depth here, not a color used anywhere else) low and
   to the right for a touch of depth/dimension rather than every blob
   being the same warm accent glow. position:fixed (not absolute/static)
   anchors it to the VIEWPORT rather than the document, so it reads as
   ambient light in the room rather than a pattern printed on a specific
   point of a very tall page; z-index:-1 plus pointer-events:none keep it
   purely decorative, behind and unreachable by every section's own
   content. Sections that set their own opaque background — .hero,
   .section-ambient (Visit), and now .surface-light/.surface-white
   (Story, Menu, Gallery) — simply paint over this locally, which is
   fine; this is the baseline only for Dishes and the footer now, the
   two remaining sections with no opaque background of their own. The
   light surfaces painting over it is deliberate, not incidental — see
   their own comment for why ambient glow doesn't belong on them. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(42% 38% at 12% 12%, color-mix(in srgb, var(--accent) 11%, transparent), transparent 70%),
    radial-gradient(48% 42% at 88% 22%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 70%),
    radial-gradient(52% 46% at 28% 88%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%),
    radial-gradient(40% 38% at 92% 92%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 70%),
    radial-gradient(60% 50% at 78% 70%, rgba(42, 51, 80, 0.3), transparent 70%);
}
a { color: inherit; text-decoration: none; }

h1, h2, h3 {
  font-family: "Bebas Neue", sans-serif;
  font-weight: 400;
  letter-spacing: 0.01em;
  margin: 0;
  text-transform: uppercase;
}

.eyebrow {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
}

.container { max-width: 1180px; margin: 0 auto; }
section { position: relative; padding: 130px 56px; }
.section-head { max-width: 620px; margin: 0 auto 60px; text-align: center; }
.section-head h2 { font-size: clamp(38px, 5vw, 60px); margin-top: 10px; }

/* Ambient background for sections with no real photography of their own
   yet (Menu, Visit) — the same radial-glow-over-var(--bg) technique
   .hero already uses, reused rather than invented fresh so it reads as
   one system. Isolated in its own class, applied directly on the
   <section> in index.html, so it's a one-line removal once real photos
   make it unnecessary there — nothing else about those sections depends
   on it. */
.section-ambient {
  background:
    radial-gradient(55% 45% at 12% 8%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 60%),
    radial-gradient(45% 40% at 92% 88%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 60%),
    var(--bg);
}

@media (max-width: 900px) {
  /* Desktop's 130px top padding was carrying straight over to mobile, so
     jumping to a section via the hamburger nav (below) landed with a
     disproportionately large gap above the heading — most noticeable on
     Menu since its tabs sit right there to make the empty space obvious,
     but every section had it equally. */
  section { padding: 64px 24px; }

  /* scroll-margin-top so an anchor jump lands just below the fixed mobile
     topbar (measured at 69px) instead of tucked underneath it — 16px of
     breathing room on top of that. Only the nav-jump targets need this
     (Home/hero, Story, Menu, Gallery, Find Us — the same five links in
     mobileNav.js); desktop doesn't get this rule at all since it has no
     fixed topbar to clear in the first place. */
  #hero, #story, #menu, #events, #gallery, #visit { scroll-margin-top: 85px; }
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px 30px;
  border-radius: 8px;
  font-family: "Space Grotesk", sans-serif;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.03em;
  border: none;
  cursor: pointer;
  transform: translateY(0);
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
/* A flat fill with a thin contact shadow (2px, not the old 6px raised
   block) and a softened outer glow — enough to read as "the one button
   that matters" without the glossy, neon-adjacent look the original 3D
   treatment had at this palette's higher saturation. */
.btn-primary {
  background: var(--accent-fill);
  color: var(--on-accent);
  box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 2px 0 0 color-mix(in srgb, var(--accent-deep) 65%, transparent), 0 10px 20px -12px color-mix(in srgb, var(--accent-fill) 35%, transparent);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 3px 0 0 color-mix(in srgb, var(--accent-deep) 65%, transparent), 0 14px 26px -12px color-mix(in srgb, var(--accent-fill) 40%, transparent); }
.btn-primary:active { transform: translateY(1px); box-shadow: 0 1px 0 rgba(255,255,255,0.15) inset, 0 1px 0 0 color-mix(in srgb, var(--accent-deep) 65%, transparent); }
/* A quiet outline, not a second raised button — no fill shadow at all,
   just a hairline border that picks up the accent color on hover. On
   light surfaces this gets a different border/background entirely (see
   the .surface-light/.surface-white override below) — "transparent
   outline" reads right on a cream/white page, but var(--surface) here
   is what keeps it visible as its own shape against the dark page. */
.btn-ghost {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  box-shadow: none;
}
.btn-ghost:hover { transform: translateY(-2px); border-color: var(--accent); }
/* Secondary button on a light surface: a quiet outline with no fill at
   all (var(--surface) on dark is itself a visible navy-on-navy shade;
   the equivalent "visible shape" on cream/white would be a flat white/
   cream block sitting on a cream/white page, which doesn't read as
   anything) — ink at ~25% is noticeably stronger than --line's own ~12%
   specifically so the outline alone stays legible with no fill behind
   it. Hover already turns the border to var(--accent) (brass, on light
   surfaces) via the shared rule above — nothing extra needed for that. */
.surface-light .btn-ghost, .surface-white .btn-ghost {
  background: transparent;
  border-color: color-mix(in srgb, var(--ink) 25%, transparent);
}
.btn-ghost:active { transform: translateY(0); }

/* ---------- Shortcut pill nav ---------- */
.shortcut-nav {
  position: fixed; top: 22px; left: 50%; transform: translateX(-50%); z-index: 50;
  display: flex; gap: 4px; padding: 6px; border-radius: 999px;
  background: var(--glass);
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  backdrop-filter: blur(16px);
  box-shadow: 0 16px 40px -16px rgba(5,8,18,0.7);
  opacity: 1;
  transition: opacity 0.5s ease;
}
/* Full opacity on the hero; dimmed (but still fully clickable — this is
   opacity, not visibility/pointer-events) everywhere past it, restoring
   to full the moment a visitor's cursor or touch actually reaches it
   (nav.js's IntersectionObserver toggles this class, not a scroll
   listener, so it doesn't fire on every scroll frame — a slow 0.5s
   transition on top of that is what keeps it feeling smooth rather than
   flickery crossing section boundaries). */
.shortcut-nav--dimmed { opacity: 0.4; }
.shortcut-nav--dimmed:hover, .shortcut-nav--dimmed:focus-within { opacity: 1; }
.shortcut-nav a {
  position: relative; padding: 9px 18px; font-size: 12px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; border-radius: 999px;
  color: color-mix(in srgb, var(--ink) 60%, transparent); transition: color 0.2s ease;
}
.shortcut-nav a.active { color: var(--on-accent); }
.shortcut-nav a .pill-fill {
  position: absolute; inset: 0; border-radius: 999px; background: var(--accent);
  opacity: 0; transform: scale(0.85); transition: opacity 0.25s ease, transform 0.25s ease; z-index: -1;
}
.shortcut-nav a.active .pill-fill { opacity: 1; transform: scale(1); }
.shortcut-nav a:hover:not(.active) { color: var(--ink); }
@media (max-width: 720px) { .shortcut-nav { display: none; } }

#brand-mark {
  position: fixed; top: 24px; left: 32px; z-index: 50;
  font-family: "Bebas Neue", sans-serif; font-size: 22px; letter-spacing: 0.04em;
}
@media (max-width: 900px) { #brand-mark { display: none; } }

/* Shared wordmark/logo image (components/logo.js), used inside #brand-mark,
   .mobile-topbar-brand, .mobile-nav-brand, and .footer-logo — one base rule
   plus a per-placement height so the same image reads as "topbar-sized" in
   the nav and noticeably larger in the footer, without needing separate
   image files per spot. If data/site.json has no `logo` set, or the file
   404s/fails to decode, these containers instead hold the plain text mark
   they always rendered (components/logo.js's onerror fallback) — sized off
   each container's own font-size above/below, untouched by this block. */
.brand-logo-img { display: block; width: auto; height: 26px; }
#brand-mark .brand-logo-img { height: 38px; }
.footer-logo .brand-logo-img { height: 56px; }
.mobile-nav-brand .brand-logo-img { height: 46px; }

/* ---------- Utility bar: language switch ---------- */
.utility-bar {
  position: fixed; top: 22px; right: 32px; z-index: 50;
  display: flex; align-items: center; gap: 14px;
}
.lang-switch {
  display: flex; align-items: center; gap: 6px;
  padding: 8px 12px; border-radius: 999px;
  background: var(--glass);
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  backdrop-filter: blur(16px);
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
}
.lang-option {
  background: none; border: none; padding: 2px; margin: 0; cursor: pointer;
  font: inherit; letter-spacing: inherit; color: var(--muted);
  transition: color 0.18s ease;
}
.lang-option:hover:not(.active) { color: var(--ink); }
.lang-option.active { color: var(--accent); }
.lang-sep { color: color-mix(in srgb, var(--ink) 25%, transparent); font-size: 11px; }

/* ---------- Mobile topbar (<=900px only) ---------- */
.mobile-topbar { display: none; }
@media (max-width: 900px) {
  .utility-bar { display: none; }

  .mobile-topbar {
    display: flex; align-items: center; justify-content: space-between;
    position: fixed; top: 0; left: 0; right: 0; z-index: 60;
    padding: 16px 20px;
    background: var(--glass);
    border-bottom: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
    backdrop-filter: blur(16px);
    box-shadow: 0 16px 40px -16px rgba(5,8,18,0.7);
    transition: transform 0.3s ease;
  }
  .mobile-topbar.topbar--hidden { transform: translateY(-100%); }

  .mobile-topbar-brand {
    font-family: "Bebas Neue", sans-serif; font-size: 20px; letter-spacing: 0.04em; color: var(--ink);
  }
  .mobile-topbar-brand .brand-logo-img { height: 34px; }
  .mobile-topbar-controls { display: flex; align-items: center; gap: 10px; }

  /* Reuses .lang-switch's text styling but drops the glass pill chrome,
     since the topbar itself already provides that. */
  .mobile-topbar .lang-switch {
    background: none; border: none; backdrop-filter: none; padding: 0;
  }

  .mobile-nav-toggle {
    display: flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%;
    background: none; border: none;
    font-size: 20px; line-height: 1; color: var(--ink); cursor: pointer;
    transition: color 0.18s ease;
  }
  .mobile-nav-toggle:hover { color: var(--accent); }
}

@media (max-width: 480px) {
  /* .utility-bar itself is already hidden at <=900px (mobile topbar takes
     over) — this still matters there since it also sizes the topbar's
     nested lang-switch instance. */
  .lang-switch { padding: 6px 9px; }
}

/* ---------- Mobile nav overlay (<=900px only) ---------- */
/* Full-screen, opened by .mobile-nav-toggle in the mobile topbar. z-index
   95 sits above the FAB (90) — no reason for the FAB to float above an
   open nav — but below the event modal (100), which should always win.
   Background is the same radial-glow-over-var(--bg) treatment as .hero,
   so it reads as part of the site rather than a flat blank screen. */
.mobile-nav-overlay {
  position: fixed; inset: 0; z-index: 95;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background:
    radial-gradient(90% 55% at 50% 0%, color-mix(in srgb, var(--accent) 11%, transparent), transparent 60%),
    radial-gradient(70% 45% at 85% 100%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 60%),
    var(--bg);
}
.mobile-nav-overlay[hidden] { display: none; }

/* Entrance/exit are separate keyframes (not a single transition) so the
   overlay always renders its "from" state on the very first frame after
   [hidden] is removed, with no flash of unstyled content. --closing plays
   while [hidden] is still absent (JS removes it only once this finishes),
   so the slide-up is actually visible instead of an instant cut. The
   panel itself only ever slides (no opacity animation on the backdrop) —
   animating its opacity too briefly let the page behind it show through
   mid-fade, since the backdrop gradient itself would turn translucent. */
.mobile-nav-overlay--open { animation: mobileNavIn 0.3s ease both; }
.mobile-nav-overlay--closing { animation: mobileNavOut 0.2s ease both; }
@keyframes mobileNavIn { from { transform: translateY(-100%); } to { transform: translateY(0); } }
@keyframes mobileNavOut { from { transform: translateY(0); } to { transform: translateY(-100%); } }

.mobile-nav-close {
  position: absolute; top: 20px; right: 20px;
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  font-size: 20px; line-height: 1; cursor: pointer;
  box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 4px 0 0 var(--surface-edge), 0 10px 20px -8px rgba(5,8,18,0.5);
  transition: transform 0.18s ease, box-shadow 0.18s ease, color 0.18s ease;
}
.mobile-nav-close:hover { color: var(--accent); transform: translateY(-2px); }
.mobile-nav-close:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 1px 0 0 var(--surface-edge); }

.mobile-nav-brand {
  font-family: "Bebas Neue", sans-serif; font-size: 30px; letter-spacing: 0.04em;
  margin-bottom: 44px; color: var(--ink);
}
.mobile-nav-brand span { color: var(--accent); }

.mobile-nav-links { display: flex; flex-direction: column; align-items: center; }
.mobile-nav-divider { width: 36px; height: 1px; margin: 6px 0; background: color-mix(in srgb, var(--accent) 45%, transparent); }
.mobile-nav-links a {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  padding: 10px 40px; border-radius: 14px;
  font-family: "Bebas Neue", sans-serif; font-size: 38px; letter-spacing: 0.03em;
  color: var(--ink); text-transform: uppercase; transition: color 0.18s ease;
}
/* The label sits in its own span (translated independently by
   applyTranslations()) so it can be layered above .pill-fill — same split
   nav.js uses for the desktop pills, reused here for the same hover/tap
   treatment: a solid accent shape growing in behind the text. */
.mobile-nav-links a > span:last-child { position: relative; z-index: 1; }
.mobile-nav-links a .pill-fill {
  position: absolute; inset: 4px; border-radius: 10px; background: var(--accent);
  opacity: 0; transform: scale(0.85); transition: opacity 0.2s ease, transform 0.2s ease; z-index: 0;
}
.mobile-nav-links a:hover .pill-fill, .mobile-nav-links a:active .pill-fill, .mobile-nav-links a:focus-visible .pill-fill {
  opacity: 1; transform: scale(1);
}
.mobile-nav-links a:hover, .mobile-nav-links a:active, .mobile-nav-links a:focus-visible { color: var(--on-accent); }

/* Staggered entrance — each link's animation-delay is set inline by
   mobileNav.js (60ms apart), so this only needs to define what plays;
   .mobile-nav-overlay--closing intentionally doesn't re-trigger it — the
   links just revert to their resting opacity/position instantly while the
   overlay itself fades out around them, which reads fine over 200ms. */
.mobile-nav-overlay--open .mobile-nav-brand,
.mobile-nav-overlay--open .mobile-nav-links a {
  animation: mobileNavItemIn 0.3s ease both;
}
@keyframes mobileNavItemIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

@media (min-width: 901px) {
  /* The toggle that opens this is only reachable inside the mobile topbar
     (itself hidden above 900px), but guard the overlay directly too in
     case a resize leaves it open across the breakpoint. */
  .mobile-nav-overlay { display: none !important; }
}

/* ---------- Floating action button ---------- */
/* Fixed bottom-right on every section, independent of the top-anchored
   .shortcut-nav/.utility-bar/.mobile-topbar (all top: ...; z-index 50-60) —
   living in the opposite corner means it never has to fight them for
   space. z-index 90 keeps it above ordinary page content but below the
   modal overlay (100) so opening the modal doesn't leave the FAB floating
   on top of it. Same flattened shadow language as .btn-primary now (a
   thin 2px contact shadow, a softened outer glow) rather than its own
   separate, more raised treatment. Visible by default — fab.js adds
   .fab--hero-hidden only once it confirms (via IntersectionObserver)
   that the hero's own buttons are in view, so a browser without
   IntersectionObserver support, or a visitor with JS partially broken,
   fails open (FAB stays reachable) rather than fails closed (booking
   button silently gone forever). */
.fab {
  position: fixed; bottom: 24px; right: 24px; z-index: 90;
  display: flex; align-items: center; gap: 8px;
  padding: 14px 20px 14px 16px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent);
  font-family: "Space Grotesk", sans-serif; font-size: 13px; font-weight: 700; letter-spacing: 0.02em;
  border: none; cursor: pointer;
  box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 2px 0 0 color-mix(in srgb, var(--accent-deep) 65%, transparent), 0 10px 20px -12px color-mix(in srgb, var(--accent) 35%, transparent);
  transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.4s ease;
}
.fab:hover { transform: translateY(-2px); box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 3px 0 0 color-mix(in srgb, var(--accent-deep) 65%, transparent), 0 14px 26px -12px color-mix(in srgb, var(--accent) 40%, transparent); }
.fab:active { transform: translateY(1px); box-shadow: 0 1px 0 rgba(255,255,255,0.15) inset, 0 1px 0 0 color-mix(in srgb, var(--accent-deep) 65%, transparent); }
.fab--hero-hidden { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .fab { transition: transform 0.18s ease, box-shadow 0.18s ease; }
}
.fab-icon { display: flex; }
.fab-label { white-space: nowrap; }

/* Icon-only circular puck once the screen's too narrow for a pill with a
   label — same corner, same shadow language, just no text to wrap. */
@media (max-width: 720px) {
  .fab {
    width: 52px; height: 52px; padding: 0;
    align-items: center; justify-content: center;
    bottom: 20px; right: 20px;
  }
  .fab-label { display: none; }
}

/* ---------- Hero ---------- */
/* Full-bleed real photo (images/hero/ — generated by scripts/build-hero.mjs;
   see README's "Hero photo") replacing the old photo-less radial-gradient-
   over-var(--bg) treatment. Three stacked layers inside .hero: .hero-media
   (the <picture>, z-index 0) under .hero-overlay (the navy tint + the
   text-legibility gradient, z-index 1) under .hero-inner (the actual
   headline/buttons, z-index 2) — a plain stacking context, not
   position:fixed/sticky, so it scrolls away normally with the rest of
   the page.
   100svh (small viewport height — excludes a mobile browser's own
   collapsible toolbar) is what's wanted here; 100vh is written first as
   the fallback for a browser that doesn't understand svh, which simply
   ignores the second declaration and keeps the first rather than erroring. */
.hero {
  position: relative; overflow: hidden;
  min-height: 100vh; min-height: 100svh;
  display: flex; align-items: center; justify-content: flex-start;
  text-align: left;
  padding: 140px 56px 100px;
}
.hero-media { position: absolute; inset: 0; z-index: 0; }
.hero-media picture { display: block; width: 100%; height: 100%; }
.hero-img {
  width: 100%; height: 100%; object-fit: cover;
  /* Desktop: the sign + kitchen window sit roughly 50-80% across the
     source photo (verified by sampling its own pixels) — 65% keeps that
     band on screen at every required breakpoint (1280x720/1440x900/
     1920x1080, plus ultrawide and short-but-wide windows, all checked by
     calculation against object-fit:cover's own crop math) without drifting
     so far right that an ultrawide screen's near-zero horizontal cropping
     would push it off-center instead. Mobile overrides this below — the
     mobile photo's own sign/window band sits elsewhere in the frame. */
  object-position: 65% 50%;
}
/* Navy tint across the whole photo (the task's own "dark overlay" ask)
   plus a stronger, localized gradient specifically behind the text.
   Verified by calculation against the photo's own actual pixel values in
   the text band (99th-percentile brightness there, not the single
   brightest pixel — a stray ceiling-light speck a few pixels wide
   shouldn't dictate the whole gradient): even fully untinted, that band
   already read >12:1 for the headline/subtext (--ink) and >6:1 for the
   eyebrow (--accent) against sRGB #e8e0d0/#c29a6b text, both comfortably
   past the 4.5:1 (body) / 3:1 (large text) AA floor, since it's genuinely
   dark wall/ceiling there in the source photo. This gradient is for a
   confident extra margin and the moodier look the task asks for, not
   because the text would otherwise fail AA — see README's "Hero photo"
   for the full numbers. */
.hero-overlay {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(
      to right,
      color-mix(in srgb, var(--navy) 55%, transparent) 0%,
      color-mix(in srgb, var(--navy) 55%, transparent) 30%,
      transparent 50%
    ),
    color-mix(in srgb, var(--navy) 25%, transparent);
}
.hero-inner { position: relative; z-index: 2; width: min(44vw, 520px); }
.hero .eyebrow { display: block; margin-bottom: 20px; font-size: 11px; font-weight: 500; }
.hero h1 { font-size: clamp(64px, 13vw, 148px); line-height: 0.92; letter-spacing: 0.02em; margin-bottom: 20px; }
.hero p { color: color-mix(in srgb, var(--ink) 65%, transparent); font-size: 18px; max-width: 460px; margin: 0 0 40px; }
.hero-actions { display: flex; gap: 16px; justify-content: flex-start; flex-wrap: wrap; }
#hero-buttons { display: flex; gap: 16px; flex-wrap: wrap; justify-content: flex-start; }
/* Mobile (<=768px): a "cinematic poster" layout — the photo anchored to
   the top (object-position: center top; see build-hero.mjs's own
   comment for why the mobile image is a tight crop rather than the
   full portrait photo — a plain object-position can't put the sign in
   the upper portion of the screen on its own, since object-fit:cover
   always shows the full image height on a viewport this narrow/tall,
   and the sign's own band is too large a fraction of the full photo's
   height to land near the top that way), with ONE text block — eyebrow,
   headline, subline, the two buttons in a row — pinned to the bottom
   instead of split top/bottom the way the desktop layout is. No scroll
   cue here; the space is needed for the text block instead. */
@media (max-width: 768px) {
  .hero {
    flex-direction: column; align-items: stretch; justify-content: flex-start;
    text-align: left; padding: 0;
    /* A floor ON TOP OF 100svh, not a replacement of it — min-height is
       a single property, so writing plain `min-height: 600px` here would
       cascade-override the base rule's 100svh entirely (confirmed: it
       capped the hero at exactly 600px even at an 812px-tall viewport,
       leaving 212px of the next section visibly in view and squeezing
       the text block into a box far shorter than the real viewport).
       max() keeps both constraints alive at once. */
    min-height: 100vh;
    min-height: max(600px, 100svh);
  }
  .hero-img { object-position: center top; }
  .hero-overlay {
    background:
      /* Short fade behind the fixed topbar — not behind any text, since
         the text block lives at the bottom on this layout now. */
      linear-gradient(to bottom, color-mix(in srgb, var(--navy) 50%, transparent) 0%, transparent 15%),
      /* Fades the photo into solid --bg starting ~38% from the bottom
         (62% from top) — verified against the cropped mobile photo's
         own pixels (see build-hero.mjs) to land just below the sign/
         window band, not over it. Front-loaded (most of the opacity
         gained by 78%, not a straight ramp to 100%) rather than a plain
         linear fade: measured by rendering real text at its actual
         position and sampling the pixels behind it, a straight fade
         left a bright metal-counter reflection showing through right
         where the eyebrow sits (~71% from top, just past the gradient's
         own start point) at only ~3.8:1 contrast — under the 4.5:1 AA
         floor. This curve gets there faster while keeping the same
         start point (so it still clears the sign/window) and still
         reaching fully solid by the very bottom. */
      linear-gradient(to bottom, transparent 0%, transparent 62%, color-mix(in srgb, var(--bg) 80%, transparent) 78%, var(--bg) 100%);
  }
  /* position:absolute + inset:0, not height:100% — see this same rule's
     own comment further up in git history / README's "Story and Menu
     photo edges" for why height:100% doesn't reliably resolve against
     a flex item whose own container is sized by min-height rather than
     an explicit height. justify-content:flex-end pins hero-text +
     hero-bottom together as one group at the bottom (not space-between
     — there's no separate top group any more). */
  .hero-inner {
    position: absolute; inset: 0; width: 100%;
    display: flex; flex-direction: column; justify-content: flex-end;
    padding: 0 24px calc(24px + env(safe-area-inset-bottom, 0px));
  }
  .hero-text { margin-bottom: 20px; }
  .hero .eyebrow { margin-bottom: 10px; }
  /* -webkit-line-clamp enforces the "max 2 lines" the task asks for as
     a hard cap, not just a hint — both are short enough in practice to
     rarely hit it, but a very long future rewrite of either string
     (or a long German-style compound in some other language) won't
     silently grow the text block past where it was sized to fit. */
  .hero h1 {
    font-size: clamp(48px, 15vw, 72px); line-height: 0.95; margin-bottom: 12px;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .hero p {
    font-size: 15px; margin: 0; max-width: none;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  /* One row, equal-width buttons. text-overflow:ellipsis is a safety
     net (not the primary plan — the EN/FI labels were checked to fit
     at 320px without triggering it) so a future longer label degrades
     gracefully instead of overflowing/wrapping. */
  #hero-buttons { display: flex; gap: 12px; width: 100%; }
  #hero-buttons .btn {
    flex: 1 1 0; min-width: 0; height: 52px; padding: 0 6px; border-radius: 10px;
    font-size: 15px; letter-spacing: normal; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
}
/* Below 360px (the task's own cutoff), the subline is dropped entirely
   rather than shrunk further — at that width there's barely room for
   the headline and buttons as it is. */
@media (max-width: 359px) {
  .hero p { display: none; }
  .hero-text { margin-bottom: 14px; }
}

/* ---------- Scroll reveal ---------- */
.reveal { opacity: 1; transform: none; }
@supports (animation-timeline: view()) {
  .reveal { opacity: 0; transform: translateY(40px); animation: revealUp linear both; animation-timeline: view(); animation-range: entry 0% cover 30%; }
}
@keyframes revealUp { to { opacity: 1; transform: translateY(0); } }

/* ---------- 3D tilt ---------- */
.tilt-frame { perspective: 1200px; }
.tilt-card {
  transform-style: preserve-3d;
  transition: transform 0.4s cubic-bezier(0.2,0.8,0.2,1), box-shadow 0.4s ease;
  box-shadow: var(--card-shadow);
}
.tilt-frame:hover .tilt-card {
  transform: rotateX(6deg) rotateY(-8deg) translateY(-6px) translateZ(10px);
  box-shadow: 0 40px 70px -20px rgba(5,8,18,0.8);
}

/* ---------- Full-bleed image + gradient overlay ---------- */
.visual {
  position: relative; border-radius: 22px; overflow: hidden;
  display: flex; align-items: flex-end;
  background-color: var(--surface-edge);
}
/* A real <img> (dishCard.js), not a CSS background-image —
   background-images have no equivalent to loading="lazy"/decoding="async"
   and browsers are inconsistent about deferring them for off-screen
   elements, which on a page with a dozen-plus tiles meant several were
   still empty (showing this fallback background-color) well after the
   photos above the fold had already loaded. The fallback is harmless
   while an image is genuinely still loading, but this element is also
   overflow:hidden + border-radius on the same node as .tilt-card's
   transitioned transform (the hover tilt), and Chromium's rounded-corner
   clip on a transform-composited layer can leave a hairline of whatever
   sits behind the <img> visible along one edge. Tying the fallback to
   --surface-edge (a token, not a hardcoded color) means that hairline —
   if it ever shows — blends with the palette instead of being an
   unrelated color dropped in from outside it. */
.visual-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The scrim darkens a photo toward var(--navy) — fixed, not the scoped
   var(--bg) — so the caption text sitting on top of it stays readable
   against a reliably dark gradient regardless of which surface the card
   itself sits on; var(--bg) would turn this cream/white on a light
   surface (see --navy's own comment at the top of the file), which is
   the opposite of what a scrim darkening toward legibility needs. */
.visual .scrim {
  position: absolute; inset: 0;
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--navy) 92%, transparent) 0%,
    color-mix(in srgb, var(--navy) 55%, transparent) 38%,
    transparent 68%
  );
}
.visual .caption { position: relative; z-index: 2; padding: 26px 24px; width: 100%; color: var(--ink); }
.caption p { color: var(--muted); font-size: 14px; margin: 6px 0 0; }

/* ---------- Full-bleed wrapper ---------- */
/* Breaks an element out of its parent's padding to span the full viewport
   width, regardless of that parent's own padding value at any breakpoint —
   robust across breakpoints without tracking each one's padding number.
   body's overflow-x:hidden (set above) absorbs the sub-pixel rounding this
   can introduce. Used for imagery/content rows, not text — text keeps its
   own natural max-width (.section-head, .story-body, etc.) per the site's
   readability rule; only .dish-carousel uses this now (the gallery mosaic
   is a deliberately contained composition, not full-bleed — see "Gallery:
   crossfading mosaic" below). */
.full-bleed { width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); }

/* ---------- Dishes: auto-scrolling carousel ---------- */
/* One component, one behavior, at every screen size — see
   components/dishCarousel.js for the loop/pause/arrow logic. The card
   list is rendered twice back-to-back (.dish-carousel-set, display:contents
   so it groups for aria-hidden without breaking the track's flex layout)
   and a rAF loop drives .dish-carousel-track's transform; overflow:hidden
   here is what actually clips it down to one visible row. */
.dish-carousel { position: relative; overflow: hidden; padding: 4px 56px; }
.dish-carousel-track { display: flex; gap: 16px; width: max-content; will-change: transform; }
.dish-carousel-set { display: contents; }
.dish-carousel .tilt-frame { flex: 0 0 240px; }
.dish-visual { aspect-ratio: 3 / 4; cursor: pointer; }
.dish-visual:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.dish-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.dish-top h3 { font-size: 26px; }
.dish-price { font-family: "Bebas Neue", sans-serif; color: var(--accent); font-size: 22px; }

/* Optional manual controls layered on top of the row — supplementary to
   the auto-scroll/loop, never required to see every dish. */
.carousel-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  font-size: 22px; line-height: 1; cursor: pointer; opacity: 0.7;
  box-shadow: 0 1px 0 rgba(255,255,255,0.06) inset, 0 4px 0 0 var(--surface-edge), 0 10px 20px -8px rgba(5,8,18,0.5);
  transition: transform 0.18s ease, box-shadow 0.18s ease, color 0.18s ease, opacity 0.18s ease;
}
.carousel-arrow:hover, .carousel-arrow:focus-visible { opacity: 1; color: var(--accent); transform: translateY(-50%) translateY(-2px); }
.carousel-arrow:active { transform: translateY(-50%) translateY(2px); box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 1px 0 0 var(--surface-edge); }
.carousel-arrow--prev { left: 12px; }
.carousel-arrow--next { right: 12px; }
@media (max-width: 720px) {
  .carousel-arrow { width: 38px; height: 38px; font-size: 18px; }
  .dish-carousel { padding: 4px 16px; }
}

/* ---------- Story: asymmetric two-column on desktop ---------- */
/* Stacked (text, then photo) below 901px — same DOM order as the desktop
   row below, just a different flex-direction, so no order-reversal trick
   is needed to get "text left, image right" on desktop from "text above
   image" on mobile. */
.story-layout { display: flex; flex-direction: column; gap: 48px; align-items: center; }
.story-text { width: 100%; }
.story-body { max-width: 680px; margin: 0 auto; text-align: center; }
.story-body p { color: color-mix(in srgb, var(--ink) 70%, transparent); font-size: 17px; margin: 0 0 20px; }
.story-quote {
  margin: 44px auto 0; padding: 28px 32px; max-width: 560px;
  border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-radius: 0 12px 12px 0; text-align: left;
}
.story-quote p {
  font-family: "Bebas Neue", sans-serif; font-size: 24px; letter-spacing: 0.01em;
  color: var(--ink); margin: 0; line-height: 1.25;
}

/* Soft-edged photo treatment, reused for both the Story photo and the
   Menu section's rotating image column (Part 2) — the photo stays its
   full rectangular shape at full size; only a band near each of the 4
   edges fades to transparent, letting the section's own background show
   through right at the edges. NOT a radial vignette: an earlier version
   used a single radial-gradient mask, which fades outward from the
   center in an oval, shrinking the visible photo into a blob rather than
   keeping it rectangular — wrong effect entirely, not a bug in that
   gradient's syntax.
   Built from two linear-gradient mask layers, one fading the left/right
   edges (to right) and one fading the top/bottom edges (to bottom), each
   opaque through the middle 60% and transparent in the outer ~20% band.
   The two layers must INTERSECT (multiply), not stack the default way:
   with the default "add" compositing, a pixel near the left edge but
   vertically in the untouched middle band would get (low alpha from the
   horizontal layer) + (alpha 1 from the vertical layer) = still ~1, so
   the left edge wouldn't visibly fade at all except right at the
   corners. `mask-composite: intersect` (standard) / `-webkit-mask-
   composite: source-in` (legacy WebKit/Safari, comma-separated per
   layer-pair) make each layer actually cut into the one below it instead
   of adding — confirmed empirically in a standalone test page: a
   rectangular photo, sharp center, feathered on all 4 sides including
   the corners, no circular blob.
   No overflow:hidden/border-radius on this or its wrapper — that's the
   old card-style clipping (.visual etc. use it deliberately), and it
   would clip the image to a hard rectangle right at the box edge,
   overriding the mask fade instead of complementing it. */
.soft-fade-img {
  --fade-edge: 20%;
  object-fit: cover; display: block;
  mask-image:
    linear-gradient(to right, transparent 0%, black var(--fade-edge), black calc(100% - var(--fade-edge)), transparent 100%),
    linear-gradient(to bottom, transparent 0%, black var(--fade-edge), black calc(100% - var(--fade-edge)), transparent 100%);
  mask-composite: intersect;
  mask-repeat: no-repeat;
  mask-size: 100% 100%;
  mask-position: center;
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, black var(--fade-edge), black calc(100% - var(--fade-edge)), transparent 100%),
    linear-gradient(to bottom, transparent 0%, black var(--fade-edge), black calc(100% - var(--fade-edge)), transparent 100%);
  -webkit-mask-composite: source-in;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  -webkit-mask-position: center;
}

.story-photo { position: relative; width: 100%; max-width: 460px; aspect-ratio: 4 / 5; }
.story-photo-img { width: 100%; height: 100%; }
/* A radial version of this wash (an earlier pass) ramped up to a full
   var(--bg) blend at the edges, reinforcing the edge-fade mask below —
   but that made the edges read as over-darkened on top of the mask
   already dissolving them. A flat, very light 12% tint does the "ground
   the photo into the section" job the wash exists for without fighting
   the mask or dulling the subject: --fade-edge below is what actually
   handles the edge falloff (a rectangular 4-side feather, not this). */
.story-photo-wash {
  position: absolute; inset: 0; z-index: 1;
  background: color-mix(in srgb, var(--bg) 12%, transparent);
}
/* --fade-edge history: 20% (the shared .soft-fade-img default) read as
   barely-there; 42% then went too far the other way, leaving only the
   innermost 16% of the photo (42–58%) fully clear and fading well over
   a third of the image in from each side; 4% then over-corrected the
   other direction — the fade band got so narrow it read as a sharp,
   almost hard edge rather than a soft blend. 18% lands between the two:
   noticeably softer than 4% (a wide enough band to actually read as a
   gradient, not a cutoff), while still leaving the center 64% of the
   photo (18–82%) fully clear — much closer to fully exposed than 42%'s
   16%. Same linear-gradient .soft-fade-img always builds, just a
   different width. */
.story-photo-img, .story-photo-wash { --fade-edge: 18%; }

@media (min-width: 901px) {
  .story-layout { flex-direction: row; align-items: center; gap: 72px; }
  .story-text { flex: 1 1 0; }
  .story-text .section-head { text-align: left; margin: 0 0 32px; max-width: none; }
  .story-body { text-align: left; margin: 0; max-width: 560px; }
  .story-quote { margin-left: 0; }
  .story-photo { flex: 1 1 0; max-width: 520px; }
}

/* ---------- Menu ---------- */
/* Image-left/content-right — DOM order is photo, then content, so plain
   flex-direction: row already puts them in that order on desktop with no
   reversal trick, same technique Story uses for its (mirrored) text-left/
   image-right split. Mobile uses column-reverse instead of column so the
   list — the part visitors actually came for — shows first and the photo
   stacks below it, per the "list stays easy to scan on small screens"
   priority; column-reverse is exactly "reverse the same DOM order",
   consistent with how the desktop row doesn't reverse anything either. */
.menu-layout { display: flex; flex-direction: column-reverse; gap: 32px; align-items: center; }
.menu-content { width: 100%; }
/* Wider cap than a plain "photo thumbnail" would need, and a tighter gap
   above/below (see .menu-layout) — a test pass found the column reading as
   a small floating box in a lot of empty section background; this and the
   440px→520px desktop cap below are what let it actually fill its column
   instead of just sitting capped well under it. */
.menu-photo { position: relative; width: 100%; max-width: 510px; aspect-ratio: 4 / 5; }
/* position:absolute + inset:0 (not width/height:100%) — .menu-photo's
   height comes from aspect-ratio on a flex item, and a normal-flow
   child's height:100% doesn't reliably resolve against that (collapsed
   to 0 in testing); absolute positioning anchors to the containing
   block's box directly instead, sidestepping that percentage-resolution
   edge case entirely. Same pattern .visual-img already uses. */
.menu-photo-frame { position: absolute; inset: 0; }
.menu-photo-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; transition: opacity 0.9s ease;
}
.menu-photo-img--active { opacity: 1; }
/* Same radial-glow technique as .section-ambient, scoped to just this box
   — shown only when the active category has no on-theme photo at all
   (see components/menuPhoto.js), so an empty/mismatched image is never
   the fallback for "no photo yet." Kept under 5% now that #menu is a
   light (.surface-white) section — the 9-11% this used on the old dark
   Menu background would glow as a visible brass smudge on cream/white
   instead of the barely-there depth cue it's meant to be. */
.menu-photo-ambient {
  position: absolute; inset: 0; opacity: 0; transition: opacity 0.9s ease;
  background:
    radial-gradient(60% 55% at 25% 20%, color-mix(in srgb, var(--accent) 4%, transparent), transparent 60%),
    radial-gradient(55% 50% at 80% 85%, color-mix(in srgb, var(--accent) 3%, transparent), transparent 60%);
}
.menu-photo-frame--ambient-only .menu-photo-ambient { opacity: 1; }

/* Same flat, light wash as Story (see .story-photo-wash's comment) — a
   uniform 12% var(--bg) tint, not a radial ramp to a full edge blend,
   so the dish/drink stays close to its natural brightness and the
   4-side --fade-edge mask below is what does the edge falloff. z-index
   (not DOM order) puts it above both the rotating <img>s and the ambient
   glow, since .menu-photo-img elements are appended after this element
   at runtime (components/menuPhoto.js) and would otherwise paint over
   it. Hidden in ambient-only mode — there's no photo underneath to wash
   out. */
.menu-photo-wash {
  position: absolute; inset: 0; z-index: 2;
  background: color-mix(in srgb, var(--bg) 12%, transparent);
  transition: opacity 0.9s ease;
}
.menu-photo-frame--ambient-only .menu-photo-wash { opacity: 0; }

/* Same --fade-edge history as .story-photo-img (see its comment): 20%
   default read as barely-there, 42% over-corrected to a 16%-wide
   fully-clear center, 4% over-corrected the other way into a sharp,
   almost hard edge. 18% settles between the two — a visibly soft band,
   64% of the photo (18–82%) left fully clear. */
.menu-photo-img, .menu-photo-wash { --fade-edge: 18%; }

@media (min-width: 901px) {
  /* 40/60 split (photo/content) rather than an even 50/50 — favors the
     list's own readability over a strictly equal column, per the "don't
     cramp item descriptions/prices" note. Gap and max-width tightened
     from 56px/440px (see .menu-photo's comment above) for the same
     "larger, more dominant, less empty space around it" pass. */
  .menu-layout { flex-direction: row; align-items: flex-start; gap: 40px; }
  .menu-photo { flex: 0 1 44%; max-width: 560px; position: sticky; top: 140px; }
  .menu-content { flex: 1 1 60%; min-width: 0; }
}

/* Sticky within the Full Menu section only: .tabs-sticky is a normal
   in-flow child of .menu-content (section-head, then tabs-sticky, then
   menu-list), so CSS's own sticky algorithm already gives the "pins at
   top while scrolling through the list, then releases once past
   .menu-content's bottom edge" behavior for free — no scroll listener
   needed. top:88px clears the shortcut nav pill (bottom edge ~73px) and
   the mobile topbar (bottom edge ~69px) at every breakpoint, with a
   little breathing room under either. z-index:40 keeps it below the
   pill nav (50) and mobile topbar (60) it sits under, but above
   ordinary menu-list content scrolling past it.
   A flat var(--bg) + hairline now that #menu is a solid .surface-white
   section with no ambient glow behind it to clash with — a background
   here used to read as a visible floating panel against that glow (an
   earlier version tried it), but there's no glow left to fight now, and
   without it, menu items scrolling up would show straight through the
   pill row with nothing marking where the sticky band actually is.
   Also establishes the positioning context for the edge-fade
   pseudo-elements below — sticky already does this (any position other
   than static does), called out explicitly since it's easy to assume
   otherwise. A fixed, single-row height here (see .tabs below) is what
   actually keeps this from ever overlapping .menu-note/the first
   .menu-item under it — a wrapped 2-3 row version of the old layout
   made the sticky band tall enough to cover that content the moment a
   visitor scrolled even slightly, at any width under ~640px. */
.tabs-sticky {
  position: sticky; top: 88px; z-index: 40; margin-bottom: 56px;
  background: var(--bg); border-bottom: 1px solid var(--line);
}
/* One row, always — never wraps, scrolls horizontally instead once the
   six pills don't fit. `safe center` centers them (like the old
   wrapping layout did) whenever they DO all fit, but falls back to
   plain start-alignment the instant they overflow, so scrolling never
   has to fight a centered start position to reach the first pill —
   without `safe`, a centered flex row that overflows can land its
   initial scroll position mid-content, making the first item require
   scrolling backward to reach. `scroll-snap-type: x proximity` (not
   `mandatory`) gives each pill a gentle resting point without fighting
   the visitor's own momentum scroll. Scrollbar hidden three ways (one
   per engine) since this is a deliberately chrome-free pill row, not a
   visible scroll widget — the edge fades below are what signal "more
   content" instead. No border-radius/blur/tint of its own beyond that —
   .tabs-sticky (above) carries the shared background. Each .tab pill
   still carries its own solid var(--surface) fill, so the pills read as
   floating individually, closer to how .shortcut-nav's own pills read. */
.tabs {
  display: flex; flex-wrap: nowrap; justify-content: safe center; align-items: center;
  gap: 8px; padding: 6px 4px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  scrollbar-width: none; -ms-overflow-style: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  flex: 0 0 auto; scroll-snap-align: center;
  padding: 12px 24px; border-radius: 999px; border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  background: var(--surface); color: color-mix(in srgb, var(--ink) 60%, transparent); font-size: 13px; font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase; cursor: pointer;
  box-shadow: 0 4px 0 0 var(--surface-edge); transform: translateY(0); transition: all 0.18s ease;
}
.tab:hover { color: var(--ink); transform: translateY(-2px); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Soft fade hinting "this scrolls" at each edge of the pill row — a
   solid-to-transparent gradient painted on var(--tabs-sticky's own
   background, not a mask on the pills themselves (a mask would fade the
   pills' own borders/fill to see-through right at the clip, which reads
   as broken rather than as a scroll hint). Harmless/invisible when the
   row doesn't overflow (centered pills never reach these 32px edge
   zones, so there's nothing to fade over), so no extra logic is needed
   to hide them on wide screens — see the .tabs comment above for how
   `safe center` handles that case. bottom:1px keeps the fade just above
   .tabs-sticky's own hairline border rather than painting over it;
   pointer-events:none so the fade strip never blocks a tap on a pill
   scrolled half underneath it. */
.tabs-sticky::before, .tabs-sticky::after {
  content: ""; position: absolute; top: 0; bottom: 1px; width: 32px; z-index: 1; pointer-events: none;
}
.tabs-sticky::before { left: 0; background: linear-gradient(to right, var(--bg), transparent); }
.tabs-sticky::after { right: 0; background: linear-gradient(to left, var(--bg), transparent); }
.tab.active { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); box-shadow: 0 4px 0 0 var(--accent-deep); transform: translateY(-2px); }
/* Pills on a light surface: transparent/bordered at rest instead of a
   filled var(--surface) pill (a near-white fill on an already white/
   cream page wouldn't read as its own shape), and NAVY — not brass —
   when active. Brass is already the surface's own accent/text color
   (eyebrows, prices); a brass-filled active pill right next to brass
   prices/eyebrows nearby would blur together, where navy reads as a
   clear, deliberate "this one's selected" signal instead. --navy (fixed,
   not scoped) is what makes that work regardless of which light surface
   the pills happen to sit on — see its own comment at the top of the
   file. This only ever matches the Full Menu's own sticky tabs: the
   gallery overlay reuses the same .tab/.tab.active classes for its album
   pills, but that overlay always stays dark (see "Overlays stay dark" in
   the README's Palette section) and is never a descendant of
   .surface-light/.surface-white, so it's unaffected by this. */
.surface-light .tab, .surface-white .tab {
  background: transparent; border-color: var(--line); color: var(--ink);
  box-shadow: none;
}
.surface-light .tab.active, .surface-white .tab.active {
  background: var(--navy); border-color: var(--navy); color: var(--on-accent);
  box-shadow: 0 4px 0 0 color-mix(in srgb, var(--navy) 70%, black);
}
.menu-list { max-width: 740px; margin: 0 auto; }
/* Name, then description, then price — always stacked in that order inside
   .menu-item-body rather than price sharing a row with the name, so a long
   name (or wine's price grid) never ends up vertically misaligned against
   wrapped text. The thumbnail (when present) is the only sibling; items
   with no photo (.menu-item--no-photo) skip it and the body just takes the
   full row width. */
.menu-item {
  position: relative;
  display: flex; align-items: flex-start; gap: 18px; padding: 20px 6px;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  cursor: pointer; border-radius: 10px;
  transition: background 0.2s ease, transform 0.2s ease;
}
/* A row fades its own opacity out as it nears the top of the viewport
   (where the pinned category tabs sit), rather than being hidden behind
   an opaque panel/overlay sitting on top of it — see .tabs-sticky's
   comment for why that overlay approach was dropped. This is the same
   view-timeline technique "Scroll reveal" above uses to fade rows IN on
   the way down; here it runs the opposite direction, driven by the
   item's own exit from the viewport rather than its entry.
   A negative offset on an "exit" range moves the trigger point earlier
   than that phase's own 0% (empirically: "exit -Npx" fires while the
   row's top edge is still ~Npx down the viewport, not yet at 0).
   animation-range: exit -220px exit -90px starts the fade while the
   row's top is still ~220px down (comfortably below the tabs) and
   finishes it by ~90px down — measured against .tabs-sticky's own
   top:88px, that's essentially fully transparent right as a row would
   otherwise reach the tabs, not still fading (or already gone and
   popping back) once it's behind them.
   .menu-note (the Lunch tab's price/schedule line, Cocktails' tequila
   note — appendGroupNote() in script.js) and .menu-group-heading (the
   Drinks tab's Soft Drinks/Beer/Cocktails/Wine sub-headings) are also
   direct children of #menu-list sitting right alongside .menu-item rows
   — the exact same "scrolls up toward the tabs" situation, so they need
   the same treatment or they're left showing through exactly like a
   .menu-item would without it. */
@supports (animation-timeline: view()) {
  #menu .menu-item, #menu .menu-note, #menu .menu-group-heading {
    animation: menuItemFadeOut linear both;
    animation-timeline: view();
    animation-range: exit -220px exit -90px;
  }
}
@keyframes menuItemFadeOut { to { opacity: 0; } }
.menu-item:last-child { border-bottom: none; }
/* A thin accent bar fades in at the left edge on hover, matching the
   button/tab language's accent-as-signal treatment elsewhere on the site
   rather than a color the row wouldn't otherwise use. Inset top/bottom
   (not full-height) so it doesn't visually clip through the row's own
   border-radius at its corners. */
.menu-item::before {
  content: "";
  position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px; border-radius: 2px;
  background: var(--accent);
  opacity: 0; transform: scaleY(0.5); transform-origin: center;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.menu-item:hover {
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  transform: translateX(6px);
}
.menu-item:hover::before { opacity: 1; transform: scaleY(1); }
.menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.menu-item-thumb {
  flex: 0 0 auto; width: 60px; height: 60px; border-radius: 12px; object-fit: cover;
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  box-shadow: 0 4px 10px -4px rgba(5,8,18,0.55);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.menu-item-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.menu-item-top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.menu-item-name { font-family: "Bebas Neue", sans-serif; font-size: 24px; }
.menu-item-tag { font-size: 10px; padding: 2px 7px; border-radius: 4px; border: 1px solid color-mix(in srgb, var(--ink) 20%, transparent); color: color-mix(in srgb, var(--ink) 55%, transparent); text-transform: uppercase; letter-spacing: 0.06em; }
.menu-item-desc { color: color-mix(in srgb, var(--ink) 50%, transparent); font-size: 14px; max-width: 460px; margin-top: 6px; }
.menu-item-price { font-family: "Bebas Neue", sans-serif; color: var(--accent); font-size: 22px; white-space: nowrap; margin-top: 10px; }
.menu-empty { text-align: center; color: color-mix(in srgb, var(--ink) 40%, transparent); padding: 26px 6px; }

/* Wine's four-size price row: a fixed-width grid (not flex) so 12cl/16cl/
   24cl/Btl line up in the same columns on every wine row, size labels
   above the numbers, "–" for sizes not offered (e.g. House Sparkling
   Wine's still/16cl/24cl glasses). */
.menu-item-price--wine { display: grid; grid-template-columns: repeat(4, 58px); gap: 8px 10px; margin-top: 10px; }
.price-col { display: flex; flex-direction: column; align-items: center; }
.price-label { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: color-mix(in srgb, var(--ink) 45%, transparent); margin-bottom: 3px; }
.price-value { font-family: "Bebas Neue", sans-serif; font-size: 16px; color: var(--accent); }

/* .menu-item-tag/.menu-item-desc/.menu-empty/.price-label's own ink-alpha
   blends above (45-55%) were tuned against the dark surfaces they were
   written for — the same alpha against a light background's own math
   lands much lighter in absolute terms, not darker, so on Menu (now
   .surface-white) they dropped to ~3.2-3.3:1 against white, under the
   4.5:1 floor. --muted is already verified ≥6:1 on both light surfaces
   (see the Palette README section) and is the token this is for — body/
   secondary text on a scoped surface. .price-label is shared with the
   item detail modal (#item-modal-price .menu-item-price), which always
   stays dark (see "Overlays stay dark" below) and was never part of this
   contrast problem — scoping this to .surface-light/.surface-white
   keeps that modal instance on its own, already-fine, ink-alpha blend
   rather than touching it too. */
.surface-light .menu-item-tag, .surface-white .menu-item-tag,
.surface-light .menu-item-desc, .surface-white .menu-item-desc,
.surface-light .menu-empty, .surface-white .menu-empty,
.surface-light .price-label, .surface-white .price-label {
  color: var(--muted);
}

/* On desktop there's ample horizontal room, so a wine row's name and its
   price grid sit on one line instead of stacking (mobile keeps the
   stacked layout/sizing above untouched — wine rows have no description,
   so .menu-item-body's only two children are .menu-item-top and the
   price grid, and switching that column to a row is all it takes). Sized
   up slightly for legibility now that they're not competing for width
   the way the stacked mobile layout does. */
@media (min-width: 901px) {
  .menu-item--wine .menu-item-body { flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
  .menu-item--wine .menu-item-name { font-size: 28px; }
  .menu-item--wine .menu-item-price--wine { margin-top: 0; grid-template-columns: repeat(4, 64px); }
  .menu-item--wine .price-label { font-size: 10px; }
  .menu-item--wine .price-value { font-size: 19px; }
}

/* A standing note above a group's items (Lunch's fixed price/schedule,
   Cocktails' tequila & mezcal note). */
.menu-note {
  text-align: center; font-size: 13px; color: color-mix(in srgb, var(--ink) 65%, transparent);
  margin: 0 0 32px; padding: 12px 20px; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}

/* Sub-section heading inside the grouped Drinks tab (Soft Drinks/Beer/
   Cocktails/Wine sharing one panel). */
.menu-group-heading {
  font-family: "Bebas Neue", sans-serif; font-size: 20px; letter-spacing: 0.03em;
  color: var(--accent); text-align: center; text-transform: uppercase;
  margin: 40px 0 8px;
}
.menu-group-heading:first-of-type { margin-top: 0; }

/* ---------- Events ---------- */
/* Stays on the plain dark :root surface (no .surface-light/.surface-white
   class on the section) — see index.html's own comment on why. Cards
   reuse .menu-item's "thumb-left, body-right" skeleton rather than
   inventing a new one, just with a date block standing in for the photo
   thumb. The soonest occurrence (always index 0 — resolveUpcomingEvents
   already sorts by it) gets .event-card--hero: a brass accent wash + a
   bigger date block, so it reads as "the one that matters" the same way
   .btn-primary already does for buttons, not a new visual language. */
.events-grid {
  display: flex; flex-direction: column; gap: 18px;
  max-width: 760px; margin: 0 auto;
}
.event-card {
  position: relative; display: flex; gap: 20px; align-items: flex-start;
  padding: 22px; border-radius: 16px;
  background: var(--surface);
  box-shadow: var(--card-shadow);
}
.event-date {
  flex: 0 0 auto; width: 64px; padding: 10px 0; border-radius: 12px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}
.event-date-weekday { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.event-date-day { font-family: "Bebas Neue", sans-serif; font-size: 30px; line-height: 1; margin-top: 2px; }
.event-body { flex: 1 1 auto; min-width: 0; }
.event-top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.event-title { font-family: "Bebas Neue", sans-serif; font-size: 24px; letter-spacing: 0.01em; }
.event-time { color: var(--muted); font-size: 14px; margin-top: 4px; }
.event-desc { color: var(--muted); font-size: 14px; max-width: 460px; margin-top: 8px; }
.event-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.event-tag {
  font-size: 10px; padding: 2px 7px; border-radius: 4px;
  border: 1px solid color-mix(in srgb, var(--ink) 20%, transparent);
  color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em;
}
.event-price { font-family: "Bebas Neue", sans-serif; color: var(--accent); font-size: 18px; white-space: nowrap; }
.event-image {
  flex: 0 0 96px; width: 96px; height: 96px; border-radius: 12px; object-fit: cover;
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
/* "Up next"/"Tonight" — small uppercase tags, not full badges-with-icons,
   same restrained tag language .event-tag/.menu-item-tag already use.
   "Up next" is filled (brass, --on-accent text — the same pairing
   .btn-primary uses, already ≥4.5:1) since it only ever appears on the
   hero card's own accent-tinted background; "Tonight" is an outline so
   the two can sit side by side without reading as two filled pills. */
.event-upnext {
  font-size: 10px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
  background: var(--accent-fill); color: var(--on-accent);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.event-badge {
  font-size: 10px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
  border: 1px solid var(--accent); color: var(--accent);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.event-card--hero {
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  padding: 28px;
}
.event-card--hero .event-date { background: var(--accent-fill); }
.event-card--hero .event-date-weekday { color: color-mix(in srgb, var(--on-accent) 75%, transparent); }
.event-card--hero .event-date-day { color: var(--on-accent); }
.event-card--hero .event-title { font-size: 30px; }

@media (max-width: 560px) {
  .event-card { flex-wrap: wrap; }
  .event-image { flex-basis: 100%; width: 100%; height: 160px; order: 10; }
}

/* ---------- Gallery: crossfading mosaic ---------- */
/* One big tile (spans both rows) + two smaller tiles stacked beside it —
   a 1.4fr/1fr column split, not an even 50/50, so the big tile actually
   reads as the "hero" of the three rather than just a taller version of
   the same size. Clean rectangles this time (no scrapbook rotation/tilt/
   overlap) — aspect-ratio (not a fixed height) is what lets the SAME
   grid-template-columns/rows structure just scale up or down between
   mobile and desktop instead of needing a different layout per
   breakpoint; a max-width keeps it from sprawling edge-to-edge on very
   wide screens, since this reads as one deliberate composition, not
   full-bleed imagery like the dish carousel above it. */
.gallery-mosaic {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 14px;
  max-width: 820px;
  margin: 0 auto;
  aspect-ratio: 16 / 9;
}
/* Tiles are <button>s now (each one opens the full gallery overlay, not
   just the "See all N photos" CTA below them) — reset the button chrome
   back down to exactly what the old <div> tiles looked like, then add
   the hover affordance a clickable photo should have. */
.mosaic-tile {
  position: relative;
  border-radius: 22px;
  overflow: hidden;
  background-color: var(--surface-edge);
  box-shadow: var(--card-shadow);
  padding: 0; border: none; cursor: pointer;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}
/* navy-based, not the fixed rgba(5,8,18) near-black other hover shadows
   on the (still-dark) Dishes/Gallery-overlay sections use — the gallery
   teaser sits on a light (.surface-light) section now, where that near-
   black at 70% alpha would read as a heavy dark halo rather than lift. */
.mosaic-tile:hover { transform: translateY(-3px); box-shadow: 0 24px 44px -16px color-mix(in srgb, var(--navy) 55%, transparent); }
.mosaic-tile--big { grid-row: 1 / 3; }
/* Same crossfade mechanic as the Menu section's rotating category photos
   (.menu-photo-img/.menu-photo-img--active) — two stacked, absolutely
   positioned images per tile, only one ever at opacity:1. Each tile's own
   pair loops independently and starts on a staggered delay (see
   galleryMosaic.js) so the three tiles don't all visibly flip at once. */
.mosaic-tile-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity 1.4s ease;
}
.mosaic-tile-img--active { opacity: 1; }

.gallery-mosaic-cta { text-align: center; margin-top: 32px; }

@media (max-width: 900px) {
  .gallery-mosaic { gap: 10px; border-radius: 16px; }
  .mosaic-tile { border-radius: 16px; }
  .gallery-mosaic-cta { margin-top: 24px; }
}

/* ---------- Gallery overlay ("See all N photos") ---------- */
/* A dedicated full-screen layer, not the .modal-overlay/.modal-card
   system the other modals use — those are centered cards meant to sit
   over the page; this is meant to BE the page while it's open, with its
   own internal scroll region and a sticky header+pill row riding above
   it. z-index 150 clears every other overlay on the site (modals: 100,
   mobile nav: 95); the photo viewer that opens from inside it (below)
   goes one layer higher still. */
.gallery-overlay {
  position: fixed; inset: 0; z-index: 150;
  display: flex; flex-direction: column;
  background: var(--bg);
  overflow: hidden;
}
.gallery-overlay[hidden] { display: none; }

/* Header + pill row travel together as one sticky unit at the scroll
   region's own top — see .gallery-overlay-scroll below, which is what
   actually scrolls (not this element, so position:sticky has the right
   containing block to stick within). */
.gallery-overlay-sticky {
  position: sticky; top: 0; z-index: 2;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.gallery-overlay-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 24px;
}
.gallery-overlay-header h2 {
  font-family: "Bebas Neue", sans-serif; font-size: 28px; letter-spacing: 0.02em; margin: 0;
}
.gallery-overlay-close {
  width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink); font-size: 20px; line-height: 1; cursor: pointer;
  transition: color 0.18s ease, transform 0.18s ease;
}
.gallery-overlay-close:hover { color: var(--accent); transform: rotate(90deg); }

/* Same pill visual language as the Full Menu's category tabs (.tab) —
   reusing that class directly (.tab) plus this component's own
   .gallery-pill for the few things that differ (the count badge). No
   sticky position of its own: it travels with .gallery-overlay-sticky,
   same reasoning the Full Menu's sticky tabs comment gives for not
   duplicating position:sticky across a parent/child pair that would
   both try to stick independently. */
.gallery-overlay-pills {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 8px;
  padding: 0 24px 18px;
}
.gallery-pill-count {
  opacity: 0.65; font-size: 11px; margin-left: 2px;
}
.gallery-pill--active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); box-shadow: 0 4px 0 0 var(--accent-deep); }

.gallery-overlay-scroll { flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch; }

/* Masonry via CSS columns rather than a JS packing library — each tile's
   own aspect-ratio (from data/gallery.json's w/h, set as an inline style
   per tile by galleryOverlay.js) reserves its real height up front, so
   nothing reflows once its thumbnail finishes loading, and
   break-inside:avoid keeps a tile from being split across two columns. */
.gallery-masonry {
  column-count: 2; column-gap: 12px;
  padding: 20px 16px 60px;
  max-width: 1400px; margin: 0 auto;
}
.gallery-tile {
  display: block; width: 100%; margin: 0 0 12px; padding: 0; border: none;
  position: relative; border-radius: 14px; overflow: hidden;
  break-inside: avoid;
  background: var(--surface-edge);
  cursor: zoom-in;
  box-shadow: 0 0 0 0 transparent;
  transition: box-shadow 0.25s ease, transform 0.25s ease, opacity 0.2s ease;
}
.gallery-tile-img {
  display: block; width: 100%; height: 100%; object-fit: cover;
}
/* The "developing" reveal: a tile starts dark/desaturated/blurred/
   slightly oversized, and animates to normal once it first scrolls into
   view (IntersectionObserver in galleryOverlay.js adds --developed,
   then un-observes — it only ever plays once per tile, not every time
   it re-enters view). Scoped inside the no-preference media query so a
   visitor with reduced motion gets every tile already in its finished
   state with nothing to animate at all, not a fast version of the same
   animation. */
@media (prefers-reduced-motion: no-preference) {
  .gallery-tile-img {
    filter: grayscale(1) blur(10px) brightness(0.5);
    transform: scale(1.04);
    transition: filter 900ms ease-out, transform 900ms ease-out;
  }
  .gallery-tile-img--developed {
    filter: grayscale(0) blur(0) brightness(1);
    transform: scale(1);
  }
}
/* Filtering by album: a quick fade rather than an instant cut, and
   pointer-events:none so a tile mid-fade-out can't still be clicked.
   Deliberately opacity-only, not display:none — removing a tile from
   layout entirely would also kill the transition (nothing to animate
   between "there" and "gone" in the same frame), so the brief overlap
   where a filtered-out tile still occupies its column is the tradeoff
   for the fade actually being visible. */
.gallery-tile--filtered-out { opacity: 0; pointer-events: none; }

@media (hover: hover) {
  .gallery-tile:hover {
    transform: translateY(-4px);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 55%, transparent), 0 20px 40px -16px rgba(5, 8, 18, 0.7);
  }
}

@media (max-width: 720px) {
  .gallery-masonry { column-count: 2; column-gap: 10px; padding: 16px 12px 40px; }
  .gallery-overlay-header h2 { font-size: 22px; }
}
@media (min-width: 721px) and (max-width: 1100px) {
  .gallery-masonry { column-count: 3; }
}
@media (min-width: 1101px) {
  .gallery-masonry { column-count: 4; }
}

/* ---------- Gallery photo viewer (the lightbox opened from a tile) ---------- */
/* z-index 160 — one above .gallery-overlay's 150, so the lightbox always
   paints over the grid it was opened from. */
.gallery-viewer {
  position: fixed; inset: 0; z-index: 160;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--bg);
  overflow: hidden;
}
.gallery-viewer[hidden] { display: none; }

/* Two stacked, absolutely positioned, heavily blurred copies of the
   current photo — background-image (not <img>) since this needs a
   uniform filter stack applied directly, not per-element loading state.
   Crossfading between the two on every photo change (only one ever at
   opacity:1, same mechanic as .mosaic-tile-img) is what makes the glow
   change smoothly with the photo instead of snapping. */
.gallery-viewer-glow { position: absolute; inset: 0; z-index: 0; }
.gallery-viewer-glow-img {
  position: absolute; inset: -10%; /* oversized so the heavy blur below never reveals a hard edge */
  background-size: cover; background-position: center;
  filter: blur(60px) saturate(1.4) brightness(0.45);
  opacity: 0; transition: opacity 1.2s ease;
}
.gallery-viewer-glow-img--active { opacity: 1; }

.gallery-viewer-close, .gallery-viewer-arrow {
  position: absolute; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(4px);
  border: 1px solid var(--line);
  color: var(--ink); cursor: pointer;
  transition: color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.gallery-viewer-close:hover, .gallery-viewer-arrow:hover { color: var(--accent); background: color-mix(in srgb, var(--bg) 75%, transparent); }
.gallery-viewer-close {
  top: 20px; right: 20px; width: 44px; height: 44px; border-radius: 50%;
  font-size: 22px; line-height: 1;
}
.gallery-viewer-close:hover { transform: rotate(90deg); }
.gallery-viewer-arrow {
  top: 50%; transform: translateY(-50%);
  width: 52px; height: 52px; border-radius: 50%;
  font-size: 28px; line-height: 1;
}
.gallery-viewer-arrow:hover { transform: translateY(-50%) scale(1.08); }
.gallery-viewer-arrow--prev { left: 20px; }
.gallery-viewer-arrow--next { right: 20px; }
.gallery-viewer-arrow[hidden] { display: none; }

.gallery-viewer-stage {
  position: relative; z-index: 1;
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  padding: 80px 90px 110px;
}
.gallery-viewer-img {
  max-width: 100%; max-height: 100%;
  object-fit: contain;
  border-radius: 6px;
  box-shadow: 0 30px 70px -20px rgba(5, 8, 18, 0.8);
}
/* Shorter and lighter than the masonry grid's own "developing" reveal
   (500ms vs 900ms, not quite as dark/blurred to start) — this plays on
   every photo change, not once ever, so a slower/heavier version of it
   would make rapid prev/next clicks feel sluggish. Forcing it to replay
   on every change is handled in JS (remove the class, force a reflow,
   re-add it) since re-adding an already-present class doesn't restart a
   CSS animation on its own. */
@media (prefers-reduced-motion: no-preference) {
  .gallery-viewer-img--developing { animation: galleryViewerDevelop 500ms ease-out; }
}
@keyframes galleryViewerDevelop {
  from { filter: grayscale(1) blur(14px) brightness(0.5); transform: scale(1.03); }
  to { filter: grayscale(0) blur(0) brightness(1); transform: scale(1); }
}

.gallery-viewer-meta {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 18px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  text-align: center; pointer-events: none;
}
.gallery-viewer-album { font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent); }
.gallery-viewer-counter { font-size: 12px; color: var(--muted); }

/* Desktop-only filmstrip, auto-scrolled to keep the active thumb in
   view (galleryPhotoViewer.js calls scrollIntoView on it whenever the
   index changes) — hidden on mobile via the media query below rather
   than not rendering it at all, since a photo set browsed on mobile
   might later be viewed on desktop in the same session without a
   re-open. */
.gallery-viewer-filmstrip {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 56px;
  display: flex; gap: 8px; overflow-x: auto;
  padding: 4px 24px; max-width: 100%;
  scrollbar-width: none;
}
.gallery-viewer-filmstrip::-webkit-scrollbar { display: none; }
.gallery-viewer-filmstrip[hidden] { display: none; }
.gallery-viewer-thumb {
  flex: 0 0 56px; width: 56px; height: 56px; border-radius: 8px; overflow: hidden;
  padding: 0; border: 2px solid transparent; cursor: pointer;
  opacity: 0.55; transition: opacity 0.15s ease, border-color 0.15s ease;
}
.gallery-viewer-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-viewer-thumb:hover { opacity: 0.85; }
.gallery-viewer-thumb--active { opacity: 1; border-color: var(--accent); }

@media (max-width: 900px) {
  .gallery-viewer-stage { padding: 70px 20px 90px; }
  .gallery-viewer-arrow { display: none; } /* swipe covers navigation on touch */
  .gallery-viewer-filmstrip { display: none; }
  .gallery-viewer-meta { bottom: 24px; }
}

/* ---------- Visit ---------- */
/* Tighter than .section-head's shared 60px bottom margin — scoped to just
   this section rather than changing that shared rule, since Visit's
   heading-then-buttons reads better as one close-knit cluster than the
   other sections' heading-then-a-full-row-of-content spacing does. */
#visit .section-head { margin-bottom: 28px; }
.visit-actions { display: flex; justify-content: center; }
#visit-buttons { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }

/* Same card treatment as .visual (dish/gallery tiles) — border-radius,
   overflow:hidden, --card-shadow — so the map reads as part of the same
   design system rather than a plain embedded widget. The larger top
   margin (vs. the tight gap under the buttons/email line above it) is
   deliberate: heading+buttons+email fallback read as one cluster, then a
   clear break before the map starts. "Get Directions" sits close under
   the map as its direct action, not overlapping it. */
.visit-map { max-width: 900px; margin: 56px auto 0; text-align: center; }
.map-card {
  width: 100%; height: 340px;
  border-radius: 22px; overflow: hidden;
  box-shadow: var(--card-shadow);
}
#visit-directions { margin-top: 16px; }
@media (max-width: 900px) {
  .map-card { height: 260px; }
}

/* OpenStreetMap only ships one (light) tile style — CartoDB's free dark/
   light tiles now require a registered API key on a live domain, so
   plain OSM tiles replace them, always run through a dark tint filter
   on .leaflet-tile-pane specifically, not .map-card as a whole, so it
   hits only the raster tiles — the marker (.map-marker, further down)
   and the popup/attribution panes are separate Leaflet panes and stay
   untouched, which is what keeps the pin matching var(--accent) exactly
   instead of also getting inverted. See the README's "Known gaps" note.
   invert(90%) hue-rotate(180deg) (an earlier version of this) is the
   common "poor man's dark map" recipe, but the partial 90% invert
   doesn't cleanly land the hue-preserving math the full trick relies on,
   and — worked out against the Standard OSM palette's actual colors —
   it also crowds roads, buildings and the land fill into a narrow, very
   dark 25–70 range where they're hard to tell apart. invert(100%) is
   the full version of the same trick (white ↔ black, hue roughly
   preserved for saturated colors); brightness(1.25) contrast(0.8)
   lifts and compresses that dark cluster so the road casing stroke
   reads as a visibly lighter gray against the land fill instead of
   nearly merging into it, and saturate(0.6) pulls hue-rotate's output
   back from anything oversaturated toward a subdued, not neon, look.
   Re-checked (same filter-math-against-OSM's-actual-palette approach,
   not a tile screenshot — this sandbox can't reach tile servers) against
   the muted slate repaint: at the previous saturate(0.85), water landed
   a distinctly saturated teal-blue that read too vivid against the new
   calmer palette; dropping to saturate(0.6) pulls it toward a near-
   neutral slate-grey much closer to --bg-alt, and major-road fills land
   in the same brass-grey family as the new --accent instead of orange.
   No separate light-theme override needed now — this is the only
   treatment. */
.map-card .leaflet-tile-pane {
  filter: invert(100%) hue-rotate(180deg) brightness(1.2) contrast(0.82) saturate(0.6);
}

/* Leaflet's own chrome (popups, attribution) re-themed to match the site
   instead of Leaflet's default white-card look. */
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--surface); box-shadow: var(--card-shadow); }
.leaflet-popup-content { font-family: "Space Grotesk", sans-serif; font-size: 13px; color: var(--ink); margin: 10px 14px; }
.leaflet-popup-close-button { color: color-mix(in srgb, var(--ink) 60%, transparent) !important; }
.leaflet-control-attribution { background: color-mix(in srgb, var(--surface) 85%, transparent) !important; color: color-mix(in srgb, var(--ink) 60%, transparent) !important; font-size: 10px; }
.leaflet-control-attribution a { color: color-mix(in srgb, var(--ink) 75%, transparent) !important; }

/* The marker itself: a plain div (not a raster pin image) so it's fully
   palette-driven via var(--accent) — a classic pin silhouette built from
   a circle plus a small triangular tip, both pure CSS. */
.map-marker {
  box-sizing: border-box;
  width: 26px; height: 26px;
  background: var(--accent);
  border: 3px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 2px 6px rgba(5,8,18,0.45);
  position: relative;
}
.map-marker::after {
  content: "";
  position: absolute; left: 50%; top: 100%; transform: translateX(-50%);
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 8px solid var(--accent);
}

.visit-fallback {
  text-align: center; margin: 18px 0 0; font-size: 14px;
  color: color-mix(in srgb, var(--ink) 55%, transparent);
}
.visit-fallback a { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); }

/* Every tel: link gets this: padding expands the actual tappable area to
   a 44px-plus touch target (WCAG 2.5.5) without changing the text's own
   size or the surrounding layout — the matching negative margin cancels
   the padding's own footprint, so neighboring elements sit exactly where
   they would without it. inline-block is what lets a padded/margined
   anchor do that while still flowing inline with its sibling text. */
.tel-link { display: inline-block; padding: 12px 2px; margin: -12px -2px; }

/* ---------- Special Event modal ---------- */
.modal-overlay {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: rgba(5,8,18,0.6);
  backdrop-filter: blur(4px);
}
.modal-overlay[hidden] { display: none; }

.modal-card {
  position: relative; width: 100%; max-width: 460px; max-height: 90vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  border-radius: 22px;
  padding: 36px 32px;
  box-shadow: var(--card-shadow);
}
.modal-card h3 { font-size: 28px; color: var(--ink); margin-bottom: 10px; }
.modal-sub { font-size: 14px; color: color-mix(in srgb, var(--ink) 60%, transparent); margin: 0 0 24px; }

.modal-close {
  position: absolute; top: 20px; right: 20px;
  width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  color: var(--ink); font-size: 18px; line-height: 1; cursor: pointer;
  transition: color 0.18s ease, transform 0.18s ease;
}
.modal-close:hover { color: var(--accent); transform: rotate(90deg); }

.modal-form { display: flex; flex-direction: column; gap: 16px; }
.modal-form label {
  display: flex; flex-direction: column; gap: 6px;
  font-size: 13px; font-weight: 600; letter-spacing: 0.02em;
  color: color-mix(in srgb, var(--ink) 70%, transparent);
}
/* .modal-form label's own display:flex above has equal specificity to a
   plain [hidden] selector and comes later in source order, so it would
   otherwise win and keep a hidden field visible — same cascade gotcha as
   .modal-overlay[hidden] above. */
.modal-form label[hidden] { display: none; }
.modal-form input, .modal-form textarea, .modal-form select {
  font: inherit; font-size: 15px; padding: 12px 14px; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  background: var(--bg); color: var(--ink);
  resize: vertical;
}
.modal-form select { cursor: pointer; }
.modal-form input:focus, .modal-form textarea:focus, .modal-form select:focus { outline: none; border-color: var(--accent); }
.modal-form button[type="submit"] { margin-top: 8px; }

/* ---------- Item detail modal (menu rows + dish cards) ---------- */
/* Reuses .modal-overlay/.modal-card/.modal-close as-is (same backdrop,
   corner radius, card-shadow as the Special Event modal) — only what's
   different about this one gets its own rules: a photo bleeding to the
   card's own edges at the top (so .modal-card's usual 36px/32px padding
   is zeroed out here and reapplied just to the text body below it), and
   a fade/scale-in entrance since this one opens far more often per visit
   (every menu row/dish card, not just one CTA) and benefits more from
   feeling snappy/alive rather than an instant hard cut. Sized as a
   genuinely large centered card (640px, vs. the Special Event modal's
   460px form) since it's meant to be a proper "look at this dish" moment,
   not a compact popup; full-screen below 720px, see the media query. */
.item-modal-card { max-width: 640px; padding: 0; overflow: hidden; }
.item-modal-overlay:not([hidden]) .item-modal-card { animation: itemModalCardIn 0.28s cubic-bezier(0.2,0.8,0.2,1) both; }
.item-modal-overlay:not([hidden]) { animation: itemModalOverlayIn 0.2s ease both; }
@keyframes itemModalCardIn { from { opacity: 0; transform: scale(0.94) translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes itemModalOverlayIn { from { opacity: 0; } to { opacity: 1; } }

.item-modal-photo { position: relative; width: 100%; aspect-ratio: 4 / 3; overflow: hidden; background: var(--surface); }
.item-modal-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* No photo at all for this item — a small stylized strip using the site's
   own accent-glow technique (.menu-photo-ambient/.section-ambient use the
   same radial-gradient pair elsewhere) instead of either a broken-image
   icon or a big empty photo-shaped hole. Collapsing the aspect-ratio down
   to a fixed, much shorter height is what keeps it reading as "a little
   decorative strip" rather than "the photo failed to load." */
.item-modal-photo--empty {
  aspect-ratio: auto; height: 64px;
  background:
    radial-gradient(60% 120% at 20% 50%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
    radial-gradient(60% 120% at 80% 50%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%);
}

/* A small tappable strip under the primary photo, only shown when an item
   has more than one photo (data/menu.json's "images" array — e.g.
   Quesabirria's two shots) — a lightweight product-gallery pattern rather
   than a full swipeable carousel, since it's a rare case (most items have
   at most one photo) that doesn't need more machinery than "click a
   thumbnail, the primary image swaps." */
.item-modal-thumbs { display: flex; gap: 8px; padding: 10px 16px 0; background: var(--surface); }
.item-modal-thumb {
  position: relative; flex: 0 0 56px; aspect-ratio: 1; border-radius: 8px; overflow: hidden;
  padding: 0; border: 2px solid transparent; cursor: pointer; background: none;
  opacity: 0.55; transition: opacity 0.15s ease, border-color 0.15s ease;
}
.item-modal-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.item-modal-thumb:hover { opacity: 0.85; }
.item-modal-thumb--active { opacity: 1; border-color: var(--accent); }

/* z-index matters here: .item-modal-photo is itself position:relative
   (a positioned element) and comes AFTER this button in the DOM — among
   sibling elements that are equally "positioned" with no z-index set,
   later DOM order wins the paint order regardless of which one is
   visually "on top" of the other in intent, so the photo (and its <img>)
   was silently painting over the close button and swallowing every click
   aimed at it. This close button only ever worked via the backdrop-click
   fallback as a result. An explicit z-index fixes it for good, rather
   than relying on DOM order never changing again. Also its own
   contrast-safe treatment rather than inheriting the base .modal-close's
   (tuned to sit on var(--surface), not a photo) — same top/right position
   as the base rule, only the visual treatment and stacking differ. */
.item-modal-close {
  z-index: 5;
  background: color-mix(in srgb, var(--bg) 55%, transparent); border: none; color: var(--ink);
  backdrop-filter: blur(2px);
}
.item-modal-close:hover { color: var(--accent); background: color-mix(in srgb, var(--bg) 70%, transparent); }

.item-modal-body { padding: 30px 34px 36px; }
.item-modal-top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.item-modal-card h3 { font-size: 32px; margin: 0; }
.item-modal-desc { color: color-mix(in srgb, var(--ink) 55%, transparent); font-size: 16px; line-height: 1.6; margin: 14px 0 0; }
#item-modal-price .menu-item-price { margin-top: 20px; font-size: 28px; }
#item-modal-price .menu-item-price--wine { margin-top: 20px; }

/* Full-screen rather than a smaller floating card once the viewport's too
   narrow for a large centered card to read comfortably. */
@media (max-width: 720px) {
  .item-modal-overlay { padding: 0; }
  .item-modal-card { max-width: 100%; width: 100%; height: 100%; max-height: 100%; border-radius: 0; }
  .item-modal-photo { aspect-ratio: 4 / 3; }
}

/* ---------- Footer ---------- */
footer { background: var(--footer-bg); padding: 56px 56px 28px; transition: background 0.25s ease; }
.footer-grid { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 36px; padding-bottom: 36px; border-bottom: 1px solid color-mix(in srgb, var(--ink) 8%, transparent); }
.footer-logo { font-family: "Bebas Neue", sans-serif; font-size: 26px; letter-spacing: 0.03em; margin-bottom: 10px; }
.footer-logo span { color: var(--accent); }
.footer-social { display: flex; gap: 10px; margin-top: 18px; }
.footer-social-link {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  color: color-mix(in srgb, var(--ink) 55%, transparent);
  transition: color 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}
.footer-social-link:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-2px); }
.footer-col h4 { font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: color-mix(in srgb, var(--ink) 70%, transparent); margin-bottom: 14px; font-weight: 600; }
.footer-col span, .footer-col a { display: block; font-size: 14px; margin-bottom: 9px; }
.footer-col span { color: color-mix(in srgb, var(--ink) 45%, transparent); }
/* The phone number is the one real link in this column (address/email
   are plain text) — same accent/underline treatment as .visit-fallback
   a's existing mailto: link, so every tel: link on the site reads the
   same way. */
.footer-col a { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.footer-bottom { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; padding-top: 24px; font-size: 12.5px; color: color-mix(in srgb, var(--ink) 35%, transparent); }
