/* GENERATED from design-tokens.css + src/styles/* — do not edit directly */

/* ── design-tokens.css ── */
@font-face {
  font-family: "Archivo";
  src: url("/fonts/Archivo-Variable.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Space Grotesk";
  src: url("/fonts/SpaceGrotesk-Variable.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

:root {
  
  --paper: #FBFAF8;        
  --surface: #FFFFFF;      
  --terrazzo: #F5F2EE;     
  --terrazzo-2: #F1EDE7;   
  --ink: #1B1512;          
  --ink-2: #4A423C;        
  --muted: #726A62;        
  --line: #E9E3DC;         
  --line-2: #D8D0C6;       

  
  --ocean: #B4472A;        
  --ocean-deep: #8F3620;   
  --ocean-tint: #F6E7E0;   
  --flamingo: #A66A1F;     
  --flamingo-text: #7D5E21;
  --sun: #B0842F;          
  --sun-text: #7D5E21;     

  
  --viz-ocean: #0072B2;      
  --viz-vermillion: #D55E00; 
  --viz-teal: #009E73;       
  --viz-amber: #E69F00;      
  --viz-sky: #56B4E9;        
  --viz-rose: #CC79A7;       
  --viz-ink: var(--ink);     

  
  --up: #B0842F;
  --up-text: #7D5E21;        
  --down: #B4472A;
  --down-text: #8F3620;      

  
  --font-ui: "Archivo", "Trebuchet MS", Verdana, sans-serif;
  --font-data: "Space Grotesk", "Archivo", sans-serif;

  
  --wdth-display: 118;
  --wdth-h1: 110;
  --wdth-h2: 106;
  --wdth-body: 100;
  --wdth-marquee: 125;      

  --fs-display: clamp(2rem, 6vw, 3.25rem);   --lh-display: 1.08;
  --fs-h1: clamp(1.6rem, 4.5vw, 2.4rem);     --lh-h1: 1.15;
  --fs-h2: clamp(1.3rem, 3vw, 1.7rem);       --lh-h2: 1.25;
  --fs-h3: 1.125rem;                          --lh-h3: 1.4;
  --fs-body: 1rem;                            --lh-body: 1.6;   
  --fs-ui: 0.9375rem;                         --lh-ui: 1.5;
  --fs-receipt: 0.8125rem;                    --lh-receipt: 1.45;
  --fs-stat: clamp(1.8rem, 5vw, 2.6rem);      --lh-stat: 1.1;

  
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
  --max-w: 1120px;
  --measure: 68ch;
  --tap: 44px;              
  --bar-app: 56px;          
  --bar-filter: 48px;       

  
  --r-card: 14px;           
  --r-ui: 10px;             
  --r-chip: 8px;            
  --r-round: 999px;         

  
  --elev-1: 0 2px 0 var(--line);
  --elev-2: 0 2px 0 var(--line-2);

  
  --t-fast: 150ms;          
  --t-sheet: 240ms;         
  --t-med: 320ms;           
  --ease-out: cubic-bezier(.23, 1, .32, 1);   
  --ease-drawer: cubic-bezier(.32, .72, 0, 1); 

  
  --z-bar: 100; --z-map-ui: 150; --z-popover: 200; --z-sheet: 300; --z-toast: 400;

  
  --map-land: #F8F6F2;
  --map-water: #D8E1E5;
  --map-park: #E7EDE4;
  --map-road-casing: #E7E1D9;
  --map-dot: var(--ocean);        
  --map-dot-selected: var(--flamingo); 
  --map-cluster-bg: var(--surface);    
}



html {
  font-family: var(--font-ui);
  font-variation-settings: "wdth" var(--wdth-body);
  color: var(--ink);
  background: var(--paper);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

::selection { background: var(--ocean-tint); color: var(--ink); }

a { color: var(--ocean-deep); text-underline-offset: 2px; }
a:hover { color: var(--ocean); }

h1, h2, h3 { margin: 0; text-wrap: balance; }
h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); font-weight: 700;
     font-variation-settings: "wdth" var(--wdth-h1); }
h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); font-weight: 650;
     font-variation-settings: "wdth" var(--wdth-h2); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); font-weight: 600; }


.num, .receipt, .stat {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}
.receipt { font-size: var(--fs-receipt); line-height: var(--lh-receipt); }
.stat { font-size: var(--fs-stat); line-height: var(--lh-stat); font-weight: 500; }


.marquee {
  font-variation-settings: "wdth" var(--wdth-marquee);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.2; 
}


:lang(es) p { hyphens: auto; }


.delta-up   { color: var(--up-text); }
.delta-down { color: var(--down-text); }
.delta-flat { color: var(--muted); }


.stream-card { border: 1px solid var(--line); background: var(--surface);
               border-start-end-radius: var(--r-card); }
.stream-chip { border: 1px solid var(--line); background: var(--terrazzo);
               border-start-end-radius: var(--r-chip); }


@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

/* ── base.css ── */
/* ============================================================
   BASE — extra reset + a11y beyond design-tokens.css.
   design-tokens.css (miami-site root) already owns :root tokens,
   @font-face, and the bare html/body/a/h1-h3/.num/.receipt/.stat/
   .marquee, .delta-x, .stream-x (x = suffix wildcard; literal asterisks
   are banned in this comment — an asterisk touching a slash terminates
   the block comment early and silently swallows the box-sizing reset
   below as garbage selector tokens; this EXACT bug shipped twice) and
   reduced-motion rules — this file adds
   what a static-site reset still needs on top of that (LTR only,
   no RTL plumbing — brand-separation + DESIGN-DIRECTION.md §14).
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
img, svg, video, canvas { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
ul, ol { list-style: none; padding: 0; }
p, ul, ol, figure { margin-block-end: var(--sp-4); }
p:last-child, ul:last-child, ol:last-child { margin-block-end: 0; }

:focus-visible {
  outline: 2px solid var(--ocean);
  outline-offset: 2px;
  border-radius: var(--r-chip);
}

/* ── Locale-aware quotation marks (DESIGN-DIRECTION.md §3: real quotes,
   never straight, per-locale glyphs). Content authors use <q>, not literal
   quote characters, where the language may switch. ────────────────────── */
q:lang(en) { quotes: "\201C" "\201D" "\2018" "\2019"; }
q:lang(es) { quotes: "\00AB" "\00BB" "\2039" "\203A"; }

/* ── Skip link ──────────────────────────────────────────────────────────── */
.skip-link {
  position: absolute;
  top: -100%;
  left: var(--sp-4);
  z-index: var(--z-toast);
  padding: var(--sp-2) var(--sp-4);
  background: var(--ink);
  color: var(--paper);
  font-weight: 700;
  border-start-end-radius: var(--r-ui);
}
.skip-link:focus { top: var(--sp-2); }

/* ── Screen-reader-only utility ───────────────────────────────────────────── */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* ── typography.css ── */
/* ============================================================
   TYPOGRAPHY — utility classes for the type scale (DESIGN-DIRECTION.md
   §3). design-tokens.css already styles the bare h1/h2/h3/.num/.receipt/
   .stat/.marquee elements; these classes apply the same scale to any
   element (e.g. a <p> used as a display line, or ES width headroom on
   chips/marquee strings).
   ============================================================ */

.display {
  font-family: var(--font-ui);
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  font-weight: 750;
  font-variation-settings: "wdth" var(--wdth-display);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.text-ui {
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  line-height: var(--lh-ui);
  font-weight: 500;
}

.text-body { font-size: var(--fs-body); line-height: var(--lh-body); }

.lede {
  font-size: 1.125rem;
  line-height: var(--lh-body);
  color: var(--ink-2);
  max-width: var(--measure);
}

/* ES gets +25% width headroom on short chip/marquee strings so accented
   caps and longer Spanish labels never clip or force a mid-diacritic
   ellipsis (DESIGN-DIRECTION.md §3 bilingual rule). */
:lang(es) .marquee,
:lang(es) .receipt-chip { padding-inline: calc(var(--sp-3) * 1.25); }

/* Bilingual prose: hyphenate Spanish only (mirrors design-tokens.css's
   :lang(es) p rule for any prose container, not just <p>). */
:lang(es) .lede, :lang(es) .text-body { hyphens: auto; }

/* ── ledger.css ── */
/* ============================================================
   LEDGER — the evidence-grammar components (DESIGN-DIRECTION.md §10):
   answer block + receipt chip row, ledger tables, result rows. This is
   Miami's ownable identity kit — every important page opens with an
   answer sentence and its receipt, never a naked number.
   ============================================================ */

/* ── Answer block ─────────────────────────────────────────────────────── */
.answer-block { margin-block-end: var(--sp-6); }
.answer-block__sentence {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  font-variation-settings: "wdth" var(--wdth-h1);
  color: var(--ink);
  margin-block-end: var(--sp-3);
}

/* ── Receipt chip row ─────────────────────────────────────────────────── */
.receipt-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center; /* flex's default (stretch) forced every chip to the
    row's tallest sibling's height, leaving visible empty vertical padding
    inside any shorter chip (DESIGN-SPEC-V2.md's "receipt-chip stretch"
    flag, W11-D2) -- each chip should size to its own content. */
  gap: var(--sp-2);
}
/* W14-A1 "band rhythm": the chip row directly under every answer-block
   headline (home/hub/state-index/leaderboard/venue-profile all share this
   markup shape) becomes a small terrazzo exhibit strip -- a printed
   "receipt tape" rather than chips floating loose on the page ground.
   Framed like DESIGN-DIRECTION.md's own "featured exhibit" convention
   (1px --line-2 + terrazzo). Scoped to .answer-block so it never touches
   the venue-hero's price-level receipt-row or the filter bar's chips. The
   home hero band (home.css) overrides this with a higher-specificity
   selector when the row sits inside the dark hero. */
.answer-block .receipt-row {
  background: var(--terrazzo);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-card);
  padding: var(--sp-3) var(--sp-4);
  /* Hugs its own chips (a real receipt is a printed tape, not a stretched
     banner) -- a page with a single chip (home's methodology statement)
     would otherwise leave a mostly-empty band; a page with several (hub/
     leaderboard/state-index) still reads as one framed strip either way. */
  width: fit-content;
  max-width: 100%;
}
.receipt-chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--ocean);
  border-start-end-radius: var(--r-chip);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  line-height: var(--lh-receipt);
  color: var(--ink);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  /* Most chips are short tags ("35 cities", "Ranked") where nowrap+ellipsis
     never engages. But the venue-profile answer-block's verdict chip is a
     full generated sentence — e.g. "This business is not currently ranked
     among 4 fast-food spots (not enough data to rank it), confidence
     insufficient evidence." (131 chars, live on thousands of r/*.html
     pages). At the ~340px column a 375px viewport gives this chip, nowrap
     forces that whole sentence onto one line and ellipsis then truncates
     it around the 40-45ch mark — cutting off the confidence/evidence
     conclusion the chip exists to carry. That is silent information loss
     on exactly the "receipt" this brand's whole premise is not to fake
     (PRODUCT.md: "every number here carries its receipt or it doesn't
     ship"). Wrapping instead of truncating costs nothing for the short
     chips (a single word/phrase never wraps regardless) and fixes the
     long-sentence case everywhere it occurs. */
  white-space: normal;
  overflow-wrap: anywhere;
  max-width: 100%;
}
/* Low/early confidence reads as quiet and provisional -- the OPPOSITE of the
   ocean-outlined evidence default above (chip-semantics law, W14-A1 §4:
   "self-described/neutral chips stay quiet"). "early" is the leaderboard
   generator's actual low-signal value (content/generators/leaderboards.mjs
   confidenceTier() only ever returns 'high' | 'medium' | 'early' -- 'low'
   never appears in built output today but is kept as a synonym in case a
   future caller emits it). */
.receipt-chip[data-confidence="low"],
.receipt-chip[data-confidence="early"] {
  background: transparent;
  border-color: var(--line);
  color: var(--muted);
}
.receipt-chip[data-confidence="medium"] {
  background: color-mix(in srgb, var(--terrazzo) 50%, var(--surface));
}
a.receipt-chip { cursor: pointer; }
a.receipt-chip:hover { background: var(--terrazzo); border-color: var(--ocean); color: var(--ink); }

/* The venue-hero's own receipt-chip is always the price level, read
   straight off the business's own public listing (self-described, not
   evidence this platform computed) -- chip-semantics law: it stays quiet
   neutral even though the ocean-outlined default above now reads as
   "evidence." Scoped to the hero specifically -- verified against
   lib/entity/sections.mjs (priceChips): the hero renders at most one
   receipt-chip, the price-level badge, never a second. */
.venue-hero .receipt-chip {
  background: transparent;
  border-color: var(--line);
  color: var(--ink-2);
}

/* ── Stream chip sizing (venue-profile tag rows) ──────────────────────────
   design-tokens.css defines `.stream-chip`'s border/background/corner only
   (a "streamline corner helper" shared with `.stream-card`); it was written
   before any page actually rendered a bare `.stream-chip`, so it carries no
   padding, display, or type size of its own. It is now live on every venue
   profile's "What diners mention" and "Amenities" rows (lib/entity's
   sections.mjs) as a plain inline `<span>` — with zero padding, the border
   sits flush against the text on every one of those pages, worst on mobile
   where the tag row is one of the first things a reader's thumb meets.
   Adding the missing box model here (not in design-tokens.css, out of this
   item's allowed paths) restores the chip look these tags were always
   meant to have, at any viewport. */
.stream-chip {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-receipt);
  line-height: var(--lh-receipt);
  color: var(--ink-2);
}

/* ── Ledger table (rankings, comparisons) ─────────────────────────────── */
.ledger-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-ui);
}
.ledger-table th, .ledger-table td {
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  border-bottom: 1px solid var(--line);
}
.ledger-table th {
  background: var(--terrazzo);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  font-weight: 500;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.ledger-table td.num, .ledger-table th.num { text-align: right; }
.ledger-table .num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}
.ledger-table tbody tr:hover { background: var(--terrazzo-2); }

/* Mobile shrink-to-fit fix (venue-details "Website" row): `.ledger-table`
   is `table-layout: auto` (its own rule above), so an unbroken long URL
   string (e.g. "passionrestaurantgroup.com/restaurants/crazy-about-you")
   inside this cell's <a> forces that column -- and the whole table -- wider
   than the viewport at 375px, which then shrinks mobile Safari's entire
   layout viewport to fit it (a plain scrollWidth check misses this; only an
   innerWidth check at the built page catches it). lib/entity/sections.mjs's
   infoCard already emits this exact class on the website link
   (`<a class="url-wrap" ...>`) -- it had no matching CSS rule until now.
   `anywhere` (not `break-word`) allows a break mid-URL, not just at
   existing soft-wrap points, since a domain/path has none. */
.url-wrap {
  overflow-wrap: anywhere;
}

/* Scroll wrapper for a ledger table with more columns than a narrow
   viewport can hold horizontally (hood-edition tables carry rank+name+
   rating+vs-avg+price -- one column more than the mobile card transform
   below is designed around). Same overflow-x:auto pattern as
   `.busy-chart__tabs` (venue.css) and `.app-bar nav` (layout.css) -- a
   real class instead of an inline style="" (design-lint's inline-style
   allowlist only permits custom-prop/inline-size declarations). */
.ledger-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ── Row anatomy (RD visual-redesign pass): a ranked row is not a bare
   hyperlink -- the rank gets a real fixed-width numeral column, the name
   carries a muted meta subline, and the rating renders as one designed
   right-aligned unit (star + value on top, review count small underneath)
   instead of a flat "4.8★ (1,580)" string. Same classes on <th> and <td>
   so header + data columns agree on width/alignment; the mobile card
   transform (@media block below) keeps the same classes, just re-flows
   them into the card's own line layout. */
.ledger-table .ledger-cell--rank {
  font-family: var(--font-data);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: 1.1em;
  color: var(--ocean);
  width: 3.5ch;
}
.ledger-table .venue-name-line {
  display: block;
  font-weight: 600;
  color: var(--ink);
}
.ledger-table .venue-meta-line {
  display: block;
  margin-block-start: 2px;
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 400;
  color: var(--muted);
}
.ledger-table .rating-unit {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  line-height: 1.3;
}
.ledger-table .rating-unit__stars {
  font-weight: 600;
  color: var(--ink);
}
/* Accent discipline (W14-A1): the numeral itself reads ocean; the &#9733;
   glyph beside it (a bare text node in the same span, not its own element)
   stays ink -- the star-color law (design-tokens.css --sun/--sun-text) is
   "star glyphs are graphics-only, never text," so this scopes the accent to
   exactly the tabular value, never recolors the star itself. */
.ledger-table .rating-unit__stars .num {
  color: var(--ocean);
}
.ledger-table .rating-unit__count {
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 400;
  color: var(--muted);
}

/* ── Split platform rating (V3 answer-first wave, operator 2026-07-21) — a
   rating that used to print as one flat number splits "Google 4.8 / Yelp 4.2"
   onto its own line under the stars. Same muted receipt-size treatment as
   .rating-unit__count above. */
