/* ==========================================================================
   OZE'S RESTAURANT — components.css   (phase 2, shared layer)

   Implements brand/ART-DIRECTION-V2.md §1 and §3 against css/tokens.css,
   under CRAFT-FLOOR.md §A3, §A4, §D and §E. Sections D and E of the craft
   floor override every earlier document; where V2 and phase 1 disagree, V2
   wins; where the craft floor and V2 disagree, the craft floor wins.

   LOAD ORDER IS FIXED:
       tokens.css  →  main.css  →  [menu.css]  →  components.css
   This file is last on purpose. One rule here deliberately re-points a
   browser surface main.css set earlier (::selection), and equal-specificity
   source order is what carries it. Nothing here re-declares a token value.

   CONTENTS
     C0  Browser surfaces          selection, caret, scrollbars, links, numerals
     C1  Icon system               authored SVG, 1px hairline, seven glyphs
     C2  Photography — treatment 1 .oz-plate      "the lit window"   (tone light)
     C3  Photography — treatment 2 .oz-bleed      "the dark bleed"   (mid | dark)
     C4  Marquee strip             .oz-marquee                       (V2 §3.1)
     C5  Live status pill          .oz-status                        (V2 §3.2)
     C6  Stats row                 .oz-stats                         (V2 §3.3)
     C7  Signature-dish grid       .oz-dishes                        (V2 §3.4)
     C8  Breakfast-for-two feature .oz-feature                       (V2 §3.5)
     C9  Reviews plaque            .oz-plaque                        (V2 §3.6)
     C10 Instagram grid            .oz-ig                            (V2 §3.7)
     C11 Map panel                 .oz-map                           (V2 §3.8)

   MOTION CONTRACT (CRAFT-FLOOR §D, non-negotiable)
     - transform / opacity / clip-path / filter only. Never width, height,
       margin, padding, top, left.
     - Never `transition: all`. Every transition names its properties.
     - Never `ease-in`. --ease-brand is the default strong ease-out;
       --ease-move is the strong ease-in-out for things morphing ON screen;
       `linear` for the marquee and nothing else.
     - Never scale(0). Nothing here starts below scale(0.985).
     - Every hover transform sits inside
       @media (hover: hover) and (pointer: fine).
     - Every reveal START state is scoped to `html.js`, so a document with no
       JS — or one where motion.js fails — renders complete and visible.
     - Reduced motion is fewer and gentler, not zero. tokens.css already
       drops every animation-name and narrows transitions to non-moving
       properties; the block at the foot of this file adds only what that
       cannot express (parking the marquee and unmasking it, so a static row
       is legible and finished rather than cut off at both ends).

   REVEAL HOOKS this file expects from js/motion.js (already built — do not
   change motion.js to suit this file):
     data-reveal="arch"     → the plate unmask, foot upward, 120ms apart.
                              USE THIS ON EVERY .oz-plate SLOT. motion.js
                              assigns --i per sibling group and main.css §09
                              owns the clip-path; nothing is duplicated here.
     data-reveal="rise"     → the quiet settle for a text block.
     data-reveal="rules"    → C6, the stats row's vertical rules drawing down.
     data-reveal="stars"    → C9, the star row's partial fill drawing across.
   Both new kinds are plain [data-reveal] values: motion.js adds .is-revealed
   to any element carrying the attribute, so they need no JS change.
   ========================================================================== */


/* ==========================================================================
   C0. BROWSER SURFACES  (CRAFT-FLOOR §A3)
   The parts we did not draw. Leaving these at the UA default is the cheapest
   possible signal that a page was assembled rather than built.
   ========================================================================== */

/* Gold ink on a deep-green wash — the sign, selected. main.css sets the
   inverse of this earlier; source order is the override. */
::selection {
  background-color: var(--c-bg-deep);
  color: var(--c-heading);
  text-shadow: none;
}
/* On paper the wash inverts: gold ground, green ink (12.58:1). */
.theme-light ::selection,
[data-theme="light"] ::selection {
  background-color: var(--c-heading);
  color: var(--c-bg-deep);
}

/* The typing cursor is a light source too. */
input,
textarea,
select,
[contenteditable] { caret-color: var(--c-accent); }

