/* Tabaq Al Hejazi — guest menu styles.  Design system: docs/03-design-system.md */
:root{
  color-scheme: light;
  /* Restaurant website palette: navy, ivory and restrained brass. */
  --ink:#15333c; --navy:#082c3c; --deep:#062330; --deep2:#0d3b4c;
  --sea:#246985; --aqua:#287fa2; --foam:#eae6dc; --paper:#f5f1e8; --card:#FFFFFF;
  --brass:#d8ba80; --bronze:#80602d; --chili:#D9503A; --leaf:#4F9A5B; --muted:#64706e;
  --line:#ded8cd; --line-soft:#e8e2d8;
  --shadow:0 5px 18px rgba(6,35,48,.07);
  --brass-lt:#E3C877;
  /* ---- the fluid scale (design spec §4).  Every size a guest could notice being wrong on a
     different phone is a clamp() between a floor and a ceiling, so a 320px phone and a 430px phone
     do not get the same layout with different margins.  The values are the approved prototype's,
     not a re-derivation. */
  --radius:clamp(14px, 3.6vw, 20px); --radius-sm:clamp(12px, 3vw, 16px);
  --wrap:100%;
  --gutter:clamp(14px, 4.2vw, 24px); --gap:clamp(10px, 3vw, 16px);
  --bar-h:clamp(52px, 14vw, 60px);
  --fs-kicker:clamp(9.5px, 2.6vw, 11.5px); --fs-title:clamp(20px, 5.6vw, 26px);
  --fs-name:clamp(16px, 4.4vw, 19px); --fs-body:clamp(12.5px, 3.4vw, 14.5px);
  --fs-caption:clamp(10.5px, 2.9vw, 12.5px); --fs-price:clamp(16px, 4.4vw, 19px);
  --thumb:clamp(88px, 24vw, 116px); --hero-h:clamp(230px, 62vw, 320px);
  --serif:"Fraunces",Georgia,"Times New Roman",serif;
  --sans:"Manrope",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; font-family:var(--sans); color:var(--ink); background:var(--paper);
  line-height:1.5; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
img{display:block; max-width:100%}
button{font:inherit; color:inherit; border:0; background:none; cursor:pointer; padding:0}
a{color:inherit; text-decoration:none}
h1{margin:0}
/* Fill the browser on phones and laptops; section gutters provide the reading inset.
   overflow-x:clip preserves viewport scrolling and the sticky browsing controls. */
#app{max-width:var(--wrap); margin-inline:auto; min-height:100vh; position:relative;
  overflow-x:hidden; overflow-x:clip; background:var(--paper); box-shadow:0 0 60px rgba(12,46,74,.06)}
.view{will-change:transform,opacity}
.tnum{font-variant-numeric:tabular-nums}
/* Brand bar — sticky, and the only dark surface above the fold. Its height is the one token the
   search field offsets itself by, so the two cannot drift apart: the shipped word mark is 36px and
   an owner upload is 32px, and both fill the same bar.  The padding is capped so the mark plus
   padding never exceeds --bar-h at the narrowest phone: --bar-h is a *floor* on the bar, and .tools
   is offset by the token, so an over-tall bar would slide under the sticky search field. */
.topbar{position:sticky; top:0; z-index:40; display:flex; align-items:center; gap:var(--gap);
  padding:clamp(6px, 1.7vw, 9px) var(--gutter);
  min-height:var(--bar-h);
  color:#fff; background:linear-gradient(180deg,var(--deep2),var(--deep));
  box-shadow:0 1px 0 rgba(255,255,255,.08), 0 10px 24px rgba(10,43,73,.22)}