.ledger-table .rating-unit__split {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* "Why this result?" receipt expander (leaderboards.mjs receiptHtml).
   padding-block alone stretches the tap target to ≥44px tall across the
   full row width (§10 tap law). Motion wave update (2026-07-21): `display:
   flex` replaces the native list-item marker with the custom, rotating
   caret defined in motion.css -- `transform`/`rotate` is not a
   spec-supported ::marker property, so a real caret element is the only
   cross-browser way to animate the open/close affordance. */
details > summary {
  cursor: pointer;
  min-height: var(--tap);
  padding-block: var(--sp-3);
  font-weight: 600;
  color: var(--ocean-deep);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  list-style: none;
}
details > summary::-webkit-details-marker { display: none; }
@media (hover: hover) {
  details > summary:hover { color: var(--ocean); }
}

/* ── Mobile leaderboard: table -> card-stack (DESIGN-DIRECTION.md §10,
   W11-D2 mobile pass). `.ledger-table--cards` is applied ONLY to the ranked
   + small-sample leaderboard tables (content/generators/leaderboards.mjs) —
   infoCard/ratingDist/comparisons tables elsewhere keep the plain <table>
   layout, which already fits at 375px (2-3 short columns, no wide receipt/
   expander cell to fight). The <thead> stays in the accessibility tree
   (clip, not display:none) — same technique base.css's .visually-hidden
   utility uses, inlined here since a <thead> can't carry that class without
   a markup edit this item doesn't need. ─────────────────────────────────── */
@media (max-width: 640px) {
  .ledger-table--cards thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
    padding: 0;
    margin: -1px;
  }
  .ledger-table--cards,
  .ledger-table--cards tbody { display: block; width: 100%; }
  .ledger-table--cards tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--sp-3);
    row-gap: var(--sp-1);
    border: 1px solid var(--line);
    border-start-end-radius: var(--r-card);
    padding: var(--sp-3);
    margin-block-end: var(--sp-3);
  }
  .ledger-table--cards tr:last-child { margin-block-end: 0; }
  .ledger-table--cards td { display: block; padding: 0; border: 0; }
  /* .ledger-cell--rank's numeral styling (font/weight/color/size) is now
     the shared, non-gated .ledger-table rule above -- inherited here
     unchanged, so it isn't redeclared. */
  .ledger-table--cards .ledger-cell--name {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .ledger-table--cards .ledger-cell--price {
    font-size: var(--fs-receipt);
    color: var(--ink-2);
  }
  /* The rating unit stacks star/value over the review count on desktop
     (flex-column, right-aligned); inside a wrapping card row that reads
     better as one inline baseline group instead of a floating stack. */
  .ledger-table--cards .rating-unit {
    flex-direction: row;
    align-items: baseline;
    gap: var(--sp-1);
  }
  .ledger-table--cards .ledger-cell--receipt { flex-basis: 100%; }
  /* W18-CORE: THE COLUMN REGISTRY's generic `mobileCardSlot: 'wide'` hook
     (src/kits/table.mjs's renderLedgerTable) -- any registry column marked
     'wide' (the standing/confidence receipt today; a future column can opt
     in with zero CSS changes) gets its own full-width line in the mobile
     card, same treatment `.ledger-cell--receipt` already had before this
     class existed. */
  .ledger-table--cards .ledger-cell--wide { flex-basis: 100%; }

  /* ── V3 answer-first wave mobile additions (operator 2026-07-21).
     .ledger-cell--chg mirrors the existing .ledger-cell--price idiom: a
     typographic-only override, no flex-basis, so the cell keeps flowing
     inline right after the rating cell instead of forcing its own line.
     .ledger-cell--vs and the action-row nested in .ledger-cell--name both
     force a full-width card line, the same way .ledger-cell--receipt/--wide
     already do. */
  .ledger-table--cards .ledger-cell--chg { font-size: var(--fs-receipt); }
  .ledger-table--cards .ledger-cell--vs { flex-basis: 100%; }
  .ledger-table--cards .ledger-cell--vs .vs-line { display: inline; margin-inline-end: var(--sp-3); }
  .ledger-table--cards .ledger-cell--name .action-row { flex-basis: 100%; margin-block-start: var(--sp-2); }

  /* ── Card-order fix (wave-review 2026-07-21): the action-row markup lives
     INSIDE .ledger-cell--name's <td>, so it always rendered right after the
     name/meta lines -- ahead of the rating/Δ/vs/price cells, which are
     separate <td> siblings the tr's flex flow already places later. The
     card's intended read order is name/meta -> rating(+Δ) -> vs -> action
     row LAST. `display: contents` drops the name <td>'s own box so its
     children become direct flex items of the row (same flex context the
     rating/chg/vs/price cells already sit in), letting a plain `order` push
     the action row past every other (order:0 by default) cell without
     touching those cells at all. */
  .ledger-table--cards .ledger-cell--name { display: contents; }
  .ledger-table--cards .ledger-cell--name > .venue-name-line,
  .ledger-table--cards .ledger-cell--name > .venue-meta-line {
    flex: 1 1 100%;
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .ledger-table--cards .ledger-cell--name .action-row { order: 1; }
}

/* ── W18-CORE new registry columns (Yelp / combined / gap) — additive only.
   Yelp and combined cells reuse the EXACT `.rating-unit` markup the Google
   rating column already uses, so they inherit its desktop stacking + mobile
   row-flow styling above for free; the one genuinely new visual is the gap
   column's delta glyph, which needs the same tabular/weight treatment every
   other numeric ledger cell already carries (design-tokens.css's own
   .delta-up/.delta-down/.delta-flat already supply the color). ──────────── */
.ledger-table .ledger-cell--gap {
  font-family: var(--font-data);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ── Change-over-time ledger cell (V3 answer-first wave, operator
   2026-07-21) — clones .ledger-cell--gap's typography as a sibling class so
   a "vs last week" column can appear in the same table without colliding;
   right-alignment comes the same way .ledger-cell--gap already gets it —
   via the shared .ledger-table td.num/th.num rule when markup pairs this
   class with .num. */
.ledger-table .ledger-cell--chg {
  font-family: var(--font-data);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ── Head-to-head ledger cell ("this venue vs. the block/kind average") —
   right-aligned like the other numeric ledger cells; each reference point
   prints as its own .vs-line so more than one comparison can stack in a
   single cell without a second row. The neutrality law: ▲/▼ vocabulary is
   reserved for .ledger-cell--chg (time-change only) -- .vs-line never
   carries a directional glyph, a same/higher/lower comparison is signed
   text (+0.6/-0.2/0.0) only. */
.ledger-table .ledger-cell--vs {
  text-align: right;
}
.vs-line {
  display: block;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.vs-line__ref {
  color: var(--muted);
  font-size: var(--fs-receipt);
}

/* ── Small-sample flag — a dashed, muted inline marker for a stat drawn from
   a thin sample. Deliberately not a .receipt-chip (that family's colors are
   reserved for confidence tiers); this is a quieter, separate signal. */
.sample-flag {
  text-transform: uppercase;
  font-family: var(--font-data);
  font-weight: 600;
  font-size: 0.65rem;
  letter-spacing: 0.04em;
  border: 1px dashed var(--line-2);
  border-start-end-radius: var(--r-chip);
  padding: 0 6px;
  color: var(--muted);
  vertical-align: middle;
}

/* ── Result row (leaderboard/list rows — §10) ─────────────────────────── */
.result-row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 48px;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  text-decoration: none;
  transition: background-color var(--t-fast) var(--ease-out);
}
.result-row:active { background: var(--terrazzo-2); }
@media (hover: hover) {
  .result-row:hover { background: var(--terrazzo-2); }
}
.result-row__rank {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--muted);
  min-width: 2ch;
  text-align: right;
}
/* flex:1/min-width:0/ellipsis so a long venue or category name truncates
   instead of forcing the row (and the page) to scroll horizontally — the
   hard rule, hit on the venue-page "nearby" rows (lib/entity/sections.mjs)
   where a real business name can run long. */
.result-row__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.result-row__meta { flex-shrink: 0; color: var(--muted); font-size: var(--fs-receipt); }
.result-row .receipt-chip { flex-shrink: 0; }
.result-row__signal {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Momentum glyphs — color NEVER alone; the ▲ ▼ · glyph is markup, not
   generated content, so it survives copy/paste and screen readers. */
.result-row__signal .delta-up,
.result-row__signal .delta-down,
.result-row__signal .delta-flat { font-weight: 600; }

/* High-confidence receipt chips carry the brand accent (DESIGN-SPEC-V2 brand-
   color application): the strongest evidence tier reads in ocean, everything
   else stays quiet ink. Attribute set by lib/entity/verdict.mjs from the
   engine's own confidence tier — never parsed client-side. */
.receipt-chip[data-confidence="high"] {
  background: var(--ocean-tint);
  color: var(--ocean-deep);
  border-color: var(--ocean);
}

/* A filtered-out row's [hidden] must always win: the mobile card transform's
   `display:flex` (0,1,1) outspecifies the UA's [hidden] rule (0,1,0), leaving
   hidden rows visually present on phones. (P1's live-verified finding.) */
.ledger-table--cards tr[hidden] { display: none; }

/* ── At-floor note — a row/venue sitting exactly at a data floor (e.g. the
   evidence-floor cutoff) gets a quiet, terrazzo-framed caveat instead of
   silently looking identical to a fully-qualified row. */
.at-floor-note {
  padding: var(--sp-2) var(--sp-3);
  background: var(--terrazzo);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-chip);
  color: var(--muted);
  font-size: var(--fs-receipt);
}

/* ── Recede — a row/section that still renders but is deliberately
   de-emphasized (e.g. a below-floor or non-qualifying entry kept for
   context). Hides the rank numeral rather than showing a misleading one. */
.recede { opacity: 0.72; }
.recede .result-row { background: transparent; }
.recede .result-row__rank { visibility: hidden; }

/* ── Gem badge — a scarce, flamingo-family (amber/harissa) marker for a
   high-rated, low-review-count "hidden gem" venue. Deliberately not a
   .receipt-chip (that family's colors are reserved for confidence tiers). */
.gem-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  background: var(--surface);
  border: 1px solid var(--flamingo);
  border-start-end-radius: var(--r-chip);
  color: var(--flamingo-text);
  font-size: 0.75rem;
}
.gem-badge svg { width: 12px; height: 12px; }

/* ── buttons.css ── */
/* ============================================================
   BUTTONS — the streamline rule (design-tokens.css §6): one rounded
   corner (start-end), hairline + offset-rule elevation, no blurred
   shadows, no scale-on-press (terrazzo tint instead — DESIGN-DIRECTION
   §13 "press state").
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid transparent;
  border-start-end-radius: var(--r-ui);
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}

.btn--primary {
  background: var(--ocean);
  color: #FFFFFF;
  box-shadow: var(--elev-1);
}
@media (hover: hover) {
  .btn--primary:hover { background: var(--ocean-deep); }
}
.btn--primary:active { background: var(--ocean-deep); }

.btn--secondary {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--line);
  box-shadow: var(--elev-1);
}
@media (hover: hover) {
  .btn--secondary:hover { border-color: var(--line-2); background: var(--terrazzo); }
}
.btn--secondary:active { background: var(--terrazzo); }

.btn:disabled {
  cursor: not-allowed;
  opacity: 0.5;
  box-shadow: none;
}

.btn--sm { min-height: 36px; padding: var(--sp-2) var(--sp-4); font-size: var(--fs-receipt); }

/* Quiet inline link that isn't the default <a> (nav items, "view all") */
.link-quiet {
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid var(--line-2);
}
.link-quiet:hover { color: var(--ocean-deep); border-color: var(--ocean-deep); }

/* Segmented control (EN|ES toggle — §10 sticky stack) */
.segmented {
  display: inline-flex;
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-ui);
  overflow: hidden;
}
.segmented__option {
  min-width: var(--tap);
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--ink-2);
  background: var(--surface);
  border: 0;
  text-decoration: none;
  cursor: pointer;
}
.segmented__option + .segmented__option { border-left: 1px solid var(--line); }
.segmented__option[aria-pressed="true"] {
  background: var(--ocean-tint);
  color: var(--ocean-deep);
}

/* ============================================================
   FILTER SYSTEM (Kit I — src/kits/filter.mjs markup, public/filter.js
   behavior). Not wired into any page yet (no generator calls filterBar()/
   filterSheet() today) — styled ahead of that landing per the W11-D2
   mobile-pass mandate, so a future wave item only has to call the kit, not
   also design it. Mobile-first: the full-height bottom sheet is the primary
   mobile UI; `.filter-row--desktop`'s inline bar shows only >=641px,
   replaced below that by `.filter-row--mobile`'s single "Filter" trigger
   (public/filter.js already branches its own animation mode on this same
   641px `config.breakpoint`). Streamline corner + offset-rule elevation,
   terrazzo press-tint (never scale) — DESIGN-DIRECTION.md §6/§10/§13.
   ============================================================ */
.filter-bar {
  position: sticky;
  top: var(--bar-app);
  z-index: var(--z-bar);
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  padding-block: var(--sp-3);
}

.filter-results {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-3);
  padding-block-end: var(--sp-3);
  margin-block-end: var(--sp-3);
  border-bottom: 1px solid var(--line);
}
.filter-results__count { font-size: var(--fs-receipt); color: var(--muted); }
.filter-results__lb {
  margin-inline-end: auto;
  font-size: var(--fs-receipt);
  color: var(--ocean-deep);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.filter-results__lb[hidden] { display: none; }

.filter-sort { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-receipt); color: var(--muted); }
.filter-sort__select {
  font: inherit;
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  color: var(--ink);
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-ui);
  cursor: pointer;
}
.filter-sort__select:focus-visible { outline: 2px solid var(--ocean); outline-offset: 1px; }

.filter-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.filter-row--desktop { display: none; }
.filter-row--mobile { display: flex; }
@media (min-width: 641px) {
  .filter-row--desktop { display: flex; }
  .filter-row--mobile { display: none; }
}

.filter-trigger, .filter-more {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding-inline: var(--sp-4);
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--ink);
  border-start-end-radius: var(--r-ui);
  cursor: pointer;
}
@media (hover: hover) {
  .filter-trigger:hover, .filter-more:hover { background: var(--terrazzo); }
}
.filter-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 1.4em;
  padding-inline: 0.3em;
  background: var(--ocean);
  color: #FFFFFF;
  border-start-end-radius: var(--r-chip);
  font-size: 0.8em;
  font-weight: 700;
}
.filter-badge[hidden] { display: none; }

/* Cuisine/category chip row (city-hub's "other cuisines"/"other categories"
   cross-links) -- promoted from a page-local rule (first written on
   city-hub.html) once kind-leaderboard.html reused the same two class names
   without bringing the styling along, rendering as bare unstyled links. One
   shared rule so every page using .chip-row/.cuisine-chip gets the same
   receipt-chip-adjacent chip treatment. Declared BEFORE .filter-chip--locked
   below (same tie-break the confidence-tier chips rely on) so the locked
   "you are here" background still wins when both classes combine on one
   element, instead of this rule's plain --surface background winning the
   same-specificity tie by sheer source order. */
.chip-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.cuisine-chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-chip);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--fs-ui);
}
.cuisine-chip:hover { border-color: var(--ocean); background: var(--terrazzo); }
.cuisine-chip .num { margin-left: 0.4em; opacity: 0.62; font-size: 0.85em; }

/* toggle chip (rising/creator-pick) + locked static context chip — both
   layer onto .receipt-chip (filter.mjs always pairs the two classes). */
.filter-chip, .filter-toggle { cursor: pointer; }
.filter-chip[aria-pressed="true"], .filter-toggle[aria-pressed="true"] {
  background: var(--ocean-tint);
  border-color: var(--ocean);
  color: var(--ocean-deep);
  font-weight: 600;
}
.filter-chip--locked { cursor: default; background: var(--ocean-tint); color: var(--ocean-deep); }

/* chip/option count nodes — without separation the price chips read as
   dollar amounts ("$3847" = band "$" + count 3847, operator-visible on the
   map chip bar; same glue on segmented "4.5+100" options). Space + mute so
   the count reads as metadata, never the label. filter.mjs bakes the bare
   number (no parens) so client count re-renders are markup-stable. */
.filter-chip .num, .filter-toggle .num, .segmented__option .num {
  margin-left: 0.4em;
  font-size: 0.82em;
  opacity: 0.62;
  font-weight: 400;
}

/* ── popover (5+ option facets, desktop bar only — narrow screens fall
   through to the sheet's accordion instead of opening this) ───────────── */
.filter-popover { position: relative; display: inline-flex; }
.filter-popover__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-ui);
  cursor: pointer;
}
.filter-popover__trigger.is-filled,
.filter-popover__trigger[aria-expanded="true"] {
  border-color: var(--ocean);
  color: var(--ink);
  background: var(--ocean-tint);
}
.filter-popover__caret { font-size: 0.7em; color: var(--muted); }
.filter-popover__panel {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: calc(100% + var(--sp-2));
  z-index: var(--z-popover);
  min-inline-size: 220px;
  max-inline-size: min(320px, calc(100vw - var(--sp-8)));
  max-block-size: 60vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-card);
  box-shadow: var(--elev-2);
  padding: var(--sp-3);
}
.filter-popover__panel[hidden] { display: none; }
.filter-popover__foot {
  display: flex;
  justify-content: flex-end;
  margin-block-start: var(--sp-2);
  padding-block-start: var(--sp-2);
  border-block-start: 1px solid var(--line);
}

.filter-checklist { display: flex; flex-direction: column; gap: var(--sp-1); }
.filter-optlist { display: flex; flex-direction: column; gap: 2px; }
.filter-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding-inline: var(--sp-2);
  font: inherit;
  font-size: var(--fs-ui);
  color: var(--ink-2);
  text-align: start;
  background: transparent;
  border: 0;
  border-start-end-radius: var(--r-chip);
  cursor: pointer;
}
@media (hover: hover) {
  .filter-option:hover { background: var(--terrazzo); color: var(--ink); }
}
.filter-option.is-active { background: var(--ocean-tint); color: var(--ocean-deep); font-weight: 600; }
.filter-option[hidden] { display: none; }
/* large-facet collapse: options past the top-N stay hidden until the group
   is expanded ("Show more") or being searched; an already-selected value
   (.is-active) always stays visible so a chosen filter never disappears
   behind the fold — mirrors public/filter.js's openExpand/applyFacetSearch. */
[data-facet]:not(.is-expanded):not(.is-searching) .filter-optlist > .filter-option--overflow:not(.is-active) {
  display: none;
}
.filter-expand {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-size: var(--fs-receipt);
  color: var(--ocean-deep);
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 0;
}
.filter-expand:hover { text-decoration: underline; text-underline-offset: 2px; }
.filter-search {
  inline-size: 100%;
  min-height: var(--tap);
  font: inherit;
  font-size: var(--fs-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-ui);
  padding-inline: var(--sp-3);
  margin-block-end: var(--sp-2);
}
.filter-search:focus-visible { outline: 2px solid var(--ocean); outline-offset: 1px; }

/* ── applied ✕-chips row (public/filter.js appends .filter-applied__chip /
   --locked buttons at runtime; the empty container ships in the baked
   HTML) ───────────────────────────────────────────────────────────────── */
.filter-applied {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--sp-2);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin-block-start: var(--sp-3);
  padding-block-end: var(--sp-1);
}
.filter-applied[hidden] { display: none; }
.filter-applied__chip { flex-shrink: 0; cursor: pointer; }
.filter-applied__chip::after { content: '✕'; margin-inline-start: var(--sp-2); opacity: 0.7; }
.filter-applied__chip--locked { cursor: default; }
.filter-applied__chip--locked::after { content: none; }
.filter-applied__clear { order: -1; margin-inline-end: var(--sp-2); }