/* --- Scrollbars ---------------------------------------------------------
   Spec syntax first, then the WebKit set. The track is 10px so the pointer
   has something to grab, but the visible thumb is 4px, held in by a
   padding-box border. Gold stays a line, never an area (AD §8). */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--gold-700) var(--c-bg-deep);
}
.theme-light,
[data-theme="light"] { scrollbar-color: var(--gold-700) var(--c-surface); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track {
  background-color: var(--c-bg-deep);
  border-left: var(--hairline) solid var(--c-border);
}
::-webkit-scrollbar-thumb {
  border-radius: var(--r-pill);
  background-image: var(--grad-foil);
  background-clip: padding-box;
  border: 3px solid transparent;
  transition: background-image var(--dur-fast) ease;
}
::-webkit-scrollbar-thumb:hover { background-image: var(--grad-foil-sweep); }
::-webkit-scrollbar-corner { background-color: var(--c-bg-deep); }

/* The menu page's category rail hides its scrollbar deliberately — the active
   chip auto-centres instead — so this does NOT un-hide it. It only makes sure
   that where a UA or an OS setting forces a scrollbar into view, the one that
   appears is the brand's and not the platform's. */
.cat-rail__track,
.gallery__rail,
.oz-rail { scrollbar-color: var(--c-border-strong) transparent; }

/* --- Prose links --------------------------------------------------------
   The underline is a hairline that clears the descenders, like every other
   rule on this site. */
.prose a,
.oz-prose a,
.oz-plaque a:not(.btn),
.oz-map__text a {
  color: var(--c-accent);
  text-decoration-line: underline;
  text-decoration-thickness: var(--hairline);
  text-underline-offset: 0.18em;
  text-decoration-color: var(--c-border-strong);
  text-decoration-skip-ink: auto;
  transition: color var(--dur-base) var(--ease-brand),
              text-decoration-color var(--dur-base) var(--ease-brand);
}
.prose a:hover,
.oz-prose a:hover,
.oz-plaque a:not(.btn):hover,
.oz-map__text a:hover {
  color: var(--c-accent-hover);
  text-decoration-color: var(--c-accent);
}

/* --- Numerals -----------------------------------------------------------
   Anywhere digits stack or tick, they must not reflow: prices, hours, the
   stats figures, the rating, the status pill's closing time. */
.leader__price,
.hours__row,
.hours__time,
.oz-stat__figure,
.oz-plaque__number,
.oz-status__label,
.oz-feature__price,
.oz-dish__price,
[data-tabular] { font-variant-numeric: tabular-nums lining-nums; }


/* ==========================================================================
   C1. ICON SYSTEM  (CRAFT-FLOOR §A4)
   Seven authored glyphs on one grid at one stroke weight — 1.25 in a 24×24
   box, which resolves to a true 1px hairline at the 20px display size, so the
   icons sit in the same line language as every rule on the page. Emoji and
   unicode glyphs standing in for icons are banned; so is a third-party set.

   They are carried as CSS masks rather than inline <svg> so the fill is a
   token (currentColor) instead of a hex frozen inside a data URI, and so the
   HTML author needs no markup beyond one empty span:

       <span class="oz-icon oz-icon--instagram" aria-hidden="true"></span>

   Always aria-hidden. Every one of these sits beside a real text label; an
   icon on this site never carries meaning on its own.
   ========================================================================== */
:root {
  --oz-icon-instagram:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'><rect x='3.6' y='3.6' width='16.8' height='16.8' rx='5.2'/><circle cx='12' cy='12' r='4.1'/><circle cx='17.15' cy='6.85' r='0.95'/></svg>");
  --oz-icon-facebook:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'><path d='M13.9 20.9v-7.6h2.6l.4-3h-3V8.4c0-.9.3-1.5 1.5-1.5h1.6V4.2c-.3 0-1.3-.1-2.4-.1-2.3 0-3.9 1.4-3.9 4v2.2H8.1v3h2.6v7.6'/></svg>");
  --oz-icon-tiktok:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'><path d='M14.15 3.4v10.95a3.65 3.65 0 1 1-3.1-3.6'/><path d='M14.15 3.4c.4 2.4 2.05 4.05 4.45 4.3'/></svg>");
  --oz-icon-pin:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'><path d='M12 20.8c3.95-4.6 5.9-7.75 5.9-10.35A5.9 5.9 0 0 0 6.1 10.45c0 2.6 1.95 5.75 5.9 10.35Z'/><circle cx='12' cy='10.4' r='2.35'/></svg>");
  --oz-icon-external:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'><path d='M13.6 4.4h6v6'/><path d='M19.6 4.4 10.9 13.1'/><path d='M17.9 14.3v4.2a1.5 1.5 0 0 1-1.5 1.5H5.6a1.5 1.5 0 0 1-1.5-1.5V7.7a1.5 1.5 0 0 1 1.5-1.5h4.2'/></svg>");
  --oz-icon-chevron:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'><path d='M9.4 5.2 16 12l-6.6 6.8'/></svg>");

  /* Re-export main.css's micro-arch under a local name with a literal
     fallback, so this file still renders if it is ever loaded alone. */
  --oz-arch-glyph: var(--micro-arch, url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 10'><path d='M1,9.5 V5 A5,5 0 0 1 11,5 V9.5' fill='none' stroke='black' stroke-width='1.1'/></svg>"));
}

.oz-icon {
  display: inline-block;
  flex: none;
  width: 20px;
  height: 20px;
  vertical-align: -0.28em;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: contain;
          mask-size: contain;
}
.oz-icon--sm { width: 16px; height: 16px; vertical-align: -0.2em; }

.oz-icon--instagram { -webkit-mask-image: var(--oz-icon-instagram); mask-image: var(--oz-icon-instagram); }
.oz-icon--facebook  { -webkit-mask-image: var(--oz-icon-facebook);  mask-image: var(--oz-icon-facebook); }
.oz-icon--tiktok    { -webkit-mask-image: var(--oz-icon-tiktok);    mask-image: var(--oz-icon-tiktok); }
.oz-icon--pin       { -webkit-mask-image: var(--oz-icon-pin);       mask-image: var(--oz-icon-pin); }
.oz-icon--external  { -webkit-mask-image: var(--oz-icon-external);  mask-image: var(--oz-icon-external); }
.oz-icon--chevron   { -webkit-mask-image: var(--oz-icon-chevron);   mask-image: var(--oz-icon-chevron); }
/* The 12×10 micro-arch, the seventh glyph, doing its fourth job. */
.oz-icon--arch {
  width: 12px; height: 10px; vertical-align: 0.02em;
  -webkit-mask-image: var(--oz-arch-glyph); mask-image: var(--oz-arch-glyph);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}

/* A link that ends in an icon: the glyph tracks the label's colour and nudges
   on hover. transform only, pointer-gated. */
.oz-iconlink {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--c-accent);
  text-decoration: none;
  transition: color var(--dur-base) var(--ease-brand);
}
.oz-iconlink:hover { color: var(--c-accent-hover); }
.oz-iconlink .oz-icon { transition: transform var(--dur-base) var(--ease-brand); }
@media (hover: hover) and (pointer: fine) {
  .oz-iconlink:hover .oz-icon--chevron,
  .oz-iconlink:hover .oz-icon--external { transform: translate3d(2px, -1px, 0); }
  .oz-iconlink:hover .oz-icon--pin      { transform: translate3d(0, -2px, 0); }
}


/* ==========================================================================
   C2. PHOTOGRAPHY, TREATMENT 1 — "THE LIT WINDOW"   (V2 §1.2)
   For the 13 light-ground shots: the whole dish set plus breakfast-board,
   spread-tall, coffee-grid and shared-table. tone === "light" in gallery.js.

   Three layers, all three required. One or two alone looks unfinished:
     (a) the warm grade, which moves the cold #EDEEF1 speckle to ~#EFEAE0 —
         within a couple of dE of --marble, so the photograph's ground stops
         being foreign and becomes the paper the brand already owned;
     (b) the arch, so a bright plate in a dark green field is literally a lit
         window, which is the whole concept;
     (c) the 1px ring and the foot veil, so the window has a frame and its
         foot dissolves into the wall instead of ending on a hard cut.

   NEVER a blanket filter across all 21 photographs — see C3 for the other
   treatment, and V2 §5.1 for why a duotone / multiply / cut-out is refused.

   MARKUP CONTRACT — the HTML author sets `tone` straight off gallery.js:

       <figure class="oz-shot" data-tone="light" data-shot="dish-cilbir"
               data-reveal="arch" style="--obj: 50% 58%"> … </figure>

   `data-tone` picks the treatment; `data-shot` supplies the per-slot
   object-position for the frames V2 names explicitly (C2.1 below), and --obj
   overrides it for any slot the art direction has not already fixed.
   Explicit .oz-plate / .oz-bleed classes win over data-tone, which is how the
   ONE documented exception is expressed: mezze-flatlay measures "mid" but V2
   §1.2 puts src13 in the primary treatment at lite strength, so it is
   authored `class="oz-plate oz-plate--lite" data-tone="mid"`.
   ========================================================================== */
.oz-plate,
.oz-shot[data-tone="light"]:not(.oz-bleed) {
  /* --plate-filter is the strength selector. --warm-amt in tokens.css
     declares which strength a slot is on: 1 = full, 0.45 = lite. */
  --plate-filter: var(--filter-warm-plate);
  display: block;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin: 0;
  aspect-ratio: var(--ar-plate);
  border-radius: var(--r-arch);
  box-shadow: var(--inset-hairline);
  /* Holds the window's shape and its colour before the image decodes, so the
     arch never flashes as an empty green hole. */
  background-color: var(--c-photo-plate);
  transition: box-shadow var(--dur-slow) var(--ease-brand);
}
.oz-plate--lite,
.oz-shot--lite { --plate-filter: var(--filter-warm-plate-lite); --warm-amt: 0.45; }

/* The landscape variant: breakfast-board is 16:9 and its composition — board
   centre, pan right, bowls arcing left — cannot survive a 4:5 crop, so it
   takes the elliptical cap instead of the circular one (AD §6 technique 2).
   Requires the #ozArch clipPath from the tokens.css header comment. */
.oz-plate--wide {
  aspect-ratio: var(--ar-band);
  border-radius: 0;
  clip-path: url(#ozArch);
  box-shadow: none;             /* a clip-path cuts an inset shadow off */
}

.oz-plate > picture,
.oz-shot[data-tone="light"]:not(.oz-bleed) > picture { display: block; height: 100%; }

.oz-plate > img,
.oz-plate > picture > img,
.oz-shot[data-tone="light"]:not(.oz-bleed) > img,
.oz-shot[data-tone="light"]:not(.oz-bleed) > picture > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* A 16:9 source in a 4:5 arch is a 55% crop budget — the largest on the
     site. The default is NEVER correct (V2 §5.14); the dish generally sits
     low-centre in these frames, so this is the working default only. */
  object-position: var(--obj, 50% 58%);
  filter: var(--plate-filter);
  scale: var(--img-scale, 1);
  transition: scale var(--dur-slow) var(--ease-brand);
}

/* (c) the foot. 0.55 at its darkest — a window, not a fade-out. */
.oz-plate::after,
.oz-shot[data-tone="light"]:not(.oz-bleed)::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 34%;
  z-index: var(--z-raised);
  pointer-events: none;
  background-image: var(--grad-plate-foot);
}