.topbar__logo{height:36px; width:auto}
.topbar__mark{display:inline-flex; align-items:center; height:32px; padding:0 10px; border-radius:9px;
  background:#fff; box-shadow:0 2px 8px rgba(6,24,40,.28)}
.topbar__mark img{height:24px; width:auto}
.topbar__tag{margin-left:auto; font-family:var(--serif); font-style:italic; font-size:12.5px; color:#d8ba80}
/* Search — pinned under the brand bar; results render directly beneath it (G04) */
.tools{position:sticky; top:var(--bar-h); z-index:35;
  padding:clamp(8px, 2.4vw, 12px) var(--gutter) clamp(10px, 3vw, 14px);
  background:linear-gradient(180deg,var(--paper) 78%,rgba(245,241,232,0));
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px)}
/* The pill's floor is 44px and the field inside it is never shorter, so the control a guest
   actually touches is the 44px target the pill advertises. */
.search{display:flex; align-items:center; gap:10px; width:100%; height:clamp(44px, 11vw, 48px);
  background:var(--card); border:1px solid var(--line); border-radius:999px;
  padding:0 clamp(12px, 3.4vw, 16px); box-shadow:var(--shadow);
  transition:box-shadow .18s, border-color .18s}
.search:focus-within{border-color:var(--aqua); box-shadow:0 0 0 3px rgba(79,166,214,.24), var(--shadow)}
.search input{flex:1 1 auto; min-width:0; min-height:44px; border:0; outline:0; background:none;
  font-family:var(--sans); font-size:var(--fs-body); color:var(--ink)}
.search input::-webkit-search-cancel-button,.search input::-webkit-search-decoration{-webkit-appearance:none; display:none}
.search svg{flex:0 0 auto; color:var(--sea)}
.search__clear{display:none; flex:0 0 auto; width:26px; height:26px; border-radius:50%;
  place-items:center; color:var(--sea); background:var(--foam)}
.search.has-query .search__clear{display:grid}
/* Search results — attached to the field, replacing the page while a query is active */
.results{padding:2px var(--gutter) 6px}
/* Only the render that opens the panel rises; the element is new on every keystroke, so animating
   it unconditionally would flash the whole panel while the guest types. */
.results--rise{animation:results-rise .22s ease both}
@keyframes results-rise{from{opacity:0; transform:translateY(-6px)} to{opacity:1; transform:none}}
.results__bar{display:flex; align-items:center; gap:10px; padding:2px 4px 10px}
.results__count{font:600 var(--fs-kicker) var(--sans); letter-spacing:.12em; text-transform:uppercase; color:var(--sea)}
.results__clear{font:600 var(--fs-caption) var(--sans); color:var(--sea); padding:7px 12px;
  min-height:44px; display:inline-flex; align-items:center; border-radius:999px; background:var(--foam)}
.results .list{padding-top:0}
/* The no-hits card sits straight in .results, not in a .list, so its own margin is the whole
   inset — the same --gutter the result cards get from .results plus .list. */
.results .empty{margin:6px var(--gutter)}
.dish__cat{display:block; font:700 var(--fs-kicker) var(--sans); letter-spacing:.14em; text-transform:uppercase;
  color:var(--sea); margin-bottom:3px}
@media (prefers-reduced-motion:reduce){ .results--rise{animation:none} }
/* One persistent polite status node, kept outside #view so it survives a re-render. Hidden with
   clipping rather than display:none or visibility:hidden, both of which suppress announcements. */
.status-live{position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  overflow:hidden; white-space:nowrap; clip-path:inset(50%)}
/* Today's Special — auto-rotating carousel with animated waves (ADR 0006).
   Only .hero itself takes the scale; its internals are V3-2's, and .hero__slide's own 222px floor
   sits under --hero-h's 230px floor so it never overrides the token. The 222px declaration is a
   fallback for a client that cannot resolve the clamp: the banner is never shorter than that. */
.hero{position:relative; margin:var(--gap) var(--gutter) clamp(4px, 1.5vw, 8px);
  border-radius:var(--radius); overflow:hidden; color:#fff;
  min-height:222px; min-height:var(--hero-h);
  background:linear-gradient(150deg,var(--deep2),var(--deep)); box-shadow:var(--shadow); isolation:isolate}
.hero__waves{position:absolute; left:0; right:0; bottom:0; height:58px; z-index:1; overflow:hidden; pointer-events:none}
.wave{position:absolute; bottom:0; left:0; height:100%; width:200%;
  background-repeat:repeat-x; background-position:bottom left; background-size:50% 100%; will-change:transform}
/* The waves are decoration behind the photo, so they carry the approved prototype's lighter paint:
   a paler aqua at partial fill under the same element opacity. Geometry, drift speed and direction
   are untouched -- only the paint and the opacity. */
.wave--1{opacity:.15; animation:wave-drift 7s linear infinite;
  background-image:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'><path d='M0 20 Q25 6 50 20 T100 20 V40 H0 Z' fill='%234FA6D6' fill-opacity='.55'/></svg>")}
.wave--2{opacity:.09; height:74%; animation:wave-drift 11s linear infinite reverse;
  background-image:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'><path d='M0 22 Q25 8 50 22 T100 22 V40 H0 Z' fill='%2390C8E8' fill-opacity='.45'/></svg>")}
@keyframes wave-drift{to{transform:translate3d(-50%,0,0)}}
.hero__track{position:relative; z-index:2; display:flex; height:100%; min-height:inherit;
  transition:transform .5s cubic-bezier(.4,0,.2,1); touch-action:pan-y}
.hero__slide{position:relative; flex:0 0 100%; min-width:100%; text-align:left; color:#fff; min-height:222px; transition:transform .16s}
.hero__slide:active{transform:scale(.995)}
.hero__art{position:absolute; inset:0; opacity:.20; background-size:cover; background-position:center}
.hero__seal{position:absolute; right:-28px; top:-28px; width:150px; z-index:0; opacity:.30; pointer-events:none}
.hero__photo ~ .hero__seal{opacity:.80}
.hero__slide.is-broken .hero__seal{opacity:.30}
.hero__body{position:relative; z-index:3; padding:20px clamp(18px, 5vw, 26px) clamp(30px, 8vw, 42px)}
.hero__eyebrow{font-size:var(--fs-kicker); letter-spacing:.22em; text-transform:uppercase; color:var(--brass); font-weight:700}
/* The hero type is fluid like everything else (design spec §4). A fixed 31px title wrapped to a
   third line on a 320px phone, and because .hero__track stretches every slide to the tallest one,
   that single tall slide made the banner taller on the narrowest phone than on the widest. */
.hero__title{font-family:var(--serif); font-weight:400; font-size:clamp(24px, 7vw, 32px); line-height:1.04; margin:9px 0 7px; max-width:80%}
/* The caption is the one line of copy that reaches the lit end of the directional mask: it is the
   lowest, widest and smallest text on the slide, so its line-ends land where the mask has gone
   clear. 30ch keeps those line-ends inside the dark region -- and 34ch, the measure an earlier
   prototype used, was never narrower than the 88% it replaced (ch is ~6.6px here, so 34ch is the
   225px box exactly), which is why capping the measure appeared to do nothing. */
.hero__desc{font-size:var(--fs-caption); color:#CFDAEA; max-width:min(88%, 30ch);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}
/* The copy needs a floor of its own where the directional mask has gone clear, and the display type
   and the caption need different weights to get one. Measured on rendered pixels over a bright
   photo, the worst line-end of each block at 320px and 430px. */
.hero__title{text-shadow:0 0 2px rgba(2,12,22,1), 0 0 5px rgba(3,16,28,.95), 0 0 12px rgba(8,34,56,.75), 0 0 24px rgba(8,34,56,.5)}
/* Small type is the harder case: a wide soft bloom pools around a 10.5px glyph without darkening
   the pixel its edge actually meets, so the last characters of a line measured 3.1:1. An opaque core
   at 1px/2px/4px hugs the glyph, and the graded 8px/14px/26px layers carry the falloff so it reads
   as a shadow rather than a stroke. */
.hero__eyebrow,.hero__category,.hero__desc{text-shadow:0 0 1px rgba(2,12,22,1), 0 0 2px rgba(2,12,22,1), 0 0 4px rgba(3,16,28,1),
  0 0 8px rgba(4,18,32,.95), 0 0 14px rgba(8,34,56,.8), 0 0 26px rgba(8,34,56,.5)}
.hero__row{display:flex; align-items:center; gap:14px; margin-top:15px}
.hero__badge{display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:700;
  color:#3a2c05; background:linear-gradient(180deg,#e3c877,var(--brass)); padding:6px 12px; border-radius:999px}
.hero__badge svg{width:13px; height:13px}
.hero__price{font-family:var(--serif); font-size:24px; font-weight:600}
.hero__dots{position:absolute; right:12px; bottom:12px; z-index:4; display:flex; gap:6px}
.hero__dots button{width:20px; height:20px; display:grid; place-items:center; background:none}
.hero__dots button::before{content:""; width:7px; height:7px; border-radius:50%;
  background:rgba(255,255,255,.42); transition:width .25s, background .25s}
.hero__dots button.on::before{background:#fff; width:17px; border-radius:4px}
/* Section header — kicker, serif title, then the fish band filling the row.
   The kicker is --bronze, not --brass: 10.5px brass on the pale page is 2.12:1.
   The row wraps rather than overflowing: at 320-360px "Today's Specials" plus "Ask for today's
   catch" is wider than the page, and #app clips, so an overflowing tag took the band with it.
   Title and tag stay unshrinkable so the band is the only flexible item, and the band's floor keeps
   the signature divider visible on a tight line. */
.section{padding:clamp(22px, 6vw, 32px) var(--gutter) clamp(8px, 2.4vw, 12px)}
.section__kicker{display:block; font:700 var(--fs-kicker) var(--sans); letter-spacing:.24em;
  text-transform:uppercase; color:var(--bronze); margin-bottom:5px}
.section__row{display:flex; align-items:center; gap:clamp(8px, 2.5vw, 14px); flex-wrap:wrap}
.section__title{font-family:var(--serif); font-size:var(--fs-title); font-weight:400; letter-spacing:-.01em;
  flex:0 0 auto; max-width:100%}
.section__wave{flex:1 1 auto; min-width:24px; height:clamp(10px, 2.6vw, 14px); background:url("../img/fish-band.png") repeat-x left center/auto 100%; opacity:.4}
.section__tag{font-family:var(--serif); font-style:italic; color:var(--sea); font-size:var(--fs-caption); flex:0 0 auto}
/* Category grid — every category visible, nothing scrolls sideways. The column count comes from the
    shared .cats/.grid--cats template further down, not from a hardcoded 1fr 1fr. .grid is on the
    element too, because the odd-trailing-tile rule is declared on .grid and the grid must never
    end with a hole. */
.cats{display:grid; gap:var(--gap); padding:2px var(--gutter) 4px}
/* Category tile — ported from the approved prototype (/tmp/opencode/v3/v-a.css). The photo, the
    gradient scrim and the serif name over it are the shipped treatment, with the prototype's fluid
    clamps and the --fs-kicker count token in place of its literals. */
.ctile{display:block; position:relative; width:100%; border-radius:var(--radius); overflow:hidden;
  box-shadow:var(--shadow); aspect-ratio:16/11;
  background:linear-gradient(150deg,var(--deep2),var(--deep)); transition:transform .18s, box-shadow .18s}
.ctile__thumb{position:absolute; inset:0; display:block}
.ctile__thumb img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover}
.ctile__scrim{position:absolute; inset:0;
  background:linear-gradient(180deg,rgba(10,43,73,0) 34%,rgba(10,43,73,.88) 100%)}
.ctile__body{display:block; position:absolute; left:clamp(11px,3.4vw,15px); right:clamp(10px,3vw,13px);
  bottom:clamp(10px,3vw,13px); color:#fff}
.ctile__name{display:block; font-family:var(--serif); overflow-wrap:anywhere;
  font-size:clamp(15.5px,4.4vw,19px); font-weight:400; line-height:1.1;
  text-shadow:0 1px 6px rgba(6,24,40,.55)}
.ctile__count{display:block; font:600 var(--fs-kicker) var(--sans); letter-spacing:.14em;
  text-transform:uppercase; color:rgba(255,255,255,.82); margin-top:3px}
.ctile:active{transform:scale(.985)}
/* No photo means the navy plate plus the fish band, never a broken image */
.ctile:not(.ctile--photo)::after{content:""; position:absolute; left:0; right:0; bottom:0; height:26px;
  background:url("../img/fish-band.png") repeat-x left bottom/auto 100%; opacity:.30}
/* Dish list + card. The list is the .list grid: one column on a phone, two once
   the reading column is wide enough for two 228px cards. .grid is on the element too, because the
   odd-trailing rule is declared there and a dish list is a grid like any other: without it an odd
   number of cards in two columns left a whole empty column beside the last one. */
.list{padding:8px var(--gutter) 6px; display:grid; gap:var(--gap)}
.dish{display:flex; gap:var(--gap); width:100%; text-align:left; background:var(--card);
  border:1px solid var(--line-soft); border-radius:var(--radius-sm); padding:clamp(8px, 2.4vw, 11px);
  box-shadow:0 4px 16px rgba(12,46,74,.08); transition:transform .16s ease, box-shadow .16s}
.dish:active{transform:scale(.987)}
.dish__thumb{flex:0 0 var(--thumb); width:var(--thumb); height:var(--thumb); border-radius:var(--radius-sm); overflow:hidden; position:relative; background:var(--foam)}
.dish__body{flex:1 1 auto; min-width:0; display:flex; flex-direction:column}
.dish__name{font-family:var(--serif); font-size:var(--fs-name); font-weight:400; line-height:1.16; color:var(--ink)}
.dish__desc{font-size:var(--fs-body); color:var(--muted); margin-top:.4em; display:-webkit-box;
  -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden}
.dish__foot{margin-top:auto; display:flex; align-items:center; gap:var(--gap); padding-top:clamp(8px, 2.4vw, 10px)}
.dish__price,.card__price{font-family:var(--serif); font-size:var(--fs-price); font-weight:600; color:var(--ink)}
.dish__chev,.card__chev{margin-left:auto; flex:0 0 auto; width:clamp(26px, 7vw, 32px); height:clamp(26px, 7vw, 32px);
  border-radius:50%; background:var(--foam); color:var(--sea); display:grid; place-items:center}
/* ---- cover: the curated highlight block. Ported from the approved prototype
   (/tmp/opencode/v3/v-a.css, direction A — "Seafood Showcase"), on the V3-1 fluid tokens rather
   than the prototype's literals. ---- */
/* Feature card: the first curated pick, full-bleed, copy on the photo's own bottom scrim. The
   copy block is pushed down by a viewport-derived reserve, so the card is a real banner on a small
   phone and a wide plate on a tablet without the name ever sitting over the unscrimmed photo. */
.feature{position:relative; display:block; width:100%; border-radius:var(--radius); overflow:hidden;
  color:#fff; box-shadow:var(--shadow); isolation:isolate}
.feature img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0}
.feature__scrim{position:absolute; inset:0; z-index:1;
  background:linear-gradient(180deg,rgba(10,43,73,.05) 20%,rgba(10,43,73,.9) 100%)}
.feature__body{position:relative; z-index:2; display:flex; flex-direction:column; align-items:flex-start;
  padding:clamp(96px,30vw,170px) clamp(14px,4.4vw,22px) clamp(14px,4vw,20px)}
/* --brass-lt, not --brass: this eyebrow sits on the scrim, and it is the light brass the owner
   approved for exactly that surface. */
.feature__eyebrow{display:block; font:700 var(--fs-kicker) var(--sans); letter-spacing:.24em;
  text-transform:uppercase; color:var(--brass-lt)}
.feature__name{display:block; font-family:var(--serif); overflow-wrap:anywhere;
  font-size:clamp(21px,6vw,30px); font-weight:400; line-height:1.05; margin:.25em 0 .2em}
.feature__note{display:block; font-size:var(--fs-caption); color:#DCE8F3; max-width:38ch}
.feature__foot{display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:.8em; width:100%}
.feature__foot .card__price{color:#fff}
.feature__foot .card__chev{margin-left:auto; background:rgba(255,255,255,.16); color:#fff}
/* tagsHTML() wraps its pills in a .tags block so a dish card can hold them on their own line.
   In a card foot they are siblings of the price instead, so the block's top margin would drop
   them 7px below the price and the chevron: the approved prototype puts bare pills in the row. */
.feature__foot .tags,.pcard__foot .tags{margin-top:0}
/* The photo-top tile the remaining highlights render as: square photo over a white body, the name
   and then the price and tags in one foot. */
.pcard{display:flex; flex-direction:column; background:var(--card); border:1px solid var(--line-soft);
  border-radius:var(--radius-sm); overflow:hidden; box-shadow:var(--shadow); width:100%;
  transition:transform .16s}
.pcard:active{transform:scale(.985)}
.pcard__thumb{position:relative; aspect-ratio:1/1; background:var(--foam); overflow:hidden}
.pcard__thumb img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover}
/* A sold-out tile keeps the badge on the photo and greys the photo itself, so an unavailable dish
   reads as unavailable here exactly as it does on a dish card. */
.pcard__flag{position:absolute; left:7px; top:7px; z-index:1;
  -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); background:rgba(15,42,66,.86)}
.pcard__body{display:flex; flex-direction:column; flex:1; padding:clamp(8px,2.6vw,12px)}
.pcard__name{display:block; font-family:var(--serif); overflow-wrap:anywhere;
  font-size:clamp(14.5px,4.1vw,17.5px); font-weight:400; line-height:1.14}
.pcard__foot{display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-top:auto; padding-top:.5em}
/* ---- category page chrome (design spec 5.2 and 5.8). Ported from the approved prototype
   (/tmp/opencode/v3/base.css and /tmp/opencode/v3/v-a.css) onto the shipped .topbar and the
   --fs-* tokens, which is where the prototype had its own literals. ---- */
/* The bar's category form: the way back, where you are, and the brand seal. The back control is the
   same chevron the dish detail hero uses, so the mark and its stroke weight do not drift between the
   two pages that offer a way back. No vertical padding, which is the prototype's own value and is
   also what makes the bar exactly --bar-h: the 44px floor on the back control plus any padding at
   all is taller than the token, and .tools is offset by the token. */
.bar--category{padding-top:0; padding-bottom:0}
.bar__back{display:grid; place-items:center; flex:0 0 auto; width:38px; height:38px;
  margin-left:-8px; color:#fff}
.bar__id{min-width:0; display:flex; flex-direction:column; justify-content:center}
/* One line with an ellipsis, and this is not a cosmetic choice. --bar-h is the height .tools is
   offset by, so a name that wrapped would grow the bar past the offset and slide the sticky search
   field under it on the first scroll. The bar is a locator: the page's own name is the heading in
   the header below it, and the ellipsis is visual only, so the DOM still spells the whole name. */
.bar__name{display:block; font-family:var(--serif); font-size:clamp(15px,4.2vw,18px); font-weight:400;
  line-height:1.1; overflow:hidden; overflow-wrap:anywhere; white-space:nowrap; text-overflow:ellipsis}
.bar__count{display:block; font:600 var(--fs-kicker) var(--sans); letter-spacing:.16em;
  text-transform:uppercase; color:#9cc6e8; margin-top:2px}
/* The seal is the recurring brand mark on a white plate: a wordmark was considered and ruled out in
   the design spec because it stops being legible below ~360px. min-width/min-height come from the
   shared 44px rule below, so the 38px box is what it draws and the 44px box is what a guest taps. */
.bar__seal{margin-left:auto; flex:0 0 auto; display:grid; place-items:center; border-radius:50%;
  width:clamp(28px,8vw,36px); height:clamp(28px,8vw,36px); background:#fff;
  box-shadow:0 2px 8px rgba(6,24,40,.3)}
/* object-fit so an owner-uploaded logo of any shape sits inside the circle instead of being cropped
   by it; the shipped emblem is square and unaffected. */
.bar__seal img{width:76%; height:76%; object-fit:contain}
/* The section header: 16:9 with a viewport-derived floor, so it is a banner over the list rather
   than a wall, and the copy sits on the darkest end of its own gradient. At every acceptance width
   the ratio is the taller of the two, which is why the floor is a floor and not the shape. */
.chead{position:relative; margin:var(--gap) var(--gutter) var(--gap); border-radius:var(--radius);
  overflow:hidden; aspect-ratio:16/9; min-height:clamp(150px,42vw,220px); color:#fff;
  background:linear-gradient(150deg,var(--deep2),var(--deep)); box-shadow:var(--shadow)}
.chead__img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover}
.chead__scrim{position:absolute; inset:0;
  background:linear-gradient(180deg,rgba(8,34,56,.35) 0,rgba(10,43,73,.55) 45%,rgba(10,43,73,.94) 100%)}
.chead__body{position:absolute; left:clamp(14px,4.4vw,22px); right:clamp(14px,4.4vw,22px);
  bottom:clamp(12px,3.6vw,18px)}
.chead__kicker{display:block; font:700 var(--fs-kicker) var(--sans); letter-spacing:.24em;
  text-transform:uppercase; color:var(--brass-lt)}
/* Three lines is what the narrowest phone's frame can hold. The clamp is visual only -- the h1 still
   spells the whole name to a screen reader, and the bar above it spells it too -- so a long category
   name loses its fourth line, not its meaning. */
.chead__name{font-family:var(--serif); font-size:clamp(24px,7vw,32px); font-weight:400; line-height:1.05;
  margin:.2em 0 .25em; overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:3;
  -webkit-box-orient:vertical; overflow:hidden}
/* The copy needs a floor of its own over a photo, and it gets it the same way V3-2 gave the banner
   copy: an opaque core at 1px/2px/4px hugging the glyph and graded 8px/14px/26px layers carrying the
   falloff, so it reads as a shadow rather than a stroke. Measured on rendered pixels over a
   deliberately bright plate, the prototype's own gradient alone left the brass kicker at 3.3-4.4:1;
   with this stack every one of the three lines holds 4.5:1 and the photo behind them is untouched. */
.chead__name{text-shadow:0 0 2px rgba(2,12,22,1), 0 0 5px rgba(3,16,28,.95), 0 0 12px rgba(8,34,56,.75),
  0 0 24px rgba(8,34,56,.5)}
.chead__kicker,.chead__count{text-shadow:0 0 1px rgba(2,12,22,1), 0 0 2px rgba(2,12,22,1), 0 0 4px rgba(3,16,28,1),
  0 0 8px rgba(4,18,32,.95), 0 0 14px rgba(8,34,56,.8), 0 0 26px rgba(8,34,56,.5)}
/* --fs-caption rather than the prototype's literal: the count is a caption, and the same token keeps
   it at the size the bar's own count is. */
.chead__count{display:block; font:600 var(--fs-caption) var(--sans); letter-spacing:.16em;
  text-transform:uppercase; color:#b6d2e8}
/* No photo means the navy plate plus the fish band along its base, exactly as a photo-less category
   tile does: the scrim has something to darken and the guest never sees a broken image. The band is
   shorter than the tile's and the copy sits a band-height higher, because here the last line of the
   copy block is the count and the number has to be readable, not decorated. The frame does not have
   to grow for that clearance: three lines of the name plus the kicker and the count need 146px of
   the 168px the narrowest phone's frame gives. */
.chead:not(.chead--photo) .chead__body{bottom:calc(clamp(12px, 3.6vw, 18px) + 18px)}
.chead:not(.chead--photo)::after{content:""; position:absolute; left:0; right:0; bottom:0; height:18px;
  background:url("../img/fish-band.png") repeat-x left bottom/auto 100%; opacity:.30}
/* Today's Special reads as brass-edged without a second colour */
.list--specials .dish{border-left:3px solid var(--brass)}
/* Tags */
.tags{display:flex; flex-wrap:wrap; gap:6px; margin-top:7px}
.tag{display:inline-flex; align-items:center; gap:4px; font-size:11px; font-weight:700; padding:3px 9px; border-radius:999px}
.tag svg{width:12px; height:12px}
.tag--best{background:#f6edd6; color:var(--bronze)}
.tag--sig{background:linear-gradient(180deg,#e3c877,#c6a24a); color:#3a2c05}
.tag--spicy{background:#fbe6e1; color:var(--chili)}
.tag--veg{background:#e7f2e9; color:var(--leaf)}
/* Image fade-in + branded placeholder */
.img{width:100%; height:100%; object-fit:cover; opacity:0; transition:opacity .5s ease}
.img.is-loaded{opacity:1}
/* Photo blend and readable fallback preserve the sea/wave identity. */
.hero__photo{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:1;opacity:1;filter:brightness(1.1) saturate(1.08);
  -webkit-mask-image:linear-gradient(180deg,#000 0,#000 68%,rgba(0,0,0,.5) 96%,rgba(0,0,0,.5) 100%);
  mask-image:linear-gradient(180deg,#000 0,#000 68%,rgba(0,0,0,.5) 96%,rgba(0,0,0,.5) 100%)}
/* The mask is directional, not a flat wash: opaque under the copy on the left and clear over the
   food on the right, so the dish is actually visible. At 100deg the dark stop is the top-left
   corner, which is where .hero__body sits, and the 0% alpha stop lands on the bottom-right.
   Measured, not assumed: over a bright photo the copy's median backdrop is >10:1 and the food band
   keeps ~87% of the photo's own luminance, where the old 90deg wash kept ~42%. */
.hero__mask{position:absolute;inset:0;z-index:1;background:linear-gradient(100deg,rgba(10,43,73,.92) 0%,rgba(10,43,73,.62) 34%,rgba(10,43,73,.14) 68%,rgba(10,43,73,0) 100%);pointer-events:none}
.hero__slide.is-broken .hero__photo{display:none}
.hero__dots button{width:44px;height:44px}
.hero__dots{bottom:2px;gap:0}
.hero__soldout{font-weight:700;background:#354b61;padding:6px 10px;border-radius:12px}
.hero[data-paused="true"] .wave{animation-play-state:paused}
.tag--top,.tag--special{background:#f6edd6;color:#705212}
.back,.bar__back,.ctile,.pcard,.feature,.retry{min-width:44px;min-height:44px}
/* The stray .detail__desc rule that sat here repeated the detail block's, properties and all. */
.footer__row{overflow-wrap:anywhere;min-width:0}
.freshness{padding:12px 16px;background:var(--foam);font-size:13px}
.freshness[hidden]{display:none}
.review{padding:20px 16px;overflow-wrap:anywhere}
.review h1{font-family:var(--serif);font-size:27px;margin:20px 0 12px}
.review__field{margin:16px 0}
.review__field label{display:block;font-weight:600;margin-bottom:6px}
.review input,.review textarea,.review select{width:100%;min-height:44px;font:inherit;color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:10px;padding:10px}
.review__error{font-size:13px;color:#973122;margin:4px 0}
.review [aria-invalid="true"]{border-color:#973122}
.review__demo{background:var(--foam);padding:12px;border-radius:10px}
.review__trap{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}
.review button:disabled{opacity:.6;cursor:wait}
.dish__foot,.hero__row{flex-wrap:wrap}
.ph{position:absolute; inset:0; display:grid; place-items:center;
  background:radial-gradient(120% 120% at 30% 20%,#1c527f,#0b2941)}
.ph::after{content:""; width:66%; height:66%; opacity:.5;
  background:url("../img/emblem.png") center/contain no-repeat}
/* Sold out — both thumb surfaces dim together, so the card reads as unavailable whether the dish
   has a photo or falls back to the branded placeholder. data.sample.js ships every dish without a
   photo, so the placeholder is the normal case, not the exception. */
.dish.is-soldout .dish__thumb img,.dish.is-soldout .dish__thumb .ph::after,
.pcard.is-soldout .pcard__thumb img,.pcard.is-soldout .pcard__thumb .ph::after{filter:grayscale(1) opacity(.6)}
.soldout{display:inline-block; font-size:11px; font-weight:700; color:#fff;
  background:var(--muted); padding:4px 10px; border-radius:999px; margin-top:2px}
/* ---- Dish detail · "Menu Card" (spec 2026-10-02) ----
   The idea: a restaurant menu is a printed card. The dish name and its price sit on ONE baseline with
   leader dots between them, and the portion table IS the price list. Everything below the lockup does
   exactly one job. */
.detail{min-height:100vh;background:var(--paper)}
/* Sold out is a gate, not a badge. The body is the OWNER's fresh_catch_note and nothing else: there is
   no fallback copy, so an owner who has written nothing gets a heading and no invented sentence. */
.detail__gate{margin:var(--gutter) var(--gutter) 0;padding:15px 17px;border-radius:16px;
  background:#fff3f1;border:1px solid #f4cdc6}
.detail__gate h2{font-size:15px;font-weight:700;color:#973122}
.detail__gate p{margin-top:4px;font-size:13.5px;color:var(--muted);overflow-wrap:anywhere}
/* 4:5 replaces the two fixed hero heights (300px, and 58vh when the dish has one photo). One rule, no
   viewport arithmetic, and a square restaurant photo is no longer cropped to a 2.4:1 letterbox band --
   the single biggest reason the photography read as unprofessional at 1440px. */
.detail__photo{position:relative;margin:var(--gutter);border-radius:var(--radius);overflow:hidden;
  aspect-ratio:4/5;background:var(--foam);box-shadow:var(--shadow)}
.detail__photo .img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;
  transition:opacity .35s ease}
.detail__photo .img.is-loaded{opacity:1}
/* A sold-out dish must not look orderable, so the photo desaturates. */
.detail__photo--out .img{filter:grayscale(1) contrast(.92) brightness(.86)}
.detail__photo--nophoto{aspect-ratio:4/5}
.detail__photo--nophoto .ph{position:absolute;inset:0;display:grid;place-items:center;
  background:radial-gradient(120% 120% at 30% 20%,#1c527f,#0b2941)}
.detail__photo--nophoto .ph::after{content:"";width:66%;height:66%;opacity:.5;
  background:url("../img/emblem.png") center/contain no-repeat}
/* The counter and the dots replace the filmstrip. The strip had overflow-x:auto with
   scrollbar-width:none and no counter, no dots and no fade: at 7 photos on a 360px phone
   scrollWidth 514 beat clientWidth 360, so two thumbs were wholly off-screen with nothing to say so.
   The dots stay aria-hidden because the full-screen viewer already offers arrow-key navigation and an
   aria-live counter; a second focusable widget would be redundant. */
.detail__counter{position:absolute;top:16px;left:64px;padding:5px 11px;border-radius:999px;
  background:rgba(11,46,78,.62);color:#fff;font-size:12px;font-weight:600;
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}
.detail__dots{position:absolute;left:0;right:0;bottom:14px;display:flex;justify-content:center;gap:6px}
.detail__dots i{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.55);cursor:pointer}
.detail__dots i.on{background:#fff;width:20px;border-radius:3px}
.back{position:absolute;top:12px;left:12px;z-index:5;width:44px;height:44px;border-radius:50%;
  background:rgba(255,255,255,.94);display:grid;place-items:center;box-shadow:var(--shadow);
  color:var(--ink);transition:transform .15s}
.back:active{transform:scale(.92)}
/* A dark translucent plate, not a white one: white on rgba(255,255,255,.22) only clears the 3:1
   non-text bar against a photo darker than about 14% grey, and real food photography is not
   always that. -webkit- prefixed for Safari below 18. */
.detail__expand{position:absolute;right:12px;bottom:12px;width:44px;height:44px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;color:#fff;
  background:rgba(11,46,78,.46);border:1px solid rgba(255,255,255,.62);
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}
.detail__body{padding:20px var(--gutter) 0}
.detail__kicker{display:block;font:700 var(--fs-kicker) var(--sans);letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted)}
/* align-items:baseline is the whole point: the price sits on the name's FIRST baseline, so a two-line
   name still reads as one lockup rather than two stacked blocks. Both are flex children with
   min-width:0 and overflow-wrap, because #app is overflow-x:clip -- an 80-character price_label or an
   unbroken 120-character name would be clipped into nothing rather than scrollable. */
.detail__lockup{display:flex;align-items:baseline;gap:8px;margin-top:6px}
.detail__name{flex:1;min-width:0;font-family:var(--serif);font-size:clamp(26px,7.4vw,34px);
  font-weight:400;line-height:1.06;overflow-wrap:anywhere}
.detail__leader{flex:0 1 34px;height:1px;margin-bottom:6px;border-bottom:2px dotted var(--line)}
.detail__price{flex:0 1 auto;min-width:0;font-family:var(--serif);font-size:clamp(22px,6vw,28px);
  font-weight:600;color:var(--sea);text-align:right;overflow-wrap:anywhere}
.detail__unit{display:block;font:500 var(--fs-caption) var(--sans);color:var(--muted);margin-top:1px}
.detail__note{margin-top:18px;padding:11px 13px;border-radius:12px;background:var(--foam);
  font-size:13px;color:var(--sea);white-space:pre-wrap;overflow-wrap:anywhere}
.detail__desc{margin-top:22px;color:var(--muted);font-size:var(--fs-body);white-space:pre-wrap;
  overflow-wrap:anywhere}
.detail__detail{margin-top:12px;color:var(--muted);font-size:13.5px;white-space:pre-wrap;
  overflow-wrap:anywhere}
.detail__label{font:700 var(--fs-kicker) var(--sans);letter-spacing:.14em;text-transform:uppercase;
  color:var(--muted);margin:24px 0 9px}
.portions{list-style:none;margin:0;padding:0;border:1px solid var(--line);border-radius:var(--radius-sm);
  overflow:hidden}
.portion{display:flex;align-items:center;gap:12px;padding:14px;background:var(--card);
  border-bottom:1px solid var(--line)}
.portion:last-child{border-bottom:none}
.portion__label{flex:1;min-width:0;font-weight:600;color:var(--ink);overflow-wrap:anywhere}
.portion__price{color:var(--sea);font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.portion.is-soldout .portion__label,.portion.is-soldout .portion__price{color:var(--muted)}
/* --bronze, not --chili: this is a small uppercase kicker on white, and the design system
   documents #8A6D1C at 4.34:1 for exactly that role (--chili measures about 4.07:1 here). */
.portion__note{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--bronze)}
.detail__more{margin-top:28px;padding-top:20px;border-top:1px solid var(--line)}
.detail__more-title{font-family:var(--serif);font-size:19px;font-weight:400;color:var(--ink)}
/* A horizontal snap row instead of three stacked 58px rows: 124px of height carries the same three
   dishes that cost 336px stacked, and a guest who wants the fourth scrolls sideways. */
.siblings{list-style:none;margin:12px 0 0;padding:0 0 2px;display:flex;gap:10px;overflow-x:auto;
  scrollbar-width:none;scroll-snap-type:x mandatory}
.siblings::-webkit-scrollbar{display:none}
/* The flex basis belongs on the <li>, not the button inside it: .siblings' children are the list items,
   so a flex on .sibling sized nothing and the card fell back to its content width (279px measured). */
.sibling__cell{flex:0 0 124px;scroll-snap-align:start}
.sibling{display:block;width:100%;background:var(--card);border:1px solid var(--line);
  border-radius:14px;overflow:hidden;text-align:left;cursor:pointer}
/* position:relative is what .dish__thumb does too: imagesReady() turns any failed <img> into a .ph,
   which is position:absolute;inset:0, and without a positioned ancestor here it escapes the box. */
.sibling__thumb{position:relative;display:block;width:100%;aspect-ratio:1;overflow:hidden}
.sibling__thumb .img,.sibling__thumb .ph{width:100%;height:100%;object-fit:cover}
/* Both text rows are line-clamped AND given a min-height in em, not a fixed px height. A fixed
   height of 32px left only 24px of text once the 8px top padding was taken -- less than two 16.25px
   lines -- so a two-line name like "Shrimp Fried Rice" had its second line clipped by the card. The
   min-height is two lines plus the padding, so the clamp and the box agree and every card matches. */
.sibling__name{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
  min-height:calc(2.5em + 8px);padding:8px 9px 0;font-weight:600;color:var(--ink);font-size:13px;
  line-height:1.25;overflow-wrap:anywhere}
.sibling__price{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;overflow:hidden;
  min-height:calc(1.5em + 10px);padding:2px 9px 8px;color:var(--sea);font-weight:600;font-size:12px;
  overflow-wrap:anywhere}
.detail__cta{display:block;width:100%;margin-top:24px;padding:15px;border:none;border-radius:13px;
  background:var(--navy);color:#fff;font-size:15px;font-weight:600;cursor:pointer}
.detail__cta:active{transform:scale(.99)}
.detail__cta:focus-visible,.detail__expand:focus-visible,.sibling:focus-visible{
  outline:3px solid var(--aqua);outline-offset:2px}
/* Footer */
.footer{background:var(--navy); color:#cfe0ef; margin-top:24px; padding:8px 20px 34px; text-align:center}
.footer__band{height:20px; background:url("../img/fish-band.png") repeat-x center/auto 100%;
  filter:brightness(0) invert(1); opacity:.28; margin:0 -20px 20px}
.footer__logo{height:52px; width:auto; margin:0 auto 14px}
.footer__row{font-size:13.5px; margin:5px 0; display:flex; gap:8px; justify-content:center; align-items:center}
/* A phone number and an address are links, so they are touch targets: 20px of text was not one. */
.footer__row a{color:#fff; display:inline-flex; align-items:center; min-height:44px}
.footer__note{margin-top:16px; font-size:12px; color:#7fa4c4; max-width:280px; margin-inline:auto}
/* The cover's own name, for the outline and the accessibility tree only. The cover's sections are
   all h2, so without this the document starts at level 2. Clipped to a 1px box rather than
   display:none or visibility:hidden, both of which would drop it from the tree as well as from the
   screen, and taken out of flow so the banner still starts one --gap below the sticky search field. */
.cover__name{position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  overflow:hidden; white-space:nowrap; clip-path:inset(50%)}
/* Empty state — the same card as a dish, holding the seal and one line of copy. The horizontal
   margin is --gutter, not a literal: in .results this card IS the whole inset (.results has no .list
   around it), and inside a .list the list's own padding is already the gutter, so the card's margin
   has to be dropped there rather than doubled. */
.empty{margin:6px var(--gutter); padding:44px 22px; text-align:center; color:var(--muted);
  background:var(--card); border:1px solid var(--line-soft); border-radius:16px;
  box-shadow:0 4px 16px rgba(12,46,74,.06)}
.list .empty{margin-inline:0}
.empty__seal{width:64px; opacity:.28; margin:0 auto 12px}
/* ---- grid + stack helpers. The page's own containers (.list, .cats) share the templates declared
   here instead of repeating them, so a column count is changed in exactly one place. The cover's
   category tiles are the first user of .wrap/.grid/.grid--cats/.stack; its highlight grid carries
   its own template in the owner's block below, and a section page reuses .wrap's column and .grid's
   trailing rule. */
.wrap{padding:0 var(--gutter) clamp(8px, 2.4vw, 12px)}
.grid{display:grid; gap:var(--gap)}
.grid--cats,.cats{grid-template-columns:repeat(auto-fit,minmax(min(100%,clamp(140px,42vw,220px)),1fr))}
/* .grid--cards, .grid--photo and .grid--3 were declared here and rendered by nothing. .list carries
   the dish-card template itself, so deleting the three aliases changed no layout; the cover's tile and
   category grids carry their own above. test/e2e/responsive.spec.js fails if a .grid--* modifier ever
   appears in the sheet without a guest page rendering it. */
.list{grid-template-columns:repeat(auto-fit,minmax(min(100%,clamp(228px,58vw,300px)),1fr))}
/* An odd trailing tile spans the full row, so no grid ends with a hole. .cats and the .list that
   carries dish cards both reach this rule through .grid. */
.grid > :last-child:nth-child(odd){grid-column:1/-1}
/* ---- the owner's chosen highlights treatment (refined, ruled 2026-10-03). The cover's highlight
   grid is .grid--tiles and its cards are .pcard, the names the code already renders: this block
   restyles them and renames nothing, so every hook the page and the suite use is kept. Declared
   after the odd-trailing rule above because that is what it overrides. ---- */
/* Keep curated tiles two-up on phones and tablets as the wrapper expands; each track can shrink
   without long dish names forcing horizontal overflow. Categories retain their separate template. */
.grid--tiles{grid-template-columns:repeat(2,minmax(0,1fr))}
/* …and an odd trailing tile keeps that column instead of taking the whole row. The shared rule above
   is what produced the oversized single photo the owner rejected; scoping the override to this one
   grid leaves the category tiles and every dish list with the no-hole behaviour they depend on. */
.grid--tiles > :last-child:nth-child(odd){grid-column:auto}
/* A sole tile has no partner; preserve its established full-row treatment. */
.grid--tiles > :only-child:nth-child(odd){grid-column:1/-1}
/* The tile as the owner approved it: a 5:6 crop (taller than the square it replaces, which is what
   makes a two-up set read as a set), a --radius-sm card on its own soft shadow, and a body padded
   tighter than the square tile's so the name has room to be set larger. */
.grid--tiles .pcard{border-radius:var(--radius-sm); box-shadow:0 3px 14px rgba(6,35,48,.07); border-color:var(--line-soft)}
.grid--tiles .pcard__thumb{aspect-ratio:5/6}
.grid--tiles .pcard__body{padding:clamp(9px,2.8vw,12px) clamp(10px,3vw,13px) clamp(10px,3vw,13px)}
.grid--tiles .pcard__name{font-size:clamp(14.5px,4.1vw,17px); line-height:1.16}
/* The foot spans the body so the price sits at the near edge and the tags hang off the far one --
   space-between, not a content-width row grouped at the left inside a full-width card. margin-top
   and padding-top are restated because this is still the element that pushes the foot to the
   bottom of a card whose name wrapped to a different number of lines. */
.grid--tiles .pcard__foot{margin-top:auto; padding-top:.55em; justify-content:space-between; align-items:center; width:100%}
.grid--tiles .card__price{font-size:clamp(15.5px,4.3vw,18px)}
/* The pill is scaled with the tile it belongs to. 9.5px is --fs-kicker's own floor and it is the one
   fixed size the ruled treatment carries: a tag is a label, not layout, and it is what keeps a
   two-up row from reading as two lines of full-size type and one small one. */
.grid--tiles .tag{font-size:9.5px; padding:.2em .55em}
.stack{display:flex; flex-direction:column; gap:var(--gap)}
/* Tablet and up: use the full available width while retaining each section's page gutters. */
@media (min-width:700px){
  .wrap,.section,.results,.tools{max-width:var(--wrap); margin-inline:auto}
  /* The existing margins supply the banner inset; auto margins would remove that gutter. */
  .hero,.chead{max-width:calc(var(--wrap) + var(--gutter) * 2)}
}
/* --- load + error states (Phase 2 data layer) --- */
.loading{ padding:56px 24px; text-align:center; color:var(--muted);
  display:grid; justify-items:center; gap:14px; }
.loading__seal{ width:78px; opacity:.3; }
.loading__spin{ width:26px; height:26px; border-radius:50%;
  border:3px solid var(--line); border-top-color:var(--sea);
  animation:tabaq-spin .8s linear infinite; }
@keyframes tabaq-spin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce){ .loading__spin{ animation:none; } }
.retry{ margin-top:6px; padding:10px 18px; border:0; border-radius:999px;
  background:var(--sea); color:#fff; font:inherit; font-weight:600; cursor:pointer; }
.retry:hover{ filter:brightness(1.06); }
.retry:focus-visible{ outline:2px solid var(--sea); outline-offset:2px; }
/* Motion */
@keyframes slideIn{from{opacity:0; transform:translateX(16px)} to{opacity:1; transform:none}}
@keyframes fadeUp{from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:none}}
.view--enter{animation:slideIn .28s ease both}
/* The card rise is a first-paint event, not a per-render one. render() rebuilds #view on every
   keystroke and on every category tap, so an ungated animation replays for every card and flashes
   the whole page while the guest types or walks into a category. app.js sets .home--rise on the
   render that first paints the browsed lists, and .results--rise on the render that opens the
   search panel; those two markers are the only first paints. */
[data-home-content].home--rise .dish,.results--rise .dish{animation:fadeUp .3s ease both}
@media (prefers-reduced-motion:reduce){ [data-home-content].home--rise .dish,.results--rise .dish{animation:none} }
/* Accessibility */
:focus-visible{outline:2px solid var(--sea); outline-offset:2px}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important; transition-duration:.001ms!important}
  .img{opacity:1}
}
/* .detail__category is gone: it was renamed .detail__kicker when the lockup landed, and this rule
   outlived it as dead CSS. The kicker rule itself lives in the Menu Card block above. */
.hero__category { font-size: var(--fs-caption); margin: 4px 0; }
.hero__price, .dish__price, .detail__price { overflow-wrap: anywhere; }
.restaurant-content { max-width: 680px; margin: 20px auto; white-space: pre-line; overflow-wrap: anywhere; }
.restaurant-content h2 { font-size: 18px; margin: 16px 0 8px; }
.restaurant-content p { font-size: 14px; line-height: 1.6; }

/* ---- Full-screen photo viewer (photo-viewer.js) ----
   The dialog is built in JS and ships hidden through BOTH the `hidden` attribute and an inline
   `display:none`, because the UA `[hidden]{display:none}` rule loses to any author `display`
   declaration -- without the inline style a later `.viewer{display:flex}` would leave the dialog
   permanently on screen.

   The photo is CAPPED, not full-bleed: `object-fit:contain` on a full-viewport box is what keeps the
   near-black backdrop visible around the image. Owner ruling 2026-10-01: a full-screen view must not
   expose photo defects by running edge to edge, and the hero already shows the photo large. */
.viewer{position:fixed; inset:0; z-index:80; display:flex; align-items:center; justify-content:center;
  background:#08131D}
.viewer__track{display:flex; height:100%; width:100%; transition:transform .22s ease}
.viewer__slide{min-width:100%; height:100%; object-fit:contain}
.viewer__close{position:absolute; top:calc(12px + env(safe-area-inset-top)); right:12px; width:44px;
  height:44px; border-radius:50%; display:grid; place-items:center; color:#fff;
  background:rgba(255,255,255,.16)}
.viewer__count{position:absolute; top:calc(20px + env(safe-area-inset-top)); left:16px; color:#fff;
  font-size:13px; font-weight:600; font-variant-numeric:tabular-nums}
.viewer__dots{position:absolute; left:0; right:0; bottom:calc(16px + env(safe-area-inset-bottom));
  display:flex; justify-content:center; gap:6px}
.viewer__dots i{width:6px; height:6px; border-radius:50%; background:rgba(255,255,255,.4)}
.viewer__dots i.on{background:#fff; width:20px; border-radius:3px}
@media (prefers-reduced-motion:reduce){ .viewer__track{transition:none} }

/* Keep portrait photos readable at intermediate tablet widths, too. */
.detail__photo{width:calc(100% - var(--gutter) * 2); max-width:480px; margin-inline:auto}
@media (min-width:700px){
  .detail__photo{max-width:440px}
  .chead{aspect-ratio:auto; height:clamp(220px,26vw,280px)}
  .chead__scrim{background:linear-gradient(180deg,rgba(8,34,56,.55) 0,rgba(10,43,73,.72) 45%,rgba(10,43,73,.94) 100%)}
}
/* Desktop refinement: existing sections and cards, with space for a laptop. */
@media (min-width:1000px){
  .hero__title{font-size:40px; max-width:22ch}
  .hero__body{padding:32px 36px 48px}
  .chead{aspect-ratio:auto; height:240px}
  .chead__name{font-size:36px}
  .highlights .stack:has(.grid--tiles){display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-items:stretch}
  .highlights .feature__body{height:100%; justify-content:flex-end}
  .grid--tiles .pcard__thumb{aspect-ratio:4/3}
  .grid--cats{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
  .grid--cats > :last-child:nth-child(odd),.list > :last-child:nth-child(odd){grid-column:auto}
  .detail{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-content:start; align-items:start;
    gap:0 var(--gap); padding:var(--gutter)}
  .detail > .footer{grid-column:1/-1; margin-top:var(--gutter)}
  .detail__gate{grid-column:1/-1; margin:0 0 var(--gap)}
  .detail__photo{margin:0; width:100%; max-width:480px; justify-self:center}
  .detail__body{min-width:0; padding:16px 0 0}
  .review{max-width:680px; margin-inline:auto}
}