/* ── filter sheet — mobile-first full-height bottom sheet; desktop (>=641px)
   becomes a centered modal panel. JS toggles the [hidden] attribute
   synchronously on open/close (public/filter.js openSheet/closeSheet). ──── */
.filter-sheet { position: fixed; inset: 0; z-index: var(--z-sheet); display: none; }
.filter-sheet:not([hidden]) { display: block; }
.filter-sheet__scrim { position: absolute; inset: 0; background: color-mix(in srgb, var(--ink) 45%, transparent); }
.filter-sheet__panel {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  max-block-size: 86vh;
  max-block-size: 86dvh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-start-start-radius: var(--r-card);
  border-start-end-radius: var(--r-card);
}
@media (min-width: 641px) {
  .filter-sheet__panel {
    inset-block-start: 8vh;
    inset-block-end: auto;
    inset-inline: 0;
    margin-inline: auto;
    inline-size: min(720px, calc(100% - var(--sp-6)));
    max-block-size: 84vh;
    border-start-start-radius: 0;
    border: 1px solid var(--line-2);
  }
}
.filter-sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line);
  flex-shrink: 0;
}
.filter-sheet__title { font-size: var(--fs-h3); font-weight: 700; }
.filter-sheet__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--tap);
  block-size: var(--tap);
  background: transparent;
  border: 0;
  color: var(--muted);
  font-size: 1.1rem;
  cursor: pointer;
}
.filter-sheet__body { padding-inline: var(--sp-5); flex: 1 1 auto; overflow-y: auto; }
.filter-sheet__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--line);
  background: var(--surface);
  flex-shrink: 0;
  position: sticky;
  inset-block-end: 0;
}
body.filter-lock { overflow: hidden; }

.filter-accordion { border-bottom: 1px solid var(--line); }
.filter-accordion__head { margin: 0; }
.filter-accordion__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  inline-size: 100%;
  min-height: var(--tap);
  font: inherit;
  font-weight: 600;
  font-size: var(--fs-ui);
  color: var(--ink);
  background: transparent;
  border: 0;
  padding-block: var(--sp-3);
  cursor: pointer;
  text-align: start;
}
.filter-accordion__trigger::after { content: '▾'; font-size: 0.75em; color: var(--muted); }
.filter-accordion__trigger[aria-expanded="true"]::after { content: '▲'; }
.filter-accordion__body { padding-block-end: var(--sp-4); }
.filter-accordion__body[hidden] { display: none; }

/* ── Action row (Call/Reserve/website — V3 answer-first wave, operator
   2026-07-21) — a compact-cta button family reused by the venue hero, the
   sticky act-bar (venue.css), and per-row leaderboard actions. Distinct from
   .btn: a bordered ink outline by default, primary variant fills ocean. ──── */
.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-block: var(--sp-4);
}
.action-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tap);
  padding: 8px 16px;
  background: var(--surface);
  border: 1px solid var(--ink);
  border-start-end-radius: var(--r-ui);
  box-shadow: var(--elev-2);
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
}
.action-btn:hover { background: var(--terrazzo); }
.action-btn--primary {
  background: var(--ocean);
  border-color: var(--ocean);
  color: #FFFFFF;
}
/* Compact variant — per-row leaderboard actions and the act-bar's own
   Call/Reserve pair, where the full-size action-btn's padding/shadow would
   overwhelm a table row or a slim sticky bar. */
.action-row--compact { gap: var(--sp-2); }
.action-row--compact .action-btn {
  min-height: 44px;
  padding: 6px 12px;
  box-shadow: none;
  font-size: var(--fs-receipt);
}

/* ── layout.css ── */
/* ============================================================
   LAYOUT — container, alternating section bands, sticky app bar,
   staging banner, brand lockup composition (DESIGN-DIRECTION.md §7/§8/
   §10). Mobile-first, single-column by default (§7).
   ============================================================ */

.container {
  max-width: var(--max-w);
  margin-inline: auto;
  padding-inline: var(--sp-4);
}
@media (min-width: 768px) {
  .container { padding-inline: var(--sp-5); }
}

/* Several generators emit the breadcrumb nav as `class="crumbs container"`
   nested INSIDE the page's own `.container` div (e.g. florida.html,
   florida/miami.html, every florida/<city>/<category>.html leaderboard,
   florida/miami/map.html — verified across EN + ES builds). Without this
   reset, the breadcrumb's own `.container` padding-inline stacks on top of
   its ancestor's, indenting it ~16px further than every sibling heading/
   paragraph on the same page (worst on mobile, where 16px is a bigger
   fraction of the ~340px content column). This is a defensive reset for
   ANY nested `.container`, not just the breadcrumb case — a static-HTML
   site with no component boundary to prevent double-nesting should never
   let two ancestors both claim max-width/padding. */
.container .container {
  max-width: none;
  padding-inline: 0;
  margin-inline: 0;
}

/* Breadcrumb nav (src/kits/shell.mjs's crumbs()) — quiet muted trail, no
   underline (nav, not in-prose links); current-page label reads as ink
   to mark it as "you are here". Hover gets the same terrazzo tint the
   rest of the nav/row family uses. */
.crumbs {
  padding-block: var(--sp-3);
  font-size: var(--fs-receipt);
  color: var(--muted);
}
.crumbs a {
  color: var(--muted);
  text-decoration: none;
  border-radius: var(--r-chip);
  padding-inline: 2px;
}
@media (hover: hover) {
  .crumbs a:hover { background: var(--terrazzo); color: var(--ink); }
}
.crumbs [aria-current="page"] { color: var(--ink); font-weight: 600; }

.section { padding-block: var(--sp-6); }
.section--band { background: var(--terrazzo); }
@media (min-width: 768px) {
  .section { padding-block: var(--sp-7); }
}

.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* ── Staging banner (shell.mjs already emits .staging-banner — this fills
   the previously-unstyled gap). Reads as a printed system notice, not a
   generic yellow warning box — ledger voice, instrument face. ────────── */
.staging-banner {
  background: var(--terrazzo-2);
  color: var(--ink-2);
  border-bottom: 1px solid var(--line-2);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  line-height: var(--lh-receipt);
  text-align: center;
  letter-spacing: 0.02em;
  padding: var(--sp-2) var(--sp-4);
}

/* ── Sticky app bar (§10: mark, search, EN|ES toggle) ─────────────────── */
.app-bar {
  position: sticky;
  top: 0;
  z-index: var(--z-bar);
  min-height: var(--bar-app);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-inline: var(--sp-4);
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

/* ── Brand lockup (mark + wordmark, lib/mark.mjs) — min-width:0 lets the
   name truncate instead of pushing the EN|ES switch off-screen at narrow
   widths (mobile masthead pass, W11-D2). ───────────────────────────────── */
.brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  /* The brand NEVER truncates (a wordmark reduced to "P." shipped live and
     the operator saw it — W13-RD). The nav is a scroll strip and absorbs
     every squeeze; the lockup is the one fixed element besides the switch. */
  flex-shrink: 0;
  color: var(--ink);
  text-decoration: none;
}
.brand-lockup__mark { display: block; width: 1.35em; height: 1.35em; flex-shrink: 0; }
.brand-lockup__name {
  font-size: 1.15rem;
  white-space: nowrap;
}
.brand-lockup__dateline { margin-inline-start: var(--sp-2); color: var(--muted); }

/* Language switch never shrinks/wraps away — it's the smallest, most
   important control in the bar (§10 tap law: both segments stay ≥44px). */
.app-bar .segmented { flex-shrink: 0; }

/* ── Mobile nav disclosure menu (M-NAV fix) ───────────────────────────────
   Native <details class="nav-menu">/<summary class="nav-menu__toggle">,
   zero JS — replaces the old always-visible horizontal-scroll strip
   (`.nav-scroll`), which on a real phone (~90px of strip width once the
   brand lockup + EN|ES switch + search trigger claim the rest of a 375px
   bar) showed only "Florida" with no affordance that four more links +
   Subscribe existed past the clipped edge. The `<details>` wraps the SAME
   `<nav class="row">` (5 links) + Subscribe pill the bar has always
   rendered — src/kits/shell.mjs's masthead() doc comment covers why
   Subscribe stays a sibling of that nav rather than a 6th link
   (nav.test.mjs / flows.test.mjs assert exactly 5).
   <768px (media block further down): `.nav-menu__toggle` (the <summary>)
   renders as a bordered pill tap target; opening it reveals
   `.nav-menu__panel` as a bordered dropdown spanning the bar's own content
   width, just under the sticky bar, links + Subscribe stacked.
   >=768px (media block further down): the toggle is hidden and the panel
   is forced inline exactly as the old strip was. */
.app-bar .nav-menu__toggle {
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  gap: var(--sp-2);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-ui);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: var(--fs-ui);
}
.app-bar .nav-menu__toggle::-webkit-details-marker { display: none; }
@media (hover: hover) {
  .app-bar .nav-menu__toggle:hover { background: var(--terrazzo); }
}
.app-bar .nav-menu__toggle:active { background: var(--terrazzo-2); }

/* Subscribe CTA (wave PUB-SHIFT) — a bordered pill so it reads as a
   distinct action beside the plain-text nav links, not another nav item.
   Lives inside `.nav-menu__panel` — stacked full-width below the links on
   mobile, inline beside them on desktop (media blocks below). */
.subscribe-nav-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  border: 1px solid var(--ocean-deep);
  border-start-end-radius: var(--r-chip);
  color: var(--ocean-deep);
  font-weight: 600;
  font-size: var(--fs-ui);
  text-decoration: none;
  white-space: nowrap;
}
@media (hover: hover) {
  .subscribe-nav-link:hover { background: var(--ocean-tint); }
}
.subscribe-nav-link:active { background: var(--ocean-tint); }

/* <768px: the panel only ever renders while the browser itself has the
   <details> open (native behavior, left untouched here) — this rule
   supplies its layout/paper-panel chrome for that state only. `inset-inline`
   mirrors `.app-bar`'s own `padding-inline`, so the panel's edges always
   land exactly on the bar's own content edges — it can never extend past
   the viewport the bar itself already fits inside. */
@media (max-width: 767.98px) {
  .app-bar .nav-menu[open] .nav-menu__panel {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    position: absolute;
    top: 100%;
    inset-inline: var(--sp-4);
    margin-block-start: var(--sp-2);
    z-index: var(--z-bar);
    padding: var(--sp-2);
    background: var(--paper);
    border: 1px solid var(--line-2);
    border-start-end-radius: var(--r-card);
  }
  .app-bar .nav-menu__panel > nav {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 0;
  }
  .app-bar .nav-menu__panel > nav a,
  .app-bar .nav-menu__panel > .subscribe-nav-link {
    width: 100%;
  }
  .app-bar .nav-menu__panel > .subscribe-nav-link { margin-block-start: var(--sp-1); }
  /* Icon-only hamburger toggle on mobile: the visible "Menu" text label is
     dropped (aria-label on the summary keeps the accessible name) so the
     toggle collapses to a ~44px tap target. Without this the toggle text +
     wordmark + EN|ES + search overrun a 320px bar (~50px horizontal
     overflow); a bare hamburger is the standard compact mobile control. */
  .app-bar .nav-menu__toggle > span { display: none; }
  .app-bar .nav-menu__toggle { padding-inline: var(--sp-2); gap: 0; }
}

/* >=768px: force the panel inline regardless of the <details>'s (never
   toggled at this width, since the toggle itself is hidden) `[open]` state.
   Verified live (DevTools computed-style inspection, not just spec reading):
   a closed <details>'s non-summary children render inside an internal UA
   `::details-content` box carrying `content-visibility: hidden; height: 0`
   -- NOT a plain `display:none` on the light-DOM child. Overriding `display`
   on `.nav-menu__panel` itself (an author declaration beating the UA's
   `details:not([open]) > *:not(summary){display:none}` rule by origin,
   regardless of specificity) is therefore necessary but NOT sufficient on
   its own: the panel's OWN box still inherits the `::details-content`
   ancestor's zero-height size containment, so `align-items:center` on
   `.app-bar` centers it as a 0-height item and its actual (44px) content
   renders ~16px below the bar's own bottom edge. Two overrides, both
   required:
   1. `display: contents` on `.nav-menu` dissolves the <details> element's
      own box so `.nav-menu__panel` becomes a DIRECT app-bar flex child
      (the role the old `.nav-scroll` strip had) instead of a nested block
      item collapsing to width:0.
   2. `::details-content` itself must be un-contained (`content-visibility:
      visible; height: auto`) or its zero-size containment still wins even
      though the summary/panel's own `display` was already overridden. */
@media (min-width: 768px) {
  .app-bar .nav-menu { display: contents; }
  .app-bar .nav-menu::details-content {
    content-visibility: visible;
    height: auto;
    /* This internal box is the actual app-bar flex item at desktop (the
       details' own box is dissolved by display:contents). It must be able to
       shrink and scroll its content, otherwise its min-content floor (the nav
       links are nowrap/flex-shrink:0) forces the bar past the viewport at the
       tightest desktop widths (~26px page overflow at 768px). min-width:0 lets
       it shrink; overflow-x:auto keeps its own content on one line, scrolling
       instead of overrunning the page -- the harmless scroll safety net the
       old `.nav-scroll` strip already had. */
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .app-bar .nav-menu__toggle { display: none; }
  .app-bar .nav-menu__panel {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-width: 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .app-bar .nav-menu__panel::-webkit-scrollbar { display: none; }
  .app-bar .nav-menu__panel > nav {
    flex-wrap: nowrap;
    flex-shrink: 0;
  }
  .app-bar .nav-menu__panel > .subscribe-nav-link {
    flex-shrink: 0;
  }
}

/* Bare `<a>` nav links (header main nav + footer legal nav) have no
   sizing of their own beyond the inherited body line-box (~24px) — well
   under the ≥44px tap-target floor. `inline-flex` + `min-height` gives
   every link a real tap height (text stays vertically centered via
   align-items) without touching the shared `<a>` rule (design-tokens.css)
   other inline text links rely on. */
.app-bar nav a,
footer nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  text-decoration: none;
}

/* Main nav reads as chrome, not a row of in-prose links: quiet ink-2,
   background-tint on hover/press (the same terrazzo press pattern
   .result-row/.filter-option already use), no underline. */
.app-bar nav a {
  padding-inline: var(--sp-2);
  border-radius: var(--r-chip);
  color: var(--ink-2);
  transition: background-color var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out);
}
@media (hover: hover) {
  .app-bar nav a:hover { background: var(--terrazzo); color: var(--ink); }
}
.app-bar nav a:active { background: var(--terrazzo-2); color: var(--ink); }

/* ── Footer subscribe block (colophon(), wave PUB-SHIFT) — container
   placement only; src/kits/subscribe.mjs's own form markup carries its
   field/button/consent styling from src/styles/subscribe.css. Capped to
   the prose measure so the heading/sub read as a short editorial pitch,
   not a full-width banner, and sits above the brand lockup + posture line
   with the same rhythm `.stack` already gives every other colophon
   child. ───────────────────────────────────────────────────────────────── */
.footer-subscribe { max-width: var(--measure); }
.footer-subscribe__sub { color: var(--ink-2); }

/* ── Mark size presets (design-preview + favicon-adjacent uses) ───────── */
.mark--16 { width: 16px; height: 16px; }
.mark--24 { width: 24px; height: 24px; }
.mark--32 { width: 32px; height: 32px; }
.mark--160 { width: 160px; height: 160px; }

/* ── Map page chrome (content/generators/city-map.mjs + src/kits/maproom.mjs)
   Page chrome only — the map engine's own tap/hit-test discipline is
   src/assets/map.js's own concern (already ≥44px-padded query boxes there).
   Mobile-first: #map is a shorter frame on phones, taller on desktop, and
   the "+N more in this area" density control (map.js's makeDensityControl,
   documented there as unstyled until this pass) gets real Miami styling. ── */
.maproom h1 { margin-block-end: var(--sp-3); }
.maproom p { max-width: var(--measure); }

#map {
  inline-size: 100%;
  block-size: 60vh;
  min-block-size: 320px;
  max-block-size: 480px;
  margin-block: var(--sp-5);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-card);
  background: var(--terrazzo);
}
@media (min-width: 768px) {
  #map { block-size: 65vh; max-block-size: 640px; }
}

.map-fallback {
  display: grid;
  place-items: center;
  block-size: 100%;
  padding: var(--sp-5);
  color: var(--muted);
  text-align: center;
  font-size: var(--fs-ui);
}
.map-fallback button {
  margin-block-start: var(--sp-3);
  min-height: var(--tap);
  padding-inline: var(--sp-4);
  background: var(--ocean);
  color: #FFFFFF;
  border: 0;
  border-start-end-radius: var(--r-ui);
  font-family: var(--font-ui);
  font-weight: 600;
  cursor: pointer;
}

.map-note { margin-block-start: var(--sp-3); font-size: var(--fs-receipt); color: var(--muted); }

/* #map-scoped so these beat the vendor stylesheet (linked after styles.css
   in mapFrame()'s own markup order). */
#map .maplibregl-popup-content {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-ui);
  font-family: var(--font-ui);
  box-shadow: var(--elev-2);
}
#map .maplibregl-popup-close-button { color: var(--muted); font-size: 1.1rem; }
.map-popup__name { font-family: var(--font-ui); font-weight: 600; margin-block-end: 0.3em; }
.map-popup__meta { font-family: var(--font-data); font-size: var(--fs-receipt); color: var(--muted); }
.map-popup__link { display: inline-block; margin-block-start: var(--sp-2); color: var(--ocean-deep); }