/* The image inside moves. The arch never does — that is the phase-1 rule and
   it is what makes a row of plates read as architecture, not a carousel. */
@media (hover: hover) and (pointer: fine) {
  .oz-plate:hover,
  .oz-plate:focus-within,
  .oz-shot[data-tone="light"]:not(.oz-bleed):hover,
  .oz-shot[data-tone="light"]:not(.oz-bleed):focus-within {
    box-shadow: inset 0 0 0 1px var(--c-border-strong);
  }
  .oz-plate:hover > img,
  .oz-plate:hover > picture > img,
  .oz-plate:focus-within > picture > img,
  .oz-shot[data-tone="light"]:not(.oz-bleed):hover > picture > img,
  .oz-shot[data-tone="light"]:not(.oz-bleed):hover > img { --img-scale: 1.04; }
}

/* --- C2.1 Per-slot object-position, fixed by the art direction -----------
   V2 §5.14: object-position is specified per slot, always. These are the
   frames V2 names; everything else passes --obj inline. */
.oz-shot[data-shot="spread-tall"]     > picture > img,
.oz-shot[data-shot="spread-tall"]     > img { object-position: 50% 40%; }
.oz-shot[data-shot="shared-table"]    > picture > img,
.oz-shot[data-shot="shared-table"]    > img { object-position: 50% 45%; }
.oz-shot[data-shot="breakfast-board"] > picture > img,
.oz-shot[data-shot="breakfast-board"] > img { object-position: 50% 50%; }
/* The blue neon is cropped OUT with object-position, never filtered out — a
   filtered sign leaves a dead grey smear where a light source should be. */
.oz-shot[data-shot="room-display"]    > picture > img,
.oz-shot[data-shot="room-display"]    > img { object-position: 22% 50%; }
.oz-shot[data-shot="room-wide"]       > picture > img,
.oz-shot[data-shot="room-wide"]       > img { object-position: 35% 45%; }
.oz-shot[data-shot="table-neon"]      > picture > img,
.oz-shot[data-shot="table-neon"]      > img { object-position: 50% 72%; }
.oz-shot[data-shot="table-kavurma"]   > picture > img,
.oz-shot[data-shot="table-kavurma"]   > img { object-position: 50% 55%; }


/* ==========================================================================
   C3. PHOTOGRAPHY, TREATMENT 2 — "THE DARK BLEED"   (V2 §1.3)
   For the 3 dark and the 5 mid frames. tone === "mid" | "dark".

   These are already the page's value, so the verb inverts: a white-ground
   photo sits IN the wall; a dark photo IS the wall. No arch, no ring, no
   plate. The grade lifts the shadow into the brand green instead of warming
   a white, and the hue lock is what kills the lavender cast in the interiors.

   mix-blend-mode: color is the correct operator — not multiply, not hue. It
   transplants hue and saturation and leaves luminance alone, so a dark
   photograph stays exactly as legible as it was.
   ========================================================================== */
.oz-bleed,
.oz-shot[data-tone="mid"]:not(.oz-plate),
.oz-shot[data-tone="dark"]:not(.oz-plate) {
  display: block;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin: 0;
  background-color: var(--c-bg-deep);
}
/* --bleed-tint: 0.22 for the interiors, which need it. 0.10 for the two
   coffee shots — coffee-hands' warm rim light on wood is the best photograph
   in the set and must not be sanded down. 0.14 for the single neon tile, so
   the sign survives. */
.oz-bleed--lift { --bleed-tint: 0.10; }
.oz-bleed--neon { --bleed-tint: 0.14; }

.oz-bleed > picture,
.oz-shot[data-tone="mid"]:not(.oz-plate) > picture,
.oz-shot[data-tone="dark"]:not(.oz-plate) > picture { display: block; height: 100%; }

.oz-bleed > img,
.oz-bleed > picture > img,
.oz-shot[data-tone="mid"]:not(.oz-plate) > img,
.oz-shot[data-tone="mid"]:not(.oz-plate) > picture > img,
.oz-shot[data-tone="dark"]:not(.oz-plate) > img,
.oz-shot[data-tone="dark"]:not(.oz-plate) > picture > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: var(--obj, 50% 50%);
  filter: var(--filter-dark-bleed);
  scale: var(--img-scale, 1);
  transition: scale var(--dur-slow) var(--ease-brand);
}