#map .maplibregl-ctrl-group.map-density-ctrl {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-2);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-ui);
  box-shadow: var(--elev-1);
}
.map-density-ctrl__chip {
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  color: var(--muted);
  white-space: nowrap;
}
.map-density-ctrl__action {
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  background: var(--ocean);
  color: #FFFFFF;
  border: 0;
  border-start-end-radius: var(--r-chip);
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

/* ── maproom.css ── */
/* ============================================================
   MAPROOM — the city map explore surface (W11-M1, "learn from Zillow"
   operator mandate). Map-specific styles only; every other component here
   (.result-row, .receipt-chip, .btn, .filter-*, .segmented) is styled in its
   OWN kit file (ledger.css/buttons.css) and simply reused, per
   docs/waves/wave-w11-miami-site.md's fork-not-package convention.

   Wired into public/styles.css via build.mjs's KIT_FILES array
   (build.mjs:93 includes 'maproom.css') -- verified during W13-D3's mobile
   audit; an earlier revision of this comment said otherwise.

   Full-bleed breakout: lib/shell.mjs's renderPage() wrapper never forwards
   kits/shell.mjs's `wide` flag, so every page (including this one) renders
   inside `.container` (max-width: var(--max-w)). The split layout wants
   real viewport width, so `.maproom__layout` breaks out of that container
   with a width:100vw + margin-inline-start:calc(50% - 50vw) rule (see that
   rule's own comment for why), rather than editing either shell file.
   ============================================================ */

.maproom { margin-block-start: var(--sp-2); }
.map-note { color: var(--muted); font-size: var(--fs-receipt); margin-block-start: var(--sp-4); }

/* ── Full-bleed breakout (see header note). NOT the left/right:50% +
   margin:-50vw recipe -- that one only cancels out when the element's own
   containing block already spans the viewport; nested inside `.container`
   (max-width: var(--max-w), centered) it does not, and produces a box
   wider than the viewport (verified during this item's browser proof: a
   1280px viewport measured a 2400px-wide layout + 1100px+ of horizontal
   overflow). width:100vw + margin-inline-start:calc(50% - 50vw) is the
   correct recipe for breaking out of a centered, narrower ancestor -- BUT
   plain `100vw` itself is 1-2 scrollbar-widths WIDER than the real usable
   viewport in most browsers (100vw includes the vertical scrollbar
   gutter; window.innerWidth/clientWidth does not), which the same browser
   proof caught as a residual 40px horizontal-scroll overflow even after
   the formula fix. src/assets/map.js measures the real width
   (window.innerWidth) into --vw100 on load + resize (a file this item
   already owns) specifically so this rule never has to guess; `100vw`
   below is ONLY the pre-JS/no-JS fallback for the first paint. ─────────── */
.maproom__layout {
  /* Explicit, not just inherited from base.css's `*{box-sizing:border-box}`
     -- this box's width is an exact pixel budget (real viewport width via
     --vw100), so it self-defends against box-sizing:content-box (from any
     override, or the base rule somehow not applying) adding padding on top
     instead of inside; belt-and-suspenders, same instinct as the explicit
     `overflow:hidden` below. */
  box-sizing: border-box;
  width: var(--vw100, 100vw);
  max-width: var(--vw100, 100vw);
  margin-inline-start: calc(50% - (var(--vw100, 100vw) / 2));
  margin-block-start: var(--sp-4);
  display: flex;
  flex-direction: column;
  overflow: hidden; /* belt-and-suspenders for the brief pre-JS 100vw fallback */
}

/* ── Mobile (default): map fills the viewport below the app bar; the result
   panel is a draggable bottom sheet over it (peek/half/full, JS-driven
   inline `height`); a full-screen-map FAB sits on the map itself. Map
   ALWAYS visible (standing explore-surface ruling) — the sheet never
   fully covers it, even at its "full" snap point (92vh, per map.js's
   SHEET_SNAPS). ────────────────────────────────────────────────────────── */
.maproom__mapwrap {
  position: sticky;
  top: var(--bar-app);
  height: 56vh;
  height: 56dvh;
}
/* Explicit resets, not just position/inset: a concurrent mobile-pass commit
   (layout.css, outside this item's allowed paths) added its own unscoped
   `#map { block-size:60vh; margin-block:...; border:...; background:... }`
   rule (plus a `@media (min-width:768px) { #map { max-block-size:640px } }`
   companion), written against the OLD bare-map markup this item's split
   layout has superseded. `.maproom__mapwrap #map` (specificity 1,1,0)
   already beats layout.css's bare `#map` (1,0,0) for any property BOTH
   declare, so explicitly re-declaring every property that rule (and its
   768px media companion) sets -- not just leaving them uncontested --
   neutralizes it instead of silently inheriting it. Caught during this
   item's own browser proof: without the max-block-size reset, #map was
   still capped at a stale 640px tall regardless of its wrapper's real
   height. #map must fill `.maproom__mapwrap` edge to edge with no border/
   margin/background/size-cap of its own now that the wrapper owns framing. */
.maproom__mapwrap #map {
  position: absolute;
  inset: 0;
  inline-size: auto;
  block-size: auto;
  max-inline-size: none;
  max-block-size: none;
  min-inline-size: 0;
  min-block-size: 0;
  margin: 0;
  border: 0;
  background: transparent;
}

.maproom__panel {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-map-ui);
  height: 50vh; /* JS drag/FAB rewrite this inline once interacted with -- matches SHEET_SNAPS.half, map.js also sets this explicitly on load */
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border-start-start-radius: var(--r-card);
  border-start-end-radius: var(--r-card);
  box-shadow: var(--elev-2);
  transition: height 200ms var(--ease-out); /* mandate: snap transitions 200ms ease-out -- inert at desktop (media block below forces transition:none) */
  padding-inline: var(--sp-4);
  padding-block-end: max(var(--sp-3), env(safe-area-inset-bottom));
}

/* Full-width drag strip, not a thin sliver -- Google-Maps-class grab
   affordance: a real 44px+ tall, full-width tap/drag zone with a clearly
   visible pill (operator: "the list isn't really reachable by hand"). The
   old 40x4px bar met the --tap height on paper but read as too small to
   notice or aim for on a phone. */
.maproom__sheet-handle {
  flex-shrink: 0;
  width: 100%;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  padding-block: var(--sp-3);
  background: transparent;
  border: 0;
  cursor: grab;
  touch-action: none;
}
.maproom__sheet-handle::before {
  content: '';
  display: block;
  width: 48px;
  height: 5px;
  background: var(--line-2);
  border-radius: var(--r-round);
  transition: background-color var(--t-fast) var(--ease-out);
}
.maproom__sheet-handle:active::before { background: var(--muted); }
@media (hover: hover) {
  .maproom__sheet-handle:hover::before { background: var(--muted); }
}
.maproom__sheet-handle:active { cursor: grabbing; }

.maproom__panel [data-filter-root] { flex-shrink: 0; }
/* The shared .filter-bar sticks to `var(--bar-app)` on a page's own scroll
   (buttons.css) -- nested here inside the panel's own scroll context there
   is no duplicate app bar above it, so it should stick to the panel's own
   top edge instead. Scoped override, not an edit to the shared rule. */
.maproom__panel .filter-bar { position: static; top: 0; border-bottom: 0; padding-block: var(--sp-2) 0; }

.maproom__toolbar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  padding-block: var(--sp-2);
}
.maproom__follow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  font-size: var(--fs-receipt);
  color: var(--ink-2);
  cursor: pointer;
}
.maproom__follow input { width: 18px; height: 18px; accent-color: var(--ocean); cursor: pointer; }

/* map.js toggles `researchBtn.hidden` at runtime (true on click, and again
   whenever "follow map" re-enables) — not just a static initial-load state.
   `.btn`'s own `display: inline-flex` (buttons.css) is an unconditional
   author rule, so without this it wins over the plain `[hidden]` the
   browser sets by default: the button silently stays visible after every
   click, defeating the show/hide interaction (same regression class as the
   `.ledger-table--cards tr[hidden]` fix above — any class that sets an
   unconditional `display` needs its own `[hidden]` carve-out). */
.maproom__research[hidden] { display: none; }

.maproom__count {
  flex-shrink: 0;
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-receipt);
  color: var(--muted);
  margin-block-end: var(--sp-2);
}

.maproom__cards {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* The list is the ONE thing that still scrolls once the mobile viewport
     is locked (see the `.maproom-locked` block below) -- without this,
     swiping past the last card chains the scroll gesture to whatever
     ancestor scrolls next. Before the lock existed that ancestor was the
     document itself (the operator's "can't scroll to list" / footer-overlap
     report); with the lock in place there is no scrollable ancestor left at
     all, so a chained overscroll would just rubber-band the (fixed, inert)
     body. Containing it here is correct in both cases, not locked-specific. */
  overscroll-behavior: contain;
  overflow-x: hidden; /* axis-pairing gave this an implicit auto; pills must shrink, not scroll (judge S3) */
}

.maproom__card { cursor: pointer; }
.maproom__card:focus-visible { outline: 2px solid var(--ocean); outline-offset: -2px; }
.maproom__card.is-hover { background: var(--ocean-tint); }
.maproom__card-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.maproom__card .result-row__name { white-space: normal; overflow-wrap: anywhere; }

/* ── Card row layout: the rating chip gets its own line (W-fix: chip pushed
   past the row's right edge at 375px). ledger.css's .result-row lays rank/
   name/chip out as flex SIBLINGS on one line; `.receipt-chip` itself has
   flex-shrink:0 (by design -- its own text should never wrap mid-digit) but
   its immediate flex parent, `.result-row__signal`, does not share that
   protection, so the outer row's shrink distribution can size that parent
   narrower than the chip actually needs -- the chip then just overflows its
   own parent, and `.maproom__cards`' overflow-x:hidden only clips the
   result instead of fixing the layout. Live-DOM measurement during this
   fix (real fonts, real 40-row dataset) found `.result-row__signal` is
   assigned its full natural width in almost every case regardless of name
   length -- i.e. there is no reliable shrink margin here to tune; the chip's
   fit depends entirely on rank+gaps+chip width leaving zero structural
   safety margin against a longer vote count, a wider font, or a multi-digit
   rank. Grid removes that dependency for this component only: rank+name
   stay side by side exactly as before (that pairing never overflowed), the
   chip becomes its own full-width line underneath, right-aligned like a
   receipt subtotal -- taking it out of the horizontal budget entirely, so
   it can never be pushed past the edge no matter how long the name, vote
   count, or rank number gets. Single-class specificity ties with ledger.css's
   `.result-row` (both 0,1,0); this file loads after ledger.css in build.mjs's
   KIT_FILES order, so it wins on source order alone, same as the
   `.result-row__name` override just above. */
.maproom__card {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "rank body"
    ".    signal";
  column-gap: var(--sp-4);
  row-gap: var(--sp-1);
}
.maproom__card .result-row__rank { grid-area: rank; }
.maproom__card .maproom__card-body { grid-area: body; }
.maproom__card .result-row__signal {
  grid-area: signal;
  justify-self: end;
  margin-left: 0;
}

.maproom__fab {
  position: absolute;
  inset-inline-end: var(--sp-4);
  inset-block-end: var(--sp-4);
  z-index: var(--z-map-ui);
  width: 56px;
  height: 56px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  color: var(--paper);
  border: 0;
  border-radius: var(--r-round);
  box-shadow: var(--elev-2);
  font-size: 1rem;
  cursor: pointer;
}
.maproom__fab[aria-pressed="true"] span { transform: rotate(180deg); display: inline-block; }

/* ── Map controls relocated to the bottom-right thumb zone (map.js now adds
   BOTH the zoom control and the density control at 'bottom-right' --
   Google-Maps-class stacking). `.maproom__mapwrap` is a fixed 56vh box, well
   above the bottom sheet's 84vh peek line, so the stack stays reachable with
   one thumb and is only ever covered when the reader deliberately drags the
   sheet up over it. Clearance below so MapLibre's own control stack never
   overlaps the full-screen-map FAB sitting in the same corner. ─────────── */
.maproom__mapwrap .maplibregl-ctrl-bottom-right {
  margin-block-end: calc(56px + var(--sp-4) + var(--sp-2));
}

/* ── Mobile filter chip bar + focused facet panel (operator: "the filter
   opens up and down, very hard to reach"). kits/filter.mjs's
   filterBar(variant:'map') already renders every facet as a segmented/
   toggle/popover chip PLUS a single mobile "Filter" trigger -- buttons.css's
   own 641px breakpoint hides the chip row below that width and shows only
   the trigger, so on a phone every facet (price, rating, cuisine) was buried
   behind one button that opened an 86vh near-full-screen accordion sheet.
   Below, mobile sees the same pinned chip row the desktop bar already has
   (`.maproom__panel [data-filter-root]` is already flex-shrink:0, sitting
   above the scrolling card list, so it stays visible at every sheet snap),
   made horizontally scrollable instead of wrapping so it never crowds. A
   facet needing more than a chip (5+ options, e.g. cuisine) still opens the
   SAME filter-sheet dialog the "All filters" trigger already used (focus
   trap + Escape + sticky Apply/Clear, built once) -- src/assets/map.js
   redirects that ONE trigger type below this same breakpoint and pre-
   focuses the tapped facet's own section; only the overlay's SHAPE changes
   here, capped + bottom-anchored (max 60vh) instead of the site-wide 86vh
   near-full-screen sheet, so it reads as a small panel over the bottom
   sheet, never a takeover. Scoped to <900px (map.js's own SPLIT_BREAKPOINT)
   -- the desktop split layout is untouched. ─────────────────────────────── */
@media (max-width: 899.98px) {
  .maproom__panel { overflow: hidden; }

  .maproom__panel .filter-row--desktop { display: flex; }
  .maproom__panel .filter-row--mobile { display: none; }
  .maproom__panel .filter-row--facets {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  .maproom__panel .filter-row--facets > * { flex-shrink: 0; }

  .maproom__panel .filter-sheet__panel {
    inset-block-start: auto;
    inset-block-end: 0;
    margin-inline: 0;
    inline-size: 100%;
    max-block-size: 60vh;
    max-block-size: 60dvh;
    border-start-start-radius: var(--r-card);
    border-start-end-radius: var(--r-card);
    border: 0;
  }
}

/* ── Locked-viewport mobile map room (W16-A2b, operator phone screenshots) ──
   Two bugs, one root cause ("the map room still uses normal document flow
   on mobile while the sheet is viewport-fixed"):
     1. At first paint, crumbs + h1 + lede (full-size, stacked) ate most of
        the viewport before the map even started -- "the map is a thin
        strip", the bottom sheet covering the rest.
     2. Once the reader scrolled, the DOCUMENT (still normal-flow) scrolled
        the real <footer> colophon up underneath/over the fixed sheet's own
        list -- "can't scroll to list", "Showing 200 of 7082 overlapped by
        the colophon".
   Fix: the Google-Maps locked-viewport model. src/assets/map.js's
   updateViewportLock() toggles `.maproom-locked` on <html> whenever this
   page has the split filter/list layout (a bare dot-viewer city has no
   `.maproom__panel` at all and is never locked) AND the viewport is below
   SPLIT_BREAKPOINT (900px -- must match map.js's own constant, same as
   every other mobile block in this file). Locked: the whole document stops
   scrolling; body becomes a single fixed-to-viewport flex column (app bar,
   fixed height, then one area that fills exactly what's left); the ONLY
   thing that still scrolls is the sheet's own internal list
   (.maproom__cards, already `overflow-y:auto` + `overscroll-behavior:
   contain` above). Desktop (>=900px) is completely untouched -- this
   entire block is gated inside the same <900px query every other mobile
   rule in this file uses. ───────────────────────────────────────────────── */
@media (max-width: 899.98px) {
  html.maproom-locked,
  html.maproom-locked body {
    overflow: hidden;
  }
  html.maproom-locked body {
    position: fixed;
    inset: 0;
    width: 100%;
    display: flex;
    flex-direction: column;
  }
  html.maproom-locked .app-bar { flex-shrink: 0; }
  /* The document colophon is physically unreachable once the page can't
     scroll to it -- its six legal links are RELOCATED (map.js, the real DOM
     node, not a duplicate) into the sheet's own list instead, so hiding the
     now-empty husk here loses nothing. */
  html.maproom-locked footer.section--band { display: none; }

  html.maproom-locked #main {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  html.maproom-locked #main > .container {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  html.maproom-locked .maproom {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    margin-block-start: 0;
  }
  /* map-note ("Public Google data...") is supplementary disclosure already
     duplicated in the page's own meta description -- dropped visually on
     the locked mobile page, same allowance the lede gets below, so the map
     gets 100% of the remaining height rather than sharing it with a
     trailing paragraph the reader would need to scroll a scroll-locked page
     to see anyway. */
  html.maproom-locked .map-note { display: none; }

  html.maproom-locked .maproom__layout {
    flex: 1 1 auto;
    min-height: 0;
    margin-block-start: 0;
  }
  html.maproom-locked .maproom__mapwrap {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
    position: relative;
    top: auto;
  }
  /* The FAB is `position:absolute` bottom-anchored WITHIN `.maproom__mapwrap`
     (unchanged, non-locked rule below) -- fine when mapwrap was a short,
     fixed 56vh box, but once locked it grows to fill the ENTIRE remaining
     height (often 500-600px+), so a bottom anchor drifts the FAB deep into
     territory the fixed sheet visually covers at its "half"/"full" snaps
     (verified in this item's own screenshot proof: at "full" the FAB sat on
     top of the relocated legal row's text). Anchoring to mapwrap's TOP
     instead keeps it pinned just under the compact header -- a region the
     sheet can never reach (it only ever grows from the BOTTOM, capped at
     SHEET_SNAPS.full) -- so the button stays reachable and never overlaps
     sheet content at any snap point. */
  html.maproom-locked .maproom__fab {
    inset-block-start: var(--sp-4);
    inset-block-end: auto;
  }
}

/* ── Compact mobile header bar (crumbs + h1 one-liner over the map) ───────
   `.maproom__topbar` wraps crumbs+h1+lede as one unit (content/generators/
   city-map.mjs) so it can compress into a single small bar instead of three
   full-size blocks. Applies whenever the viewport is mobile-width, whether
   or not JS has locked the page yet (avoids a flash of the full-size header
   for the brief pre-JS instant, matching this file's existing pre-JS
   fallback discipline elsewhere, e.g. `.maproom__layout`'s 100vw fallback). */
@media (max-width: 899.98px) {
  .maproom__topbar {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding-block: var(--sp-2) var(--sp-1);
  }
  .maproom__topbar .crumbs {
    padding-block: 0;
    white-space: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .maproom__topbar .crumbs::-webkit-scrollbar { display: none; }
  .maproom__topbar h1 {
    font-size: var(--fs-h3);
    line-height: var(--lh-h3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* The full lede sentence is not needed on mobile (map.js's header comment
     + this file's own header note) -- its content is already carried by the
     page's <meta name="description">, so dropping it here de-duplicates
     rather than removes information. */
  .maproom__lede { display: none; }
}

/* ── Relocated footer legal row (inside the sheet's list, mobile-locked
   only) -- map.js moves the real <footer> legal-links <nav> here (never a
   duplicate) once the page locks; restored to the footer on resize back to
   desktop. Reuses `.row`'s own flex/gap (layout.css) via the SAME element,
   this just adds the in-list spacing/separator. Desktop never receives this
   class (map.js only relocates below 900px) -- the width gate below is a
   defensive no-op, not the primary guard. */
.maproom__legal-row {
  flex-shrink: 0;
  padding-block: var(--sp-4) var(--sp-2);
  margin-block-start: var(--sp-2);
  border-block-start: 1px solid var(--line);
  font-size: var(--fs-receipt);
}
@media (min-width: 900px) {
  .maproom__legal-row { display: none; }
}

/* ── Desktop (>=900px, must match map.js's SPLIT_BREAKPOINT): list column +
   sticky map pane, both plain in-flow -- the mobile sheet/FAB/handle retire
   entirely. Sticky right pane >= 440px (DESIGN-DIRECTION.md #9). ────────── */
@media (min-width: 900px) {
  .maproom__layout {
    flex-direction: row;
    align-items: flex-start;
    gap: var(--sp-5);
    padding-inline: var(--sp-5);
    overflow: visible;
  }
  .maproom__panel {
    position: sticky;
    inset-inline: auto;
    inset-block-end: auto;
    top: calc(var(--bar-app) + var(--sp-4));
    z-index: auto;
    flex: 0 0 400px;
    width: 400px;
    height: auto !important; /* clears any mobile drag-set inline height */
    max-height: calc(100vh - var(--bar-app) - var(--sp-6));
    max-height: calc(100dvh - var(--bar-app) - var(--sp-6));
    overflow-y: auto;
    border-radius: 0;
    box-shadow: none;
    border-inline-end: 1px solid var(--line);
    padding-inline: 0 var(--sp-4);
    padding-block-end: var(--sp-4);
    transition: none !important;
  }
  .maproom__sheet-handle, .maproom__fab { display: none; }
  .maproom__mapwrap {
    position: sticky;
    top: calc(var(--bar-app) + var(--sp-4));
    flex: 1 1 auto;
    height: calc(100vh - var(--bar-app) - var(--sp-6));
    height: calc(100dvh - var(--bar-app) - var(--sp-6));
    min-height: 440px;
    border-start-end-radius: var(--r-card);
    overflow: hidden;
  }
}

/* ── Density control (top-right MapLibre IControl: "+N more in this area" +
   "Search this area", src/assets/map.js's makeDensityControl()) ───────────
   The SAME concurrent layout.css commit above also styled this control,
   under the compound selector `#map .maplibregl-ctrl-group.map-density-ctrl`
   (specificity 1,2,0 — higher than a plain `.map-density-ctrl` class rule
   would be). Repeating that exact selector string here (rather than just
   `.map-density-ctrl`) ties the specificity; layout.css precedes
   maproom.css in build.mjs's KIT_FILES concat order, so on a tie THIS rule
   (later in the cascade) wins, cleanly superseding the stale one instead of
   silently losing to it. */
#map .maplibregl-ctrl-group.map-density-ctrl {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding-inline: var(--sp-2);
  background: var(--surface);
}
.map-density-ctrl__chip {
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  color: var(--ink-2);
  white-space: nowrap;
}
.map-density-ctrl__action {
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ocean-deep);
  background: transparent;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
}
.map-density-ctrl__action:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ── Venue popup (MapLibre Popup content -- the wrapping .maplibregl-popup*
   chrome comes from the vendored maplibre-gl.css) ────────────────────────── */
.map-popup { padding: var(--sp-1) 0; font-family: var(--font-ui); }
.map-popup__name { font-weight: 700; color: var(--ink); margin-block-end: var(--sp-1); }
.map-popup__meta { font-family: var(--font-data); font-size: var(--fs-receipt); color: var(--muted); }
.map-popup__link {
  display: inline-block;
  margin-block-start: var(--sp-2);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ocean-deep);
}

/* ── Card rating chip (W13 RD round 2, visual only) ───────────────────────
   map.js's renderCards() emits the rating+vote pair as a plain
   `.receipt-chip` inside `.result-row__signal` ("4.6 · 1,234", no star
   glyph). Scoped to `.maproom__card` only -- never touches the shared
   `.receipt-chip` rule (ledger.css, out of this item's allowed paths) or
   its confidence-tier color variants. A star glyph is the only addition,
   matching the rating-display grammar used on the venue page and the
   platform-compare unit (venue.css) -- purely decorative, so it is a CSS
   generated-content star, not a DOM node a screen reader would announce
   out of context. ─────────────────────────────────────────────────────── */
.maproom__card .receipt-chip::before {
  content: '\2605';
  margin-inline-end: 0.3em;
  color: var(--sun);
}

/* ── No-WebGL / context-lost fallback ─────────────────────────────────────── */
.map-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: var(--sp-5);
  text-align: center;
  color: var(--ink-2);
  background: var(--terrazzo);
}
.map-fallback [data-map-reload] {
  display: inline-flex;
  margin-inline-start: var(--sp-2);
  color: var(--ocean-deep);
  font-weight: 600;
  background: transparent;
  border: 0;
  cursor: pointer;
}

/* ── Mini-map preview (W21 maps wave) — a compact, NON-interactive MapLibre
   box wrapped in one <a> straight into the full map room (src/kits/
   minimap.mjs + src/assets/mini-map.js). Used by the venue-page locator
   section and the city-hub mini-map. `pointer-events: none` on the box
   itself is a CSS-level guarantee that a tap always reaches the wrapping
   <a> regardless of what MapLibre attaches internally -- belt-and-braces
   alongside the script's own `interactive:false` (mobile scroll-trap
   avoidance: this box must never capture a touch scroll passing over it). */
.mini-map-link {
  display: block;
  border: 1px solid var(--line-2);
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--terrazzo);
  text-decoration: none;
}
.mini-map-link:hover .mini-map-cta,
.mini-map-link:focus-visible .mini-map-cta {
  text-decoration: underline;
  text-underline-offset: 2px;
}
.mini-map {
  display: block;
  width: 100%;
  height: 160px;
  pointer-events: none;
  background: var(--terrazzo);
}
@media (min-width: 900px) {
  .mini-map { height: 220px; }
}
.mini-map-cta {
  display: block;
  padding: var(--sp-3) var(--sp-4);
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ocean-deep);
}

/* ── Minimize/expand toggle (map-toggle wave) — a plain button, sibling of
   `.mini-map-link` (never nested inside it, see src/assets/mini-map.js's
   header). Collapsing sets `.is-minimized` on `.mini-map-wrap`, which drops
   `.mini-map`'s box to a thin (0-height) strip -- the CTA link stays visible
   and reachable either way. Never draggable/floating (explicitly rejected):
   this only ever collapses/expands in place. */
.mini-map-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-block-end: var(--sp-2);
}
.mini-map-toggle {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ocean-deep);
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: var(--r-card);
  cursor: pointer;
}
.mini-map-toggle:hover,
.mini-map-toggle:focus-visible {
  color: var(--ink);
}
.mini-map-wrap.is-minimized .mini-map {
  height: 0;
}

/* ── brand.css ── */
/* ============================================================
   BRAND — the Platerian wordmark treatment (W13-D1 brand-mark pass).
   Styles the two-tone logotype src/kits/shell.mjs's masthead() now
   builds beside lib/mark.mjs's Solstep glyph (mark + word lockup,
   the same SHAPE the sibling brand's masthead uses -- a colored icon
   next to a styled word). Own tokens, own type family, own case
   treatment throughout (brand-separation hard rule, CLAUDE.md) --
   never the sibling brand's palette, fonts, or script.
   ============================================================ */

.brand-word {
  font-family: var(--font-data);
  font-weight: 600;
  /* Operator directive 2026-07-22: the masthead wordmark renders in caps.
     Uppercase is CSS-only (text-transform) -- the DOM text stays
     mixed-case ("Platerian", two-tone-split "Plate"/"rian") so
     shell.test.mjs/nav.test.mjs's text assertions stay green and the
     two-tone split logic in shell.mjs's twoToneWordmark() needs no
     change. Letter-spacing flips from the old -0.02em (tuned for
     mixed-case, where negative tracking reads as confident/tight) to
     +0.02em -- negative tracking on true caps collides adjacent glyphs
     (no descenders/ascenders to create visual air), so caps need
     breathing room, not less of it. Kept modest (bottom of the
     0.02-0.04em band that reads well for short caps wordmarks) since
     this face is only weight 600 with no width-axis widening, unlike
     the footer's own caps wordmark (lib/mark.mjs's wordmarkLockup(),
     the .marquee register in design-tokens.css) which is weight 700 +
     a "wdth" 125 variation and earns its own wider 0.04em tracking --
     different font/weight, so not required to match this value. */
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.1;
}
.brand-word__seg--ink { color: var(--ink); }
.brand-word__seg--ocean { color: var(--ocean); }

/* ============================================================
   MARK ANIMATION — the utensil bar-chart crossing into a star
   (W14-A4, operator spec 2026-07-18: "redo the logo, can be the bar
   chart, but with fork, knife and spoon, animate that they cross to
   like a star and come back"). lib/mark.mjs draws each utensil as its
   own `<g class="mark__utensil mark__utensil--{fork,knife,spoon}">`.

   W14-A5 update: the glyph SHAPES were swapped for real, licensed
   icon-set paths (v2's silhouettes read as blobs per operator verdict) --
   this animation layer needed ZERO changes to do it. lib/mark.mjs's new
   utensils were deliberately built to share the exact same bounding-box
   CENTER as v2's (fork 7/22.5, knife 16/19.5, spoon 25/16.5 -- verified via
   getBBox(), see that file's header), which is exactly what the
   `transform-origin` values and translate-distance keyframes below are
   calibrated against. The new shapes are bulkier than v2's minimal ones
   (a real 4-tine fork, a real curved blade, a real oval bowl all cover
   more area than a notch/point/dome), so the peak-cross collision this
   file's fork-paint-order-last comment already solved once was re-checked
   against the new geometry via a mid-animation screenshot -- still reads
   as a clean 3-spoke burst, see repo-root shots-a5/.

   Palette v2 update (operator directive 2026-07-21): the three utensils were
   redrawn to equal heights (each spans SVG y 5-28, no longer a "bar chart" of
   different heights) and re-laid-out left-to-right as fork/knife/spoon, each
   tinted its own hue (fork harissa, knife slate, spoon sage) instead of the
   shared two-tone ink/ocean fill. Every utensil's raw (untransformed) path
   bbox was measured live via getBBox() (Playwright + system Chrome, not
   estimated) and the per-utensil `translate(tx,ty) scale(s)` on the INNER
   <g> (lib/mark.mjs) recomputed from that measurement to hit the new equal-
   height layout with balanced ~1.766 unit margins/gaps. transform-origin and
   the translate-keyframe distances below were recalibrated from the NEW
   rendered bbox centers (fork 6.608/16.5, knife 17.242/16.5, spoon
   26.634/16.5) -- mechanism, rotation angles (-60/0/+60), durations (860ms),
   and delay (400ms) are all unchanged from W14-A5; only the geometry moved.
   Re-verified peak-pose collision-free via the same getBBox + screenshot
   method (see this item's build report for the shots). The A4/A5 bbox
   centers/heights cited two paragraphs up are history, not live -- the
   transform-origin/keyframe values actually in force are the ones just
   above and in the rules below.

   Masthead-only flourish: the footer's lockup (lib/mark.mjs's
   wordmarkLockup(), '--static' modifier) opts out below, so the crossing
   stays something seen at the top of the page, not on every scroll to
   the colophon.

   Mechanism (revised from a first draft): each utensil TRANSLATES from
   its bar-chart slot to a shared hub point (16,16) while ROTATING to its
   own target angle, arriving at the hub already spun into place. A first
   attempt used ONE literal shared transform-origin with rotate() alone
   (no translate) -- screenshot-verified (W14-A4 build report) that this
   collapses fork+spoon into a single downward checkmark, not a star:
   each bar's own centroid sits at a very different radius/angle from any
   one shared point, so a uniform rotation delta sweeps them past each
   other instead of fanning them out. Moving each utensil's OWN center to
   the hub while rotating gives full control over the peak pose and
   reliably reads as a 3-spoke burst (screenshot-verified, repo-root
   shots-a4/6-peak-cross-35pct.png). transform-origin is therefore each
   utensil's OWN bounding-box center (not literally identical across the
   three), but all three still converge on and rotate through the SAME
   hub coordinate (16,16) -- the shared "crossing point" the operator
   asked for.

   transform-box: view-box makes each origin resolve in the SVG's OWN
   0-0-32-32 coordinate space rather than the rendered pixel box, so the
   same relative motion holds whether the mark renders at 16px
   (favicon-equivalent inline use) or 160px (hero contexts).

   Reduced motion: design-tokens.css already forces
   `*, *::before, *::after { animation: none !important }` globally under
   prefers-reduced-motion (see design-tokens.css's own closing block) --
   no separate override needed here. With animation cancelled, the
   utensils simply hold their base `rotate: 0deg` / `translate: 0 0` --
   static, upright, exactly the idle pose.
   ============================================================ */

/* A strong ease-in-out for the "return" leg only (design-tokens.css
   declares --ease-out but no ease-in-out yet) -- the "cross" leg reuses
   the site's existing --ease-out (entering/exiting rule); the "return"
   leg is on-screen movement, which the animation standard calls for
   ease-in-out, not another ease-out. Scoped to this file, not promoted
   to design-tokens.css (out of this item's allowed paths). */
.mark__utensil {
  --ease-in-out-mark: cubic-bezier(0.65, 0, 0.35, 1);
  transform-box: view-box;
  rotate: 0deg;
  translate: 0 0;
}
/* Each utensil's own bounding-box center in the palette-v2 layout (fork
   6.608,16.5 / knife 17.242,16.5 / spoon 26.634,16.5 -- measured via
   getBBox() against the live markup, see the header comment above) -- the
   point that spins in place before being carried to the shared hub by the
   `translate` keyframes below. */
.mark__utensil--fork  { transform-origin: 6.608px 16.5px; }
.mark__utensil--knife { transform-origin: 17.242px 16.5px; }
.mark__utensil--spoon { transform-origin: 26.634px 16.5px; }

/* Fork/spoon rotate to -60deg/+60deg (knife stays upright, 0deg -- the
   center spoke of the asterisk) while all three translate their own
   center to the shared hub (16,16), so the peak pose reads as a single
   3-spoke burst rather than three bars merely tilting in place. Distances
   are each utensil's (16,16) minus its own transform-origin above. */
@keyframes utensil-cross-fork {
  0%   { translate: 0 0;             rotate: 0deg;   animation-timing-function: var(--ease-out); }
  35%  { translate: 9.392px -0.5px;  rotate: -60deg; }
  49%  { translate: 9.392px -0.5px;  rotate: -60deg; animation-timing-function: var(--ease-in-out-mark); }
  100% { translate: 0 0;             rotate: 0deg; }
}
@keyframes utensil-cross-knife {
  0%   { translate: 0 0;              animation-timing-function: var(--ease-out); }
  35%  { translate: -1.242px -0.5px; }
  49%  { translate: -1.242px -0.5px; animation-timing-function: var(--ease-in-out-mark); }
  100% { translate: 0 0; }
}
@keyframes utensil-cross-spoon {
  0%   { translate: 0 0;              rotate: 0deg;  animation-timing-function: var(--ease-out); }
  35%  { translate: -10.634px -0.5px; rotate: 60deg; }
  49%  { translate: -10.634px -0.5px; rotate: 60deg; animation-timing-function: var(--ease-in-out-mark); }
  100% { translate: 0 0;              rotate: 0deg; }
}

/* On page load: fires once, ~400ms after paint (lets first paint settle
   before drawing attention to it), 860ms round trip (<=900ms budget). */
.brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--fork {
  animation: utensil-cross-fork 860ms var(--ease-out) 400ms 1 both;
}
.brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--knife {
  animation: utensil-cross-knife 860ms var(--ease-out) 400ms 1 both;
}
.brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--spoon {
  animation: utensil-cross-spoon 860ms var(--ease-out) 400ms 1 both;
}

/* Replay on hover/focus-visible of the lockup: a distinct animation-name
   is required to force CSS to restart an already-finished animation (only
   an animation-name CHANGE restarts one; re-declaring the same name with
   a different delay does not) -- @keyframes are identical to the ones
   above, just referenced via a second name so entering hover/focus always
   plays from frame 0 with no delay. Hover is gated to fine pointers
   (touch taps register as a false hover); focus-visible is not, since it
   is keyboard-driven regardless of pointer type. */
@keyframes utensil-cross-fork-replay {
  0%   { translate: 0 0;             rotate: 0deg;   animation-timing-function: var(--ease-out); }
  35%  { translate: 9.392px -0.5px;  rotate: -60deg; }
  49%  { translate: 9.392px -0.5px;  rotate: -60deg; animation-timing-function: var(--ease-in-out-mark); }
  100% { translate: 0 0;             rotate: 0deg; }
}
@keyframes utensil-cross-knife-replay {
  0%   { translate: 0 0;              animation-timing-function: var(--ease-out); }
  35%  { translate: -1.242px -0.5px; }
  49%  { translate: -1.242px -0.5px; animation-timing-function: var(--ease-in-out-mark); }
  100% { translate: 0 0; }
}
@keyframes utensil-cross-spoon-replay {
  0%   { translate: 0 0;              rotate: 0deg;  animation-timing-function: var(--ease-out); }
  35%  { translate: -10.634px -0.5px; rotate: 60deg; }
  49%  { translate: -10.634px -0.5px; rotate: 60deg; animation-timing-function: var(--ease-in-out-mark); }
  100% { translate: 0 0;              rotate: 0deg; }
}
@media (hover: hover) and (pointer: fine) {
  .brand-lockup:hover .brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--fork {
    animation: utensil-cross-fork-replay 860ms var(--ease-out) 0ms 1 both;
  }
  .brand-lockup:hover .brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--knife {
    animation: utensil-cross-knife-replay 860ms var(--ease-out) 0ms 1 both;
  }
  .brand-lockup:hover .brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--spoon {
    animation: utensil-cross-spoon-replay 860ms var(--ease-out) 0ms 1 both;
  }
}
.brand-lockup:focus-visible .brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--fork {
  animation: utensil-cross-fork-replay 860ms var(--ease-out) 0ms 1 both;
}
.brand-lockup:focus-visible .brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--knife {
  animation: utensil-cross-knife-replay 860ms var(--ease-out) 0ms 1 both;
}
.brand-lockup:focus-visible .brand-lockup__mark:not(.brand-lockup__mark--static) .mark__utensil--spoon {
  animation: utensil-cross-spoon-replay 860ms var(--ease-out) 0ms 1 both;
}