/* The hue lock. */
.oz-bleed::before,
.oz-shot[data-tone="mid"]:not(.oz-plate)::before,
.oz-shot[data-tone="dark"]:not(.oz-plate)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  pointer-events: none;
  background-color: var(--c-bg-deep);
  mix-blend-mode: color;
  opacity: var(--bleed-tint, 0.22);
  transition: opacity var(--dur-slow) var(--ease-brand);
}
/* The edge dissolve, so nothing ends on a hard rectangle. */
.oz-bleed::after,
.oz-shot[data-tone="mid"]:not(.oz-plate)::after,
.oz-shot[data-tone="dark"]:not(.oz-plate)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  pointer-events: none;
  background-image: var(--grad-veil-bottom);
}

/* The neon's one and only appearance gets its bloom anchored to the sign,
   exactly the way --glow-candle is anchored to the menu-teaser flame. The
   blue is decorative light here and nothing else: --c-nazar is forbidden as
   text, as a control border and as a UI fill, at every size, on every ground. */
.oz-bleed--neon .oz-bleed__glow {
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  pointer-events: none;
  background-image: var(--glow-nazar);
  mix-blend-mode: screen;
}

/* Aspect helpers for the shapes a bleed takes. */
.oz-bleed--band { aspect-ratio: var(--ar-band); }
.oz-bleed--tall { aspect-ratio: 3 / 4; }
.oz-bleed--tile { aspect-ratio: var(--ar-tile); }


/* ==========================================================================
   C4. MARQUEE STRIP  (V2 §3.1)
   A horizon, not a ticker. Words come only from strings that already exist in
   content.js — the renderer never authors Turkish vocabulary, and caps at 10.

   MARKUP CONTRACT — the track holds EXACTLY TWO identical groups, the second
   aria-hidden, because the keyframe travels exactly -50% and that is what
   removes the loop seam:

     <div class="oz-marquee">
       <div class="oz-marquee__bg"> … coffee-grid, texture only … </div>
       <div class="oz-marquee__track">
         <ul class="oz-marquee__group"> … 10 words … </ul>
         <ul class="oz-marquee__group" aria-hidden="true"> … the same 10 … </ul>
       </div>
     </div>

   Build it STILL first: it must be legible and finished when parked, which is
   exactly what a reduced-motion visitor gets.
   ========================================================================== */
.oz-marquee {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: flex;
  align-items: center;
  height: clamp(72px, 9vw, 112px);
  background-color: var(--c-bg);
  border-top: var(--rule);
  border-bottom: var(--rule);
  /* The strip has no beginning: words arrive out of the dark and return. */
  -webkit-mask-image: var(--mask-marquee-fade);
          mask-image: var(--mask-marquee-fade);
}

/* coffee-grid is the only image safe for full bleed, and here it is texture
   rather than image: 0.16 under --c-veil-strong reads as a faint field of
   cups. It appears nowhere else; if the marquee is cut, the image is cut. */
.oz-marquee__bg {
  position: absolute;
  inset: 0;
  z-index: var(--z-below);
  overflow: hidden;
}
.oz-marquee__bg img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: 50% 50%;
  filter: var(--filter-warm-plate);
  opacity: 0.16;
}
.oz-marquee__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--c-veil-strong);
}

.oz-marquee__track {
  display: flex;
  width: max-content;
  will-change: transform;
  animation: oz-marquee-drift var(--dur-marquee) linear infinite;
}
@keyframes oz-marquee-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

.oz-marquee__group {
  display: flex;
  align-items: center;
  gap: clamp(var(--sp-5), 3vw, var(--sp-7));
  padding-inline-end: clamp(var(--sp-5), 3vw, var(--sp-7));
  margin: 0;
}
.oz-marquee__word {
  font-family: var(--ff-deco);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-marquee);
  line-height: var(--lh-ui);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--c-text-muted);            /* 6.69:1 on --c-bg */
}
/* Every third word lifts, so the row has a cadence rather than a rhythm. */
.oz-marquee__word--lift { color: var(--c-heading); }   /* 10.75:1 on --c-bg */

/* The separator is the 12×10 micro-arch — the nav marker's glyph, the
   eyebrow bullet's glyph, now doing a third job. */
.oz-marquee__sep {
  flex: none;
  width: 12px;
  height: 10px;
  background-color: var(--c-accent);
  opacity: 0.55;
  -webkit-mask-image: var(--oz-arch-glyph); mask-image: var(--oz-arch-glyph);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}


/* ==========================================================================
   C5. LIVE STATUS PILL  (V2 §3.2)
   An indicator lamp on a dark panel. js/status.js owns the state and writes
   data-state; this file owns nothing but the look.

     <p class="oz-status" data-oz-status data-state="open"> … </p>

   data-state is one of: open | closed | static. `static` is the honest
   fallback — the verbatim hours string in the same pill, shipped whenever the
   America/Toronto computation cannot be proven exact. A wrong pill sends
   someone to a closed restaurant, so `static` is a first-class state and it
   must look finished, not degraded.

   Never colour-only: the label always states the status in words. The dot is
   reinforcement (WCAG 1.4.1).
   ========================================================================== */
.oz-status {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 32px;
  padding-inline: var(--sp-4);
  border: var(--rule);
  border-radius: var(--r-pill);
  background-color: transparent;
  font-family: var(--ff-ui);
  font-weight: var(--fw-book);
  font-size: var(--fs-sm);
  line-height: var(--lh-ui);
  letter-spacing: var(--ls-ui);
  color: var(--c-text);
  transition: border-color var(--dur-base) var(--ease-brand),
              color var(--dur-base) var(--ease-brand);
}
/* Nothing renders before JS has decided which state is true. */
.oz-status:empty { display: none; }

.oz-status__dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background-color: currentColor;
}

/* Open. The dot does NOT blink or pulse — blinking says "error". It breathes
   on the room's own 22s candle cycle at ±8% opacity, so it belongs to the
   light in the room rather than to a dashboard. */
.oz-status[data-state="open"] { color: var(--c-text); }            /* 11.95:1 */
.oz-status[data-state="open"] .oz-status__dot {
  background-color: var(--c-status-open);                          /*  7.74:1 */
  box-shadow: 0 0 14px -2px rgb(127 199 155 / 0.4);
  animation: oz-status-breathe var(--dur-ambient) var(--ease-in-out) infinite alternate;
}
@keyframes oz-status-breathe {
  from { opacity: 0.92; }
  to   { opacity: 1; }
}

/* Closed. No bloom — an unlit lamp. */
.oz-status[data-state="closed"] { color: var(--c-text-muted); }    /*  6.69:1 */
.oz-status[data-state="closed"] .oz-status__dot {
  background-color: var(--c-status-closed);                        /*  5.51:1 */
  box-shadow: none;
}