/* ── venue.css ── */
/* ============================================================
   VENUE — venue-profile composition (W13 RD round 2): hero identity +
   display-type rating, the cross-platform comparison unit, and the shared
   section rhythm (heading + hairline + spacing) reused across every
   lib/entity/sections.mjs + divergence.mjs section. Answers the DESIGN.md
   audit findings: no heading element on most sections, no section banding,
   rating/reviews buried in a receipt-chip-sized line instead of carrying
   display weight.

   NOT YET WIRED: this file is new and must be added to build.mjs's
   KIT_FILES array (after 'maproom.css') before public/styles.css includes
   it — that edit is outside this item's allowed paths; the integrator
   commit adds the one line.
   ============================================================ */

/* ── Shared section rhythm ─────────────────────────────────────────────
   infoCard/topics/ratingDist/attributes/busyHours/comparisons/
   acrossPlatforms all carry this class: a top hairline + consistent block
   padding so the page reads as a sequence of exhibits, not one
   undifferentiated stack of headings and tables. methodology keeps its
   own `.section--band` treatment (layout.css) instead. ──────────────── */
.venue-section {
  padding-block: var(--sp-6);
  border-block-start: 1px solid var(--line);
}
.venue-section h2 {
  margin-block-end: var(--sp-3);
}

/* ── Band rhythm (W14-A1): every other exhibit gets a full-bleed terrazzo
   backdrop -- alternating paper/terrazzo so the page reads as a sequence
   of designed exhibits, not one flat scroll (wave-w14-ui3-social-miami.md
   Part A). :nth-of-type counts every <section> sibling under
   main .container (the ungated "What the data shows" answer section
   included), so this naturally starts on the SECOND section down the
   page and keeps alternating regardless of how many exhibits a given
   venue has. The "featured exhibit" frame (1px --line-2 top+bottom,
   DESIGN-DIRECTION.md §6) replaces the plain single top hairline only on
   the banded sections -- the full-bleed math (100vw + matching negative/
   positive margin-inline) is the standard break-out-of-a-centered-
   container technique: it cancels out regardless of .container's own
   max-width or padding, so the band always reaches both viewport edges
   while the heading/table/text inside stays aligned with every other
   section's text column. */
.venue-section:nth-of-type(even) {
  background: var(--terrazzo);
  border-block-start: 1px solid var(--line-2);
  border-block-end: 1px solid var(--line-2);
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);
  box-sizing: border-box;
  overflow: hidden;
}

/* ── Hero identity + rating ─────────────────────────────────────────────
   Mobile-first: identity stacks above the rating stat; >=640px they sit
   side by side, rating right-aligned so the name still reads first. ──── */
.venue-hero {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-block: var(--sp-4) var(--sp-2);
}
.venue-hero__identity { min-width: 0; }
.venue-hero__meta { margin-block-start: var(--sp-1); }

.venue-hero__rating {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}
.venue-hero__rating-value {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}
.venue-hero__rating-glyph {
  font-size: var(--fs-h2);
  color: var(--sun);
  line-height: 1;
}
.venue-hero__rating-num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-stat);
  line-height: var(--lh-stat);
  font-weight: 700;
  /* Accent discipline (W14-A1): every stat numeral reads ocean, not ink --
     4.86:1 on terrazzo / 5.19:1 on paper (palette v2), both clear AA large-text
     (>=3:1) and body-text (>=4.5:1) at this font-size. */
  color: var(--ocean);
}
.venue-hero__rating-scale {
  font-family: var(--font-data);
  font-size: var(--fs-ui);
  color: var(--muted);
}
.venue-hero__rating-count {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-receipt);
  color: var(--muted);
}

@media (min-width: 640px) {
  .venue-hero {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
  }
  .venue-hero__rating { align-items: flex-end; text-align: right; }
}

/* ── Platform comparison unit ("Across platforms") ──────────────────────
   Two stat units side by side with a hairline divider — same
   label-over-numeral grammar as the hero rating above. The full sentence
   equivalent (divergence.mjs's unitHtml) renders via `.visually-hidden`
   (base.css) since the visible units here are aria-hidden fragments. ─── */
.platform-compare {
  display: flex;
  margin-block: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-card);
}
.platform-compare__unit {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-4);
}
.platform-compare__unit + .platform-compare__unit {
  border-inline-start: 1px solid var(--line);
}
.platform-compare__platform {
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ink-2);
}
.platform-compare__num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-stat);
  line-height: var(--lh-stat);
  font-weight: 700;
  color: var(--ocean); /* accent discipline (W14-A1): stat numerals read ocean */
}
.platform-compare__count {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-receipt);
  color: var(--muted);
}

/* ── Platform-compare delta line (V3 answer-first wave, operator
   2026-07-21) — a "+0.6 vs. Yelp" style delta printed under a platform's
   rating unit. The inner .delta-up/.delta-down/.delta-flat spans
   (design-tokens.css) carry the color; this only supplies the line's own
   block layout + tabular type. */
.platform-compare__delta {
  display: block;
  margin-block-start: var(--sp-1);
  font-size: var(--fs-receipt);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}

/* ── Sticky act-bar (name + receipt + Call/Reserve — V3 answer-first wave,
   operator 2026-07-21). Profile-only per operator directive 2026-07-21
   (DESIGN-DOC.md §2.2) — every other query page carries the same Call/
   Reserve weight via its hero action row instead. Placement law: last
   child of `main > .container`, never after `</footer>`. Parked (hidden)
   via `.act-bar--parked` while the hero's own action row (the
   `[data-act-anchor]` element) is on-screen — an IntersectionObserver
   toggles the class, not a CSS-only mechanism, since it needs to know when
   the anchor scrolls out of view. */
.act-bar {
  position: sticky; bottom: 0; z-index: var(--z-bar);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
  margin-block-start: var(--sp-6); padding: var(--sp-2) var(--sp-4);
  background: var(--surface); border-top: 2px solid var(--ocean);
  border-start-end-radius: var(--r-card); box-shadow: 0 -2px 0 var(--line);
  transition: opacity var(--t-fast) var(--ease-out), visibility var(--t-fast) var(--ease-out);
}
.act-bar--parked { opacity: 0; visibility: hidden; }
.act-bar__answer { display: flex; flex-direction: column; min-width: 0; }
.act-bar__name { font-family: var(--font-data); font-weight: 700; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.act-bar__receipt { color: var(--muted); font-size: var(--fs-receipt); }

/* ── "Find them online" official channels (wave CREATIVE-MEDIA Phase 0) ──
   Icon+handle pills, one per verified official social channel -- reuses
   .row (layout.css) for the wrapping flex row; each link itself gets the
   full --tap (44px) minimum hit target (design-tokens.css), not just the
   icon glyph, so the tappable area covers the whole pill on mobile. ────── */
.official-channel {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-card);
  color: var(--ink-2);
  text-decoration: none;
}
.official-channel:hover,
.official-channel:focus-visible {
  border-color: var(--line-2);
  color: var(--ink);
}
.official-channel__icon {
  display: inline-flex;
  color: var(--ink-2);
}
.official-channel__handle {
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
}
.official-channel__fallback {
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
}

/* ── Diner-sentiment chip count (wave W22) ───────────────────────────────
   lib/entity/sections.mjs's upgraded "What diners mention" secondary chip
   row inserts a literal " &middot; " between a chip's label and its count
   (never a bare space -- a whitespace-only text node between two
   .stream-chip flex-item boundaries is suppressed per the flexbox spec,
   which is what glued the legacy chips into "kind staff529"). This just
   mutes the count itself so it reads as secondary/receipt-like data next
   to the label, same --muted token every other secondary count in this
   file already uses (.venue-hero__rating-count, .platform-compare__count). */
.stream-chip__count {
  color: var(--muted);
}

/* ── Fine print (boilerplate-compression wave) ───────────────────────────
   Compact, muted footnote-style line for the fixed provenance/sourcing
   caveats that repeat on every venue page (price/hours/amenities
   self-described, review-sample basis, cross-platform comparability, the
   verdict evidence note) -- reuses the SAME --muted token + receipt-size
   scale already used for secondary counts (.venue-hero__rating-count,
   .platform-compare__count) so this de-emphasizes fixed copy without a new
   color/size scale. */
.fine-print {
  font-size: var(--fs-receipt);
  color: var(--muted);
}

/* ── THE FAST ANSWER (W17-S1) ────────────────────────────────────────────
   verdict.mjs's rebuilt first-content section keeps the SHARED
   .answer-block__sentence class (ledger.css) unchanged in markup, so
   intent-manifest.json's profile-family pattern and flow-walks.test.mjs's
   cross-page receipt check both still match it -- but a deterministic
   sentence tuned to the 40-60 word AI-snippet range (W17-R amendment 1)
   reads far too large at that shared class's display-heading size on a
   375px viewport. Scoped to this section only via the section's own
   data-section attribute, so home/hub/leaderboard (still short headline
   sentences) keep ledger.css's default unchanged. */
[data-section="fast-answer"] .answer-block__sentence {
  font-family: var(--font-ui);
  font-size: 1.125rem;
  line-height: var(--lh-body);
  font-weight: 600;
  font-variation-settings: normal;
  max-width: var(--measure);
}

/* ── "Creator buzz" (wave CREATIVE-MEDIA Phase 1/2) — evidence-proportional
   creator-mention summary + up to 3 click-to-load embedded posts. Facade
   pattern: a fixed, reserved-height box + a play button; the real iframe is
   built only on click (src/assets/creator-buzz.js), never shipped by
   default -- zero third-party request/cookie before that action. Mobile-
   first single column; desktop grids up to 3 posts side by side (matches
   lib/entity/creatorBuzz.mjs's EMBED_CAP). A Facebook post (never
   embeddable, docs/fnb/creative-media-plan.md §3) renders .creator-buzz__watch
   alone, no .creator-buzz__media sibling. ─────────────────────────────── */
.creator-buzz {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
  margin-block-start: var(--sp-4);
}
@media (min-width: 768px) {
  .creator-buzz { grid-template-columns: repeat(3, 1fr); }
}
.creator-buzz__post {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
/* Fixed media box -- TikTok's player needs >=325px width to render
   correctly, Instagram's /embed/ needs >=312px -- 325px clears both.
   square = IG post aspect, vertical = TikTok video / IG Reel aspect. */
.creator-buzz__media {
  position: relative;
  background: var(--ink);
  inline-size: 100%;
  max-inline-size: 325px;
  block-size: 423px;
  overflow: hidden;
  border-radius: var(--r-card);
}
.creator-buzz__media[data-aspect="vertical"] { block-size: 578px; }
.creator-buzz__play {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 0;
}
.creator-buzz__play-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--tap);
  block-size: var(--tap);
  border-radius: var(--r-round);
  background: var(--ocean);
  color: #FFFFFF;
  font-size: 1rem;
  transition: background var(--t-fast) var(--ease-out), transform 160ms var(--ease-out);
}
.creator-buzz__play:hover .creator-buzz__play-glyph { background: var(--ocean-deep); }
.creator-buzz__play:active .creator-buzz__play-glyph { transform: scale(.97); }
.creator-buzz__iframe {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;
}
.creator-buzz__watch {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ocean-deep);
  text-decoration: none;
}
.creator-buzz__watch:hover,
.creator-buzz__watch:focus-visible { color: var(--ink); }
.creator-buzz__watch-icon { display: inline-flex; color: var(--ink-2); }

/* ── Busy-times chart (V3 answer-first wave, operator 2026-07-21) — a real
   7-day CSS-only bar chart (popular_times_by_days per hour, day tabs)
   replacing the old one-line busy stat. One-corner radius + printed-offset
   elevation matching the site's card language. No transition on the bar
   itself — data-integrity personality: the value is either there or it
   isn't, it never eases in. */
.busy-chart {
  margin-block: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-card);
  box-shadow: var(--elev-1);
  padding: var(--sp-4);
}
.busy-chart__tabs {
  display: flex;
  gap: 2px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin-block-end: var(--sp-4);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-ui);
  padding: 2px;
}
.busy-chart__tabs::-webkit-scrollbar { display: none; }
.busy-chart__tab {
  flex: 1 1 0;
  min-width: 38px;
  min-height: var(--tap);
  padding-inline: var(--sp-1);
  background: transparent;
  border: 0;
  border-start-end-radius: var(--r-chip);
  font-family: var(--font-ui);
  font-size: var(--fs-receipt);
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  /* Below-the-fold law: color/border eases only, no entrance choreography. */
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
@media (hover: hover) {
  .busy-chart__tab:hover { background: var(--terrazzo); }
}
.busy-chart__tab.is-active {
  background: var(--ocean-tint);
  color: var(--ocean-deep);
}
.busy-chart__panel[hidden] { display: none; }
.busy-chart__bars {
  display: grid;
  grid-template-columns: repeat(18, 1fr);
  align-items: end;
  gap: 2px;
}
.busy-chart__col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
/* Terrazzo track ground: fixed pixel height so a child bar's percentage
   height resolves against a real value, not an auto-sized parent. */
.busy-chart__track {
  display: flex;
  align-items: flex-end;
  width: 100%;
  height: 96px;
  background: var(--terrazzo);
  /* Mockup source used an untokenized 2px here; --r-chip is the nearest
     tokenized radius (design-lint's radius-not-tokenized rule + the
     streamline rule's fixed 4-value set, DESIGN-DOC.md §1.3) and reads
     fine at this component's narrow column width. */
  border-start-end-radius: var(--r-chip);
}
.busy-chart__bar {
  width: 100%;
  min-height: 0;
  background: var(--ocean);
  border-start-end-radius: var(--r-chip);
}
.busy-chart__bar--peak { background: var(--flamingo); }
.busy-chart__hour {
  font-size: 0.625rem;
  line-height: 1;
  color: var(--muted);
}
.busy-chart__hour--blank { visibility: hidden; }
@media (max-width: 420px) {
  .busy-chart { padding: var(--sp-3); }
  .busy-chart__bars { overflow-x: auto; }
}

/* ── home.css ── */
/* ============================================================
   HOME — HOME-V2 (2026-07-22, designer-executor redesign, revised same day
   per binding operator direction: "appealing and visually rich... findings-
   as-headlines lead the page"): a masthead-led intelligence-publication
   home built as an actual editorial front page. New home-page-only classes:
   the ocean-deep masthead hero (`.hero-band`, a two-column grid at >=900px
   pairing `.hero-band__intro` with `.hero-subscribe`), a slim edition
   dateline (`.masthead-dateline`), a real named-venue LEAD STORY
   (`.lead-story` — asymmetric layout, a magazine drop cap on the dek, a
   pinned `.evidence-exhibit` card wrapping the drawn `.rating-bar-chart`
   with per-platform review counts, a color-blocked `.lead-story__pull`
   quote aside carrying the gap plus a pulled `.lead-story__quote`
   sentence), an asymmetric editorial digest for the market benchmark
   (`.category-leaders` / `.top-ranked`), a hairline "exhibit" rhythm for
   the creator-buzz section only (`.home-exhibit`), a full-bleed
   terrazzo browse band (`.home-library`), and a denser city grid.
   Deliberate section-to-section variety is the point — no two sections
   below the hero share an identical box treatment (the operator's "no
   uniform section rhythm, no card grids" law).

   IMAGERY, REVISED (operator direction same wave: "get realistic pic not
   this infographic style"): two AI-generated PHOTOREALISTIC documentary
   restaurant photos (src/assets/img, self-hosted, CSP-safe -- see
   content/generators/city-map.mjs's copyStaticAssets() header note for
   provenance; seo-research/platerian/mockups/home-v2/img/MANIFEST.md's
   "OPERATOR-PREFERRED (realistic photo pool)" section documents selection/
   rejection criteria) replace the prior round's duotone-illustration pair --
   a busy dining room behind the hero band and a kitchen pass mid-service
   behind the lead-story gap callout (both multiply-blended with a solid
   fill, never a gradient, so the text on top always stays legible). Both are
   CAPTIONLESS art direction attached to concrete content (the masthead
   identity and the named-venue finding); a standalone photo section break
   with a filler caption was removed per operator law (no decorative photo
   interludes, no atmospheric caption copy on a professional publication).

   INTEGRATION NOTE: this file is already in build.mjs's KIT_FILES array
   (verified 2026-07-22) — public/styles.css includes it.

   Every selector here is scoped to a home-page-only wrapper class
   (.hero-band / .lead-story / .evidence-exhibit / .category-leaders /
   .home-exhibit / .home-library / .top-ranked /
   .city-grid) so nothing leaks onto other
   pages once concatenated into the shared bundle. Where a rule restyles a
   shared component (.result-row, .stream-card, .receipt-row) it is written
   as a descendant override (e.g. `.city-grid .result-row`) rather than
   editing the shared class itself — content/pages/home.mjs's own comments
   explain why the markup underneath stays byte-identical to what
   ledger.css/test/home.test.mjs already expect.
   ============================================================ */

/* ── Hero band / masthead (HOME-V2) ───────────────────────────────────────
   "Commit to color" (wave-w14-ui3-social-miami.md Part A): the homepage's
   identity becomes one ocean-deep surface with paper type -- a real band,
   not a flat scroll. HOME-V2 restructures it as a two-column masthead at
   >=900px: `.hero-band__intro` (dateline + H1 + lede + source chip) on one
   side, `.hero-subscribe` as a real co-equal panel on the other -- not a
   small kicker tacked under the lede. Full-bleed via the same
   break-out-of-a-centered-container technique venue.css's alternating bands
   use. Contrast computed (WCAG relative luminance, palette v2): paper/
   ocean-deep 7.41:1, ocean-tint/ocean-deep 6.41:1 -- both clear body-text AA
   (>=4.5:1); every hero text color below reuses one of these two
   already-verified pairs, never a new one.

   W18-FIX (desktop horizontal-scrollbar bug): explicit box-sizing:border-box
   + overflow:hidden here, same defensive pair maproom.css's .maproom__layout
   already carries for this exact failure mode. Root cause (verified via
   isolated repro, not guessed): base.css's own header comment lists file
   ownership using a slash-separated notation (class names joined by a
   forward slash), and one of those joins happens to land right after an
   asterisk used for a wildcard-style class-name stem -- an asterisk glyph
   with a forward slash immediately next to it, no space between, is a
   block-comment terminator, so that comment closes many lines early. Every
   line of source between that accidental close and base.css's actual
   site-wide reset rule gets swallowed as part of one long invalid
   selector, so the reset never registers and EVERY element on the site
   silently defaults to box-sizing:content-box. A width:100vw box that
   also sets padding-inline (the calc(50vw - 50%) breakout math) then gets
   that padding added ON TOP of the 100vw content box instead of included
   in it -- overflow that grows with viewport width. base.css is outside
   this fix's allowed paths, so each full-bleed rule here is made
   self-sufficient instead of depending on the (currently broken) global
   reset. */
.answer-block.hero-band {
  /* Editorial texture (HOME-V2, operator-authorized imagery pass, REVISED
     per "appealing and visually rich... realistic pic not this infographic
     style" direction): an AI-generated PHOTOREALISTIC documentary photo, a
     busy dining room mid-service (src/assets/img -- see content/generators/
     city-map.mjs's copyStaticAssets() header note for provenance) blended
     UNDER the solid ocean-deep fill via background-blend-mode -- never a
     gradient (design-lint's hard "no gradients anywhere" rule), never a
     scrim technique that would need one. multiply blend darkens the whole
     image toward ocean-deep/near-black wherever it shows through, keeping
     the paper/white hero text legible while giving the band real material
     texture instead of a flat block. */
  background-color: var(--ocean-deep);
  background-image: url('/img/photo-dining-room-service.webp');
  background-size: cover;
  background-position: center 35%;
  background-blend-mode: multiply;
  box-sizing: border-box;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);
  padding-block: var(--sp-7);
  margin-block-end: 0;
  overflow: hidden;
}
@media (min-width: 900px) {
  .answer-block.hero-band {
    display: grid;
    grid-template-columns: 1.3fr 1fr;
    gap: var(--sp-7);
    align-items: start;
  }
}
.hero-band__intro { min-width: 0; }
.answer-block.hero-band .answer-block__sentence {
  color: var(--paper);
}
.answer-block.hero-band .lede {
  color: var(--ocean-tint);
  max-width: none;
}
/* The generic "receipt tape" band (ledger.css) is redundant inside a hero
   that is already a colored surface -- reset it to a bare row and let the
   chip itself carry the paper outline instead of a nested terrazzo box. */