/* Static hours. The lamp is a ring rather than a filled dot: nothing is being
   asserted about right now, and the shape says so before the words do. */
.oz-status[data-state="static"] { color: var(--c-text); }
.oz-status[data-state="static"] .oz-status__dot {
  background-color: transparent;
  box-shadow: inset 0 0 0 var(--hairline) var(--c-text-muted);
}


/* ==========================================================================
   C6. STATS ROW  (V2 §3.3)
   Three facts, not a dashboard. Cells are separated by 1px vertical rules at
   full cell height — not by boxes, not by cards, and with no icons. There is
   no count-up: a count-up is a growth-dashboard cliché and it violates
   "nothing bounces" in spirit even where it does not in letter.
   ========================================================================== */
.oz-stats {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  background-color: var(--c-bg-deep);
  padding-block: var(--sp-section);
}
@media (min-width: 768px) {
  .oz-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.oz-stat {
  position: relative;
  min-width: 0;
  padding: var(--sp-6) var(--sp-5);
  text-align: center;
}
.oz-stat:nth-child(2) { --i: 0; }
.oz-stat:nth-child(3) { --i: 1; }

/* The rule between cells. Horizontal below 768px, vertical above. */
.oz-stat + .oz-stat::before {
  content: "";
  position: absolute;
  inset: 0 auto auto 0;
  width: 100%;
  height: var(--hairline);
  background-color: var(--c-border);
  transform-origin: left;
}
@media (min-width: 768px) {
  .oz-stat + .oz-stat::before {
    inset: 0 auto 0 0;
    width: var(--hairline);
    height: auto;
    transform-origin: top;
  }
}

.oz-stat__figure {
  display: block;
  font-family: var(--ff-display);
  font-weight: var(--fw-light);
  font-size: var(--fs-stat);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h1);
  text-indent: -0.04em;
  color: var(--c-heading);
}
.oz-stat__label {
  display: block;
  margin-top: var(--sp-3);
  font-family: var(--ff-deco);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-muted);
}

/* The rules draw from the top edge, 140ms apart. Each figure's foil sweep
   fires as its own left-hand rule lands: motion.js adds .is-swept to any
   .u-foil--sweep inside the revealed element at the same moment. */
.js [data-reveal="rules"] .oz-stat + .oz-stat::before {
  transform: scaleX(0);
  transition: transform var(--dur-slower) var(--ease-brand)
              calc(140ms * var(--i, 0));
}
@media (min-width: 768px) {
  .js [data-reveal="rules"] .oz-stat + .oz-stat::before { transform: scaleY(0); }
}
.js [data-reveal="rules"].is-revealed .oz-stat + .oz-stat::before { transform: none; }


/* ==========================================================================
   C7. SIGNATURE-DISH GRID  (V2 §3.4)
   Six lit windows in a dark wall. SIX IS THE CAP — six is a selection, twelve
   is a catalogue, and rows of plates are the one place this site risks
   looking like a food-delivery app. No cards, no nesting, no badge marking an
   item "popular": the section heading carries the framing, the items do not.

   Each cell is one .oz-plate (C2) carrying data-reveal="arch", so the plates
   unmask from the foot upward 120ms apart on the existing machinery.
   ========================================================================== */
.oz-dishes {
  display: grid;
  gap: var(--sp-stack) var(--grid-gap);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 768px) {
  .oz-dishes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1200px) {
  .oz-dishes { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Below 768px it is a scroll-snap rail, the same mechanic as the phase-1
   gallery. The rail is a scroll container; the PAGE never scrolls sideways. */
@media (max-width: 767px) {
  .oz-dishes--rail {
    grid-auto-flow: column;
    grid-auto-columns: 78vw;
    grid-template-columns: none;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    padding-inline: var(--gutter);
    margin-inline: calc(var(--gutter) * -1);
    scrollbar-width: none;
  }
  .oz-dishes--rail::-webkit-scrollbar { display: none; }
  .oz-dishes--rail > .oz-dish { scroll-snap-align: center; }
}

.oz-dish { min-width: 0; }

/* The name and the price sit OUTSIDE the arch, on the green. */
.oz-dish__name {
  margin-top: var(--sp-4);
  font-family: var(--ff-display);
  font-weight: var(--fw-mid);
  font-size: var(--fs-h3);
  line-height: var(--lh-heading);
  color: var(--c-heading);
}
.oz-dish__price {
  margin-top: var(--sp-1);
  font-family: var(--ff-deco);
  font-size: var(--fs-price);
  letter-spacing: 0.06em;
  color: var(--c-accent);
}


/* ==========================================================================
   C8. BREAKFAST-FOR-TWO FEATURE  (V2 §3.5)
   The page's most generous block, and the one place a photograph is allowed
   to be the size of an argument. One photograph runs the whole band, edge to
   edge, and the deep green rolls over it from the right like a shadow: the
   copy is set in the darkened half of the same photograph. One image, no
   collage, no second frame.
   ========================================================================== */
.oz-feature {
  position: relative;
  /* isolation keeps the photograph layer (z-index -1) painting over this
     section's own ground instead of beneath it. */
  isolation: isolate;
  background-color: var(--c-bg-deep);
  padding-block: var(--sp-section-lg);
}

/* The photograph layer. A plain figure, deliberately not .oz-plate: a
   full-bleed band has no mat ring and no arch radius — the edge of the
   viewport is the frame. */
.oz-feature__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  margin: 0;
  /* The parallax photograph is oversized by 14%; this frame must clip it,
     or the counter spills over the sections above and below the band. */
  overflow: hidden;
  /* Half amplitude: the band is the calmest large surface on the page and
     V2 §3.5 asks the breakfast feature to run at half travel (motion.js
     reads this token; the hero and gallery arches keep the 6% default). */
  --parallax-amp: 3%;
}
.oz-feature__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* The spread sits centre-left in the source frame. */
  object-position: 44% 50%;
  /* Half a stop of warmth so the pale grey counter sits inside the gold and
     green room instead of fighting it. Static filter — never animated. */
  filter: sepia(0.14) saturate(1.06) brightness(0.97);
}
/* The room's shadow across its own photograph: deep green rolls in from the
   right where the copy sits, plus a soft floor and ceiling so the band reads
   as one lit wall. */