.answer-block.hero-band .receipt-row {
  background: transparent;
  border-color: transparent;
  padding: 0;
  width: auto;
}
.answer-block.hero-band .receipt-chip {
  background: transparent;
  border-color: var(--paper);
  color: var(--paper);
}

/* ── Masthead dateline (HOME-V2) — a slim edition flag ABOVE the H1 (the
   classic newspaper-dateline device). Used exactly ONCE on this page, never
   repeated per section -- a deliberate masthead identity mark, not the
   generic "eyebrow above every section" tell. Reuses the already-verified
   ocean-tint/ocean-deep pair (see this file's header note). ──────────── */
.masthead-dateline {
  margin-block-end: var(--sp-3);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ocean-tint);
}

/* ── Hero newsletter signup (PUB-SHIFT repositioning wave; promoted to a
   masthead PANEL by HOME-V2) ─────────────────────────────────────────────
   Lives INSIDE `.answer-block.hero-band` itself (content/pages/home.mjs),
   as the grid's second column at >=900px, or stacked last on mobile. A
   deliberately near-opaque ink-tinted fill (color-mix from existing tokens
   only, no new hex) plus rounded streamline corner reads as a real card
   floating over the hero's own textured backdrop -- the page's single
   highest-stakes conversion element gets a clean, undistracted surface
   regardless of how rich the imagery behind it is (a common, legitimate
   editorial pattern: rich art behind a headline, a clean solid card for
   the actual form). Container/placement + panel treatment ONLY: the form's
   own field/button styling comes from src/kits/subscribe.mjs's own CSS (a
   parallel builder's kit) -- nothing here reaches into the form's
   internals. */
.hero-subscribe {
  margin-block-start: var(--sp-6);
  padding: var(--sp-5);
  max-inline-size: 28rem;
  background: color-mix(in srgb, var(--ink) 65%, transparent);
  border-start-end-radius: var(--r-card);
}
@media (min-width: 900px) {
  .hero-subscribe {
    margin-block-start: 0;
    max-inline-size: none;
  }
}
.hero-subscribe__kicker {
  margin-block-end: var(--sp-3);
  font-size: var(--fs-h3);
  font-weight: 700;
  color: var(--paper);
}

/* ── Stats band — REMOVED (V3 answer-first wave, operator directive
   2026-07-21: "no giant stat bands anywhere" — DESIGN-DOC.md §2.2). The old
   giant-numeral stat band is gone; a market's totals now fold into a small
   receipt-styled meta line instead of a hero-metric band. ────────────────── */

/* ── Lead story (HOME-V2, operator direction 2026-07-22) — a real, NAMED
   Google-vs-Yelp finding told as an actual editorial lead story, not a
   receipt: asymmetric two-column layout at >=900px (the story body wider
   than the pulled-out callout, a real magazine proportion, never an equal
   split), a drawn bar-chart comparison, and a color-blocked pull-quote aside
   carrying the gap as a big pulled number. This is the page's single
   richest visual moment on purpose -- "findings-as-headlines... lead the
   page." ─────────────────────────────────────────────────────────────── */
.lead-story {
  margin-block: var(--sp-7);
  padding-block-start: var(--sp-6);
  border-block-start: 1px solid var(--line);
}
@media (min-width: 900px) {
  .lead-story {
    display: grid;
    grid-template-columns: 1.7fr 1fr;
    gap: var(--sp-7);
    align-items: start;
  }
}
.lead-story__kicker {
  margin-block-end: var(--sp-2);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ocean-deep);
}
.lead-story__headline {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  font-variation-settings: "wdth" var(--wdth-h1);
  color: var(--ink);
  margin-block-end: var(--sp-3);
  text-wrap: balance;
}
.lead-story__headline a {
  color: inherit;
  text-decoration: none;
  border-bottom: 2px solid var(--ocean);
}
@media (hover: hover) {
  .lead-story__headline a:hover { color: var(--ocean-deep); border-color: var(--ocean-deep); }
}
.lead-story__dek { max-width: var(--measure); }
/* Magazine drop cap (operator direction: "not just text and color" --
   editorial devices grafted onto the existing lead story). Pure CSS,
   ::first-letter only -- no markup change, no new element. Sized to sit
   comfortably within the dek's own ~3-4 line measure; `.evidence-exhibit`
   below carries `clear: both` as a defensive pair so the float can never
   visually leak into the exhibit card if a narrow viewport ever gives the
   dek fewer wrapped lines than the cap's own height. */
.lead-story__dek::first-letter {
  float: left;
  font-family: var(--font-ui);
  font-size: 3.5rem;
  line-height: 0.85;
  font-weight: 700;
  color: var(--ocean-deep);
  padding-inline-end: var(--sp-2);
}
.lead-story__cite {
  margin-block-start: var(--sp-4);
  font-size: var(--fs-receipt);
  color: var(--muted);
}

/* Evidence exhibit (operator direction: reframe the drawn bar chart as a
   pinned "exhibit" card, not a bare chart floating in the story body) -- a
   bordered/terrazzo-backed card, same "exhibit" language DESIGN-DOC.md
   already uses for the site's other pinned data displays. `clear: both`
   defends against the drop cap float above (see .lead-story__dek::first-
   letter's own comment). */
.evidence-exhibit {
  clear: both;
  margin-block: var(--sp-5);
  padding: var(--sp-5);
  max-width: 26rem;
  background: var(--terrazzo);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-card);
}
.evidence-exhibit__eyebrow {
  margin-block-end: var(--sp-3);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ocean-deep);
}

/* Drawn data-as-graphic object: a plain proportional read of each platform's
   own rating, using the reserved --viz-* Okabe-Ito lane (design-tokens.css:
   "explicitly NOT part of the brand palette") so neither bar reads as an
   ocean/flamingo BRAND verdict -- a rating gap is a fact, not a verdict
   (DESIGN-DOC.md §4). Widths are static, never transitioned -- the same
   "the value is either there or it isn't, it never eases in" law the venue
   profile's busy-times bar chart already states explicitly. */
.rating-bar-chart {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.rating-bar-chart__row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 3rem;
  align-items: center;
  gap: var(--sp-3);
}
.rating-bar-chart__label {
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--muted);
}
.rating-bar-chart__track {
  position: relative;
  display: block;
  block-size: 10px;
  background: var(--terrazzo-2);
  border-start-end-radius: var(--r-chip);
  overflow: hidden;
}
.rating-bar-chart__fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--pct);
  border-start-end-radius: var(--r-chip);
}
.rating-bar-chart__fill--google { background: var(--viz-ocean); }
.rating-bar-chart__fill--yelp { background: var(--viz-vermillion); }
.rating-bar-chart__value {
  font-family: var(--font-data);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: end;
}
/* Review count -- a separate span AFTER the value span (test-safety: the
   existing rating-bar-chart__label -> rating-bar-chart__value non-greedy
   regexes in test/home.test.mjs must keep matching), forced onto its own
   line under the track/value columns via an explicit grid-column span. */
.rating-bar-chart__count {
  grid-column: 2 / -1;
  margin-block-start: 2px;
  font-size: var(--fs-receipt);
  color: var(--muted);
  text-align: end;
}

/* Pull-quote aside — color-blocked (the same already-verified ocean-deep/
   paper surface the masthead hero uses, a deliberate recurring "masthead
   ink" callback elsewhere on the page) carrying the gap as one big pulled
   number, a real magazine convention -- never a decontextualized SaaS
   hero-metric tile (this one is captioned, tied to the specific finding
   above it, and appears exactly once on the page). A large decorative
   quotation-mark glyph is pure CSS content (never rendered copy, never
   scanned by the dash/arrow sweep) -- the classic pulled-quote flourish. */
.lead-story__pull {
  position: relative;
  margin-block-start: var(--sp-6);
  padding: var(--sp-6) var(--sp-5);
  /* Same multiply-blend editorial-texture technique as the hero band above,
     REVISED per the operator's realism direction: a photorealistic kitchen-
     pass-during-service shot (src/assets/img) replaces the prior duotone
     torn-paper-collage texture -- dark and moody enough to stay legible
     under the multiply blend, never a gradient. */
  background-color: var(--ocean-deep);
  background-image: url('/img/photo-kitchen-pass-plating.webp');
  background-size: cover;
  background-position: center;
  background-blend-mode: multiply;
  border-start-end-radius: var(--r-card);
  overflow: hidden;
}
@media (min-width: 900px) {
  .lead-story__pull { margin-block-start: 0; }
}
.lead-story__pull::before {
  content: '\201C';
  position: absolute;
  inset-block-start: -0.3em;
  inset-inline-start: var(--sp-3);
  font-size: 6rem;
  line-height: 1;
  color: color-mix(in srgb, var(--paper) 18%, transparent);
}
.pull-number {
  position: relative;
  font-family: var(--font-data);
  font-size: var(--fs-stat);
  font-weight: 600;
  line-height: 1;
  color: var(--paper);
}
.pull-number__unit {
  display: block;
  margin-block-start: var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--ocean-tint);
}
.pull-caption {
  position: relative;
  margin-block-start: var(--sp-3);
  font-size: var(--fs-receipt);
  color: var(--ocean-tint);
}
/* Pulled editorial sentence (operator direction: fill out the aside so it
   reads full on desktop, not half-empty below the pull-number) -- a real
   magazine pull-quote treatment, one neutral sentence about the data, never
   a second stat. Sits below the pull-number/pull-caption pairing above it,
   set off with its own hairline rule so it reads as a distinct pulled
   remark rather than a continuation of the caption. */
.lead-story__quote {
  position: relative;
  margin-block-start: var(--sp-5);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid color-mix(in srgb, var(--paper) 30%, transparent);
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-style: italic;
  line-height: var(--lh-ui);
  color: var(--paper);
}

/* ── Exhibit rhythm (HOME-V2) — a shared top-hairline + padding wrapper for
   the creator-buzz section only (the lead story and category-leaders each
   have their OWN distinct rhythm above/below -- deliberate section-to-
   section variety, "no uniform section rhythm" per the operator's law). ── */
.home-exhibit {
  padding-block: var(--sp-6);
  border-block-start: 1px solid var(--line);
}

/* ── Category leaders (HOME-V2) — an editorial digest, not a stat band:
   headline-style intro (content/pages/home.mjs) over an ASYMMETRIC grid,
   never three identical boxes -- Restaurant (this market's largest, most-
   cited category) gets the wider lead column with a bigger name treatment;
   Cafe/Bar share the narrower rail. ─────────────────────────────────────── */
.category-leaders {
  margin-block: var(--sp-6);
  padding-block: var(--sp-6);
  border-block-start: 1px solid var(--line);
}

/* ── Top-ranked — content/pages/home.mjs's topRankedColumns() emits the
   SAME .result-row markup (name/meta spans, data-venue-name wrapper)
   test/home.test.mjs already asserts on; every rule below is a scoped
   override, never a markup change. ───────────────────────────────────── */
.top-ranked {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}
@media (min-width: 768px) {
  .top-ranked { grid-template-columns: 1.5fr 1fr 1fr; gap: var(--sp-6); }
}
.top-ranked__col:first-child .result-row__name {
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
}
@media (min-width: 768px) {
  .top-ranked__col:first-child {
    padding-inline-end: var(--sp-6);
    border-inline-end: 1px solid var(--line-2);
  }
}
.top-ranked__col {
  padding-block-start: var(--sp-3);
  border-top: 1px solid var(--line-2);
}
.top-ranked__kind {
  margin-block-end: var(--sp-2);
  font-family: var(--font-data);
  font-size: var(--fs-receipt);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--muted);
}
.top-ranked .result-row {
  display: flex;
  flex-wrap: wrap;
  min-height: 0;
  padding: 0;
  border-bottom: 0;
}
.top-ranked .result-row__name {
  flex: 1 1 auto;
  font-family: var(--font-ui);
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.top-ranked .result-row__meta {
  flex-basis: 100%;
  margin-block-start: var(--sp-1);
  font-size: var(--fs-ui);
  color: var(--ink-2);
}
@media (hover: hover) {
  .top-ranked .result-row:hover .result-row__name { color: var(--ocean-deep); }
}

/* A standalone full-bleed photo section break used to sit here (HOME-V2 first
   pass). It was removed per operator law: no decorative photo interludes and
   no filler/atmospheric captions on a professional publication. Imagery on
   this page is now captionless art direction attached to concrete content
   only -- the hero masthead backdrop and the lead-story gap callout. */

/* ── Library band (HOME-V2) — the browse layer (cities/categories/
   neighborhoods) consolidated into one full-bleed terrazzo band, the same
   break-out technique venue.css's alternating exhibit bands already use, so
   this section reads as "the publication's research library," a designed
   exhibit in its own right, not three disconnected h2 blocks trailing off
   the page. ──────────────────────────────────────────────────────────── */
.home-library {
  margin-block-start: var(--sp-6);
  padding-block: var(--sp-7);
  background: var(--terrazzo);
  border-block-start: 1px solid var(--line-2);
  border-block-end: 1px solid var(--line-2);
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);
  box-sizing: border-box;
  overflow: hidden;
}
.home-library h2 { margin-block-end: var(--sp-3); }
.home-library h3 { font-size: var(--fs-h3); margin-block-end: var(--sp-2); }
.home-library h3:not(:first-of-type) {
  margin-block-start: var(--sp-6);
  padding-block-start: var(--sp-5);
  border-top: 1px solid var(--line-2);
}

/* ── City grid — 2-col mobile, denser desktop, hairline-top tiles ──────
   cityTile() wraps the unchanged `.result-row.stream-card` anchor in a new
   `.city-tile` div (what this grid lays out) so test/home.test.mjs's
   exact-opening-tag regex on the anchor never breaks. */
.city-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-3);
  margin-block: var(--sp-4);
}
@media (min-width: 640px) {
  .city-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 960px) {
  .city-grid { grid-template-columns: repeat(4, 1fr); }
}
/* Name-then-count STACKED (not side-by-side): a 4-col desktop / 2-col
   mobile grid gives a long city name ("North Miami Beach", "Sunny Isles
   Beach") too little width to share a line with the count. An earlier pass
   tried wrapping the name in place, but ledger.css's `.result-row__name`
   also carries `min-width: 0` (needed elsewhere so a flex sibling can
   ellipsis-truncate) — combined with `overflow: visible` here, the shrunk
   flex box let the wrapped glyphs spill outside their own box and visually
   collide with the count next to them. Giving both spans `flex-basis: 100%`
   removes the width fight entirely: full name on its own line, the count
   (still the tabular/bold "designed element" the count deserves) directly
   under it. */