.oz-feature__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg,
      transparent 0%,
      transparent 26%,
      color-mix(in srgb, var(--c-bg-deep) 80%, transparent) 52%,
      var(--c-bg-deep) 74%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--c-bg-deep) 45%, transparent) 0%,
      transparent 24%,
      transparent 74%,
      color-mix(in srgb, var(--c-bg-deep) 62%, transparent) 100%);
}
@media (max-width: 1199px) {
  /* Narrow: the photograph steps back into the flow — a full-width band
     above the copy, with only a soft floor shadow. No side scrim where
     there is no text to sit on. */
  .oz-feature__bg {
    position: static;
    z-index: auto;
    aspect-ratio: 4 / 3;
    overflow: hidden;
  }
  .oz-feature__bg img { object-position: 50% 42%; }
  .oz-feature__bg::after {
    background: linear-gradient(180deg,
      transparent 62%,
      color-mix(in srgb, var(--c-bg-deep) 30%, transparent) 100%);
  }
  .oz-feature .container { margin-top: var(--sp-6); }
}

/* Parallax plumbing, same pattern as the plates: the photograph is oversized
   by 14% and rides --py, which motion.js writes at this slot's amplitude
   (3% — half, per V2 §3.5). The frame never moves; the scrim ::after stays
   fixed to it, so the room's shadow holds still while the table glides
   behind it. >=1200px only; motion.js never attaches under reduced motion,
   and the reset returns the photo to its plain cover fit. */
@media (min-width: 1200px) {
  .oz-feature__bg[data-parallax] {
    --parallax-amp: 40;
  }
  .oz-feature__bg[data-parallax] img {
    position: absolute;
    top: -40%;
    left: 0;
    height: 180%;
    transform: translate3d(0, var(--py, 0px), 0);
  }
  .oz-feature__bg[data-parallax].is-parallax img { will-change: transform; }
}
@media (prefers-reduced-motion: reduce) {
  .oz-feature__bg[data-parallax] img {
    position: static;
    height: 100%;
    transform: none;
  }
}

.oz-feature__body { min-width: 0; }
.oz-feature__body > * + * { margin-top: var(--sp-4); }
.oz-feature__heading {
  font-family: var(--ff-display);
  font-weight: var(--fw-book);
  font-size: var(--fs-h2);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-h2);
  color: var(--c-heading);
  text-wrap: balance;
}
.oz-feature__cta { margin-top: var(--sp-6); }

/* The heading is the section's loudest voice — a step above the page's other
   h2s, because this is the dish the whole site exists to sell. */
.oz-feature__heading {
  font-size: clamp(2.35rem, 1.5rem + 3.4vw, 4.4rem);
}
.oz-feature__sub {
  font-size: clamp(1.2rem, 1.02rem + 0.75vw, 1.5rem);
}

/* The copy column sits in the darkened half of the photograph: pushed to the
   container's right edge, where the scrim has gone solid, so the gold type
   rests on real ground rather than on food. */
@media (min-width: 1200px) {
  .oz-feature__body {
    max-width: 36rem;
    margin-inline-start: auto;
  }
}

/* The price is the section's one number — set it like the front of a menu
   card: a foil rule above and below, the label in the engraving voice, and
   the numeral itself lit with the foil text treatment. */
.oz-feature__price {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-6);
  padding-block: var(--sp-5);
  border-top: 1px solid;
  border-bottom: 1px solid;
  border-left: 0;
  border-right: 0;
  border-image: var(--grad-foil) 1;
  max-width: 34rem;
}
.oz-feature__price-label {
  font-family: var(--ff-deco);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-muted);
}
.oz-feature__price-value {
  font-family: var(--ff-deco);
  font-size: clamp(3rem, 2.1rem + 3.8vw, 5rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.oz-feature__price-note {
  font-family: var(--ff-display);
  font-style: italic;
  font-size: var(--fs-sm);
  letter-spacing: 0.02em;
  color: var(--c-text-muted);
}


/* ==========================================================================
   C9. REVIEWS PLAQUE  (V2 §3.6)
   An engraved brass plate: one number, one line, one door. This ships in
   route 3 — a real 4.3, a genuine 30% partial fill, the women-owned badge and
   one link to Google. THAT IS THE PRIMARY STATE, not a loading state.

   Forbidden here, permanently: skeleton cards, greyed placeholder avatars, a
   carousel with one slide, "no reviews yet", "be the first", "coming soon",
   any dashed-border dropzone, and rounding 4.3 up to 4.5. If routes 1 or 2
   ever light up, their cards render BELOW this plaque, which does not change.
   ========================================================================== */
.oz-plaque {
  max-width: var(--w-narrow);
  margin-inline: auto;
  padding-block: var(--sp-section);
  text-align: center;
  /* Own stacking context: the ghost arch below sits at z-index -1 and must
     paint over the section's ground, not under it. */
  isolation: isolate;
}
.oz-plaque > * + * { margin-top: var(--sp-5); }

/* The ghost arch: the brand window in a single hairline, centred behind the
   rating — the number sits inside the arch the way a plaque sits in a door. */
.oz-plaque::before {
  content: "";
  position: absolute;
  z-index: -1;
  /* 3vw keeps the crown ABOVE the eyebrow on a phone (the plaque's own
     padding there is only 52px); at desktop widths it settles at --sp-6. */
  top: clamp(0.5rem, 3vw, var(--sp-6));
  left: 50%;
  transform: translateX(-50%);
  width: clamp(240px, 28vw, 360px);
  height: clamp(300px, 34vw, 440px);
  border: var(--rule);
  border-bottom: 0;
  border-radius: var(--r-arch);
  pointer-events: none;
}

.oz-plaque__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--ff-deco);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-muted);
}

/* The number is the section. It occupies the space review cards would have
   occupied and it is not apologising for being alone. */
.oz-plaque__rating {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--sp-3);
}
.oz-plaque__number {
  font-family: var(--ff-display);
  font-weight: var(--fw-light);
  font-size: clamp(3.25rem, 2rem + 5.2vw, 6rem);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h1);
  text-indent: -0.04em;          /* optical centring of the numeral */
  color: var(--c-heading);
}
.oz-plaque__outof {
  font-family: var(--ff-display);
  font-size: var(--fs-h3);
  color: var(--c-text-muted);
}
.oz-plaque__source {
  font-size: var(--fs-sm);
  color: var(--c-text-muted);
}

/* --- The stars, in hairline --------------------------------------------
   Five 20px arch-shouldered stars. The filled portion is --c-accent (6.82:1);
   the remainder is a 1px --c-border-strong outline. The 0.3 is a genuine
   partial fill, clipped at exactly 30% of the fifth star — the rating is 4.3
   and the graphic says 4.3. A rounded-up 4.5 would be both a lie and worse
   looking. The honesty of the 0.3 IS the design. */
.oz-plaque__stars {
  /* js/reviews.js composes the clip from these two, so the partial fill lands
     on the star and not on a gap. They are the geometry, not decoration —
     change them here and the 4.3 stays exactly 4.3. */
  --oz-star: 26px;
  --oz-star-gap: var(--sp-2);
  position: relative;
  display: inline-flex;
}
.oz-stars__row {
  display: flex;
  gap: var(--oz-star-gap);
}
.oz-stars__outline { color: var(--c-border-strong); }
.oz-stars__fill {
  position: absolute;
  inset: 0;
  color: var(--c-accent);
  pointer-events: none;
  /* --fill is written by js/reviews.js as a percentage of the whole row. */
  clip-path: inset(0 calc(100% - var(--fill, 100%)) 0 0);
}
.oz-star { width: var(--oz-star); height: var(--oz-star); display: block; flex: none; }

/* It draws left-to-right as the plaque enters view, and stops visibly short
   of the fifth star's right edge. */
.js [data-reveal="stars"] .oz-stars__fill {
  clip-path: inset(0 100% 0 0);
  transition: clip-path var(--dur-slower) var(--ease-brand);
}
.js [data-reveal="stars"].is-revealed .oz-stars__fill {
  clip-path: inset(0 calc(100% - var(--fill, 100%)) 0 0);
}

/* --- The badge ----------------------------------------------------------
   Google's own factual designation about a real business, rendered in the
   site's engraving voice at the same weight as everything else the plaque
   says. No ribbon, no rosette, no pink, no badge iconography of any kind —
   treating a true thing as a sticker would cheapen it. */
.oz-plaque__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 40px;
  padding-inline: var(--sp-5);
  border: var(--rule);
  border-radius: var(--r-pill);
  background-color: transparent;
  font-family: var(--ff-deco);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--c-heading);          /* 10.75:1 on --c-bg */
}

.oz-plaque__rule {
  width: 240px;
  max-width: 100%;
  margin-inline: auto;
}
.oz-plaque__cta { margin-top: var(--sp-6); }

/* --- The ground: the real room -------------------------------------------
   The client's own photograph of the neon wall — the room the reviewers are
   actually talking about. Dimmed and warmed under a deep-green scrim so the
   plaque and the quotes read first and the blue tile wall only glows
   through. Edges dissolve into the neighbours' grounds. */
.oz-reviews__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  margin: 0;
  overflow: hidden;
}
.oz-reviews__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* 1080px native, stretched wide: keep the crop on the sign and the table,
     and let the grade carry the softness. */
  object-position: 50% 26%;
  filter: sepia(0.24) saturate(0.78) brightness(0.6);
}
.oz-reviews__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    var(--c-bg-deep) 0%,
    color-mix(in srgb, var(--c-bg-deep) 60%, transparent) 16%,
    color-mix(in srgb, var(--c-bg-deep) 78%, transparent) 46%,
    color-mix(in srgb, var(--c-bg-deep) 82%, transparent) 80%,
    var(--c-bg-deep) 100%);
}

/* --- The quote row (Routes 1 and 2) --------------------------------------
   Renders BELOW the plaque as its SIBLING, never inside it: the plaque is
   the section's identity and does not change. The quotes are a carousel at
   every width — a scroll-snap track with dots — standing wider than the
   plaque's narrow column; only the card size changes with the viewport. */
.oz-quotes {
  max-width: var(--w-content);
  margin-inline: auto;
  margin-top: var(--sp-8);
}
.oz-quotes__track {
  display: flex;
  gap: var(--grid-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--sp-2);
}
.oz-quotes__track::-webkit-scrollbar { display: none; }
.oz-review {
  position: relative;
  min-width: 0;
  padding-top: var(--sp-6);
  border-top: 1px solid;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  border-image: var(--grad-foil) 1;
}
/* A ghost quotation mark in the deco voice — gold as light, at a whisper. */
.oz-review::before {
  content: "\201C";
  position: absolute;
  top: var(--sp-5);
  right: 0;
  font-family: var(--ff-deco);
  font-size: 2.75rem;
  line-height: 1;
  color: var(--c-accent);
  opacity: 0.4;
  pointer-events: none;
}
.oz-review__quote {
  font-family: var(--ff-display);
  font-size: var(--fs-h3);
  line-height: var(--lh-heading);
  color: var(--c-text);
}
.oz-review__author {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
  font-family: var(--ff-deco);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--c-text-muted);
}
/* Cards snap at every width; desktop cards run wider so roughly two and a
   half quotes share the row and the track has somewhere to go. */
.oz-quotes__track .oz-review {
  flex: 0 0 min(78vw, 340px);
  scroll-snap-align: center;
}
@media (min-width: 900px) {
  .oz-quotes__track .oz-review { flex-basis: min(44vw, 540px); }
}
.oz-quotes__dots {
  display: flex;
  justify-content: center;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}
.oz-quotes__cdot {
  display: block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: var(--c-border-strong);
  border: 0;
  transition: background-color var(--dur-base) var(--ease-brand),
              transform        var(--dur-base) var(--ease-brand);
}
.oz-quotes__cdot.is-active {
  background-color: var(--c-accent);
  transform: scale(1.5);
}
/* The honesty line: sample disclosure or the required platform attribution.
   Quiet, centred, and never absent when the words it describes are visible. */
.oz-quotes__note {
  margin-top: var(--sp-5);
  font-size: var(--fs-sm);
  letter-spacing: 0.02em;
  text-align: center;
  color: var(--c-text-muted);
}


/* ==========================================================================
   C10. INSTAGRAM GRID  (V2 §3.7)
   Six lit panes in a dark corridor.

   These squares are the ONLY non-arch image shape on the entire site and the
   exception is deliberate: 1:1 is Instagram's own vernacular, so the section
   reads as a quotation of another surface rather than as a failure of ours.
   The arch is not absent — it returns as the micro-arch in the follow CTA.
   Do not extend this exception to any other section.

   One photo family only (dark bleed). A grid mixing lit plates and dark
   bleeds would look like two websites.
   ========================================================================== */
.oz-ig__head { margin-bottom: var(--sp-stack); }
.oz-ig__handle {
  font-family: var(--ff-display);
  font-weight: var(--fw-book);
  font-size: var(--fs-h2);
  line-height: var(--lh-heading);
  color: var(--c-heading);
}
.oz-ig__sub {
  margin-top: var(--sp-2);
  font-family: var(--ff-ui);
  font-weight: var(--fw-light);
  font-size: var(--fs-body);
  color: var(--c-text-muted);
}

.oz-ig__grid {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding: 0;
  margin: 0;
  list-style: none;
}
/* Mobile: first tile spans both columns for an editorial featured look */
.oz-ig__cell:first-child { grid-column: span 2; }
@media (min-width: 768px)  {
  .oz-ig__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .oz-ig__cell:first-child { grid-column: span 1; } /* reset on tablet */
}
/* Desktop: 4-column editorial. First tile large (2×2), last spans 2 wide. */
@media (min-width: 1200px) {
  .oz-ig__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .oz-ig__cell:first-child {
    grid-column: span 2;
    grid-row: span 2;
  }
  .oz-ig__cell:last-child { grid-column: span 2; }
}