.city-grid .result-row {
  flex-wrap: wrap;
  align-items: baseline;
  min-height: var(--tap);
  padding: var(--sp-3);
  border-bottom: 0;
  text-decoration: none;
}
.city-grid .result-row__name {
  flex-basis: 100%;
  font-weight: 600;
  color: var(--ink);
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.city-grid .result-row__meta {
  flex-basis: 100%;
  margin-block-start: var(--sp-1);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: var(--fs-ui);
  color: var(--ink-2);
}
/* HOME-V2: the city grid now sits inside `.home-library`'s own terrazzo
   band -- a hover fill matching the band's own background would make a
   hovered tile visually disappear into it. `--ocean-tint` reads as a real
   highlight against both the tile's white `.stream-card` default and the
   surrounding terrazzo, and matches that token's own documented role
   ("selected rows/chips, band highlights", design-tokens.css). */
@media (hover: hover) {
  .city-grid .result-row:hover { background: var(--ocean-tint); border-color: var(--ocean); }
}

/* ── motion.css ── */
/* ============================================================
   MOTION — site-wide motion layer. Pure CSS, zero JS. Reuses the
   tokens design-tokens.css already declares (--t-fast, --ease-out);
   no new duration/easing tokens invented here.

   Brand rule this file respects (DESIGN-DIRECTION.md §13, "Press
   state: Terrazzo press tint, no scale" — the explicit differentiator
   from the sibling IL brand's `:active scale(.97)`, echoed in buttons.css's own
   comment "no scale-on-press (terrazzo tint instead)"): NOTHING below
   uses transform-based press feedback. Every existing pressed state in
   this codebase (.btn--secondary:active, .result-row:active) is
   already a background tint with zero scale() calls anywhere in
   src/styles/ or design-tokens.css — this file extends that same
   convention to the chip/link family instead of introducing scale.

   `.result-row` already owns a full transition + hover/active tint in
   ledger.css (background-color, var(--t-fast), correct no-scale
   pattern) — intentionally NOT duplicated or overridden here.
   ============================================================ */

/* ── Page-level entrance (main content only, once per page load) ──────────
   @starting-style is the progressive-enhancement fade-in: browsers that
   don't support it simply render #main at its resting opacity:1/
   translateY(0) immediately (no broken/invisible state, no flash). Only
   opacity + transform are animated — both are compositor-only properties,
   so this can never shift layout or contribute to CLS. 200ms is a
   deliberate one-off for this single page-level moment, distinct from the
   --t-fast (150ms, small UI transitions) and --t-sheet (240ms, drawers)
   tokens already in use elsewhere. ────────────────────────────────────── */
/* The standalone `translate` property, NOT `transform`: a non-none transform
   on #main makes it the containing block for every position:fixed descendant
   (the map filter sheet anchored ~18px off the real viewport bottom — caught
   by the W14-A2 live measurement). `translate` animates identically on the
   compositor without establishing a containing block. */
#main {
  opacity: 1;
  translate: 0 0;
  transition: opacity 200ms var(--ease-out), translate 200ms var(--ease-out);
}
@starting-style {
  #main { opacity: 0; translate: 0 8px; }
}

/* ── Interactive: smooth what already reacts, add pressed feedback where
   there was none. All transitions reuse --t-fast (150ms) rather than a
   stray one-off value, matching every other transition in this system. ──── */

/* Every <a> (nav links in the masthead's <nav class="row">, in-copy links,
   crumbs) already changes color on :hover/:active via design-tokens.css --
   this only adds the missing transition so that color change eases instead
   of snapping. */
a { transition: color var(--t-fast) var(--ease-out); }
a:active { color: var(--ocean); }

.link-quiet {
  transition: color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}
.link-quiet:active { color: var(--ocean-deep); border-color: var(--ocean-deep); }

/* .receipt-chip itself declares no transition in ledger.css (only
   `a.receipt-chip:hover` sets an instant color/border snap) -- this is
   purely additive. Also smooths .filter-chip/.filter-toggle, which layer
   onto .receipt-chip per buttons.css's own comment, at no extra cost. */
.receipt-chip {
  transition: background-color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out);
}
/* Pressed feedback via --terrazzo-2 (design-tokens.css's own "sunken
   wells, pressed states" tint) -- no scale, matching .result-row:active
   and .btn--secondary:active exactly. */
a.receipt-chip:active,
.filter-chip:active,
.filter-toggle:active {
  background: var(--terrazzo-2);
}

/* ── Motion utility layer (palette-v2 wave, operator directive 2026-07-21) ──
   General-purpose entrance/press/lift primitives any page markup can opt
   into with a class, on top of the page-level/component rules above. Resting
   state is ALWAYS the visible, final state -- @starting-style is progressive
   enhancement (no JS gate, no "hidden until a script runs" risk) and every
   transition/animation here is transform-adjacent (opacity, translate,
   scale, rotate) only, so nothing here can shift layout or contribute to
   CLS.

   LAW: this choreography is used ONLY above the fold (hero, receipt row,
   first action row) -- never on a table row, tbody, or any below-the-fold
   section (tables/rows never animate; see design-tokens.css's own "data is
   either there or it isn't" posture, echoed in venue.css's busy-chart bar
   comment). */
.rise {
  opacity: 1;
  translate: 0 0;
  transition: opacity 240ms var(--ease-out), translate 240ms var(--ease-out);
}
@starting-style {
  .rise { opacity: 0; translate: 0 6px; }
}
.rise-2 { transition-delay: 60ms; }
.rise-3 { transition-delay: 120ms; }
.rise-4 { transition-delay: 180ms; }
.rise-5 { transition-delay: 240ms; }

/* Press feedback for controls outside the terrazzo-tint family above --
   scale is the standalone property (not `transform`), same containing-
   block reasoning as #main's translate. */
.press {
  transition: scale 120ms var(--ease-out);
}
.press:active { scale: .97; }

/* Hover-only affordance -- gated to fine pointers so a touch tap never
   registers a false hover (same gate the brand-mark replay uses above). */
@media (hover: hover) and (pointer: fine) {
  .lift {
    transition: translate 150ms var(--ease-out);
  }
  .lift:hover { translate: 0 -2px; }
}

@keyframes pop-in {
  from { opacity: 0; scale: .96; }
}
@keyframes fade-in {
  from { opacity: 0; }
}
.pop-in { animation: pop-in 220ms var(--ease-out); }
.fade-in { animation: fade-in 200ms var(--ease-out); }

/* ── Search overlay entrance ── shared chrome on every page (the header's
   search-trigger button opens #search-overlay). @starting-style needs no JS
   change -- removing `hidden` (public/search.js) makes the element render
   for the first time, which is exactly when @starting-style's initial
   values apply. Exit is instant: the overlay goes back to `display: none`
   ([hidden] restored) rather than transitioning out, so there is nothing to
   animate on close. */
.search-overlay__scrim {
  opacity: 1;
  transition: opacity 200ms var(--ease-out);
}
@starting-style {
  .search-overlay__scrim { opacity: 0; }
}
.search-overlay__panel {
  opacity: 1;
  scale: 1;
  transform-origin: top center;
  transition: opacity 220ms var(--ease-drawer), scale 220ms var(--ease-drawer);
}
@starting-style {
  .search-overlay__panel { opacity: 0; scale: .97; }
}

/* ── Disclosure caret (details > summary, ledger.css's receipt expander) ──
   The custom caret itself (marker hidden, `display:flex` gap) is declared
   in ledger.css's `details > summary` rule; only the animated state lives
   here. The static 45deg forms the chevron shape (border-right+bottom
   corner); the independent `rotate` property (composes with `transform`
   per the CSS Transforms spec's fixed translate/rotate/scale/transform
   order) is what actually animates open/closed. Content never animates
   height -- `details[open]`'s children simply fade in already at their
   final, browser-computed size. */
details > summary::after {
  content: "";
  width: 6px;
  height: 6px;
  margin-inline-start: auto;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  rotate: 0deg;
  transition: rotate 150ms var(--ease-out);
  flex-shrink: 0;
}
details[open] > summary::after { rotate: 180deg; }
details[open] > *:not(summary) {
  animation: fade-in 200ms var(--ease-out);
}

/* ── Focus-visible: the shared :focus-visible rule (base.css) already
   covers .action-btn and result-row links with no changes needed -- neither
   has an overflow-clipping ancestor. `.segmented`, however, clips its two
   options with `overflow: hidden` to get the pill's rounded corners; the
   default rule's outline-offset:2px would push the ring past that clip on
   the first/last option, so it's pulled inward here (same -2px inset
   pattern `.maproom__card` already uses for the identical reason). */
.segmented__option:focus-visible {
  outline-offset: -2px;
}

/* ── Reduced motion: hard requirement, disables everything this file adds.
   design-tokens.css already forces `*, *::before, *::after { transition:
   none !important; animation: none !important; }` globally under this same
   query -- this block is a self-contained, defense-in-depth repeat scoped
   to exactly what motion.css introduces, so this file's contract holds
   even if load order ever changes. #main's entrance in particular must
   never be skipped visually: disabling its transition here means it simply
   renders at its resting opacity:1/translateY(0) state with no fade, no
   jump. */
@media (prefers-reduced-motion: reduce) {
  #main,
  a,
  .link-quiet,
  .receipt-chip,
  .rise,
  .press,
  .lift,
  .pop-in,
  .fade-in,
  .search-overlay__scrim,
  .search-overlay__panel,
  details > summary::after {
    transition: none;
    animation: none;
  }
}

/* ── lists.css ── */
/* ============================================================
   LISTS — W15-L4 "best of" list pages. Every rule here is scoped to a
   class content/generators/lists.mjs introduces; the row anatomy itself
   (rank numeral, name/meta, rating-unit, receipt-chip, ledger-table) is
   ledger.css's existing kit, reused unchanged. No new component system,
   only the list-specific arrangement/color rules ledger.css never needed
   before this wave (multiple platforms per row; a starred/"under review"
   state; colored momentum glyphs).
   ============================================================ */

/* ── Platforms shown side by side within one cell (operator direction:
   "platforms shown one NEXT TO the other") -- each platform keeps its own
   .rating-unit (ledger.css), this just arranges them horizontally instead
   of the single rating-unit ledger.css's table normally holds one of. ──── */
.list-platforms {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  justify-content: flex-end;
}
.ledger-table .ledger-cell--platforms { min-width: 12ch; }

@media (max-width: 640px) {
  .ledger-table--cards .list-platforms { justify-content: flex-start; }
  .ledger-table--cards .ledger-cell--receipt {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
  }
}

/* ── Momentum glyphs (▲ ▼ ·) — ledger.css already declares the font-weight
   for .delta-up/.delta-down/.delta-flat but (momentum had zero real records
   before this wave) never gave them color. Semantic --up/--down tokens
   already exist in design-tokens.css for exactly this, unused until now;
   never green/red (accessibility law: momentum is never color-only, but a
   color-blind-safe pair is still the right choice when color IS added). ─── */
.delta-up { color: var(--up-text); }
.delta-down { color: var(--down-text); }
.delta-flat { color: var(--muted); }

/* ── Starred ("under review") chip — a fast-change flag, not a confidence
   tier, so it does not reuse .receipt-chip[data-confidence]. Flamingo is the
   brand's scarce heat accent (design-tokens.css: "heat accent -- GRAPHICS
   ONLY... badge ground" for --flamingo-text specifically) -- a rare,
   attention-worthy state is exactly the sanctioned, scarce use. ──────────── */
.receipt-chip[data-starred] {
  background: color-mix(in srgb, var(--flamingo) 12%, var(--surface));
  border-color: var(--flamingo);
  color: var(--flamingo-text);
}

/* Current/frozen edition chip in an edition-history strip -- a plain
   .receipt-chip[aria-current="page"] otherwise reads identically to its
   linked sibling, so the reader can't tell which edition they're already
   on. */
.receipt-chip[aria-current="page"] {
  background: var(--ocean-tint);
  border-color: var(--ocean);
}

/* ── Archival strip (frozen-edition law 4, operator 2026-07-21) — the
   distinct, legible "you are viewing a past snapshot" affordance a frozen
   best-of edition needs, separate from the "current" edition's own chrome.
   A frozen edition never changes; this strip is the page's one governing
   fact. */
.archival-strip {
  margin-block: var(--sp-5);
  padding: var(--sp-4) var(--sp-5);
  background: var(--terrazzo-2);
  border-block: 2px solid var(--line-2);
}
.archival-strip__label {
  display: block;
  margin-bottom: 6px;
  font-family: var(--font-ui);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: 0.8125rem;
  color: var(--ink);
}
.archival-strip p { margin: 0; color: var(--ink-2); }
.archival-strip a { font-weight: 600; }

/* ── Narrative block — the deterministic weekly change narrative (wave doc
   §3) rendered prominently, directly under the answer-block/receipt-row,
   before the ranked table. Same "receipt tape" terrazzo motif ledger.css's
   .answer-block .receipt-row already uses, applied to a second, later band
   so the narrative reads as a distinct, considered exhibit rather than a
   plain paragraph. ──────────────────────────────────────────────────────── */
.list-narrative {
  background: var(--terrazzo);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-card);
  padding: var(--sp-4);
  margin-block-end: var(--sp-5);
}
.list-narrative .text-body { margin: 0; }

/* ── Top-insight strip (W21 "best data shortly presented" mandate) — reuses
   .list-narrative's exact band, just with up to 3 stacked one-line facts
   instead of one narrative paragraph (content/generators/insights.mjs).
   Placed near the top of hub/leaderboard/cuisine pages, directly under the
   answer-block. ──────────────────────────────────────────────────────────── */
.insight-strip .text-body + .text-body { margin-block-start: var(--sp-2); }

/* ── search.css ── */
/* ============================================================
   SEARCH OVERLAY (wave W20-S1) — header affordance + full-screen sheet
   (<900px) / anchored panel (>=900px). Same streamline-corner + offset-
   elevation + terrazzo-press language as buttons.css/filter's sheet
   (design-tokens.css's "one rounded corner, no blurred shadows" rule).
   Result rows reuse ledger.css's existing .result-row/.receipt-chip --
   no new row styling needed here, only the overlay chrome itself.
   ============================================================ */

.search-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--tap);
  block-size: var(--tap);
  color: var(--ink-2);
  background: transparent;
  border: 0;
  border-start-end-radius: var(--r-ui);
  cursor: pointer;
}
@media (hover: hover) {
  .search-trigger:hover { background: var(--terrazzo); color: var(--ink); }
}

.search-overlay { position: fixed; inset: 0; z-index: var(--z-sheet); display: none; }
.search-overlay:not([hidden]) { display: block; }
.search-overlay__scrim { position: absolute; inset: 0; background: color-mix(in srgb, var(--ink) 45%, transparent); }

.search-overlay__panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
}
@media (min-width: 900px) {
  .search-overlay__panel {
    inset-block-start: 10vh;
    inset-block-end: auto;
    inset-inline: 0;
    margin-inline: auto;
    inline-size: min(640px, calc(100% - var(--sp-6)));
    max-block-size: 76vh;
    border: 1px solid var(--line-2);
    border-start-end-radius: var(--r-card);
    box-shadow: var(--elev-2);
  }
}

.search-overlay__head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  flex-shrink: 0;
}
.search-overlay__input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  font: inherit;
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-start-end-radius: var(--r-ui);
  padding-inline: var(--sp-3);
}
.search-overlay__input:focus-visible { outline: 2px solid var(--ocean); outline-offset: 1px; }
.search-overlay__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--tap);
  block-size: var(--tap);
  flex-shrink: 0;
  background: transparent;
  border: 0;
  color: var(--muted);
  font-size: 1rem;
  cursor: pointer;
}

.search-overlay__scope {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding-inline: var(--sp-4);
  padding-block-end: var(--sp-3);
  font-size: var(--fs-receipt);
  color: var(--muted);
  flex-shrink: 0;
}
.search-overlay__scope[hidden] { display: none; }
.search-overlay__scope-toggle {
  color: var(--ocean-deep);
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: var(--fs-receipt);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.search-overlay__results {
  flex: 1 1 auto;
  overflow-y: auto;
  padding-inline: var(--sp-2);
  padding-block-end: var(--sp-4);
}
.search-overlay__group + .search-overlay__group { margin-block-start: var(--sp-3); }
.search-overlay__group-heading {
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-receipt);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.search-overlay__status {
  padding: var(--sp-3);
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-ui);
}

/* Keyboard-active option state (aria-activedescendant target) -- reuses
   .result-row's own hover/active tint so keyboard nav reads identically to
   pointer hover, no new color introduced. */
.search-overlay .result-row.is-active { background: var(--terrazzo-2); }

body.search-lock { overflow: hidden; }

/* ── subscribe.css ── */
/* ============================================================
   SUBSCRIBE FORM (wave PUB-SHIFT item B3) -- both placements share one
   markup shape (src/kits/subscribe.mjs); `--hero`/`--footer` only vary
   surface + max-width. Mobile-first, 44px min tap targets (--tap token),
   no overflow at 375px. Text colors ONLY from the brand palette:
   #1B1512 #4A423C #726A62 #8F3620 #7D5E21 #FFFFFF (mapped to the existing
   --ink/--ink-2/--muted/etc. tokens design-tokens.css already defines from
   those same hexes -- never a new literal color here).
   ============================================================ */

.subscribe-form {
  max-inline-size: 32rem;
}
.subscribe-form__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: flex-start;
}
.subscribe-form__label {
  flex-basis: 100%;
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--ink);
}
.subscribe-form__input {
  flex: 1 1 12rem;
  min-height: var(--tap);
  min-inline-size: 0;
  padding-inline: var(--sp-4);
  font: inherit;
  font-size: var(--fs-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-start-end-radius: var(--r-ui);
}
.subscribe-form__input::placeholder { color: var(--muted); }
.subscribe-form__input:focus-visible {
  outline: 2px solid var(--ocean);
  outline-offset: 1px;
  border-color: var(--ocean);
}
.subscribe-form__submit {
  flex: 0 0 auto;
  min-inline-size: var(--tap);
}
.subscribe-form__consent {
  margin-block-start: var(--sp-2);
  font-size: var(--fs-receipt);
  color: var(--muted);
}
.subscribe-form__consent a {
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.subscribe-form__consent a:hover { color: var(--ink-2); }

.subscribe-form__result {
  margin-block-start: var(--sp-2);
  font-size: var(--fs-receipt);
  color: var(--ink-2);
}
.subscribe-form__result[hidden] { display: none; }
.subscribe-form__result--error { color: var(--down-text); }

.subscribe-form__success {
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--ink);
}

/* ── Hero variant -- sits on the ocean-deep hero band (home.css), so its
   own text reads light against that dark surface. --surface (#FFFFFF), not
   --paper (#FBFAF8) -- kept inside this item's approved text-color set. ── */
.subscribe-form--hero .subscribe-form__label,
.subscribe-form--hero .subscribe-form__consent { color: var(--surface); }
.subscribe-form--hero .subscribe-form__consent a { color: var(--surface); }
.subscribe-form--hero .subscribe-form__consent a:hover { opacity: 0.8; }

/* ── Footer variant -- sits inside the colophon, single-column on mobile,
   input+button share a row once there's room (>=480px). ───────────────── */
.subscribe-form--footer {
  margin-block: var(--sp-5);
}
@media (max-width: 479px) {
  .subscribe-form--footer .subscribe-form__input,
  .subscribe-form--footer .subscribe-form__submit {
    flex-basis: 100%;
  }
}