.oz-ig__cell { min-width: 0; }
.oz-ig__tile {
  position: relative;
  display: block;
  aspect-ratio: var(--ar-tile);
  box-shadow: var(--inset-hairline);
  overflow: hidden;
  transition: box-shadow var(--dur-slow) var(--ease-brand);
}
/* Featured tile fills the 2×2 container height on desktop */
@media (min-width: 1200px) {
  .oz-ig__cell:first-child .oz-ig__tile {
    aspect-ratio: unset;
    height: 100%;
  }
}
.oz-ig__tile > .oz-bleed,
.oz-ig__tile > .oz-shot { height: 100%; }

/* A gold hairline frame scales in on hover — a gallery ring appearing around
   each photograph. Hover does NOT add a heart, like count or caption overlay.
   The photograph comes into the light; the frame says "here". */
.oz-ig__tile::after {
  content: "";
  position: absolute;
  inset: 7px;
  border: 1px solid var(--c-accent);
  opacity: 0;
  pointer-events: none;
  z-index: var(--z-raised);
  transition: opacity var(--dur-slow) var(--ease-brand),
              transform var(--dur-slow) var(--ease-brand);
  transform: scale(0.96);
}
@media (hover: hover) and (pointer: fine) {
  .oz-ig__tile:hover,
  .oz-ig__tile:focus-within { box-shadow: inset 0 0 0 1px var(--c-border-strong); }
  .oz-ig__tile:hover .oz-bleed::before,
  .oz-ig__tile:focus-within .oz-bleed::before,
  .oz-ig__tile:hover .oz-shot::before,
  .oz-ig__tile:focus-within .oz-shot::before { opacity: 0.02; }
  .oz-ig__tile:hover::after,
  .oz-ig__tile:focus-within::after {
    opacity: 0.45;
    transform: scale(1);
  }
}

.oz-ig__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5) var(--sp-6);
  margin-top: var(--sp-stack);
}
.oz-ig__social {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5);
  font-size: var(--fs-sm);
}


/* ==========================================================================
   C11. MAP PANEL  (V2 §3.8)
   A brass address plate that opens into a window.

   The placeholder is the design, not a consent gate. A visitor who never
   clicks it has still been told where the restaurant is, in the brand's own
   voice, at a size they can read from across the room — and "get directions"
   is present before, during and after the map exists, so the map is never the
   only path for someone on a slow connection, a locked-down browser, or a
   screen reader.

   Nothing third-party loads and no cookie is set until the visitor clicks.
   ========================================================================== */
.oz-map {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background-color: var(--c-surface);
  /* Warm radial behind the address plate — the lit-window motif */
  background-image: radial-gradient(ellipse 80% 70% at 50% 110%,
    rgb(209 164 121 / 0.10) 0%,
    transparent 65%);
  box-shadow: var(--inset-hairline), 0 0 0 1px var(--c-border);
  border-radius: var(--r-arch-soft);
}
@media (max-width: 767px) { .oz-map { aspect-ratio: var(--ar-plate); } }

.oz-map__panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-5);
  padding: var(--sp-6) var(--sp-5);
  text-align: center;
}
/* The arch mark, hairlined behind everything at 8%. */
.oz-map__mark {
  position: absolute;
  z-index: var(--z-below);
  width: min(280px, 60%);
  height: auto;
  opacity: 0.08;
  pointer-events: none;
}
.oz-map__address {
  font-family: var(--ff-display);
  font-weight: var(--fw-mid);
  font-size: var(--fs-h3);
  line-height: var(--lh-heading);
  color: var(--c-text);              /* 9.67:1 on --c-surface */
}
.oz-map__rule { width: 160px; max-width: 60%; }
.oz-map__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
}

/* The iframe is rectangular and UNFILTERED. It is not arch-masked — the cap
   would clip Google's zoom and attribution controls, which is a usability
   failure and an attribution problem — and it carries no saturate() or
   duotone, because a map is a tool and degrading a tool for tonal consistency
   is where art direction becomes vandalism. */
.oz-map__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* The styled canvas holds the same geometry. The tiles themselves are painted
   in the brand palette by map.js's MAP_STYLE — the tool is framed, never
   filtered, so Google's controls and attribution stay legible and live. */
.oz-map__canvas {
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
/* Instead, the panel's arched top edge survives as a 1px rule drawn OVER the
   iframe. The brand frames the tool; it does not tint it. */
.oz-map__arch-line {
  position: absolute;
  inset: 0 0 auto 0;
  height: 40%;
  z-index: var(--z-raised);
  pointer-events: none;
  border: var(--rule);
  border-bottom: 0;
  border-radius: var(--r-arch-soft);
}

/* The placeholder is CONSUMED, not hidden behind the map: once the visitor
   has asked for the window, the address plate has done its job. */
.oz-map[data-state="loaded"] .oz-map__panel { display: none; }

/* The window opens when the tiles are ready, not before. An interruptible
   transition rather than a keyframe — the visitor can click twice. */
.js .oz-map__frame,
.js .oz-map__canvas {
  opacity: 0;
  transform: scale(0.985);
  transition: opacity var(--dur-slow) var(--ease-brand),
              transform var(--dur-slow) var(--ease-brand);
}
.js .oz-map[data-state="loaded"] .oz-map__frame,
.js .oz-map[data-state="loaded"] .oz-map__canvas {
  opacity: 1;
  transform: none;
}


/* ==========================================================================
   REDUCED MOTION
   tokens.css already drops every animation-name and narrows transitions to
   the properties that do not imply movement, so the marquee's drift, the
   lamp's breath and the plate zoom are gone before this block runs. What it
   cannot express is what the marquee should LOOK like once it is parked — and
   the answer is not "cut off at both ends". Park the track, drop the edge
   mask so no word is half-hidden, and let the strip scroll on its own if a
   narrow viewport cannot hold ten words. It must be legible and finished when
   still, and the final state must always be reachable.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .oz-marquee {
    justify-content: center;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    -webkit-mask-image: none;
            mask-image: none;
  }
  .oz-marquee::-webkit-scrollbar { display: none; }
  .oz-marquee__track {
    transform: none;
    width: auto;
    animation: none;
    will-change: auto;
  }
  /* The duplicate group exists only to make the loop seamless. With no loop
     it is the same ten words twice, which a screen reader is already ignoring
     and a reader should not have to wade through. */
  .oz-marquee__group:nth-child(2) { display: none; }
  .oz-marquee__group { padding-inline-end: 0; }

  .oz-status[data-state="open"] .oz-status__dot { opacity: 1; }
}
