/* ==========================================================================
   Sidedish

   Two kinds of page, deliberately unalike.

   The LANDING is a scene: a field, a question, a search field, nothing else.
   Every reference that inspired it -- Ecosia, OceanHero, Soar -- earns its
   calm by refusing to put anything else on that screen.

   Everything after it is CONTENT: rows, rules, tight vertical rhythm, no
   scenery. HappyCow is dense and unadorned and that is why it works.

   Mixing the two is what made the first attempt look like a prototype with
   a banner stuck on top.
   ========================================================================== */

/* ---- tokens -------------------------------------------------------- */
:root {
  /* paper, not white: white goes grey next to a green scene */
  --paper: #fcfbf7;
  --card: #ffffff;
  --ink: #16190f;
  --ink-2: #5c6353;
  --ink-3: #878d7c;
  --rule: #e7e6dc;
  --rule-soft: #f1f0e8;

  --green: #2f6b34;
  --green-ink: #24522a;
  --green-wash: #e9f1e6;
  --gold: #9a6a12;
  --gold-wash: #f8efdb;
  --clay: #8a4b2a;
  --clay-wash: #f7ece5;
  --slate: #6d7367;
  --slate-wash: #efefea;

  /* the scene */
  --sky-top: #cfe3ef;
  --sky-mid: #eaf1e2;
  --sky-low: #f6f3e2;
  --sun: #ffe9b0;
  --hill-far: #b9cfa8;
  --hill-near: #94b884;
  --blade-far: #9dbf85;
  --blade-near: #244d1f;
  --scene-ink: #16190f;

  --shadow-sm: 0 1px 2px rgba(22,25,15,.06);
  --shadow-lg: 0 18px 48px -18px rgba(22,25,15,.28);
  --r: 14px;
  --maxw: 1080px;
}
/* NO DARK PALETTE. The content pages are light only -- both references
   are, and a dark restaurant listing reads like a terminal, which was the
   original complaint.

   These blocks used to redefine --paper, --ink, --card and --rule for a
   dark OS, and they OUTRANKED the light palette below no matter what
   order they appeared in: `:root:not([data-theme="light"])` scores higher
   on specificity than a bare `:root`. So anybody whose machine was set to
   dark got the old scheme and the redesign never reached them.

   The homepage keeps its dark photographic treatment; it sets its own
   --scene-ink and --scene-dim inside .scene and does not depend on these.
   ------------------------------------------------------------------- */

/* ---- base ---------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--paper); color: var(--ink);
  font: 400 16px/1.6 ui-sans-serif, -apple-system, "SF Pro Text", "Segoe UI",
        Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; }

/* one type scale, used everywhere */
.t-display { font-size: clamp(36px, 6.2vw, 68px); font-weight: 680;
             letter-spacing: -0.04em; line-height: 1.02; }
.t-title   { font-size: clamp(26px, 3.4vw, 38px); font-weight: 660;
             letter-spacing: -0.028em; line-height: 1.1; }
.t-lede    { font-size: clamp(16px, 1.5vw, 18.5px); line-height: 1.5;
             color: var(--ink-2); }
.t-row     { font-size: 17px; font-weight: 600; letter-spacing: -0.012em; }
.t-meta    { font-size: 13.5px; color: var(--ink-2); }
.t-cap     { font-size: 12.5px; color: var(--ink-3); }

.shell { max-width: var(--maxw); margin: 0 auto; padding: 0 22px; }
.narrow { max-width: 760px; }

/* ---- chrome -------------------------------------------------------- */
.bar {
  display: flex; align-items: center; gap: 22px;
  max-width: var(--maxw); margin: 0 auto; padding: 18px 22px;
}
.bar.on-scene { position: relative; z-index: 3; }
.mark { font-size: 16.5px; font-weight: 700; letter-spacing: -0.03em; }
.mark i { font-style: normal; color: var(--green); }

.bar nav { margin-left: auto; display: flex; gap: 20px; font-size: 14.5px;
           color: var(--ink-2); }
.bar nav a:hover { color: var(--ink); }
.on-scene .mark, .on-scene nav { color: var(--scene-ink); }
.on-scene nav a { opacity: .78; }
.on-scene nav a:hover { opacity: 1; }

/* ---- the scene -------------------------------------------------------
   Rebuilt using Soar's actual technique rather than my guess at it. Theirs
   is not painted; it is a composited stack, and every layer is a CSS
   gradient the GPU handles for free:

     .sky-ramp-morph  five stacked radial-gradients -- two "walls" at the
                      sides, a soft white DOME low and centre (the thing
                      that reads as cloud), a wide horizon lift, a base
     rays             two repeating-conic-gradients, mix-blend-mode:screen,
                      radial-masked so they fade before the edges
     grain            an SVG feTurbulence tile, opacity .5, soft-light
     mask             the whole unit masked out toward the bottom

   The motion is CSS custom properties transitioning, which is why it
   drifts without a single repaint. That needs @property so the browser
   knows how to interpolate a percentage or a colour.

   Adapted here for a field rather than a sky: the photograph is the base,
   the dome becomes low haze sitting on the grass, and the rays fall from
   above the way late light does across a pasture.
   -------------------------------------------------------------------- */
@property --haze-y   { syntax: "<percentage>"; inherits: false; initial-value: 112%; }
@property --haze-a   { syntax: "<number>";     inherits: false; initial-value: .50; }
@property --wall-y   { syntax: "<percentage>"; inherits: false; initial-value: 88%; }
@property --lift-y   { syntax: "<percentage>"; inherits: false; initial-value: 104%; }
@property --ray-a    { syntax: "<number>";     inherits: false; initial-value: 1; }

/* THE GROUND IS THE SCENE'S, ALL THE WAY DOWN. 100svh is the SMALL
   viewport: on an iPhone the moment Safari's toolbar folds away the page
   grows past it and the body's paper showed through as a white band under
   the hero. Riley: "why's the homepage background not fill the mobile
   height?" dvh tracks the toolbar; vh is the fallback for browsers
   without it; and the body itself is painted the scene's green so there
   is nothing white to show even while the viewport is mid-change. */
.page-home body { background: #1d2c1b; }  /* midpoint of the photo's
   rendered top (#253225) and bottom (#152712) edges -- this colour is
   what Safari paints into the status-bar and toolbar bands, which no
   layer can reach (100lvh=836, screen=956). Was #101f0e, far darker
   than either edge. See FOUR PHONE FIXES at the end of this file. */
.scene {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column;
  background: #101f0e;
  --scene-ink: #f4f6f0;
  --scene-dim: #c6cfbe;
  --par-x: 0px; --par-y: 0px;
  --tint-strong: #dff0d2;
  --tint-mid: #b9d6a8;
  --tint-band: #8fbe83;
}

/* L0 -- the photograph, AS RILEY GRADED IT.

   /field.jpg is his own export of the romaine macro: already darkened,
   already vignetted, already the olive it is meant to be. NOTHING IS
   LAYERED OVER IT. There was a #4e5331 multiply here for one build --
   his first instruction, before he sent the graded file -- and it is gone
   because the file now arrives with that treatment baked in and doing it
   twice is doing it twice.

   The old brightness(.82) is gone for the same reason. Any change to how
   dark this photograph is belongs in the file, not in a filter that
   silently disagrees with the artwork. */
.scene::before {
  content: ""; position: absolute; inset: -3%; z-index: 0;
  background: url(/field.jpg) 50% 50% / cover no-repeat;
  transform: translate(var(--par-x), var(--par-y));
  transition: transform .6s cubic-bezier(.2,.7,.3,1);
  will-change: transform;
}

/* L1 -- atmosphere. The dome is the cloud. */
.sky {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 72%,
                      rgba(0,0,0,.55) 88%, rgba(0,0,0,0) 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 72%,
              rgba(0,0,0,.55) 88%, rgba(0,0,0,0) 100%);
}
.sky-ramp {
  position: absolute; inset: 0;
  background:
    radial-gradient(48% 80% at -10% 96%,
      color-mix(in srgb, var(--tint-strong) 16%, transparent) 0%,
      transparent 58%),
    radial-gradient(48% 80% at 110% 96%,
      color-mix(in srgb, var(--tint-strong) 16%, transparent) 0%,
      transparent 58%),
    radial-gradient(90% 60% at 50% 128%,
      rgba(226,238,214,.30) 0%,
      rgba(226,238,214,.12) 34%,
      rgba(226,238,214,0) 60%),
    radial-gradient(200% 130% at 50% 118%,
      color-mix(in srgb, var(--tint-mid) 14%, transparent) 0%,
      color-mix(in srgb, var(--tint-band) 8%, transparent) 22%,
      transparent 44%);
  /* NOT animated. Soar transitions these on a state change, once, over
     1.25s -- it never loops them, because animating a gradient repaints
     the whole layer every frame. Any motion here comes from transform and
     opacity, which the compositor does for free. */
}

/* L2 -- light falling through, exactly Soar's construction */
.rays {
  display: none;
  position: absolute; left: 50%; bottom: -14%; width: 200vw; height: 118vh;
  z-index: 2; mix-blend-mode: screen; transform: translateX(-50%);
  pointer-events: none; opacity: var(--ray-a);
  background:
    repeating-conic-gradient(from 200deg at 50% 0%,
      rgba(255,255,255,0) 0deg, rgba(255,255,255,.085) 3deg,
      rgba(255,255,255,0) 11deg),
    repeating-conic-gradient(from 206deg at 50% 0%,
      rgba(255,255,255,0) 0deg, rgba(255,255,255,.05) 1.4deg,
      rgba(255,255,255,0) 6.5deg);
  -webkit-mask-image: radial-gradient(80% 95% at 50% 0%,
      rgba(0,0,0,1) 0%, rgba(0,0,0,.55) 45%, transparent 78%);
  mask-image: radial-gradient(80% 95% at 50% 0%,
      rgba(0,0,0,1) 0%, rgba(0,0,0,.55) 45%, transparent 78%);
  animation: rays-drift 78s ease-in-out infinite;
}
/* transform and opacity only: both composited, neither repaints */
@keyframes rays-drift {
  0%,100% { opacity: .55; transform: translateX(-50%) rotate(-.7deg); }
  50%     { opacity: .95; transform: translateX(-50%) rotate(.7deg); }
}

/* L3 -- grain. Soar's exact tile, and the reason a big gradient stops
   looking like a big gradient. */
.grain {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  opacity: .5; mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* L4 -- motes, on a canvas the way their starfield is */
#field { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 4;
         pointer-events: none; }

/* L5 -- the scrim the copy sits on */
/* The scrim was built to darken a bright field photograph enough to read
   white type on. The multiply on L0 now does most of that, so these are
   HALF what they were -- stacked at their old strength over the olive the
   headline sat in a black hole and the leaves stopped being visible at
   all. What is left is the part that still earns its place: a little more
   weight behind the copy, and the top and bottom edges pulled down so the
   masthead and the footer links have something to sit against. */
/* --edge IS THE FADE INTO THE BROWSER'S OWN BANDS. Zero by default, so on a
   desktop the scrim is exactly what it always was: a gradient whose two
   stops both sit at 0 takes its LAST colour everywhere, which here is
   transparent. The phone rule at the end of this file is the only place
   that gives it a width; see TWO FOOTER FIXES / FOUR PHONE FIXES there. */
.scene::after {
  content: ""; position: absolute; inset: 0; z-index: 5; pointer-events: none;
  background:
    linear-gradient(180deg, #101f0e 0px, rgba(16,31,14,0) var(--edge, 0px)),
    linear-gradient(0deg,   #101f0e 0px, rgba(16,31,14,0) var(--edge, 0px)),
    radial-gradient(46% 30% at 50% 41%, rgba(2,8,4,.34) 0%,
                    rgba(2,8,4,.15) 55%, rgba(2,8,4,0) 78%),
    linear-gradient(180deg, rgba(3,10,5,.40) 0%, rgba(3,10,5,.10) 16%,
                    rgba(3,10,5,0) 34%, rgba(3,10,5,.12) 70%,
                    rgba(3,10,5,.46) 100%);
}

.scene-body {
  position: relative; z-index: 6; flex: 1;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; padding: 0 22px;
  padding-bottom: clamp(150px, 30vh, 340px);
}
.scene-body h1 {
  margin: 0 0 8px; color: var(--scene-ink);
  font-size: clamp(28px, 3.2vw, 42px); font-weight: 600;
  letter-spacing: -0.03em; line-height: 1.1;
  text-shadow: 0 2px 34px rgba(0,0,0,.6);
}
.scene-body .kicker {
  margin: 0 0 30px; color: var(--scene-dim); font-size: 15px;
  text-shadow: 0 1px 22px rgba(0,0,0,.65);
}

@media (prefers-reduced-motion: reduce) {
  .sky-ramp, .rays { animation: none; }
  .scene::before { transition: none; }
  .peek { transition: none; }
  .peek.open { transition: none; }
  .peek iframe { transition: none; }
}

/* ---- the search bar: dark glass ------------------------------------ */
.find {
  display: flex; align-items: center; width: min(660px, 100%);
  background: rgba(20, 24, 21, .62);
  border: 1px solid rgba(255, 255, 255, .10);
  border-radius: 999px; padding: 7px 7px 7px 22px;
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  box-shadow: 0 22px 60px -26px rgba(0, 0, 0, .9);
  transition: border-color .2s ease, background .2s ease;
}
.find:focus-within {
  border-color: rgba(255, 255, 255, .2);
  background: rgba(24, 30, 25, .74);
}
.find svg { flex: 0 0 auto; color: var(--scene-dim); margin-right: 13px; }
.find input {
  flex: 1; min-width: 0; border: 0; outline: 0; background: none;
  color: var(--scene-ink); font: inherit; font-size: 15.5px;
  padding: 13px 0; -webkit-appearance: none;
}
.find input::placeholder { color: var(--scene-dim); opacity: .82; }
.find input::-webkit-search-cancel-button { display: none; }
.find button {
  flex: 0 0 auto; border: 0; border-radius: 999px; cursor: pointer;
  background: #3f8f47; color: #fff; font-weight: 560; font-size: 14px;
  padding: 11px 20px; letter-spacing: -0.005em;
  display: inline-flex; align-items: center; gap: 8px;
  box-shadow: 0 0 0 1px rgba(255,255,255,.10), 0 8px 26px -8px rgba(74,154,74,.75);
  transition: filter .16s ease;
}
.find button:hover { filter: brightness(1.1); }

/* suggestion chips. These lost their base rules when the scene block was
   rewritten, and rendered as bare square buttons on the results page. */
.suggest { display: flex; flex-wrap: wrap; gap: 7px; justify-content: center;
           margin-top: 20px; }
.suggest button {
  border: 1px solid rgba(255,255,255,.13);
  background: rgba(20,24,21,.5); color: var(--scene-dim);
  font-size: 13.5px; padding: 7px 15px; border-radius: 999px;
  cursor: pointer; backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px); transition: all .15s ease;
}
.suggest button:hover {
  color: var(--scene-ink); border-color: rgba(255,255,255,.3);
  background: rgba(28,34,28,.72);
}

/* the secondary row, the way Soar puts trip options under the search */
.opts {
  display: inline-flex; align-items: center; margin-top: 14px;
  background: rgba(20, 24, 21, .5);
  border: 1px solid rgba(255, 255, 255, .08);
  border-radius: 999px; padding: 3px;
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
.opts button {
  border: 0; background: none; cursor: pointer; color: var(--scene-dim);
  font-size: 13.5px; padding: 8px 15px; border-radius: 999px;
  display: inline-flex; align-items: center; gap: 7px;
  transition: color .15s ease, background .15s ease;
}
.opts button:hover { color: var(--scene-ink); }
.opts .div { width: 1px; height: 17px; background: rgba(255,255,255,.10); }

/* ---- scene chrome -------------------------------------------------- */
.bar.on-scene {
  position: relative; z-index: 7; padding: 20px 30px;
  max-width: none; width: 100%;
}
/* THE OLD SCENE WORDMARK, and why these two lines are gone rather than
   edited. The redesign block near the bottom of this file restates the
   masthead in full; it used to restate the font-size too, which is the
   only reason this 17px never showed. Trimming it to `color: #fff` --
   correct, since the sizes are meant to live in ONE place -- handed the
   size back to this rule, and the homepage wordmark rendered at 17px
   while every other page rendered at 39. The green <i> went with it: the
   <i> is now the "eats" half of the logotype, and tinting it would make
   the wordmark two colours as well as two weights. */
/* THE OLD OUTLINED GLASS PILLS ARE GONE TOO, and this one had teeth: it
   set font-size ON THE <a>, while the redesign sets it on the <nav> for
   the links to inherit. A declaration on the element always beats an
   inherited value whatever the specificity or the order, so the homepage
   nav sat at 13.5px in outlined pills while every other page's sat at
   17px in bare words -- Riley, comparing the two: "you still didn't
   change the text size of cities and search on the map page to match".
   The map was the one that was right.

   The rule that replaced this lives with the rest of the scene masthead
   near the bottom of the file: bare white words, 17px, one filled pill. */

.scene-foot {
  position: relative; z-index: 7; display: flex; gap: 20px;
  padding: 0 30px 24px; font-size: 12.5px; color: var(--scene-dim);
}
.scene-foot a { opacity: .75; }
.scene-foot a:hover { opacity: 1; }

/* ======================================================================
   VEGEATS — the design system for everything that is not the homepage
   ======================================================================

   THE BRIEF is now a set of six drawn screens rather than two reference
   sites, and they say something much simpler than the pairing this block
   used to describe:

     ONE FAMILY. ONE INK. WHITE PAPER. COLOUR ONLY WHERE IT IS A VERDICT.

   Sampled off the screens themselves, every word on a content page --
   the wordmark, the restaurant name, the address, the dish description,
   the opening hours -- is the SAME dark forest green, #3E5215, on pure
   #ffffff. There is no second text colour and no accent. What was a
   serif/sans pairing carrying six roles is now one geometric sans at two
   weights, and the hierarchy is made out of SIZE and WEIGHT alone.

   That is why the tomato red is gone. It existed to keep the brand out of
   the greens; in these screens the brand IS the green, and the verdict
   colours are the only other hues on the page.

   THE FIVE VERDICT COLOURS come from the legend drawn on the venue screen
   and are the only decoration in the system:

     Vegan             #3E5215   the ink itself
     Likely Vegan      #80A32B
     Shared Fryer      #82A7AD
     Vegan with Mods   #AF82AB
     Not Vegan         #C43B52   radish, = --closed (Riley, 2026-10-13)

   NONE OF THE FIRST THREE NON-INK HUES PASSES AA AS TEXT ON WHITE -- they
   run 2.1:1 to 3.2:1 (the radish is 5.13:1, but is used the same way). So they are never set as type at body size. They are
   DOTS: a 10px swatch in front of a dish name that the legend explains,
   with the name itself staying in the readable forest green and the tier
   also named in words by the section heading above it. Colour carries no
   information that words do not, which is the rule the old badge block
   was written to keep and this keeps in a quieter way.

   Where one of them has to be a FILL under white text -- the lighter
   status pill -- the deepened --leaf-deep is used instead, because white
   on #80A32B is 2.9:1 and unreadable.
   ====================================================================== */

/* ---- the face -------------------------------------------------------
   ARBORIA (TypeTogether), Book and Medium, is the face this site is drawn
   in. It comes from Riley's Adobe Fonts web project and CANNOT be
   self-hosted or downloaded -- the kit IS the licence, and it is a
   stylesheet served from Adobe rather than a font file.

     <link rel="stylesheet" href="https://use.typekit.net/hyb6eit.css">

   That link is emitted by sidedish/seo.py's head() -- one place, every
   built page -- and written by hand into the three static pages that do
   not route through it (app/site/landing.html, app/site/mappage.html,
   app/template.html). A page that misses it is not broken, it is subtly
   wrong: it renders in the fallback while its neighbours do not.

   IT IS NOT DOMAIN-LOCKED. Adobe dropped that restriction and the old
   Typekit rule is worth un-learning here rather than re-deriving under
   pressure: the kit answers from vegeats.app, from 127.0.0.1 and from
   anywhere else with no configuration. Arboria failing to render is
   therefore always a bug in the link or in this file, and there is no
   setting anybody can change to fix it.

   THE FALLBACK IS OUTFIT, SELF-HOSTED, AND IT IS NOT A PLACEHOLDER.
   Adobe can still be slow, blocked or down, and when it is every page is
   set in the fallback -- so the fallback has to be metric-close or the
   layout moves under the reader. Outfit was picked by comparing Jost,
   Poppins, Outfit and Manrope against the drawn screens: it is the only
   one of the four that matches Arboria on every letterform that
   identifies the family --
   the single-storey `a`, the single-storey `g` with a short hooked tail,
   the angle-cut apex of the `t`, and a tall x-height at a width narrower
   than Poppins. Poppins is too wide and flat-topped, Jost's x-height is
   far too small, and Manrope is a grotesque, not a geometric.

   The fallback is self-hosted, not pulled from a font CDN, for the reason
   the old block gave: the site is behind a password and should not
   announce every visit to a third party. That argument is now half-lost --
   the Arboria kit is a third-party request on every page -- but it still
   holds for the copy that answers when Adobe does not. */
@font-face {
  font-family: "Outfit";
  src: url(/fonts/outfit-latin.woff2) format("woff2-variations");
  font-weight: 100 900; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191,
                 U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Outfit";
  src: url(/fonts/outfit-latin-ext.woff2) format("woff2-variations");
  font-weight: 100 900; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
                 U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Retained, unreferenced. Nothing in the system asks for them any more, so
   no reader downloads them; they stay declared so the old serif/sans
   pairing can be read back out of one file if the rebrand has to be
   argued with. */
@font-face {
  font-family: "Source Serif 4";
  src: url(/fonts/source-serif-4.woff2) format("woff2-variations");
  font-weight: 200 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url(/fonts/inter.woff2) format("woff2-variations");
  font-weight: 100 900; font-style: normal; font-display: swap;
}

/* ---- tokens ---------------------------------------------------------- */
:root {
  color-scheme: light;

  /* PAPER IS WHITE NOW. The cream was there to stop a pure page making the
     greens look chemical, and the screens simply do not do that: they are
     #ffffff with one very dark green on top, and the cream reads as dirty
     beside the drawn comps. */
  --paper:      #ffffff;
  --card:       #ffffff;
  --band:       #f4f5f0;   /* the pressed filter chip, and a map's ground */
  --band-deep:  #ecede7;

  /* ONE INK, and it is the brand green. --ink-2 is the SAME colour: the
     screens set the address, the description and the hours in it too, and
     the step down is a step down in size and weight, not in colour.
     --ink-3 is the only grey, and it is darkened from the #a6a6a6 in the
     comps to 4.77:1 so the rail labels it carries actually pass. */
  --ink:        #3e5215;
  --ink-2:      #3e5215;
  /* PROSE IS NOT THE BRAND.
     The comps set every word on the page in the one green -- the note two
     lines up says so on purpose -- and on a drawn page of six dishes that
     reads as considered. On a real restaurant page it is 480 dish names
     over 480 descriptions plus a week of opening hours, and Riley's word
     for the result was "too many things that are green text".
     So the SPLIT is by role, not by size: green is what NAMES something --
     the restaurant, a tier, a dish, a link, a control. Near-black is what
     you READ -- descriptions, the meta line, hours, counts. It is not #000:
     a true black next to this olive reads as a different design, and the
     faint warmth here keeps it in the same family while still being, to
     any reader, black. */
  --ink-body:   #1a1c16;
  --ink-3:      #6e7566;
  --rule:       #e8e8e3;
  --rule-soft:  #f1f1ec;
  --rule-heavy: #3e5215;

  /* THE BRAND IS THE INK. There is no separate accent in these screens --
     a link is the same green, underlined, and the only hover available is
     a change of weight in the underline. --accent stays defined because
     the search app, the popup and the closed-restaurant pages all name it;
     it now resolves to the one green rather than to a red that no drawn
     screen contains. */
  --accent:      #3e5215;
  --accent-deep: #2c3b0f;
  --accent-wash: #eef1e4;
  /* CLOSED IS THE ONE RED ON THE SITE. Radish rather than a signal red: the
     palette is olive, and a pure #d00 next to #3e5215 reads as an error
     state in the interface instead of a fact about a restaurant. The pink
     cast also keeps it in the vegetable family the whole brand sits in --
     it is the only warm colour here and it should still look like produce.
     Checked, not eyeballed: 5.13:1 against white, so the pill's uppercase
     12px clears AA. #d1445e was the nicer radish and fails at 4.47:1. */
  --closed:      #c43b52;
  --closed-ink:  #ffffff;

  /* verdicts, and nothing else is allowed to use these.
     --leaf is the comps' exact hue and is used as a DOT.
     --leaf-deep is the same hue dropped to 4.88:1 and is the only one of
     the two allowed to sit under white type. */
  --yes:        #3e5215;
  --yes-ink:    #3e5215;
  --yes-wash:   #eef1e4;
  --leaf:       #80a32b;
  --leaf-deep:  #607a20;
  --fryer:      #82a7ad;   /* shared fryer */
  --mods:       #af82ab;   /* vegan with mods, and the ask tier at large */
  --nope:       #c43b52;   /* not vegan: radish, = --closed (2026-10-13) */
  --water:      #bcd9df;   /* the pale blue the comps' maps use for sea */
  /* the old three names, kept so nothing outside this block breaks */
  --ask:        #7a5a86;
  --ask-ink:    #6a4a76;
  --ask-wash:   #f4edf4;
  --no:         var(--nope);
  --no-wash:    #fdf1e0;

  --shadow-sm: 0 1px 2px rgba(62,82,21,.05);
  --shadow-md: 0 2px 4px rgba(62,82,21,.05), 0 12px 28px -14px rgba(62,82,21,.20);
  --shadow-lg: 0 18px 48px -18px rgba(62,82,21,.26);
  --r: 14px;               /* the comps' cards and maps are softly rounded */
  --r-sm: 8px;
  --pill: 999px;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;
  --measure: 42em;

  /* ---- THE FACE ------------------------------------------------------
     Everything types through --font-brand, at exactly these two weights.
     Arboria publishes 100/300/400/500/700/900 and the design uses two of
     them; anything heavier here is a mistake, not a choice. */
  --font-brand: "arboria", "Outfit", "Avenir Next", "Century Gothic",
                -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
                sans-serif;
  --font-weight-book: 400;
  --font-weight-medium: 500;
  /* A THIRD WEIGHT, AND IT HAS EXACTLY ONE JOB. The rule above -- Medium
     for names and controls, Book for sentences -- still governs every word
     on the site. Bold exists for the "veg" in the wordmark and for nothing
     else. Adding it anywhere else is the mistake this file already warns
     about; the wordmark is a drawing, not text set in the system. */
  --font-weight-bold: 700;

  /* THE MASTHEAD NAV SIZE, IN ONE PLACE.
     Riley, twice: make the map's Cities/Search match the homepage's
     Cities/Map. The first time I made the map's number the winner --
     wrong page. The homepage was 13.5px and the map 17px, and this is now
     the only place either of them reads.
     They diverged because the scene rule set font-size on the <a> and the
     redesign set it on the <nav>: a declaration on the element beats an
     inherited value whatever the order, so the homepage never took the
     new size and neither of us could see why. */
  --nav-size: 13.5px;
  /* The pairing collapsed to one family. Both names stay because the
     search app, the map popup, the feedback controls and the ranking page
     all ask for them by name, and they should all be Arboria too. */
  --serif: var(--font-brand);
  --sans:  var(--font-brand);
}

body {
  background: var(--paper); color: var(--ink);
  font: var(--font-weight-book) 16px/1.6 var(--font-brand);
  font-variant-numeric: lining-nums;
}
/* Counts are data: tabular so a column of them lines up. */
.d-label, .num, .meta, .tag, .verdict-badge, .rate, .money, table.hours,
input, select, button, nav, .crumbs, .count-block b, .led b
  { font-variant-numeric: lining-nums tabular-nums; }

/* ---- type roles ------------------------------------------------------
   Five sizes, two weights, one colour. Medium (500) for anything that is
   a NAME, a NUMBER or a CONTROL; Book (400) for anything that is a
   SENTENCE. That single rule is the whole of the typography now that the
   family no longer changes. */
/* THE RESTAURANT'S NAME IS BLACK; A CITY'S NAME IS NOT.
   Riley's call, and the distinction holds up: on /cities.html and a city
   page the green title IS the brand introducing a place we have counted.
   On a restaurant page the title is somebody else's name, sitting above a
   menu we are reporting on, and setting it in our green reads as our
   claim rather than their sign. Same size, same weight, same tracking --
   only the colour differs, and it differs for a reason. */
.d-display.vtitle { color: var(--ink-body); }
.d-display { font-family: var(--font-brand); font-weight: var(--font-weight-medium);
             font-size: clamp(34px, 4.2vw, 47px); letter-spacing: -0.028em;
             line-height: 1.04; }
.d-title   { font-family: var(--font-brand); font-weight: var(--font-weight-medium);
             font-size: clamp(26px, 2.6vw, 35px); letter-spacing: -0.024em;
             line-height: 1.12; }
.d-head    { font-family: var(--font-brand); font-weight: var(--font-weight-medium);
             font-size: 29px; letter-spacing: -0.022em; line-height: 1.18; }
.d-body    { font-family: var(--font-brand); font-weight: var(--font-weight-book);
             font-size: 16.5px; line-height: 1.6; color: var(--ink-2); }
.d-meta    { font-family: var(--font-brand); font-weight: var(--font-weight-medium);
             font-size: 14px; line-height: 1.5; color: var(--ink-2); }
.d-label   { font-family: var(--font-brand); font-weight: var(--font-weight-book);
             font-size: 12.5px; letter-spacing: .01em; color: var(--ink-3); }

/* The eyebrow. The heavy black rule and the wide letterspaced caps were
   NYT Cooking's; nothing in these screens shouts like that, so it is now
   what the comps use over a list -- a small quiet line, no rule. */
.eyebrow { display: block; margin: 0 0 var(--s4); padding: 0;
           border-top: 0; font-family: var(--font-brand);
           font-weight: var(--font-weight-book); font-size: 13px;
           letter-spacing: 0; text-transform: none; color: var(--ink-3); }
.eyebrow.hot { color: var(--leaf-deep); }

/* THE MARGIN IS THE THING TO SHRINK, not the menu column. Widening the
   rail inside a 1520 shell just moved width from one column to the other
   and left 137px of dead space down each side untouched.
   48px matches the masthead's own left padding, so the logo and the
   restaurant name finally share a left edge instead of sitting 209px
   apart. The cap is high enough that a 1728 laptop is governed by the
   padding rather than the cap, and low enough that a 27-inch monitor does
   not run a menu line the width of a desk. */
.shell { max-width: 1760px; margin: 0 auto; padding: 0 var(--s7); }
.narrow { max-width: 760px; }
.hidden { display: none !important; }
.sep { color: var(--ink); opacity: .55; }
a { color: inherit; }

/* ---- masthead ---------------------------------------------------------
   Wordmark left, two plain links and one filled pill right. The pill is
   the single filled control on a content page, so whatever sits in it is
   read as the primary action. */
/* MEASURED OFF THE COMPS RATHER THAN ESTIMATED. Screens 03, 04 and 06 all
   draw the same masthead, and scaled to a 1280 viewport it is:

       wordmark   135px wide, 51px from the left edge
       Cities     40px wide          Map  31px wide
       Sign Up    a 93 x 34 pill, its right edge 30px from the right
       the band   89px tall

   The wordmark was 30px here and measured 104px wide against the comp's
   135 -- a ratio of exactly 1.30, which is where the 39px comes from. The
   nav and the pill were the other way round: nav slightly small, pill
   noticeably chunky at 106 x 38. All three were close enough to look
   deliberate and wrong enough to look off beside the drawing. */
/* FULL-BLEED, EVERYWHERE. It carried max-width: 1280px, so on a 1440
   screen the wordmark sat 80px in from the left on a city page and hard
   against the edge on the map and the homepage -- both of which had
   already been given their own override for exactly this reason. Walking
   from the map to /cities.html moved the wordmark 80px sideways, which is
   the "jumpy" Riley kept pointing at.
   The masthead is chrome and belongs to the window; the CONTENT below it
   is still a centred column, and the comps draw it the same way. */
.bar { max-width: none; width: 100%; padding: 26px 30px 24px 48px;
       align-items: center; gap: var(--s5); }
/* THE WORDMARK IS TWO WEIGHTS, NOT TWO COLOURS.
   ----------------------------------------------------------------------
   veg | eats. Riley's drawing: "veg" in Bold, "eats" in Book with -50
   tracking. It reads as one word at a glance and as two on a second look,
   which is what the name is doing.

   -50 IS ADOBE TRACKING, WHICH IS THOUSANDTHS OF AN EM -- so -50 is
   -0.05em, not -50 of anything CSS understands. Getting that conversion
   wrong by a factor of a thousand is the classic way this instruction
   goes astray.

   The <i> is a carry-over: it used to tint half of "side|dish". It is
   still not italic and still not emphasis -- it is the second half of a
   logotype, which HTML has no element for -- so it is spelled out here
   rather than left to inherit anything. */
.mark { font-family: var(--font-brand); font-weight: var(--font-weight-bold);
        font-size: 39px; letter-spacing: -0.035em; color: var(--ink);
        line-height: 1; }
/* TWO WEIGHTS AND TWO GREENS. The tint came off for one build when the
   wordmark was split -- the <i> had been an empty hook since the rename
   and I read it as decoration rather than as the mark. It is the mark.
   --leaf-deep, not --leaf: the palette keeps the lighter one for dots and
   the darker one for anything under or over type, and this is type. */
.mark i { font-style: normal; color: var(--leaf-deep);
          font-weight: var(--font-weight-book); letter-spacing: -0.05em;
          /* A HAIRLINE BETWEEN THE TWO HALVES. In `em`, so it holds at
             39px in a masthead and at 28px on a phone. It is fighting the
             -0.035em of tracking that .mark puts after the `g`, so the
             visible gap is about half of this -- 0.04em looked like
             nothing and 0.11em broke the mark into two words. */
          margin-left: 0.07em; }
.bar nav { font-family: var(--font-brand); font-weight: var(--font-weight-medium);
           font-size: var(--nav-size); gap: 30px; color: var(--ink);
           align-items: center; }
/* THE UNDERLINE SPACE IS MIRRORED ABOVE THE TEXT. A nav link carries 2px
   of padding and a 2px transparent border under it so the hover underline
   has somewhere to appear -- four pixels that exist only below the words.
   The masthead centres its items, so those four pixels were pushing the
   link text two pixels ABOVE the Sign Up pill's text, which has symmetric
   padding and sits where it should. Adding the same four above makes the
   link's box symmetric about its own text, and the two line up. */
.bar nav a { padding: 4px 0 2px; border-bottom: 2px solid transparent; }
.bar nav a:hover { border-bottom-color: var(--ink); }
/* THE PILL IS SMALLER THAN THE NAV, not larger. In the comps its text
   sits a step below the links beside it and the padding is tight; drawn
   at the nav's own size with 26px of padding it became the biggest thing
   in the masthead, which is not what a single filled control is for. */
/* `.cta`, NOT `a.cta`. The pill is a <span> until signup.js finds it and
   turns it into a control -- and the moment it did, the element selector
   stopped matching and the pill lost its background, its padding and its
   radius. On the map that left a 59x27 patch of green text where a 93x34
   green pill should be. Both states are the same pill and there is one
   rule for it; .is-soon below only adds the not-allowed cursor for a page
   that loads without the script. */
/* Colours on :hover too, so `.bar nav a:hover` cannot turn the text ink on
   ink. Sizes NOT on :hover: `.bar nav .cta:hover` outranks the narrow
   screen's `.bar nav .cta`, so the pill grew back to desktop size whenever
   it was hovered in a small window. */
.bar nav .cta, .bar nav .cta:hover {
  border-bottom: 0; background: var(--ink); color: #fff;
}
.bar nav .cta {
  border-radius: var(--pill); padding: 10px 21px; line-height: 1;
  font-size: 15px;
}
.bar nav .cta:hover { background: var(--accent-deep); }
/* NO RULE UNDER THE MASTHEAD. The comps float the header on white; the
   hairline was the last piece of the newspaper idiom left standing. */
.masthead-rule { display: none; }

/* ---- page header band -------------------------------------------------- */
.band { padding: var(--s8) 0 var(--s5); }
.band .crumbs { margin: 0 0 var(--s5); font-family: var(--font-brand);
                font-weight: var(--font-weight-book); font-size: 13px;
                letter-spacing: 0; text-transform: none; color: var(--ink-3); }
.band .crumbs a:hover { color: var(--ink); }
/* The separator is the comps' bullet, not a slash. The <i> is emptied
   rather than styled, because build_best_cities.py -- which this pass does
   not touch -- still writes a literal "/" inside it, and a rule that only
   prepends the bullet would render "-/" on that page. */
.band .crumbs i { font-style: normal; margin: 0 var(--s2); opacity: .75;
                  font-size: 0; }
.band .crumbs i::before { content: "\2022"; font-size: 13px; }
.band h1 { margin: 0; }
.band h1 em { font-style: normal; color: var(--ink-3);
              font-weight: var(--font-weight-book); }
.band .lede { margin: var(--s3) 0 0; max-width: var(--measure);
              font-family: var(--font-brand); font-weight: var(--font-weight-medium);
              font-size: 19px; line-height: 1.45; color: var(--ink-2); }

/* THE TITLE ROW: name and status pill on one baseline, as drawn. */
/* --title-size mirrors .d-display's clamp so the tag's optical correction
   below can be expressed as a fraction of the TITLE, which is the thing it
   has to line up with. Keep the two in step if the title scale changes. */
.titlerow { display: flex; align-items: center; gap: var(--s4);
            flex-wrap: wrap; --title-size: clamp(34px, 4.2vw, 47px); }

/* THE META LINE: cuisine, address, open/closed, bullet-separated, medium
   weight, the same green as everything else. This replaced a row of
   coloured chips -- a star, a price, a heart -- none of which appears in
   any drawn screen. */
/* `js-open` is written by openstate.js in the reader's own clock, so it is
   an empty span at first paint and the meta line visibly reflowed when it
   filled. A skeleton of roughly the right width holds the gap. :empty stops
   applying the moment the script writes into it. */
/* Still, for the same reason as the map: this sits in the meta line where
   nothing else is moving. */
.js-open:empty { display: inline-block; width: 5.5em; height: .72em;
                 border-radius: 999px; background: var(--rule); }

.vline { display: flex; align-items: center; gap: var(--s3);
         flex-wrap: wrap; margin: var(--s3) 0 0;
         font-family: var(--font-brand); font-weight: var(--font-weight-medium);
         font-size: 17px; color: var(--ink-body); }
/* THE BULLET TRAILS ITS OWN ITEM, it does not lead the next one. As a
   ::before on the following span, a meta line that wrapped opened the
   second line with a stray bullet -- "- $$$ - plus 39 sides" -- because
   the separator belonged to the item that had moved down. Trailing, the
   break happens after a separator, which is where a reader expects it. */
.vline > *:not(:last-child)::after { content: "\2022"; margin-left: var(--s3);
                                     opacity: .55; }

/* THE LINK ROW: View Menu / Directions / phone, underlined, nothing else.
   The comps underline them because on a page with one colour an underline
   is the only thing left that says "clickable". */
.acts { margin-top: var(--s4); display: flex; gap: var(--s6); flex-wrap: wrap; }
.acts a, .acts button {
  font-family: var(--font-brand); font-weight: var(--font-weight-medium);
  font-size: 15px; letter-spacing: 0; color: var(--ink); border: 0;
  background: none; padding: 0 0 3px; cursor: pointer;
  border-bottom: 1.5px solid var(--ink); }
/* The hover underline THICKENS WITHOUT MOVING ANYTHING. It used to set
   border-bottom-width: 3px, which made each link 1.5px taller, so the whole
   page below it shifted on every hover -- Riley: "hovering over Website,
   Directions, (310) ... shouldn't move everything else." The extra weight is
   a shadow under the existing line, which takes no space. */
.acts a:hover, .acts button:hover { box-shadow: 0 1.5px 0 var(--ink); }

/* ---- the verdict colours ----------------------------------------------
   A legend, and the dots it explains. The dot is the whole of the colour
   coding: it sits in front of a dish name, the name stays readable green,
   and the tier is ALSO named in words by the section heading above it, so
   nothing here is carried by hue alone. */
.legend { display: flex; flex-wrap: wrap; gap: var(--s5) var(--s6);
          margin: var(--s6) 0 0; padding: 0; list-style: none;
          font-family: var(--font-brand); font-weight: var(--font-weight-book);
          font-size: 13.5px; color: var(--ink); }
.legend li { display: inline-flex; align-items: center; gap: var(--s2);
             color: var(--ink); }
.dot { display: inline-block; flex: 0 0 auto; width: 11px; height: 11px;
       border-radius: 50%; background: currentColor; }
.k-vegan  { color: var(--yes); }
.k-likely { color: var(--leaf); }
.k-fryer  { color: var(--fryer); }
.k-mods   { color: var(--mods); }
.k-no     { color: var(--nope); }
/* An add-on or option: NO verdict, so no verdict colour. The other five
   swatches are this page's vocabulary for an answer, and an option has none
   of its own -- the answer sits on the dish you add it to. Grey is the one
   thing in that palette that says nothing. */
.k-opt    { color: var(--ink-3); }

/* The swatch in front of a dish name. It pins to the FIRST line: the row
   centres its arrows, which is right for them, but a dot centred against a
   dish name that wrapped to two lines floats in the gutter beside the
   break instead of marking the name. flex-start plus a nudge puts it on
   the first line's x-height and leaves a one-line row looking centred. */
.dish-name .dot { margin-right: var(--s1); align-self: flex-start;
                  margin-top: 10px; }

/* THE OLD BADGE, kept for the search app and the map popup, which build
   their own markup and are not in this pass. Restated in the new palette
   so nothing on those screens is still wearing the amber-and-red scheme. */
.verdict-badge {
  display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
  font-family: var(--font-brand); font-weight: var(--font-weight-medium);
  font-size: 11.5px; letter-spacing: .02em; text-transform: none;
  padding: 4px 11px; border-radius: var(--pill); white-space: nowrap;
  border: 1px solid transparent;
}
.verdict-badge svg { width: 12px; height: 12px; flex: 0 0 auto; }
.v3 { background: var(--yes); color: #fff; }
.v2 { background: var(--leaf-deep); color: #fff; }
.v1 { background: var(--ask-wash); color: var(--ask-ink);
      border-color: color-mix(in srgb, var(--mods) 45%, transparent); }
.v0 { background: transparent; color: var(--ink-3); border-color: var(--rule); }

/* the status pill beside a restaurant name -- how much there is to eat.
   THE WORDS COME FROM verdicts.grade() AND ITS THRESHOLDS ARE UNCHANGED;
   only the two fills are new. The comps draw a dark pill and a light one,
   which is exactly the good/some split grade() already makes. */
.tag { display: inline-flex; align-items: center;
       font-family: var(--font-brand); font-weight: var(--font-weight-medium);
       font-size: 13px; letter-spacing: 0; padding: 6px 15px;
       border-radius: var(--pill); white-space: nowrap; line-height: 1.15; }
.tag-good { color: #fff; background: var(--yes); }
.tag-some { color: #fff; background: var(--leaf-deep); }
.tag-thin { color: var(--ink); background: var(--band-deep); }
/* The only tag that is not about what a restaurant serves. Radish, and the
   solid fill the others do not get, because the rest describe a menu and
   this one overrules the whole page.
   Sized off the comp (vegeats-07): the pill reads at roughly half the cap
   height of the name beside it, in title case. It was uppercase at 11.5px,
   which is a metadata chip -- too quiet to be the most important fact on
   the page. */
.tag-closed { color: var(--closed-ink); background: var(--closed); }

/* TITLE TAGS ARE BIGGER THAN LIST TAGS, and this is the only place the
   distinction matters. A tag beside a 47px restaurant name is a headline
   element -- in the comp it reads at about half the cap height of the name.
   The same component at 13px inside a card is correct there and far too
   quiet here. Applies to every title tag: Fully Vegan, Sides only, Whole
   menu, Permanently closed.

   OPTICALLY CENTRED, NOT BOX CENTRED. `align-items: center` lines the pill
   up with the middle of the h1's LINE BOX, and that box carries descender
   space under the baseline that the letterforms do not use -- so the pill
   sits visibly low against the caps it is meant to sit beside. Lifting it
   by a fraction of its own size puts its centre on the cap-height centre,
   which is where the comp has it. In em so it tracks the clamped title. */
.titlerow .tag {
  font-size: 15px;
  padding: 9px 19px;
  /* MEASURED, not reasoned. Two earlier attempts guessed at this and both
     were wrong -- first a -0.055em nudge that resolved against the tag's
     own 15px and moved it 0.8px, then a -0.09em correction in the wrong
     DIRECTION, on the assumption that the line box reserves descender
     space below the caps. It reserves more above.

     Rendered headless and measured in pixels: flex centring alone put the
     pill 3.3px high against the cap band of the name, and the -0.09
     correction took it to 7.5px high, which is what "aligned to the top of
     the title" looks like. +0.07 of the title size puts the two centres on
     each other. Expressed against --title-size so it tracks the clamp. */
  transform: translateY(calc(var(--title-size) * 0.07));
}
.tag-meat { color: var(--ink-3); background: var(--band); }

/* Yelp's numbers. Nothing in the comps carries a star, so these are not
   chips any more -- they are the tail of the meta line, at the same size,
   and they read as part of the sentence.

   THEY ARE BLACK, LIKE THE REST OF THAT SENTENCE. They were green because
   everything was; once the prose around them went black they were three
   coloured fragments in the middle of a black line. A rating and a price
   are facts about the restaurant, not names of it.

   `.liked` KEEPS THE GREEN, and that is the same call as `Open now`: it is
   a recommendation rather than a measurement -- Yelp's reviewers saying
   vegans do well here -- so it is the one thing in the line worth the
   reader's eye landing on. */
.rate, .money, .liked { font-family: var(--font-brand);
                        font-weight: var(--font-weight-medium);
                        font-size: 17px; color: var(--ink); white-space: nowrap; }
.rate, .money { color: var(--ink-body); }
.rate b { font-weight: var(--font-weight-medium); }
.rate small, .liked small { font-size: inherit; font-weight: inherit; }
/* OPEN IS THE GREEN ONE, CLOSED IS NOT. Both were --ink, which meant a
   restaurant that is shut said "Closed" in the same brand green as
   everything good on the page -- the one word in the meta line still
   coloured after prose went black, and coloured to mean nothing. Open now
   is worth the green because it is the answer to "can I go"; closed is a
   fact and reads as one. */
.open-now, .open-shut { font-family: var(--font-brand);
                        font-weight: var(--font-weight-medium);
                        font-size: 17px; }
.open-now { color: var(--ink); }
.open-shut { color: var(--ink-body); }

/* ---- controls ----------------------------------------------------------
   The filter chips. Outlined when idle, filled with the pale band when
   pressed -- sampled straight off the city screen, where the pressed chip
   is #ecede7 and not a black fill. */
/* THE CHIP ROW SCROLLS, IT DOES NOT WRAP.
   It was four cuisines and a sort; it is now up to eighteen categories,
   Fully Vegan and Open now, and wrapped that is three stacked lines of
   grey pills above the first restaurant -- a filter bar taller than the
   thing it filters. The map's row already solved this by scrolling, and
   this is the same control, so it scrolls the same way: taxonomy order,
   first ones visible, the rest a swipe away. The sort select is lifted
   out of the scroller so it stays reachable at the right. */
.toolbar { display: flex; align-items: center; gap: var(--s2);
           flex-wrap: nowrap; padding: 0 0 var(--s6); border-bottom: 0; }
.toolbar .grow { margin-left: auto; flex: 0 0 auto; }
.toolbar .chips { display: flex; align-items: center; gap: var(--s2);
                  min-width: 0; overflow-x: auto; overscroll-behavior-x: contain;
                  scrollbar-width: none; -ms-overflow-style: none;
                  padding-bottom: 2px; }
.toolbar .chips::-webkit-scrollbar { display: none; }
.toolbar .chips .pill { flex: 0 0 auto; }
.pill { font-family: var(--font-brand); font-weight: var(--font-weight-book);
        font-size: 14px; border: 1px solid var(--rule); background: var(--card);
        color: var(--ink); border-radius: var(--pill); padding: 8px 18px;
        cursor: pointer; white-space: nowrap;
        transition: background .14s ease, border-color .14s ease; }
.pill:hover { border-color: var(--ink-3); }
.pill[aria-pressed="true"] { background: var(--band-deep); color: var(--ink);
                             border-color: var(--band-deep);
                             font-weight: var(--font-weight-medium); }
.picker { font-family: var(--font-brand); font-weight: var(--font-weight-book);
          font-size: 14px; border: 1px solid var(--rule); background: var(--card);
          color: var(--ink); border-radius: var(--pill); padding: 8px 16px;
          cursor: pointer; }

/* ---- the listing row ---------------------------------------------------
   The city screen's row: a large numeral on the left under the word it
   counts, then the name and its status pill, a bullet meta line, one
   sentence naming what you could order, and the three underlined links.
   No card, no border box -- one hairline between rows. */
.tally { padding: var(--s6) 0 var(--s3); }
.rows { border-top: 0; }
.vrow { display: grid; grid-template-columns: 130px minmax(0, 1fr);
        gap: var(--s5); padding: var(--s6) 0;
        border-bottom: 1px solid var(--rule); }

/* The count block. The comps put a big number here and the word under it;
   we do not have a photograph for 13,000 restaurants and this is the one
   number the whole product exists to produce, so it earns the slot.
   THE WORD IS "VEGAN MAINS", NOT "DISHES". The comp says Dishes; the
   number is dish_kind.count()[0], which is mains only -- sides, drinks and
   desserts are excluded -- and labelling a mains count "Dishes" would be
   the page misstating its own number. */
.count-block { text-align: left; padding-top: 2px; }
.count-block b { display: block; font-family: var(--font-brand);
                 font-weight: var(--font-weight-medium); font-size: 42px;
                 letter-spacing: -0.035em; line-height: .95; color: var(--ink); }
.count-block.none b { color: var(--ink-3); }
.count-block span { display: block; margin-top: var(--s1);
                    font-family: var(--font-brand);
                    font-weight: var(--font-weight-book); font-size: 13px;
                    letter-spacing: 0; text-transform: none;
                    color: var(--ink-2); line-height: 1.25; }

.vhead { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }
.vname { display: inline-block; font-family: var(--font-brand);
         font-weight: var(--font-weight-medium); font-size: 30px;
         letter-spacing: -0.024em; line-height: 1.15; }
.vrow:hover .vname { text-decoration: underline;
                     text-decoration-thickness: 2px;
                     text-underline-offset: 4px; }
.vtags { display: none; }

/* What you can actually order, named. The comps set it as one sentence
   beginning "Offers", which is what turns a directory entry into a
   recommendation -- so it is a sentence, in Book, not a row of chips. */
.proof { margin-top: var(--s3); font-family: var(--font-brand);
         font-weight: var(--font-weight-book); font-size: 16.5px;
         line-height: 1.5; color: var(--ink); max-width: var(--measure); }
.proof b { font-weight: var(--font-weight-book); }
.proof b::after { content: " "; }
.proof span::after { content: ", "; }
.proof span:last-child::after { content: none; }

/* ---- rail ---------------------------------------------------------------
   A map card and a list, both flush right, no boxes. */
/* The rail holds a map, seven rows of hours and a nearby list -- all of
   which were cramped at 356px while 96px of nothing sat beside them. */
/* The rail takes the width the menu column could not use. Its text is
   capped at --measure, so past about 900px the menu column just grows white
   space -- while the rail holds a map, seven rows of hours and a nearby
   list, all of which were cramped at 356px. */
.with-rail { display: grid; grid-template-columns: minmax(0,1fr) 470px;
             gap: var(--s8); align-items: start; padding-bottom: var(--s8); }
@media (max-width: 1080px) { .with-rail { grid-template-columns: 1fr;
                                          gap: var(--s7); } }
.rail { position: sticky; top: var(--s5); }
.rail h4 { margin: 0 0 var(--s3); }
.rail + .rail, .rail-block + .rail-block { margin-top: var(--s6); }
.rail-block { margin-bottom: var(--s6); }
.near a { display: flex; justify-content: space-between; gap: var(--s3);
          align-items: baseline; padding: var(--s4) 0;
          border-bottom: 1px solid var(--rule); }
.near a:first-child { border-top: 1px solid var(--rule); }
.near a > span { font-family: var(--font-brand);
                 font-weight: var(--font-weight-medium); font-size: 18px; }
.near a:hover > span { text-decoration: underline;
                       text-decoration-thickness: 2px;
                       text-underline-offset: 4px; }
.near em { font-style: normal; color: var(--ink-3);
           font-family: var(--font-brand); font-weight: var(--font-weight-book);
           font-size: 13.5px; white-space: nowrap; }

/* ---- cities index -------------------------------------------------------- */
.filterbar { display: flex; align-items: center; gap: var(--s4);
             padding: 0 0 var(--s4); border-bottom: 1px solid var(--rule); }
.filterbar input { flex: 1; max-width: 380px; border: 0; background: none;
                   color: var(--ink); font-family: var(--font-brand);
                   font-weight: var(--font-weight-book); font-size: 20px;
                   padding: var(--s2) 0; outline: 0; }
.filterbar input::placeholder { color: var(--ink-3); }
.citylist { columns: 3; column-gap: var(--s8); padding: var(--s5) 0 var(--s8); }
@media (max-width: 900px) { .citylist { columns: 2; } }
@media (max-width: 560px) { .citylist { columns: 1; } }
.citylist a { display: flex; justify-content: space-between; gap: var(--s3);
              align-items: baseline; break-inside: avoid; padding: var(--s3) 0;
              border-bottom: 1px solid var(--rule-soft); }
.citylist a:hover b { text-decoration: underline;
                      text-underline-offset: 4px; }
.citylist b { font-family: var(--font-brand);
              font-weight: var(--font-weight-medium); font-size: 17px; }
.citylist em { font-style: normal; color: var(--ink-3);
               font-family: var(--font-brand);
               font-weight: var(--font-weight-book); font-size: 13px;
               white-space: nowrap; }

/* ---- a restaurant --------------------------------------------------------- */
/* The answer, in one sentence. It is not in the drawn screens -- they show
   a header and then the menu -- but it is the sentence this whole product
   exists to produce, and the screens' own logic puts the answer before the
   evidence. So it stays, sized as a lede rather than as a pull quote, and
   it sits between the link row and the legend where it reads as the
   summary of the sections underneath. */
.verdict { font-family: var(--font-brand); font-weight: var(--font-weight-book);
           font-size: 19px; line-height: 1.5; color: var(--ink-body);
           max-width: var(--measure); margin: var(--s6) 0 0; }
.verdict b { font-weight: var(--font-weight-medium); color: var(--ink); }
.verdict b.ask { color: var(--ink); }

/* The sibling caveat, and any other bare sentence a builder drops beside
   the verdict. It had no rule of its own and inherited the body, so on a
   restaurant whose menu we read at a different branch the caveat was the
   same size and weight as the verdict it qualifies. */
p.note { margin: var(--s3) 0 0; max-width: var(--measure);
         color: var(--ink-body);
         font-family: var(--font-brand); font-weight: var(--font-weight-book);
         font-size: 15px; line-height: 1.5; color: var(--ink-3); }
.sk-note, .sk-good { margin: var(--s3) 0 0; font-family: var(--font-brand);
                     font-weight: var(--font-weight-book); font-size: 15px; }

/* The ledger is gone from the venue page: every section heading now
   carries its own count, which is where the comps put that number. The
   rules stay for the closed-restaurant page, which still builds one. */
.ledger { display: flex; gap: var(--s7); flex-wrap: wrap;
          padding: var(--s5); margin-bottom: var(--s6);
          background: var(--band); border-radius: var(--r); }
.led b { display: block; font-family: var(--font-brand);
         font-weight: var(--font-weight-medium); font-size: 32px;
         letter-spacing: -0.035em; line-height: 1; }
.led span { display: block; margin-top: var(--s1);
            font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 13px;
            letter-spacing: 0; text-transform: none; color: var(--ink-2); }
.led.off b { color: var(--ink-3); }
.led.t3 b { color: var(--yes); }
.led.t2 b { color: var(--leaf-deep); }
.led.t1 b { color: var(--mods); }
.led.t0 b { color: var(--ink-3); }

/* The jump nav takes the filter-chip shape from the city screen. On a
   400-dish menu it is the only way to reach the ask list without a scroll,
   so it stays -- but it now looks like the rest of the system. */
.jump { display: flex; gap: 6px; flex-wrap: wrap; padding: var(--s6) 0 0;
        border-bottom: 0; position: static; background: none; }
/* A dot and a short name, per vegeats-07/08. The dot is the same swatch the
   dish rows use, so a colour carries one meaning down the whole page. */
.jump a { display: inline-flex; align-items: center; gap: 7px;
          font-family: var(--font-brand); font-weight: var(--font-weight-medium);
          font-size: 13.5px; letter-spacing: 0; text-transform: none;
          color: var(--ink-body); border: 1px solid var(--rule);
          border-radius: var(--pill); padding: 7px 13px 8px; }
.jump a .dot { width: 10px; height: 10px; }
.jump a:hover { border-color: var(--ink-3); }

/* A TIER SECTION: a big heading and its count beside it, exactly as drawn
   ("Vegan as it Comes    480 Dishes"). */
.tier { padding: var(--s8) 0 0; }
.tier-h { display: flex; align-items: baseline; gap: var(--s5);
          margin: 0 0 var(--s2); }
.tier-h em { font-style: normal; font-family: var(--font-brand);
             font-weight: var(--font-weight-medium); font-size: 16px;
             color: var(--ink-body); letter-spacing: 0; white-space: nowrap; }
.tier > .note { margin: 0 0 var(--s5); max-width: var(--measure);
                font-family: var(--font-brand);
                font-weight: var(--font-weight-book); font-size: 16px;
                color: var(--ink-body); }

/* the restaurant's own heading, inside a verdict tier. Small, over a rule,
   as drawn ("Brunch"). */
.menu-sec { margin: var(--s7) 0 0; font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 13px;
            letter-spacing: 0; text-transform: none; color: var(--ink-2);
            border-bottom: 1px solid var(--rule); padding-bottom: var(--s2); }
.menu-sec:first-of-type { margin-top: var(--s3); }
/* .drinks-divider is gone. There were TWO of them, at opposite ends of
   this file, styling the same class two different ways -- so which
   treatment the divider got depended on which rule came last, and nobody
   had chosen. Nothing emits the class any more: the drinks are behind
   their own tab now (see .mtabs at the end of this file), which is what
   Riley asked for and what the divider was standing in for. */

.dish { padding: var(--s5) 0; border-bottom: 1px solid var(--rule); }
.dish:last-child { border-bottom: 1px solid var(--rule); }
.dish-name { font-family: var(--font-brand);
             font-weight: var(--font-weight-medium); font-size: 20px;
             letter-spacing: -0.018em; display: flex; align-items: center;
             gap: var(--s3); flex-wrap: nowrap; }
.dish-name > .dname { min-width: 0; }
.dish-sec { font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 12.5px;
            letter-spacing: 0; text-transform: none; color: var(--ink-3); }
.dish-desc { margin: var(--s2) 0 0; font-family: var(--font-brand);
             font-weight: var(--font-weight-book); font-size: 15px;
             color: var(--ink-body); line-height: 1.5;
             max-width: var(--measure); }

/* The question to ask at the counter. The amber block it used to sit in
   does not exist in this palette; it is now a plain indented line under
   the dish, marked by the ask colour as a rule down its left edge. */
.dish-ask { margin: var(--s3) 0 0; font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 15px;
            color: var(--ink); line-height: 1.5; max-width: var(--measure);
            padding: 0 0 0 var(--s3); background: none;
            border-left: 2px solid var(--mods); border-radius: 0; }
.dish-ask b { font-weight: var(--font-weight-medium); letter-spacing: 0;
              text-transform: none; font-size: inherit; display: inline;
              color: var(--ink); }
/* The sub-state chips inside the ask tier: the only place a word is needed
   that the section heading does not already say. */
.fry, .mod, .optv { font-family: var(--font-brand);
             font-weight: var(--font-weight-book); font-size: 12px;
             letter-spacing: 0; text-transform: none; color: var(--ink);
             padding: 3px 10px; border-radius: var(--pill);
             white-space: nowrap; flex: 0 0 auto; }
.fry { background: color-mix(in srgb, var(--fryer) 30%, #fff); }
.mod { background: color-mix(in srgb, var(--mods) 26%, #fff); }
/* A vegan-tier OPTION's chip ("vegan choice", 2026-09-18). The leaf green of
   the likely-vegan swatch, not the mods yellow: it sits beside a vegan dot. */
.optv { background: color-mix(in srgb, var(--leaf) 22%, #fff); }
/* ADD-ONS UNDER THEIR DISH (gate addon_attach_display, lane addon_attach,
   2026-09-18). One add-on per line, each with its own dot and answer; the
   fallback "add-on" tag is a neutral pill, not the vegan green. */
.optv.addon { background: color-mix(in srgb, var(--rule) 60%, #fff); }
.dish-addons { font-family: var(--font-brand); font-size: 13px;
               color: var(--ink-body); margin: 4px 0 0; }
.dish-addons ul { list-style: none; margin: 2px 0 0; padding: 0 0 0 14px; }
.dish-addons li.ao { margin: 1px 0; }
.dish-addons li.ao .dot { margin-right: 6px; }
.byo-item .dish-addons .opt-lead { font-weight: var(--font-weight-medium); }

table.hours { width: 100%; border-collapse: collapse;
              font-family: var(--font-brand); font-size: 14px; }
table.hours th { text-align: left; font-weight: var(--font-weight-medium);
                 color: var(--ink); padding: var(--s3) var(--s4) var(--s3) 0;
                 width: 62px; border-bottom: 1px solid var(--rule);
                 vertical-align: baseline; }
table.hours td { padding: var(--s3) 0; color: var(--ink-body);
                 font-weight: var(--font-weight-book); font-size: 13.5px;
                 border-bottom: 1px solid var(--rule); vertical-align: baseline; }
table.hours tr:last-child th, table.hours tr:last-child td
  { border-bottom: 0; }
table.hours tr.today th, table.hours tr.today td
  { font-weight: var(--font-weight-medium); }

/* A DAY WE NEVER READ (gate hours_unread_day_not_closed). The cell used to
   say "Closed" and now holds a dash, which has to read as an absence and
   not as a value -- so it takes --ink-3, the one grey, which is what the
   rail already uses for a label rather than for a thing it is asserting,
   and never --ink-body, which is what a real time span is set in. The
   note under the table carries the sentence; it inherits .d-meta and only
   needs the gap. The .hrspop panel's own note rides the same class. */
table.hours td .h-unread { color: var(--ink-3); }
.hours-unread { margin: var(--s3) 0 0; }

/* The map card. Rounded and borderless in the comps, sitting on its own
   pale ground while the tiles load. */
/* Taller with the wider rail: at 470px across, 300px tall was letterbox
   proportions on the one element people zoom into. */
/* A STILL PLACEHOLDER, THEN A FADE. This first shipped as a moving
   shimmer, and frame-by-frame capture of the load showed the shimmer WAS
   the flicker: between 50ms and 400ms the only region of the page changing
   was this one, and it was changing because the sheen was sweeping across
   it. Everything else -- text, panels, the pill rows -- was already still.
   A skeleton that animates on a page where nothing else moves draws the eye
   to the one thing that is not ready yet. So: a flat, quiet ground, and the
   map eases in over it. */
.minimap { position: relative; width: 100%; height: 380px; display: block;
           border: 0; border-radius: var(--r); overflow: hidden;
           background: var(--band); }
/* THE MAP FADES IN WHEN IT IS DRAWN, not on a timer. An animation that
   starts when the canvas is inserted can finish while the tiles are still
   decoding, so the reader watches the top strip arrive before the rest --
   which is the "flashing in from the top". minimap.js now adds `is-drawn`
   on MapLibre's own `idle` event, and the canvas is invisible until then,
   sitting on the placeholder ground rather than on nothing. */
.minimap canvas { opacity: 0; transition: opacity .4s ease; }
.minimap.is-drawn canvas { opacity: 1; }
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }

/* The canvas is opaque, so the shimmer is invisible the moment the map
   draws -- but it is still running. Stop it for anyone who asked. */
@media (prefers-reduced-motion: reduce) {
  .minimap canvas { animation: none; }
}
.minimap > div:first-child { position: absolute; inset: 0; }

footer { color: var(--ink-3); font-family: var(--font-brand);
         font-weight: var(--font-weight-book); font-size: 13.5px;
         padding: var(--s7) 0 var(--s8); margin-top: var(--s8);
         border-top: 1px solid var(--rule); }
footer .shell { max-width: 1760px; }

@media (max-width: 700px) {
  .shell { padding: 0 var(--s5); }
  .bar { padding: var(--s4) var(--s5); gap: var(--s4); }
  /* Sized here and at 620px in the scene block; both had their own
     numbers, which is how the two mastheads drifted apart in the first
     place. One set, both mastheads -- see the 620px block. */
  .mark { font-size: 28px; }
  .bar nav { gap: 16px; font-size: 14px; }
  .bar nav .cta { padding: 9px 16px; font-size: 14px; }
  .band { padding: var(--s6) 0 var(--s4); }
  .vrow { grid-template-columns: 78px minmax(0,1fr); gap: var(--s4);
          padding: var(--s5) 0; }
  .count-block b { font-size: 30px; }
  .count-block span { font-size: 11.5px; }
  .vname { font-size: 23px; }
  .vline { font-size: 15px; gap: var(--s2); }
  .vline > *:not(:last-child)::after { margin-left: var(--s2); }
  .rate, .money, .liked, .open-now, .open-shut { font-size: 15px; }
  .proof { font-size: 15.5px; }
  .acts { gap: var(--s5); }
  /* Pushed right by margin-left:auto, the sort control landed alone on its
     own line hard against the edge once the chips wrapped. */
  .toolbar .grow { margin-left: 0; }
  .acts a, .acts button { font-size: 14px; }
  .dish-name { font-size: 17.5px; gap: var(--s2); }
  .dish-desc { font-size: 14px; }
  .tier { padding-top: var(--s7); }
  .tier-h { gap: var(--s3); }
  .ledger { gap: var(--s5); padding: var(--s4); }
  .rail { position: static; }
  .minimap { height: 220px; }
  .legend { gap: var(--s3) var(--s5); font-size: 13px; }
}

/* ======================================================================
   RESULTS — the split screen
   ======================================================================

   RESTORED. This block was deleted with the design-system rewrite above it
   and the search page came up with unstyled native inputs for it. It is
   back in the vegeats idiom rather than back verbatim: pill controls, one
   green, square corners gone. The search app is not otherwise part of this
   pass -- it builds its own rows and its own badges in JavaScript -- so
   nothing here is doing more than making its chrome match the pages it
   links to.
   ====================================================================== */
.page-results { --barh: 132px; }
.findbar { position: sticky; top: 0; z-index: 30; background: var(--paper);
           border-bottom: 1px solid var(--rule); }
.findbar .shell { max-width: 1440px; padding-top: var(--s3);
                  padding-bottom: var(--s3); }

/* The search field takes the landing page's shape: one long rounded bar
   with the button living inside it. */
.seek { display: flex; align-items: center; gap: var(--s3);
        border: 1px solid var(--rule); background: var(--card);
        border-radius: var(--pill); padding: 0 6px 0 var(--s5);
        transition: border-color .15s ease, box-shadow .15s ease; }
.seek:focus-within { border-color: var(--ink);
                     box-shadow: 0 0 0 3px var(--accent-wash); }
.seek > svg { flex: 0 0 auto; color: var(--ink-3); }
.seek input { flex: 1; min-width: 0; border: 0; outline: 0; background: none;
              color: var(--ink); font-family: var(--font-brand);
              font-weight: var(--font-weight-book); font-size: 18px;
              padding: 13px 0; -webkit-appearance: none; }
.seek input::placeholder { color: var(--ink-3); font-style: normal; }
.seek input::-webkit-search-cancel-button { display: none; }
.seek button { flex: 0 0 auto; border: 0; border-radius: var(--pill);
               cursor: pointer; background: var(--ink); color: #fff;
               font-family: var(--font-brand);
               font-weight: var(--font-weight-medium); font-size: 15px;
               letter-spacing: 0; padding: 11px 26px; margin: 5px 0; }
.seek button:hover { background: var(--accent-deep); }

.controls { display: flex; align-items: center; gap: var(--s5);
            flex-wrap: wrap; margin-top: var(--s4); }
.ctl { display: flex; align-items: center; gap: var(--s2); }
.ctl > .d-label { flex: 0 0 auto; font-size: 12.5px; letter-spacing: 0; }
.seg { display: flex; gap: var(--s1); }
.seg button { font-family: var(--font-brand); border: 1px solid var(--rule);
              background: var(--card); color: var(--ink); cursor: pointer;
              font-size: 13.5px; font-weight: var(--font-weight-book);
              padding: 6px 14px; border-radius: var(--pill);
              white-space: nowrap;
              transition: background .13s ease, border-color .13s ease; }
.seg button:hover { border-color: var(--ink-3); }
.seg button[aria-pressed="true"] { background: var(--band-deep);
                                   border-color: var(--band-deep);
                                   font-weight: var(--font-weight-medium); }
.seg button[disabled] { opacity: .35; cursor: not-allowed; background: none; }
.ctl select { font-family: var(--font-brand); border: 1px solid var(--rule);
              border-radius: var(--pill); background: var(--card);
              color: var(--ink); font-size: 13.5px;
              font-weight: var(--font-weight-book); padding: 6px 14px;
              cursor: pointer; max-width: 210px; }
.ctl .plain { font-family: var(--font-brand); border: 0; background: none;
              color: var(--ink); cursor: pointer; font-size: 14px;
              font-weight: var(--font-weight-medium); padding: 0 0 2px;
              border-bottom: 1.5px solid var(--ink); }
.ctl .plain:hover { box-shadow: 0 1.5px 0 var(--ink); }

.split { display: grid; grid-template-columns: minmax(0, 1fr) 40%;
         gap: var(--s7); max-width: 1440px; margin: 0 auto;
         padding: 0 var(--s6) var(--s8); align-items: start; }
.count { padding: var(--s5) 0 var(--s3); font-family: var(--font-brand);
         font-size: 13.5px; font-weight: var(--font-weight-book);
         color: var(--ink-3); }
.mappane { position: sticky; top: var(--barh);
           height: calc(100vh - var(--barh) - var(--s4));
           height: calc(100dvh - var(--barh) - var(--s4)); margin-top: var(--s5); }
.mapbox { position: relative; width: 100%; height: 100%;
          border: 0; border-radius: var(--r);
          overflow: hidden; background: var(--water); }
/* MapLibre measures its container on construction; without this the canvas
   comes up 300px tall inside a 700px box and the map looks broken. */
.mapbox > div:first-child { position: absolute; inset: 0; }
.maplegend { position: absolute; left: var(--s3); bottom: var(--s3); z-index: 2;
             display: flex; gap: var(--s3); padding: 7px 14px;
             border-radius: var(--pill);
             background: color-mix(in srgb, var(--card) 92%, transparent);
             border: 1px solid var(--rule); font-family: var(--font-brand);
             font-size: 12px; font-weight: var(--font-weight-book);
             color: var(--ink);
             backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.maplegend i { font-style: normal; display: inline-flex; align-items: center;
               gap: 5px; }
/* The dot takes the verdict colour; the WORD stays ink, for the reason the
   page legend gives -- none of these hues is readable as type on white. */
.maplegend i::before { content: ""; width: 9px; height: 9px;
                       border-radius: 999px; background: var(--dotc, currentColor); }
.maplegend .k3 { --dotc: var(--yes); }
.maplegend .k2 { --dotc: var(--mods); }
.maplegend .k1 { --dotc: var(--nope); }

/* ---- a result -------------------------------------------------------
   Not a card. Cards inside a scrolling column stack into a lumpy pile of
   rounded rectangles; both references use a full-width row and a hairline,
   which reads as a list you can scan instead of a feed you must swipe. */
.hit { scroll-margin-top: calc(var(--barh) + var(--s4));
       padding: var(--s5) 0; border-bottom: 1px solid var(--rule);
       transition: background .14s ease; }
/* and none above the first one either: the app opens its list with space,
   not with a green bar (SearchTabView.swift draws no rule above a card). */
.hit.lit { background: var(--band); box-shadow: -12px 0 0 var(--band),
                                                12px 0 0 var(--band); }
/* THE COUNT SITS WITH THE HEADER, AND ON THE DISHES' OWN LEFT EDGE
   (Riley, 2026-10-12). Two changes and each has a reference line:

   `align-items: center` measures it against the block beside it -- the
   name, the open line and the city/cuisine/distance line -- instead of
   hanging it from the top, where a two-line venue name left it sitting
   high. The 3px of optical padding went with it: that existed only to
   drop the big numeral against the title's cap height when both started
   at the top.

   `text-align: left` puts the numeral and its words on the card's left
   edge, which is where every verdict badge below starts, so the column
   of a card now reads down a single line instead of stepping in and out
   of a centred 96px box. */
/* THE COLUMN IS AS WIDE AS ITS WIDEST CONTENT, NOT A ROUND NUMBER.
   96px was chosen when the count was CENTRED, where slack is invisible
   because it sits either side of the text. Left-aligned it all collects on
   one side: a single-digit count ended up 97px from the title with nothing
   in between (Riley: "why didn't you move the title, hours,
   location/cuisine/distance lines over to the left ... to keep the
   spacing"). Measured off the rendered card, the widest this column can
   ever hold is a three-digit numeral at 69px (LA's largest count is 669 and
   6.6% of venues are three-digit), against 42px for the words, so 72px
   holds every case with 3px to spare and moves the whole header block 24px
   left. The phone's 56px becomes 48px against a 47px numeral.

   The gap cannot be constant: left-aligned, a 1-digit and a 3-digit count
   end at different places by definition. What this removes is the dead
   space, which is the part that was nobody's intention. */
/* THE GAP IS s4, NOT s5 (Riley, 2026-10-12: "less padding to the right of
   count vegan mains"). The count already carries slack on that side from
   being centred in a column sized for a three-digit numeral, so s5 on top
   of it put 39px between the words and the venue name. s4 brings that to
   31px without touching the centring he just asked for. */
.hit-top { display: grid; grid-template-columns: 72px minmax(0, 1fr);
           gap: var(--s4); align-items: center; }
/* THE NUMERAL IS CENTRED OVER ITS OWN WORDS, and the pair sits just in from
   the card's edge (Riley, 2026-10-12: "centered content with itself so the
   number is centered over the text and there needs to be a tiny bit of left
   padding"). Flush left, a one-digit count hung over the left end of
   "VEGAN MAINS" and read as two things rather than one label.

   THE INSET IS NOW AT ITS FLOOR (Riley: "less left padding on vegan main
   count"). The explicit 4px is gone and the column is back to 72px, which
   is the narrowest that still holds 669 at 38px (69px of ink) inside its
   own box. What is left, 15px on desktop and 4px on a phone, is purely
   what centring costs: the words are 42px in a column that must be 69px,
   so half the difference sits on each side. Taking it lower means letting
   three-digit counts hang out past the card's left edge, which is 6.6% of
   venues here. */
.hit-num { text-align: center; }
.hit-num b { display: block; font-family: var(--sans); font-size: 38px;
             font-weight: 700; letter-spacing: -0.045em; line-height: .92;
             color: var(--yes); font-variant-numeric: lining-nums; }
.hit-num.none b { color: var(--ink-3); opacity: .5; }
.hit-num span { display: block; margin-top: var(--s2); font-family: var(--sans);
                font-size: 10px; font-weight: 700; letter-spacing: .13em;
                text-transform: uppercase; color: var(--ink-3); line-height: 1.35; }
.hit-name { margin: 0 0 var(--s2); font-family: var(--serif); font-size: 24px;
            font-weight: 600; letter-spacing: -0.016em; line-height: 1.18; }
.hit:hover .hit-name a { color: var(--accent); }
.hit-line { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.hit-meta { margin-top: var(--s2); font-family: var(--sans); font-size: 13px;
            color: var(--ink-2); }
.hit-hours { margin-top: var(--s3); }
/* NO HOURS, NO GAP. A place with no hours still renders an empty .hit-line,
   so the name's margin and the meta's margin stacked into a hole between
   the name and "Los Angeles" (Riley, on the store screenshot of Salaya). */
.hit-line:empty + .hit-meta { margin-top: 0; }

.opener { font-family: var(--sans); border: 0; background: none; padding: 0;
          cursor: pointer; font-size: 13px; font-weight: 600; }
.opener.is-open { color: var(--yes); }
/* CLOSED IS RED. Riley: "Closed should be a tone of red to be more
   prominent than the grey." A warm brick (#b3261e, 6.5:1 on white) that sits
   with the olives rather than a signal red. "hours unknown" is not closed and
   stays grey. */
.opener.is-shut { color: var(--ink-3); }
/* Riley: only the word -- "feels too heavy with all of it red". */
.opener.is-shut .shut-word { color: #b3261e; }
.opener.is-unknown { color: var(--ink-3); }
/* CLOSING SOON IS AMBER (Riley, 2026-09-29) -- it was the purple the site
   uses for "Ask first", which said nothing about hurrying. #a85d00 is the
   brightest amber that stays readable as text on white (4.96:1). */
.opener.is-soon { color: #a85d00; }
.opener .caret { font-size: 8px; opacity: .55; margin-left: 3px; }

/* NO RULES BETWEEN THE DISHES, AND NONE ABOVE THEM (Riley, 2026-10-10).
   The app did this and the website did not, which is most of what "the
   search page UI disappeared" turned out to be: the rules make one card
   read as a little table of its own, and with them gone the 12px either
   side of a row is what separates the dishes, exactly as 40px separates
   the venues. ios/VegEats/Views/Native/VenueCard.swift is the reference:
   ".hit-dishes: the dishes under the header, no rules." */
.hit-dishes { list-style: none; margin: var(--s4) 0 0; padding: 0; }
.hit-dishes li { padding: var(--s3) 0; }
.dline { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.dname { font-family: var(--serif); font-size: 18px; font-weight: 600;
         letter-spacing: -0.011em; }
.dsec { font-family: var(--sans); font-size: 11px; font-weight: 600;
        letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3);
        /* Both this and .vote claim the gap, and the pair below decides
           which one wins when they are both present. Section takes the
           space, vote tucks in beside it; with no section, .vote takes the
           space itself so the arrows land in the SAME COLUMN on every row
           regardless of how long the dish name is. */
        margin-left: auto; }
/* 8px under the name, not 4: "the web's 4px read as too tight" (Riley,
   2026-10-09), fixed in the app that night and not here. */
.dwhy { margin: var(--s2) 0 0; font-family: var(--serif); font-size: 15.5px;
        color: var(--ink-2); line-height: 1.5; max-width: var(--measure); }
/* THE QUESTION TO ASK, IN A CONTAINER (Riley, 2026-10-10: "more integrated
   to the ask first tag"). It was a left bar with a square edge, which read as
   a pull-quote hanging off the dish. It is now the same shape and the same
   three colours as the `.v1` "Ask first" badge above it -- that tag's wash,
   that tag's edge, that tag's ink -- so the box reads as the tag's answer.
   AskBox in ios/VegEats/Views/Native/VenueCard.swift, on the search cards and
   the venue screen alike. */
.dask { margin: var(--s3) 0 3px; font-family: var(--sans); font-size: 13.5px;
        color: var(--ask-ink); line-height: 1.45; max-width: var(--measure);
        padding: 11px 14px; background: var(--ask-wash);
        border: 1px solid color-mix(in srgb, var(--mods) 45%, transparent);
        border-radius: 14px; }
.nomatch { padding: var(--s3) 0; font-family: var(--serif); font-size: 15.5px;
           color: var(--ink-3); font-style: italic; }

.meatblock { margin-top: var(--s4); padding-top: var(--s3);
             border-top: 1px solid var(--rule); }
.meatblock > .d-label { color: var(--no); }
.hit-acts { margin-top: var(--s4); display: flex; gap: var(--s5);
            flex-wrap: wrap; align-items: center; }
.hit-acts a, .hit-acts button {
  font-family: var(--sans); font-size: 13px; font-weight: 600; color: var(--ink);
  border: 0; background: none; padding: 0 0 2px; cursor: pointer;
  border-bottom: 2px solid var(--rule); }
.hit-acts a:hover, .hit-acts button:hover { color: var(--accent);
                                            border-bottom-color: var(--accent); }
/* THE FOLDING BLOCK: the dishes past the first three (template.html's
   `.hit-rest`). It is `auto` at rest so a reflow -- a window resize, a font
   loading late -- cannot leave it clipped at a stale pixel height; the
   animation sets an explicit height only while it runs and hands it back.

   `.closing` is the app's `restFromBottom`: a flex column justified to its
   end, so a shrinking box keeps its LAST row against the button and loses
   the earlier ones off the top, under the third dish. Without it the list
   is eaten from the bottom, which reads as rows being deleted rather than
   folded away. */
.hit-rest { overflow: hidden; }
.hit-rest.closing { display: flex; flex-direction: column;
                    justify-content: flex-end; }

/* NO RULE UNDER "Show all ... dishes" (Riley, 2026-10-12). It was a 2px
   accent wash standing in for an underline, and with the dish rules gone it
   was the only line left inside a card, which made it read as the card's
   bottom edge rather than as a control. The word carries the accent colour
   on its own. */
.more { margin: var(--s3) 0 0; font-family: var(--sans); border: 0;
        background: none; padding: 0 0 2px; font-size: 13px; font-weight: 600;
        color: var(--accent); cursor: pointer; }
.more:hover { color: var(--ink); }
.loadmore { display: block; width: 100%; margin: var(--s5) 0 0;
            font-family: var(--sans); font-size: 13.5px; font-weight: 600;
            border: 1px solid var(--rule); background: none; color: var(--ink);
            border-radius: 3px; padding: 13px; cursor: pointer; }
.loadmore:hover { border-color: var(--ink-3); background: var(--band); }
.nothing { padding: var(--s9) var(--s5); text-align: center; }
.nothing b { display: block; margin-bottom: var(--s2); font-family: var(--serif); }

/* ---- the popup ------------------------------------------------------- */
.sd-popup .maplibregl-popup-content { background: var(--card); color: var(--ink);
  border: 1px solid var(--rule); border-radius: var(--r);
  box-shadow: var(--shadow-md); padding: 11px 13px; font-family: var(--sans); }
.sd-popup .maplibregl-popup-tip { border-top-color: var(--card);
                                  border-bottom-color: var(--card); }
.mpop a, .mpop b { font-family: var(--serif); font-size: 17px; font-weight: 600;
                   letter-spacing: -0.013em; }
.mpop a:hover { color: var(--accent); }
.mpop-meta { margin-top: 2px; font-size: 12px; color: var(--ink-3); }
.mpop-meta em { font-style: normal; color: var(--accent); font-weight: 700; }
.mpop-count { margin-top: 5px; font-size: 12.5px; color: var(--yes-ink);
              font-weight: 600; }
.mpop-warn { margin-top: 5px; font-size: 11.5px; color: var(--ask-ink);
             line-height: 1.35; }
.maplibregl-ctrl-group { background: var(--card) !important;
                         border: 1px solid var(--rule) !important;
                         box-shadow: none !important; border-radius: 3px !important; }
.maplibregl-ctrl-attrib { background: color-mix(in srgb, var(--card) 84%,
                          transparent) !important; font-size: 10.5px; }
.maplibregl-ctrl-attrib a { color: var(--ink-3) !important; }

/* QUIETER (Riley, 2026-10-09: "the map button hovering on the bottom feels
   distracting"): a white chip like the page's others rather than a solid
   green pill with a deep shadow. It stays bottom centre -- tried hanging it
   under the search band, top right, and Riley preferred it down here. */
/* LIQUID GLASS (Riley, 2026-10-09), the iOS 26 material: mostly clear, the
   page behind it blurred and saturated, a bright hairline rim, a soft light
   across the top edge and a faint lift. The rim and the highlight are what
   read as glass on a white page, where there is little behind it to blur. */
.maptoggle { display: none; position: fixed; left: 50%; z-index: 40;
             /* 8px off the edge (was 16; Riley, 2026-10-09: "move the map
                button down slightly"), still clear of the home indicator. */
             bottom: calc(8px + env(safe-area-inset-bottom));
             transform: translateX(-50%);
             border: 1px solid rgba(255,255,255,.7); border-radius: 999px;
             background: linear-gradient(180deg, rgba(255,255,255,.62),
                                                 rgba(255,255,255,.34));
             -webkit-backdrop-filter: blur(18px) saturate(1.9);
             backdrop-filter: blur(18px) saturate(1.9);
             color: var(--ink); font-family: var(--font-brand);
             font-size: 13.5px; font-weight: var(--font-weight-medium);
             padding: 10px 18px; cursor: pointer; overflow: hidden;
             box-shadow: inset 0 1px 0 rgba(255,255,255,.95),
                         inset 0 -1px 1px rgba(255,255,255,.35),
                         inset 0 0 0 .5px rgba(20,30,6,.06),
                         0 8px 24px -10px rgba(20,30,6,.38),
                         0 1px 3px rgba(20,30,6,.10);
             align-items: center; gap: 7px;
             transition: transform .18s cubic-bezier(.2,.8,.2,1); }
/* The specular sheen: a soft band of light over the upper half. */
.maptoggle::before { content: ""; position: absolute; inset: 0 0 50% 0;
                     border-radius: 999px 999px 40% 40% / 999px 999px 100% 100%;
                     background: linear-gradient(180deg, rgba(255,255,255,.55),
                                                         rgba(255,255,255,0));
                     pointer-events: none; }
.maptoggle > * { position: relative; }
.maptoggle:active { transform: translateX(-50%) scale(.96); }
.maptoggle svg { width: 15px; height: 15px; }
@media (prefers-reduced-transparency: reduce) {
  .maptoggle { background: rgba(255,255,255,.96); -webkit-backdrop-filter: none;
               backdrop-filter: none; border-color: var(--rule); }
  .maptoggle::before { display: none; }
}


@media (max-width: 760px) {
  .controls { flex-wrap: nowrap; overflow-x: auto; gap: var(--s4);
              scrollbar-width: none; margin-top: var(--s2);
              padding-bottom: var(--s1); }
  .controls::-webkit-scrollbar { display: none; }
  .ctl { flex: 0 0 auto; }
  .ctl select { max-width: 150px; }
  .seek input { font-size: 16px; }   /* 16px or iOS zooms the page on focus */
}
@media (max-width: 1020px) {
  .split { grid-template-columns: minmax(0, 1fr); }
  .mappane { position: fixed; inset: 0; height: auto; margin: 0;
             z-index: 25; display: none; }
  .mappane.shown { display: block; }
  .page-results .bar { position: relative; z-index: 31; background: var(--paper); }
  .mapbox { border-radius: 0; border: 0; }
  .maplegend { bottom: 74px; }
  .maptoggle { display: inline-flex; }
  .hit-top { grid-template-columns: 72px minmax(0,1fr); gap: var(--s4); }
  .hit-num b { font-size: 30px; }
  .hit-name { font-size: 21px; }
}

/* ======================================================================
   THE MAP PAGE — free, and it ships venue records with not one dish in them
   ====================================================================== */
/* THE TWO BANDS SAFARI PAINTS (see PHONE FIXES below, ~line 4165): on a phone
   a page CANNOT paint under the clock or the address bar -- the only thing it
   controls there is the ROOT BACKGROUND COLOUR. `.page-home body` was given
   #101f0e for exactly this and reads as one surface; `.page-map` was never
   given one, so Safari painted both bands default WHITE and the map page
   looked like it stopped short of the top. Match the bar, which is --paper. */
.page-map, .page-map body { height: 100%; background: var(--paper); }
.page-map { overflow: hidden; }
.maproot { display: grid; grid-template-columns: 400px minmax(0, 1fr);
           height: calc(100vh - var(--maph, 84px));
           height: calc(100dvh - var(--maph, 84px)); }
.mapside { display: flex; flex-direction: column; min-height: 0;
           border-right: 1px solid var(--rule); background: var(--paper); }
.mapside-top { padding: var(--s4) var(--s5) var(--s3);
               border-bottom: 2px solid var(--rule-heavy); }
.mapside-top .seek { padding-left: var(--s3); }
.mapside-top .seek input { font-size: 17px; padding: 9px 0; }
.mapside-note { margin: var(--s3) 0 0; font-family: var(--sans);
                font-size: 12px; font-weight: 500; color: var(--ink-3); }
.mapside-list { flex: 1; min-height: 0; overflow-y: auto;
                overscroll-behavior: contain; }
.mrow { display: grid; grid-template-columns: 48px minmax(0, 1fr);
        gap: var(--s3); padding: var(--s3) var(--s5);
        border-bottom: 1px solid var(--rule-soft); cursor: pointer;
        align-items: baseline; }
.mrow:hover, .mrow.lit { background: var(--band); }
.mrow b { font-family: var(--sans); font-size: 21px; font-weight: 700;
          letter-spacing: -0.04em; line-height: 1.2; color: var(--yes);
          text-align: right; font-variant-numeric: lining-nums tabular-nums; }
.mrow.none b { color: var(--ink-3); opacity: .5; }
.mrow > span { min-width: 0; }
.mrow-n { display: block; font-family: var(--serif); font-size: 17px;
          font-weight: 600; letter-spacing: -0.012em; line-height: 1.3; }
.mrow-m { display: block; font-family: var(--sans); font-size: 12px;
          color: var(--ink-3); margin-top: 1px; }
.mapstage { position: relative; min-height: 0; }
.mapstage > div { position: absolute; inset: 0; }
@media (max-width: 860px) {
  .page-map { overflow: auto; }
  .maproot { grid-template-columns: 1fr; height: auto;
             display: flex; flex-direction: column; }
  .mapstage { order: 1; height: 58vh; }
  .mapside { order: 2; border-right: 0; border-top: 1px solid var(--rule); }
  .mapside-list { max-height: none; overflow: visible; }
}

/* The proof line needs to say what it is a list OF. Three restaurant dish
   names with no lead-in read as a subtitle, not as "here is your dinner".

   IT IS A SENTENCE AGAIN, NOT A LABEL. This rule broke it out onto its own
   line as a wide-tracked grey eyebrow, which is the idiom the whole
   rebrand removed -- and on a city row it made "Offers" look like a
   heading over the dishes rather than the first word of the line the
   comps draw: "Offers Vegan Chicken & Waffles, Breakfast Burritos". The
   rule stays here, below the block that used to lose to it, saying the
   opposite. */
.proof b { display: inline; margin: 0; font-family: var(--font-brand);
           font-size: inherit; font-weight: var(--font-weight-book);
           letter-spacing: 0; text-transform: none; color: inherit; }

/* Yelp's own reviewers saying a place is good for vegans. Corroboration,
   not a verdict.

   IT WAS SET IN THE BRAND RED so it could not be mistaken for the verdict
   green. There is no brand red now -- the brand IS the green -- so the
   separation has to be made another way, and the way the comps make it is
   position: everything in the meta line is one colour at one size, and
   what makes the verdict different is that the verdict is a filled pill
   sitting next to the name. So this joins the line it sits in. */
/* `color: inherit` HERE BEAT THE COLOUR SET FOR IT 600 LINES ABOVE, which
   is the whole trap: `inherit` is a value, not an absence, so this rule
   wins on source order and hands the element .vline's black. The colour
   belongs in one place and this is the later one, so it lives here. */
.liked { display: inline-flex; align-items: center; gap: 5px;
         font-family: var(--font-brand); font-size: inherit;
         font-weight: var(--font-weight-medium); color: var(--ink);
         white-space: nowrap; }
.liked small { color: inherit; font-weight: inherit; font-size: inherit; }

/* ======================================================================
   THE PEEK PANEL — a restaurant page WITHOUT losing the map you set up

   Riley: "when i click the restaurant in the left sidebar, i'd prefer it
   to slide open a smaller webview of the restaurant page so that i can
   keep my search radius and everything and quickly go through the options
   without toggling between full pages."

   The map view is an INPUT the reader spent effort on -- a corner of the
   city, a zoom that means "walking distance". Sending them to a full page
   throws that away and makes them rebuild it on the way back, so nobody
   checks the fifth restaurant. The panel slides over the map's left edge
   instead: the pins, the zoom and the query all survive being read.

   The panel is a same-origin iframe of the real /place page, so there is
   one restaurant page in this project and not two.
   ====================================================================== */
/* It slides in from the RIGHT and is clipped by the stage, so it never
   travels across the sidebar on the way in or on the way out. Riley: "it
   shouldn't overlay the left sidebar ever." Sliding from the left looked
   fine open and wrong for the quarter-second either side of it.
   will-change keeps the whole panel -- iframe included -- on its own
   compositor layer, so the slide is a transform and not a repaint. */
.peek { position: absolute; inset: 0; z-index: 30;
        display: flex; flex-direction: column;
        background: var(--paper); border-left: 1px solid var(--rule-heavy);
        box-shadow: -14px 0 38px rgba(20, 16, 12, .14);
        transform: translateX(101%); visibility: hidden;
        will-change: transform;
        transition: transform .34s cubic-bezier(.32, .72, 0, 1),
                    visibility 0s linear .34s; }
.peek.open { transform: translateX(0); visibility: visible;
             transition: transform .34s cubic-bezier(.32, .72, 0, 1); }
/* The stage is the panel's world: nothing it does may show up outside. */
.mapstage { overflow: hidden; }
.peek-top { display: flex; align-items: center; gap: var(--s3);
            padding: var(--s3) var(--s4);
            border-bottom: 2px solid var(--rule-heavy); }
.peek-top b { flex: 1; min-width: 0; font-family: var(--serif);
              font-size: 16px; font-weight: 600; letter-spacing: -0.012em;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.peek-top a, .peek-top button { flex: 0 0 auto; font-family: var(--sans);
              font-size: 12px; font-weight: 600; color: var(--ink-2);
              background: none; border: 0; padding: 4px 6px; cursor: pointer;
              border-radius: 6px; }
.peek-top a:hover, .peek-top button:hover { background: var(--band);
              color: var(--ink); }
/* "<< Map" rather than an x: the panel covers the map completely, so the
   control is not closing a dialog, it is going back to what was behind
   it. Riley: "have a << to close and see the map again." It sits first in
   the row, where a back control belongs. */
.peek-top .back { order: -1; display: inline-flex; align-items: center;
                  gap: 5px; font-size: 12.5px; color: var(--ink-2); }
.peek-top .back i { font-style: normal; font-size: 15px; line-height: 1; }
.peek iframe { flex: 1; width: 100%; border: 0; background: var(--paper); }
/* Hidden until the page inside has folded its chrome away (see loadPeek
   in mappage.html), then faded in, so it never paints and then jumps. */
.peek iframe { transition: opacity .16s ease-out; }
.peek.loading iframe { opacity: 0; transition: none; }
/* Which row you are reading, kept obvious while the panel is open. */
.mrow.peeking { background: var(--band);
                box-shadow: inset 3px 0 0 var(--accent); }

/* Inside the panel the page is a PREVIEW, not the site: the chrome, the
   breadcrumbs and the second map are all things the reader is already
   looking at in the window around it. */
.embed .bar, .embed .masthead-rule, .embed .crumbs,
.embed footer { display: none; }
/* Riley: "i need some more left and right side padding in the container
   with the venue." 16px left the menu touching the panel's rounded edge. */
.embed .shell { padding: 0 var(--s6); }
.embed .band { padding-top: var(--s4); }

@media (max-width: 860px) {
  /* No room to put a page beside a map on a phone; the link does what a
     link does. */
  .peek { display: none; }
}

/* The search page deliberately has no preview panel. Its result cards
   already carry the verdict, the rating, the address and the dish names,
   so a restaurant page beside one just says all of it a second time --
   Riley, looking at exactly that: "it looks repetitive." The panel earns
   its place on the map page, where a row is a name and a number and the
   page is everything the row leaves out. */

/* The dishes you searched for, lifted to the top of a venue page.
   Marked out rather than blended in: it is a copy of rows that also
   appear below, so it has to read as an answer to a question rather than
   as another part of the menu. */
section.tier.match {
  border: 1px solid var(--rule, #d8d2c8);
  border-radius: 10px;
  padding: 14px 16px 4px;
  margin: 18px 0 22px;
  background: var(--tint, rgba(0, 0, 0, 0.02));
}
section.tier.match > .tier-h { margin-top: 0; }
section.tier.match .dish:last-child { border-bottom: 0; }

/* DRINKS ARE USEFUL BUT THEY ARE NOT DINNER. "Is there oat milk" and
   "which taps are vegan" are real questions, so beverages stay -- but a
   wine list runs to 71 rows at some venues, and left in the vegan list it
   buries the food someone came here hungry for. Same treatment as the
   not-vegan block: present, clearly separate, never first. */
.drinkblock { margin: var(--s4) 0 0; padding: var(--s3) 0 0;
              border-top: 1px solid var(--rule); }
.drinkblock .d-label { color: var(--ink-3); }
.drinkblock .dname { color: var(--ink-2); }
.drinkblock .more { color: var(--ink-2); border-bottom-color: var(--rule); }

/* ---- feedback -------------------------------------------------------- */
/* Deliberately quiet. A vote control that competes with the verdict badge
   changes what the page is for; this one is legible when looked for and
   invisible when scanning. */
.dish-name .vote, .dish-name .vpanel { align-self: center; }
/* THE CONTROL SETS ITS OWN TYPE. On the restaurant page these live inside
   .dish-name, which is 19px serif, and the panel inherited it -- "What is
   wrong?" rendered at heading size next to the dish it was asking about.
   Nothing here inherits size or family from where it happens to sit. */
.vote, .vpanel {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .01em;
  line-height: 1;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  align-self: center;
  flex: 0 0 auto;
  white-space: nowrap;
  gap: 4px;
}
/* NO HOVER FADE. The arrows used to sit at .35 opacity until the row was
   hovered, which hides the whole feedback loop from anyone who is not
   already looking for it -- and on a phone there is no hover at all, so it
   was a desktop-only affordance for a product people use standing up in a
   restaurant. They are quiet enough already: small, sans, --ink-3, no
   border until you touch one. */
.vote { gap: 1px; }
.vote.voted { color: var(--ink-3); gap: 6px; }

/* The question. Quieter than the answers, which are what gets clicked. */
.vote em, .vpanel em { font-style: normal; color: var(--ink-3);
                       margin-right: 2px; }

/* The arrows: bigger tap target than they look, no visible box until hover. */
.vb { font-family: inherit; font-size: 11px; line-height: 1;
      background: none; border: 1px solid transparent; border-radius: 5px;
      cursor: pointer; padding: 4px 5px; color: var(--ink-3); }
.vb:hover { border-color: currentColor; color: inherit; }
/* Every button a reader can press looks the same. "add a note" was a text
   link sitting next to pills, which read as a different kind of thing. */
.vopt, .vsend, .nopt, .vsay {
  font-family: var(--sans); font-size: 12px; font-weight: 500;
  line-height: 1; cursor: pointer; padding: 5px 10px; border-radius: 999px;
  border: 1px solid var(--line, #ddd); background: var(--card, #fff);
  /* EXPLICIT, not inherit. "add a note" sits inside .vote.voted, which is
     --ink-3 so the "thanks" beside it reads as quiet -- and the button was
     inheriting that grey while the Yes/No pills, which sit in a container
     that sets no colour, came out black. Two pills, same rule, different
     colour, entirely because of where they happened to be. */
  color: var(--ink); white-space: nowrap;
}
.vopt:hover, .vsend:hover, .nopt:hover, .vsay:hover { border-color: currentColor; }
.vnote { font-family: var(--sans); font-size: 12px; padding: 5px 9px;
         border-radius: 6px; border: 1px solid var(--line, #ddd);
         min-width: 15rem; }

.dsec + .vote, .dsec + .vpanel,
.dish-sec + .vote, .dish-sec + .vpanel { margin-left: .6rem; }

@media (max-width: 640px) {
  .vnote { min-width: 8rem; }
}
/* ---- community notes -------------------------------------------------- */
/* Somebody else's report, not our verdict, and the card should read that
   way: set apart from the dish text, quieter than it, never shouting. */
.cnote { margin: .4rem 0 .15rem; padding: .5rem .65rem; border-radius: 8px;
         border: 1px solid var(--rule-soft, #eee);
         background: color-mix(in srgb, var(--card, #fff) 94%, var(--ink, #000));
         font-family: var(--sans); font-size: 12.5px; line-height: 1.5; }
.cn-body { color: var(--ink, #222); }
.cn-pending { display: block; margin-top: .3rem; font-size: 11.5px;
              color: var(--ink-3); font-style: italic; }
.cn-foot { display: flex; align-items: center; gap: 6px; margin-top: .45rem; }
.cn-ask { display: inline-flex; align-items: center; gap: 6px;
          font-family: var(--sans); font-size: 12px; color: var(--ink-3); }
.cn-done { font-family: var(--sans); font-size: 12px; color: var(--ink-3); }

/* ---- shared kitchens --------------------------------------------------- */
/* A vegan brand run out of an omnivore kitchen. Stated, never scored: the
   badge is about what is in the food. Same trade as the shared-fryer chip. */
.sk-note { color: var(--ink-3); }
.sk-good { color: var(--green, #2f6d3b); font-weight: 600; }

/* The whole point of these two sentences is getting the reader to the OTHER
   menu, so the restaurant name has to read as a link. The global rule is
   `a { color: inherit; text-decoration: none }`, which renders it as plain
   text -- it looked clickable to nobody. Colour stays inherited so an accent
   link does not fight the green sentence; weight and underline do the work. */
.sk-note a, .sk-good a {
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.sk-note a:hover, .sk-good a:hover,
.sk-note a:focus-visible, .sk-good a:focus-visible {
  color: var(--accent);
  text-decoration-thickness: 3px;
}

/* ---- the merchant's own kitchen notice ---------------------------------- */
/* A brand that prints its own disclaimer -- Del Taco's "we cannot confirm
   whether any of our ingredients did not come into contact with meat
   products" -- gets that sentence on the page, verbatim, beside the
   shared-kitchen line. It is the RESTAURANT talking, not us, and the styling
   has to carry that difference: quoted, attributed, and set apart from the
   verdicts around it, which are ours. Never a tier change; see
   tools/build_venue_pages.py where it is written. */
.kn-note { margin: var(--s3) 0 0; max-width: var(--measure);
           padding: var(--s3) var(--s4);
           border-left: 3px solid var(--fryer);
           background: color-mix(in srgb, var(--fryer) 10%, transparent);
           border-radius: 0 var(--r) var(--r) 0;
           font-family: var(--font-brand);
           font-weight: var(--font-weight-book);
           font-size: 14px; line-height: 1.55; color: var(--ink-3); }
/* Who is speaking, said before the quote rather than after it -- a reader
   who meets the sentence first reads it as ours. */
.kn-who { font-weight: var(--font-weight-medium); color: var(--ink); }
/* The <q> supplies its own marks, so none are written into the string: the
   stored value is the merchant's text and nothing is added to it. */
.kn-quote { font-style: italic; }
.kn-src { white-space: nowrap; text-decoration: underline;
          text-decoration-thickness: 1px; text-underline-offset: 3px; }
.kn-src:hover, .kn-src:focus-visible { color: var(--accent); }

/* The row chip. It borrows .fry wholesale -- same pill, same blue -- because
   it is the same fact at a smaller scale, and a second colour here would
   read as a fifth verdict. What it adds is that it is a LINK: the word is
   only honest if the reader can reach the sentence it stands for. */
a.kn-chip { text-decoration: none; }
a.kn-chip:hover, a.kn-chip:focus-visible {
  background: color-mix(in srgb, var(--fryer) 52%, #fff);
}

/* ---- a restaurant that has closed ------------------------------------- */
/* The whole case for keeping these pages is that they answer "is it still
   open". That only holds if the answer is the first thing on the page --
   a page listing 42 vegan dishes with a discreet note is worse than no page,
   because it is the trip-wasting error wearing a menu. So: full width, above
   the name, accent rule, and no hours, directions or order actions anywhere
   on the page. */

/* More prominent than body copy, because it is the only action on a page
   that exists to say "not here". Medium weight and full ink, where a
   normal paragraph is book weight and --ink-2.
   Defined on .onward itself rather than on `.closed-row .onward`: the same
   sentence runs again under the menu, and the reader who scrolled the whole
   list is the one MOST likely to want it. Styling only the header copy left
   the footer copy as grey body text -- the same words making a quieter
   offer to the more interested reader. Only the spacing differs now. */
.onward {
  font-family: var(--font-brand);
  font-weight: var(--font-weight-medium);
  font-size: 17px;
  line-height: 1.4;
  color: var(--ink);
}
.onward a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1.5px;
}
.onward a:hover { color: var(--accent-deep); }
.onward-foot { margin: var(--s6) 0 0; }

/* ---- the closing section on a closed page ------------------------------
   The header row answers "is it open" in four words. THIS answers "then why
   am I here", and it is the only block on the page arguing for the site
   rather than reporting on a restaurant -- so it is the one place a closed
   page should look like the brand rather than like a database record.

   IT IS THE HOMEPAGE, SHRUNK TO A CARD. Same romaine photograph
   (/field.jpg), same near-black green ground under it, same centred
   headline in brand medium with the same soft shadow, same --scene-ink and
   --scene-dim. Two earlier attempts failed for the same reason: a 148px
   thumbnail in a grey box, and then a smaller olive card, both read as
   stock art bolted onto a notice. Borrowing the homepage's own composition
   means a reader who arrives here from Google meets the site's identity
   once, instead of meeting a page that could belong to anyone.

   The ground colour under the photograph is not decoration -- it is what
   the white text is guaranteed against if the image is slow, cached out,
   or blocked. */
.usp {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin: var(--s7) 0 var(--s2);
  padding: var(--s7) var(--s6);
  border-radius: 18px;
  text-align: center;
  background-color: #101f0e;
  background-image: url("/field.jpg");
  background-size: cover;
  background-position: 50% 42%;
  --scene-ink: #f4f6f0;
  --scene-dim: #c6cfbe;
}
/* A scrim rather than a flat wash: heaviest at the foot where the buttons
   sit, lightest across the middle where the leaf ribs are worth seeing. */
.usp::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(rgba(16, 31, 14, .58),
                              rgba(16, 31, 14, .50) 42%,
                              rgba(16, 31, 14, .74));
}
.usp-h {
  margin: 0 auto var(--s2);
  font-family: var(--font-brand);
  font-weight: var(--font-weight-medium);
  font-size: clamp(26px, 2.9vw, 38px);
  line-height: 1.12;
  letter-spacing: -.028em;
  text-wrap: balance;
  color: #fff;
  text-shadow: 0 2px 30px rgba(0, 0, 0, .55);
}
.usp-say {
  /* Two lines, on purpose. 44ch was a ribbon down the middle of a wide
     photograph; no cap at all ran the sentence to a single line the width
     of the card, which is worse -- a 120-character line is a scanning
     problem however handsome the ground behind it. ~60ch lands it on two,
     and `text-wrap: balance` makes those two even rather than leaving a
     short orphan on the second. */
  margin: 0 auto var(--s5);
  max-width: 60ch;
  text-wrap: balance;
  font-family: var(--font-brand);
  font-weight: var(--font-weight-medium);
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--scene-dim);
  text-shadow: 0 1px 20px rgba(0, 0, 0, .6);
}
.usp-say strong { color: #fff; font-weight: var(--font-weight-bold); }

.usp-go {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  justify-content: center; margin: 0;
}
.usp-btn {
  display: inline-block;
  padding: 11px 20px 12px;
  border-radius: 999px;
  font-family: var(--font-brand);
  font-weight: var(--font-weight-medium);
  font-size: 15px;
  text-decoration: none;
  border: 1px solid transparent;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.usp-btn { background: #fff; color: var(--accent-deep); }
.usp-btn:hover { background: rgba(255, 255, 255, .88); color: var(--accent-deep); }
/* The city link is the same offer narrowed to one place, so it reads as an
   alternative rather than a rival: outlined on the photograph, not filled. */
.usp-btn.is-quiet {
  background: rgba(255, 255, 255, .08);
  color: #fff;
  border-color: rgba(255, 255, 255, .45);
}
.usp-btn.is-quiet:hover {
  background: rgba(255, 255, 255, .18);
  border-color: #fff;
  color: #fff;
}
/* Buttons size to their words. `flex: 1 1 auto` made them share the row and
   stretch to fill it, so "Search every menu" became a bar most of the card
   wide -- the width of a container is not a statement about the importance
   of a link. */
.usp-go .usp-btn { flex: 0 0 auto; }
/* The closing line on a closed page. Its own top margin is 0 -- the gap
   above it is `.band`'s bottom padding, which is the right amount when a
   live page follows it with a row of tags and too much when a closed page
   follows it with one sentence. `lede d-body` appears on closed pages and
   nowhere else, so :has() scopes the tighter padding to exactly those
   without touching the 13,000 live venue pages that share `.band`. */
.band:has(+ .lede.d-body) { padding-bottom: var(--s2); }
.lede.d-body { margin-top: 0; margin-bottom: var(--s6); }

@media (max-width: 620px) {
  /* ON A PHONE (Riley, 2026-10-14: "there's not enough padding on the
     sides. 'Still looking for vegan food?' needs to be a little smaller
     text"). 28px inside the card, not 16: the heading and the sentence
     were running to within a finger of the photograph's edge. The heading
     drops from 26px to 22px, so it sits on two lines instead of filling
     the card wall to wall. */
  .usp { padding: var(--s6) 28px; border-radius: 14px; }
  .usp-h { font-size: 22px; line-height: 1.18; }
  .usp-say { font-size: 14.5px; margin-bottom: var(--s4); }
  /* Every pixel between the verdict and the way out is spent on a phone.
     The lede is explanatory, not load-bearing, so it gives up its air
     first. */
  .lede.d-body { margin-bottom: var(--s5); }
    .tier-h { margin-top: var(--s3); }
}

/* GRID, NOT COLUMNS. `columns: 2` flows down-then-across and rebalances
   against the available height, so a list clipped to five rows showed
   items 1-5 beside 6-10, and the same list unclipped rebalanced to 1-50
   beside 51-100. The right-hand column changed contents the moment the
   reader expanded it, which makes the menu look like it reshuffled itself.
   Grid flows across-then-down and never rebalances: row one is dishes one
   and two, forever, and expanding only adds rows underneath. */
.closed-menu {
  list-style: none;
  padding: 0;
  margin: var(--s3) 0 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--s5);
  align-content: start;
}
/* Name left, verdict right, hairline between: each row now spans the full
   column instead of ending wherever the dish name happens to stop. That is
   what a menu looks like, and it also fixes the expand button -- centred on
   the container it read as off to the right, because the ragged right edge
   of two columns of short names put the optical centre of the text well
   left of the actual centre. Filling the columns moves the two centres onto
   each other. */
/* The original row: the dish name as running text with its verdict just
   after it. Two changes only -- the name is black instead of the olive it
   inherited, and the verdict wears a pill. */
.closed-menu li {
  padding: 5px 0;
  border-bottom: 1px solid var(--rule-soft);
  color: var(--ink-body);
}
.tier-h.d-title { color: var(--ink-body); }

/* A pill around the verdict, nothing else about it changed: same position
   just after the name, same size, same quiet register. Washed rather than
   solid because a hundred solid fills would out-shout the dish names they
   describe. */
.closed-menu .d-meta {
  margin-left: 7px;
  display: inline-block;
  padding: 2px 9px 3px;
  border-radius: 999px;
  white-space: nowrap;
}
.cm-vegan, .cm-likely { color: var(--accent-deep); background: var(--yes-wash); }
.cm-mods              { color: var(--ink-2);       background: var(--ask-wash); }
.cm-no                { color: var(--ink-2);       background: var(--no-wash); }


/* The rest of the menu, folded away so the closing card can sit near the
   top of the page on a phone. */
.menu-wrap { position: relative; }

/* THE WHOLE MENU IS ALWAYS IN THE LIST; the clip is what is conditional.
   Closed, the list is cut mid-row and faded out, because a list that stops
   cleanly after five items looks like the whole list while one fading
   through a line of text reads as continuing. max-height is in rows so it
   survives a change to the type scale.
   :has() does the work a class toggle would need JavaScript for. Where it
   is unsupported the list simply renders in full, unclipped -- the failure
   mode is seeing everything, which is the right way round. */
.menu-wrap:not(:has(.more-dishes[open])) .closed-menu.full {
  max-height: calc(5.4 * 32px);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(#000 58%, rgba(0, 0, 0, 0) 100%);
          mask-image: linear-gradient(#000 58%, rgba(0, 0, 0, 0) 100%);
}
/* Centred, so it sits between the two columns of the list rather than
   hanging off the left edge of the first one. The summary is inline-flex,
   so text-align on the details does it; the expanded list has to be put
   back to the left or the dish names centre themselves too. */
/* The negative margin was tuned against the old `columns: 2` layout, whose
   clipped block ended on a ragged, half-balanced edge. The grid ends on an
   exact row, so the same -8px pulled the button up over live text instead
   of into empty space. Sits just clear of the clip now. */
.more-dishes { margin: var(--s3) 0 0; text-align: center; }
.more-dishes > summary {
  display: inline-flex; align-items: center; gap: 7px;
  cursor: pointer;
  list-style: none;
  padding: 8px 16px 9px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--card);
  font-family: var(--font-brand);
  font-weight: var(--font-weight-medium);
  font-size: 14px;
  color: var(--ink-2);
  transition: border-color .14s ease, color .14s ease;
}
.more-dishes > summary::-webkit-details-marker { display: none; }
/* A chevron, drawn rather than an image, that turns when the list opens. */
.more-dishes > summary::after {
  content: "";
  width: 7px; height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform .16s ease;
}
.more-dishes > summary:hover { border-color: var(--ink-3); color: var(--ink); }
.more-dishes > summary:focus-visible { outline: 2px solid var(--accent);
                                       outline-offset: 2px; }
/* Opened, the button has done its job and goes away. */
.more-dishes[open] > summary { display: none; }
@media (max-width: 640px) { .closed-menu { grid-template-columns: minmax(0, 1fr); } }

/* ---- the national ranking (now /cities.html) ---------------------------
   HappyCow's ranking page is the reference and its centre is a photograph
   of the city. We have one photograph in this entire project and it is a
   field, so the picture is not available to us -- and HappyCow's own page
   illustrates "#1 New York City" with Big Ben, which is what borrowing a
   photograph you have not earned looks like.

   So the rank row is typographic: a numeral at the size a photograph would
   have been, the city set in the display serif, and then the only thing
   this site has that nobody else does, which is a count of dishes read.

   The COVERAGE bar is the load-bearing element. Our reading of these ten
   cities is wildly uneven -- one city with crawled menus, five with vendor
   listings, four barely touched -- and a ranking page that hides that is
   the same lie as the borrowed photograph. Three states, three colours,
   each one saying in words how much of the city we have actually read. */
/* The .eyebrow above already draws the 2px rule. A border here too gave
   the section two parallel bars, which reads as a rendering fault. */
.ranklist { border-top: 0; }
.crow { display: grid; grid-template-columns: 104px minmax(0, 1fr);
        gap: var(--s5); padding: var(--s6) 0;
        border-bottom: 1px solid var(--rule); }
.crank { text-align: center; }
.crank b { display: block; font-family: var(--serif); font-weight: 600;
           font-size: 62px; line-height: 1; letter-spacing: -0.03em;
           font-variant-numeric: lining-nums tabular-nums; color: var(--ink); }
.crank.top b { color: var(--accent); }
.crank .d-label { display: block; margin-top: var(--s2); color: var(--ink-3); }
.cname { margin: 0; font-family: var(--serif); font-size: 30px;
         font-weight: 600; letter-spacing: -0.02em; line-height: 1.15; }
.cname em { font-style: normal; color: var(--ink-3); font-weight: 400; }
.crow a.cname:hover, .cname a:hover { color: var(--accent); }
.cscore { margin: var(--s2) 0 0; font-family: var(--sans); font-size: 13px;
          color: var(--ink-3); }

/* the numbers, and only where we have them */
.cstats { display: flex; flex-wrap: wrap; gap: var(--s6);
          margin: var(--s4) 0 0; }
.cstat b { display: block; font-family: var(--sans); font-size: 27px;
           font-weight: 700; line-height: 1.1;
           font-variant-numeric: lining-nums tabular-nums; }
.cstat span { display: block; margin-top: 3px; font-family: var(--sans);
              font-size: 11.5px; font-weight: 600; letter-spacing: .1em;
              text-transform: uppercase; color: var(--ink-3); }
.cstat.lead b { color: var(--yes); }

/* WHERE THE NUMBERS ABOVE CAME FROM. One quiet line, not a block.
   This used to be a three-state paragraph that swallowed the card for any
   city we had not read, so nine of the ten entries were an apology and the
   page had no shape to fill in when the harvest lands. The truth is the
   same; it is now one sentence and a dot, and the stats and the cards stay
   wherever the data is thin. */
.prov { display: flex; align-items: baseline; gap: var(--s2);
        margin: var(--s3) 0 0; font-family: var(--sans); font-size: 12.5px;
        line-height: 1.5; color: var(--ink-3); max-width: var(--measure); }
.prov::before { content: ""; flex: 0 0 auto; width: 7px; height: 7px;
                border-radius: 50%; transform: translateY(-1px); }
.prov-read::before { background: var(--yes); }
.prov-listed::before { background: var(--ask); }

/* THE CARD ROW. Always CARD_SLOTS wide, in every city.
   A card is an <a> where the restaurant has a page and a <div> where it
   does not, which is why the two share every rule but the hover. */
.pcards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
          gap: var(--s3); margin: var(--s3) 0 0; }
.cardhead { margin: var(--s5) 0 0; color: var(--ink-3); }
.pcard { display: block; padding: var(--s4); border: 1px solid var(--rule);
         border-radius: var(--r); background: var(--card); min-height: 84px; }
a.pcard:hover { border-color: var(--ink-3); }
a.pcard:hover b { color: var(--accent); }

/* THE EMPTY SLOT IS A DESIGNED ELEMENT, NOT AN ABSENCE.
   Miami clears the bar at two restaurants of six, Madison three, Orlando
   four. Dropping the remaining slots would leave a lopsided row and would
   mean the layout changes shape the day the harvest lands; padding them
   with somewhere we would not send anyone would be worse. So the slot is
   drawn, dashed, and says what it is waiting for. */
.pcard.empty { background: none; border-style: dashed;
               border-color: var(--rule); display: flex;
               flex-direction: column; justify-content: center; }
.pcard.empty b { font-family: var(--sans); font-size: 11px; font-weight: 700;
                 letter-spacing: .12em; text-transform: uppercase;
                 color: var(--ink-3); }
.pcard.empty em { margin-top: var(--s1); color: var(--ink-3); opacity: .8;
                  font-family: var(--serif); font-size: 13.5px;
                  font-style: italic; }
.pcard b { display: block; font-family: var(--serif); font-size: 16px;
           font-weight: 600; line-height: 1.3; }
.pcard em { display: block; margin-top: var(--s2); font-style: normal;
            font-family: var(--sans); font-size: 12px; color: var(--ink-3);
            font-variant-numeric: lining-nums tabular-nums; }
@media (max-width: 760px) { .pcards { grid-template-columns: 1fr 1fr; } }

/* ranks 11 to 20: the same numbers, one line each */
.restlist { width: 100%; border-collapse: collapse; margin: var(--s4) 0 0; }
.restlist th { text-align: left; padding: var(--s2) var(--s3) var(--s2) 0;
               font-family: var(--sans); font-size: 11px; font-weight: 700;
               letter-spacing: .12em; text-transform: uppercase;
               color: var(--ink-3); border-bottom: 1px solid var(--rule); }
.restlist td { padding: var(--s3) var(--s3) var(--s3) 0;
               border-bottom: 1px solid var(--rule-soft);
               font-family: var(--serif); font-size: 16px;
               vertical-align: baseline; }
.restlist td.n { font-family: var(--sans); font-weight: 700;
                 color: var(--ink-3); width: 3em;
                 font-variant-numeric: lining-nums tabular-nums; }
.restlist td.q { font-family: var(--sans); font-size: 13px;
                 color: var(--ink-2); }

/* the standing disclaimer. Not small print: a section, in the type the
   rest of the page uses, because it is the most checkable thing here. */
.limits { margin: var(--s8) 0 0; }   /* its .eyebrow draws the rule */
.limits ul { margin: var(--s4) 0 0; padding: 0; list-style: none;
             max-width: var(--measure); }
.limits li { padding: var(--s3) 0; border-bottom: 1px solid var(--rule-soft);
             font-family: var(--serif); font-size: 16.5px; line-height: 1.55;
             color: var(--ink-2); }
.limits li b { color: var(--ink); font-weight: 600; }
.srcs { margin: var(--s5) 0 0; font-family: var(--sans); font-size: 13.5px;
        line-height: 1.6; color: var(--ink-2); max-width: var(--measure); }
.srcs a { border-bottom: 1px solid var(--rule); }
.srcs a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* the promo that carries a reader from the LA index to the ranking */
.promo { display: block; margin: 0 0 var(--s6); padding: var(--s5);
         border: 1px solid var(--rule); border-left: 4px solid var(--accent);
         border-radius: var(--r); background: var(--band); }
.promo b { display: block; font-family: var(--serif); font-size: 21px;
           font-weight: 600; letter-spacing: -0.015em; }
.promo span { display: block; margin-top: var(--s2); font-family: var(--sans);
              font-size: 13.5px; color: var(--ink-2); }
.promo:hover b { color: var(--accent); }

@media (max-width: 640px) {
  .crow { grid-template-columns: 62px minmax(0, 1fr); gap: var(--s4); }
  .crank b { font-size: 40px; }
  .cname { font-size: 24px; }
  /* A three-across grid rather than a wrapping flex row. Wrapping put one
     stat on its own line and the other two beside each other, which reads
     as a layout accident rather than a set of three. */
  .cstats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: var(--s3); }
  .cstat b { font-size: 21px; }
  .cstat span { font-size: 9.5px; letter-spacing: .07em; }
  /* Two across, not one. Six single-file cards make one city as tall as a
     screen, and a column of four dashed placeholders under Miami reads as
     an error rather than as a row waiting to fill. */
  .pcards { grid-template-columns: 1fr 1fr; gap: var(--s2); }
  .pcard { padding: var(--s3); min-height: 72px; }
  .pcard b { font-size: 14.5px; }
  .pcard em { font-size: 11.5px; }
  .pcard.empty em { font-size: 11.5px; }
}

/* ---- the folded not-vegan list --------------------------------------- */
/* APPENDED BLOCK -- tools/build_venue_pages.py only. Nothing above is
   touched. See the comment at the fold in that file: a venue page now
   carries the restaurant's whole menu, so the not-vegan section can run to
   250 dishes, and a vegan should not scroll past all of it. The section
   heading, its count and its note stay visible; only the list folds. */
.tier-fold { margin: 0; }
.tier-fold > summary { display: inline-block; margin: 0 0 var(--s2);
                       font-family: var(--sans); font-size: 13px;
                       font-weight: 600; letter-spacing: .01em;
                       color: var(--ink-2); cursor: pointer;
                       padding: 0 0 2px; border-bottom: 2px solid var(--rule);
                       list-style: none; }
.tier-fold > summary::-webkit-details-marker { display: none; }
.tier-fold > summary::marker { content: ""; }
.tier-fold > summary::before { content: "\25B8"; display: inline-block;
                               margin-right: var(--s3); font-size: 11px;
                               transition: transform .12s ease; }
.tier-fold[open] > summary::before { transform: rotate(90deg); }
.tier-fold > summary:hover { color: var(--ink); border-bottom-color: var(--ink-3); }
.tier-fold > summary:focus-visible { outline: 2px solid var(--accent);
                                     outline-offset: 3px; }
.tier-fold > summary .fold-shut { display: none; }
.tier-fold[open] > summary .fold-open { display: none; }
.tier-fold[open] > summary .fold-shut { display: inline; }

/* ---- SCREEN 05: the top ten as a grid of photographed cards -----------
   APPENDED BLOCK -- tools/build_best_cities.py only. Nothing above is
   edited; the two rules that had to change are restated here, where a
   later rule of the same weight wins.

   THE NOTE ABOVE .crow SAYS "THE RANK ROW IS TYPOGRAPHIC", and it was
   right for a page with no photographs: the reference we were shown
   illustrates "#1 New York City" with Big Ben, and the answer to that was
   to print no picture at all. Riley drew screen 05 with a picture on every
   card, so the rule the old design enforced by absence has to be enforced
   inside the picture instead:

     * EVERY PHOTOGRAPH CARRIES ITS OWN COVERAGE STAMP, burned into the
       bottom-left corner of the image rather than set beside it. Seven of
       the ten read "No menus read yet". A caption a reader can skip past
       is a caption that lets a handsome picture of Portland stand in for
       having read Portland; one sitting on the image cannot be read apart
       from it.
     * THE STAMP KEEPS THE PROVENANCE DOT'S TWO COLOURS, so the grid and
       the ranked detail below it are saying the same thing in the same
       hues.
     * A CITY WITH NO PHOTOGRAPH still draws the panel, at the same aspect,
       and still prints the stamp. The grid keeps its rhythm and the day a
       photograph is deleted nothing below it moves.

   The <img> carries width and height and the CSS carries the same aspect
   ratio, so every card is its final height before a byte of image has
   arrived and the grid does not reflow as the ten load.

   THE RANK RAIL LOSES ITS PICTURE. It briefly held one at 152px; with the
   grid above it that was the same photograph printed twice on one page.
   The rail goes back to the numeral it was designed as. */
.crow { grid-template-columns: 104px minmax(0, 1fr); }
.cshot { display: none; }

/* the centred header screen 05 draws */
/* TIGHTER AT THE TOP. `.band` opens with 64px, which is right on a city
   or a venue page where the band is a header ABOVE a long list and needs
   air to read as one. This page is a title and a grid, and 64px of
   nothing plus 24px under the breadcrumb put the headline 109px below the
   masthead.
   Screen 05 draws the headline 37px under the masthead and carries no
   breadcrumb at all. The breadcrumb stays -- it is the only thing on the
   page that says where you are, and the BreadcrumbList in the head should
   describe something a reader can see -- so the space it needs comes out
   of the padding instead. */
.band.rankhead { text-align: center;
                 padding-top: var(--s5); padding-bottom: var(--s7); }
.band.rankhead .crumbs { justify-content: center; margin-bottom: var(--s3); }
.band.rankhead .lede { margin-left: auto; margin-right: auto;
                       max-width: 40em; }
/* BALANCED, both lines about the same length (Riley, 2026-10-09). A centred
   two-line heading that breaks "Top 20 Vegan Cities in / the US" leaves a
   short orphan line under a long one. */
.band.rankhead h1, .band.rankhead .lede { text-wrap: balance; }
/* The cards end 48px above the city list, not 96px plus an empty section's
   64px: `.limits` holds only the photo credits and is often empty. */
.citycards { margin-bottom: var(--s7) !important; }
.limits:empty { display: none; }
/* THE CITIES PAGE'S FOOTER FOLLOWS THE LIST WITH NO RULE (Riley, 2026-10-09):
   the source for the ranking reads as the list's last line of small print,
   32px under it. Other pages keep the footer's rule and room. */
body:has(.citylinks) > footer { border-top: 0; margin-top: 0; padding-top: 0; }
body:has(.citylinks) > footer .srcs:first-child { margin-top: 0; }
/* "SHOW ALL" SITS WITH THE LIST IT OPENS (Riley, 2026-10-09: "so much space
   above the show all vegan dishes and not enough space below it on mobile").
   Its 12px top margin plus the touch padding put the label ~24px under the
   last dish's rule, while its own underline bar ran 16px into the next
   section. On a phone the margin moves to the other side. */
@media (max-width: 640px) {
  .hit .more { margin: 0 0 var(--s3); }
}
/* THE SEARCH STAYS ON SCREEN (Riley, 2026-10-09: "as i scroll on the search
   page the search bar should stay at the top", web and app alike). On a phone
   the band sticks with its masthead row (66px) scrolled off above it, so the
   Where/Mood card and the Hours/fryer row stay pinned while the cards move. */
@media (max-width: 700px) {
  /* `html.` to outrank the `position: relative` the 1020px block sets. */
  html.page-results .topband { position: sticky; top: -66px; z-index: 30; }
}
/* ON A PHONE IT FOLDS AS YOU SCROLL (Riley, 2026-10-09: "when i scroll can
   the search bar collapse into 1 line and the hours and shared fryer thing
   disappear too"). The band is FIXED, with the page padded by its open
   height (--pband, measured in template.html), so folding it never moves
   the results under the reader -- the same reason desktop's band is fixed.
   Folded, the masthead row slides up out of view, the Where/Mood box gives
   way to the one-line pill, and the Hours/fryer row goes. */
@media (max-width: 640px) {
  html.page-results .topband { position: fixed; top: 0; left: 0; right: 0;
    transition: transform .28s cubic-bezier(.2,.8,.2,1); }
  html.page-results body { padding-top: var(--pband, 246px); }
  html.page-results .topband.compact { transform: translateY(-58px); }
  html.page-results .topband.compact .refinebar { display: none; }
  /* NARROW, NOT EDGE TO EDGE (Riley, 2026-10-09: "the search bar shouldn't
     be so wide when collapsed"): the pill hugs its two words, centred, and
     never runs past 300px. */
  html:is(.page-results, .sdn-phone) .topband.compact .minipill { width: auto; max-width: 250px;
    margin: 2px auto 16px; height: 44px; padding: 0 4px 0 16px; font-size: 14px; }
  html:is(.page-results, .sdn-phone) .topband.compact .minipill .mp-where { max-width: 104px;
    padding-right: 10px; }
  html:is(.page-results, .sdn-phone) .topband.compact .minipill .mp-mood { flex: 0 1 auto; max-width: 92px;
    padding-left: 10px; }
  html:is(.page-results, .sdn-phone) .topband.compact .minipill .mp-go { width: 36px; height: 36px; }
  html.page-results .hit { scroll-margin-top: 80px; }
}
@media (max-width: 640px) and (prefers-reduced-motion: reduce) {
  html.page-results .topband { transition: none; }
}

.citycards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
             gap: var(--s7) var(--s5); max-width: 1120px;
             margin: 0 auto var(--s9); }
/* THE WHOLE CARD IS THE LINK, not just the pill.
   ----------------------------------------------------------------------
   Riley: "you should be able to click on the image or any of the container
   of the city to go to the same place as the discover button does." The
   photograph, the rank, the name and the counts all describe one
   destination, so all of them should open it.

   IT IS DONE WITH ONE ANCHOR AND NO JAVASCRIPT. The Discover pill's own
   <a> grows a pseudo-element that covers the card; the card is the
   positioned ancestor, so the pseudo-element stops at its edges. Wrapping
   the card in a second <a> would nest one anchor inside another, which is
   invalid and which browsers repair by splitting the markup; a click
   handler would leave the card unreachable by keyboard and unopenable in
   a new tab. This way there is still exactly one link, it still says
   "Discover Portland" to a screen reader, and cmd-click still works
   anywhere on the card.

   z-index keeps the pill's own text and the hover underline above the
   sheet; anything inside the card that must stay clickable in its own
   right would need the same treatment, and today nothing does. */
.ccard { position: relative; display: flex; flex-direction: column;
         min-width: 0; }
/* THE ANCHOR MUST NOT BE POSITIONED. `inset: 0` resolves against the
   nearest POSITIONED ancestor, so `position: relative` on .ccard-go made
   the pill its own containing block and the sheet covered the pill and
   nothing else -- the card looked clickable and was not. The card is the
   positioned ancestor; the link is left static so the sheet reaches its
   edges. This is the whole trick and it is the one line that breaks it. */
.ccard-go::after { content: ""; position: absolute; inset: 0; z-index: 1;
                   border-radius: 14px; }
.ccard:hover { cursor: pointer; }
/* the card lifts as one thing, so it reads as one target */
.ccard-shot img { transition: transform .25s ease; }
.ccard:hover .ccard-shot img { transform: scale(1.02); }
.ccard-shot { overflow: hidden; border-radius: 12px; }
/* KEYBOARD ONLY. This was :focus-within, which fires on a mouse click as
   well as a tab -- so clicking the photograph drew a ring round the whole
   card and left it there. `:has(:focus-visible)` asks the browser the
   question that actually matters: did this focus come from the keyboard?
   A tab still outlines the CARD rather than the pill inside it, because
   the card is what activating it opens. */
.ccard:has(a:focus-visible) { outline: 2px solid var(--ink);
                              outline-offset: 6px; border-radius: 16px; }
/* And nothing inside draws its own ring on a click: the sheet, the photo
   and the pill are one target, so one indicator. */
.ccard a:focus { outline: none; }
.ccard a:focus-visible { outline: none; }

/* THE PHOTOGRAPH AND ITS STAMP ------------------------------------------
   position: relative on the <figure> and absolute on the <figcaption>, so
   the stamp is inside the picture's box and travels with it. */
.ccard-shot { position: relative; margin: 0; }
.ccard-shot img, .ccard-noshot {
  display: block; width: 100%; height: auto; aspect-ratio: 3 / 2;
  object-fit: cover; border-radius: 12px; background: var(--band-deep); }
/* the drawn panel for a city whose photograph is missing */
.ccard-noshot { background:
  repeating-linear-gradient(135deg, var(--band) 0 9px,
                            var(--band-deep) 9px 18px); }
.ccard-shot figcaption {
  position: absolute; left: 10px; bottom: 10px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px 5px 9px; border-radius: var(--pill);
  background: rgba(30, 40, 10, .82);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  font-family: var(--font-brand); font-weight: var(--font-weight-medium);
  font-size: 11.5px; line-height: 1.2; letter-spacing: .01em; color: #fff;
  font-variant-numeric: lining-nums tabular-nums; }
.ccard-shot figcaption::before { content: ""; flex: 0 0 auto; width: 7px;
                                 height: 7px; border-radius: 50%; }
.ccard-read figcaption::before   { background: var(--leaf); }
.ccard-listed figcaption::before { background: var(--nope); }

/* THE FOOT: name and count left, one filled pill right, on one baseline. */
.ccard-foot { display: flex; align-items: center; gap: var(--s3);
              margin-top: var(--s3); }
.ccard-say { min-width: 0; flex: 1; }
.ccard-name { display: block; font-family: var(--font-brand);
              font-weight: var(--font-weight-medium); font-size: 17px;
              letter-spacing: -0.018em; line-height: 1.25; color: var(--ink); }
.ccard-name em { font-style: normal; }
.ccard-num { display: block; margin-top: 6px; font-family: var(--font-brand);
             font-weight: var(--font-weight-book); font-size: 13px;
             line-height: 1.35; color: var(--ink-2);
             font-variant-numeric: lining-nums tabular-nums; }
.ccard-go { flex: 0 0 auto; display: inline-flex; align-items: center;
            background: var(--ink); color: #fff; border-radius: var(--pill);
            font-family: var(--font-brand);
            font-weight: var(--font-weight-medium); font-size: 13.5px;
            line-height: 1; padding: 10px 20px; white-space: nowrap;
            transition: background .14s ease; }
.ccard-go:hover { background: var(--accent-deep); }
.ccard:hover .ccard-name { text-decoration: underline;
                           text-decoration-thickness: 2px;
                           text-underline-offset: 3px; }

/* THE SIGN-UP PILL HAS NOTHING BEHIND IT YET. It is drawn on every screen
   Riley sent, so it is drawn here -- as a <span>, not an <a>, because a
   link to a page that does not exist is worse than a control that says it
   is not ready. It reads at the same weight as the live pill and declines
   the pointer. */
/* FULL COLOUR, NOT FADED. The first version dropped it to .55 so it read
   as unavailable, and on a page whose one filled control this is, a washed
   pill reads as a rendering fault rather than as a state. It is drawn
   exactly as the comps draw it and says what it is by refusing the pointer
   and carrying aria-disabled and a title; it cannot mislead by navigating,
   because it is a <span> and has nowhere to go. */
/* The pre-script state: same pill, and it declines the pointer because it
   has nowhere to go until signup.js gives it somewhere. */
.bar nav .cta.is-soon { cursor: not-allowed; }

/* screen-reader-only, for the word that tells a Discover pill apart from
   the nine others on the page */
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
      overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
      white-space: nowrap; border: 0; }

@media (max-width: 900px) {
  .citycards { grid-template-columns: repeat(2, minmax(0, 1fr));
               gap: var(--s6) var(--s4); }
}
@media (max-width: 560px) {
  /* One across. Two 150px-wide photographs side by side stop being
     photographs of anywhere, and the foot row has a pill in it that does
     not shrink. */
  .citycards { grid-template-columns: minmax(0, 1fr); gap: var(--s6); }
  .ccard-name { font-size: 18px; }
  .band.rankhead { padding-bottom: var(--s6); }
}

/* ======================================================================
   SCREEN 01 + 02 — THE LANDING, as drawn
   ======================================================================

   APPENDED BLOCK. The .scene stack at the top of this file is untouched:
   the photograph, the atmosphere, the rays, the grain, the motes and the
   scrim all still do their jobs and the comp keeps every one of them. What
   changes is the three things drawn ON that scene, because the old ones
   were designed against Soar and Ecosia and these are Riley's:

     * THE NAV IS PLAIN TEXT AND ONE WHITE PILL. It used to be two
       outlined glass pills and no call to action. The drawn masthead has
       Cities and Map as bare white words and Sign Up as the only filled
       thing on the screen -- which is the same rule the content pages
       already follow, one filled control per page, inverted for a dark
       photograph.
     * THE SEARCH BAR IS TWO FIELDS, NOT ONE. Where and Mood, side by
       side in a single pill, with a round arrow at the end. The old bar
       was one dark-glass box asking only what you wanted to eat, and the
       question the product answers has always had two halves.
     * THE WHERE SIDE IS OPAQUE AND THE MOOD SIDE IS GLASS. Sampled off
       the comp: #ffffff against roughly 78% white over the leaf. That is
       not decoration, it is which field is answered first -- the opaque
       half reads as filled in and the glass half as still open.
   ---------------------------------------------------------------------- */

/* ---- the masthead over the photograph -------------------------------- */
/* THE SAME MASTHEAD AS EVERY OTHER PAGE, in white. The sizes here used to
   be their own set -- 32px mark, 15.5px nav, 34px gutter -- and the
   masthead is the one element a reader sees on all six screens, so two
   sets of numbers meant it changed size when they moved between them.
   These match the measured content masthead exactly; only the colour and
   the pill's inversion are the scene's own. */
/* White, and NOTHING ELSE. It used to restate the weight and the size,
   which meant the wordmark's two weights had to be maintained twice --
   and the moment `veg` went Bold, the homepage silently stayed Medium. */
/* ON THE PHOTOGRAPH BOTH HALVES ARE WHITE, one of them dimmer.
   The map page sets `veg` in the brand green and `eats` a step lighter --
   same hue, less contrast. Repeating that literally on a dark leaf meant
   a light green on green, which is the same trick pointing the wrong way:
   there the second half got LOUDER than the first. #dfe6d8 is the same
   relationship in the other direction -- a shade of white, at roughly the
   contrast ratio --leaf-deep has against paper -- with just enough green
   in it to belong to the palette rather than read as grey. */
.on-scene .mark { color: #fff; }
.on-scene .mark i { color: #dfe6d8; }
.bar.on-scene { padding: 26px 30px 24px 48px; gap: var(--s5); }
.bar.on-scene nav { gap: 30px; align-items: center;
                    font-family: var(--font-brand);
                    font-weight: var(--font-weight-medium);
                    font-size: var(--nav-size); }
/* Bare white words. The border, the pill padding and the .78 opacity all
   go: on this photograph a bordered pill next to a filled one reads as two
   buttons, one of which is broken. */
.bar.on-scene nav a { color: #fff; opacity: 1; border: 0; border-radius: 0;
                      padding: 4px 0 2px; background: none;
                      border-bottom: 2px solid transparent; }
.bar.on-scene nav a:hover { background: none; border-bottom-color: #fff; }

/* THE SIGN UP PILL. There is no account system behind it -- see the note
   on .cta.is-soon further up -- so on the scene it is a <span> too, drawn
   exactly as the comp draws it and declining the pointer. */
/* THE CURSOR IS NOT SET HERE, and that is the fix rather than an
   omission. This rule hard-coded `cursor: not-allowed` on `.cta` -- not
   on `.cta.is-soon` -- from the days when the pill had nowhere to go. Once
   signup.js gave it somewhere, it stripped `.is-soon` as designed and the
   cursor stayed crossed out, because `.bar.on-scene nav .cta` outranks the
   `cursor: pointer` set on `.bar nav .cta`. A control that works while
   telling you it does not is worse than one that plainly does nothing.
   The cursor now lives in exactly two places: pointer on .cta,
   not-allowed on .cta.is-soon, and the scene inherits both. */
.bar.on-scene nav .cta, .bar.on-scene nav .cta.is-soon {
  background: #fff; color: var(--ink); border-radius: var(--pill);
  padding: 10px 21px; line-height: 1; border: 0; font-size: 15px;
  font-weight: var(--font-weight-medium); opacity: 1; }
.bar.on-scene nav .cta:hover { background: #eef1e4; }

/* ---- the headline ---------------------------------------------------- */
.scene-body h1 { font-family: var(--font-brand);
                 font-weight: var(--font-weight-medium);
                 font-size: clamp(30px, 3.3vw, 46px); letter-spacing: -0.028em;
                 color: #fff; margin-bottom: 10px;
                 text-shadow: 0 2px 30px rgba(0,0,0,.55); }
.scene-body .kicker { font-family: var(--font-brand);
                      font-weight: var(--font-weight-medium); font-size: 15px;
                      color: #fff; margin-bottom: 26px;
                      text-shadow: 0 1px 20px rgba(0,0,0,.6); }

/* ---- the two-field search bar ----------------------------------------
   Measured off screen 01, scaled to a 1280 viewport: the pill is 577 x 49
   with fully round corners, and the arrow is 43px across, inset 3px from
   its right edge.

   THE HIGHLIGHT HAS NO PADDING AROUND IT. The first build gave .find2 6px
   of padding, which floated the white panel inside the glass pill with a
   ring of glass showing on three sides. The comp does not draw that: the
   white runs to the pill's edge and takes the pill's own cap.
   ---------------------------------------------------------------------- */
.find2 { position: relative; z-index: 12; width: min(577px, 100%);
         display: flex; align-items: stretch; border-radius: var(--pill);
         background: rgba(255,255,255,.74);
         -webkit-backdrop-filter: blur(16px) saturate(1.15);
         backdrop-filter: blur(16px) saturate(1.15);
         box-shadow: 0 20px 54px -22px rgba(0,0,0,.6);
         padding: 0; gap: 0;
         /* Named once, because five rules depend on them agreeing: the
            highlight's travel, the two fields' flex bases and the arrow. */
         --where: 38%; --go: 43px; --go-inset: 3px; }

/* THE HIGHLIGHT IS ONE ELEMENT AND IT SLIDES.
   ----------------------------------------------------------------------
   The first version cross-faded two backgrounds -- Where's fading out
   while Mood's faded in. Riley: "it feels a little glitchy and not
   smooth", which is exactly what a cross-fade looks like when it is
   pretending to be a movement: for 200ms there are two half-white panels
   and no white panel. What Airbnb does is move ONE panel, so that is what
   this is. `.hl` is a single white pill inside the bar and the state moves
   it; there is never more than one and it is never in two places.

   THREE STATES, and the first one is the whole bar:

     no data-on    the panel fills the bar. Rest -- one solid white pill,
                   nothing picked out. It is a thing to click, not a form
                   with a field already selected. Riley's word for what
                   the highlight is: a FOCUS AREA, and at rest you are
                   nowhere in particular.
     "where"       it shrinks to the left --where and lifts.
     "mood"        it slides right and grows to the remainder, taking the
                   pill's right cap and running UNDER the arrow, because
                   Mood is the last field and its white has to reach the
                   end the way Where's reaches the start.

   left and width rather than a transform, because the panel changes SHAPE
   as well as position and a transform would scale the rounded caps into
   ellipses on the way. One element, two properties, one paint. */
.find2 .hl { position: absolute; z-index: 0; top: 0; bottom: 0;
             left: 0; width: 100%;
             border-radius: var(--pill); background: #fff;
             box-shadow: 0 6px 16px -8px rgba(20,30,6,.42),
                         0 1px 2px rgba(20,30,6,.10);
             transition: left .3s cubic-bezier(.32,.72,.28,1),
                         width .3s cubic-bezier(.32,.72,.28,1); }
.find2[data-on="where"] .hl { left: 0; width: var(--where); }
.find2[data-on="mood"]  .hl { left: var(--where);
                              width: calc(100% - var(--where)); }
@media (prefers-reduced-motion: reduce) {
  .find2 .hl { transition: none; }
}

/* Everything the reader reads sits ON the panel.
   THE DROPDOWN IS NOT IN THIS LIST, and that is not an oversight. It was,
   for one build, and `.find2 .wheredrop` (two classes) outranks
   `.wheredrop` (one) whatever the order, so `position: relative` beat the
   `position: absolute` the panel is hung by -- the list became an in-flow
   flex item, took a third of the bar's width, and collapsed the pill into
   a circle with a menu next to it. It only needs the z-index. */
.find2 .fld, .find2 .go { position: relative; z-index: 1; }
.find2 .wheredrop { z-index: 20; }
.find2 .fld { display: flex; flex-direction: column;
              justify-content: center; min-width: 0;
              padding: 8px 30px; min-height: 49px;
              border-radius: var(--pill); border: 0; background: none;
              text-align: left; cursor: text; }
/* WHERE IS WIDER AND ITS PADDING IS EVEN. Riley asked for both: more room
   for a city name, and the same 30px inside the highlight on the right as
   on the left -- the left had 30 to clear the cap's curve and the right
   had the default 24, which put the text a visible two pixels off centre
   in its own panel. */
.find2 .fld-where { flex: 0 0 var(--where); cursor: pointer; }
/* Mood gives the arrow back the space it overlaps, so its TEXT still stops
   where the arrow starts even though its box runs to the pill's end. */
.find2 .fld-mood  { flex: 1;
                    margin-right: calc(-1 * (var(--go) + var(--go-inset)));
                    padding-right: calc(var(--go) + var(--go-inset) + 10px); }

.find2 .lab {
  display: block; font-family: var(--font-brand);
  font-weight: var(--font-weight-medium); font-size: 11.5px;
  letter-spacing: .01em; line-height: 1.2; color: var(--ink);
  margin-bottom: 3px; cursor: inherit; }
.find2 input, .find2 .val {
  display: block; width: 100%; min-width: 0; border: 0; outline: 0;
  background: none; padding: 0; font-family: var(--font-brand);
  font-weight: var(--font-weight-book); font-size: 13.5px; line-height: 1.25;
  color: var(--ink); -webkit-appearance: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.find2 input::placeholder, .find2 .val.ph { color: var(--ink-3); opacity: 1; }
.find2 input::-webkit-search-cancel-button { display: none; }
/* THE PANEL IS THE FOCUS INDICATOR NOW, so Mood's ring has gone. It was
   `:focus-within`, which fires on a mouse click as well as a tab, and an
   inset ring drawn on top of the white panel reads as a second box nested
   inside the first -- clearly wrong in a mid-slide frame, where you see
   the panel arriving and a smaller outlined box waiting inside it.
   Focusing Mood already slides the panel onto Mood, which is a larger and
   more legible focus indicator than a 2px line.

   Where keeps a ring, and only for the keyboard: it is a <button>, so a
   tab stop lands on it before the panel has any reason to move. */
.find2 .fld-where:focus-visible {
  outline: 0; box-shadow: inset 0 0 0 2px rgba(62,82,21,.22); }
/* the round arrow. z-index 2, so it sits over the sliding panel as well as
   over the Mood field's stretched box. */
.find2 .go { z-index: 2; flex: 0 0 auto; align-self: center;
             width: var(--go); height: var(--go);
             margin-right: var(--go-inset); border: 0; padding: 0;
             border-radius: 50%; background: var(--ink); color: #fff;
             display: inline-flex; align-items: center; justify-content: center;
             cursor: pointer; transition: background .14s ease; }
.find2 .go:hover { background: var(--accent-deep); }

/* ---- SCREEN 02: the Where dropdown ----------------------------------- */
/* A white rounded panel hung under the Where field, the width of that
   field plus a little, capped at five rows tall and scrolling. */
.wheredrop { position: absolute; top: calc(100% + 10px); left: 0;
             width: min(316px, 100%); max-height: 274px; overflow-y: auto;
             overscroll-behavior: contain;
             background: #fff; border-radius: 16px; padding: 6px 0;
             box-shadow: 0 24px 60px -20px rgba(20,30,6,.45),
                         0 2px 8px rgba(20,30,6,.10);
             text-align: left; z-index: 20; }
.wheredrop[hidden] { display: none; }
.wheredrop button { display: block; width: 100%; text-align: left;
                    border: 0; background: none; cursor: pointer;
                    padding: 11px 22px; }
.wheredrop button + button { border-top: 1px solid var(--rule-soft); }
.wheredrop button:hover, .wheredrop button:focus-visible {
  background: var(--accent-wash); outline: 0; }
.wheredrop b { display: block; font-family: var(--font-brand);
               font-weight: var(--font-weight-medium); font-size: 13.5px;
               line-height: 1.3; color: var(--ink); }
.wheredrop span { display: block; margin-top: 2px; font-family: var(--font-brand);
                  font-weight: var(--font-weight-book); font-size: 12px;
                  line-height: 1.3; color: var(--ink-3);
                  font-variant-numeric: lining-nums tabular-nums; }

/* The old one-box bar and its option row are gone from the markup; the
   rules stay above, unreferenced, because /search.html still draws .find
   at the top of the results page. */
.scene-body .opts { display: none; }

@media (max-width: 620px) {
  /* THE BAR MUST NOT OVERFLOW A PHONE. Two fields and a 42px button do
     not fit across 390px minus the page's own 22px gutters, so below this
     width the pill becomes a stack: Where on top, Mood and the arrow on
     one row under it. Same control, same order, still one rounded card. */
  /* STACKED, THE PANEL CANNOT SLIDE. Where becomes a full-width row and
     Mood shares the row under it with the arrow, so the highlight would
     have to travel DOWN as well as across -- two axes, on the layout with
     the least room to show the movement. Below this width it goes back to
     being a background on whichever field is lit, which is a cross-fade,
     which is fine at this size and is what the panel is hidden for. */
  .find2 .hl { display: none; }
  .find2[data-on="where"] .fld-where,
  .find2[data-on="mood"] .fld-mood { background: #fff;
    box-shadow: 0 6px 16px -8px rgba(20,30,6,.42); }
  .find2 .fld { transition: background-color .2s ease; }
  .find2 { flex-wrap: wrap; border-radius: 26px; padding: 5px;
           background: #fff; }
  .find2[data-on] { background: rgba(255,255,255,.74); }
  .find2 .fld-where { flex: 1 1 100%; border-radius: 22px;
                      padding: 8px 22px; }
  .find2 .fld-mood { flex: 1 1 auto; border-radius: 22px;
                     margin-right: 0; padding: 8px 14px 8px 22px; }
  .find2 .go { width: 40px; height: 40px; margin-right: 0; }
  .wheredrop { width: 100%; }
  .bar.on-scene { padding: 16px 20px; gap: var(--s4); }
  .bar.on-scene nav { gap: 16px; font-size: 14px; }
  .bar.on-scene nav .cta, .bar.on-scene nav .cta.is-soon { padding: 9px 16px;
                                                           font-size: 14px; }
  .on-scene .mark, .bar .mark { font-size: 28px; }
  /* ONE LINE. "What can you actually eat?" wrapped "eat?" onto its own
     line at 390px and 360px; sized off the viewport it stays one line and
     stays large. Riley: "make the what can you actually eat headline on
     mobile fit on 1 line." */
  .scene-body h1 { font-size: min(30px, 7.2vw); white-space: nowrap; }
  /* 16PX OR SAFARI ZOOMS. iOS zooms the page into any focused input set
     under 16px, and the mood field was 13.5px: tapping it scrolled the
     hero sideways and left the page zoomed. Riley: "i don't want it to
     zoom in weirdly." The cause, not a viewport lock that breaks pinch. */
  .find2 input { font-size: 16px; }
}

/* ======================================================================
   SCREEN 04 + 03 — THE MAP, and the restaurant that opens over it
   ======================================================================

   APPENDED BLOCK. The map block further up built the page as a two-column
   grid: a bordered sidebar in the first column, the map in the second,
   neither overlapping. Screen 04 draws something else -- the map is
   full-bleed under the masthead and the list is a white card FLOATING on
   top of it, rounded on all four corners, with water and coastline
   visible around and behind it. That is not a restyle of the grid, it is
   a different structure, so the grid rules are restated here.

   WHY IT MATTERS BEYOND LOOKS: the map is the page. In the grid the map
   was 68% of a screen that had already given a fifth of itself to a list
   of names; floating the list gives the coastline back and lets the card
   be narrower, because it is now sitting on context rather than replacing
   it.

   THE PHONE KEEPS THE STACK. A floating card on a 390px screen is a card
   covering the entire map, which is the one thing this layout must not
   do. Below 860px the map goes back on top at a fixed height with the
   list under it, exactly as the block above already arranged -- the float
   is a desktop affordance and is written as one.
   ---------------------------------------------------------------------- */
/* THE MAP FILLS WHAT THE MASTHEAD LEAVES, and the height is not guessed.
   The old rule was calc(100vh - var(--maph, 88px)) with --maph set by
   nobody, so the map was 88px tall short of the viewport whatever the
   masthead actually measured -- and the masthead grew when the Sign Up
   pill went into it. A flex column has the browser do the subtraction. */
.page-map body { display: flex; flex-direction: column; }
/* width: 100% IS LOAD-BEARING. .bar carries `max-width: 1280px; margin: 0
   auto`, and an auto margin on a FLEX ITEM absorbs the free space instead
   of centring a block inside it -- so as soon as the body became a flex
   column the masthead shrank to the width of its own contents and sat in
   the middle of the screen with the wordmark, the two links and the pill
   bunched together. It is a block in every other page's normal flow and
   has to be told to behave like one here. */
/* Full-bleed is now the rule for every masthead, so this only has to say
   the flex part: the body is a flex column here and a masthead that grows
   would eat the map's height. */
.page-map .bar { flex: 0 0 auto; }
.maproot { display: block; position: relative; flex: 1 1 auto;
           min-height: 0; height: auto;
           /* the floating list's width, and the panel's left edge */
           --mapcard: 380px; --mapgap: 26px; }
.mapstage { position: absolute; inset: 0; }

/* THE FLOATING CARD ---------------------------------------------------- */
.mapside {
  position: absolute; z-index: 8; left: var(--mapgap); top: 24px; bottom: 24px;
  /* WIDER THAN THE COMP DRAWS IT, on purpose. Screen 04 has this card at
     roughly 286px and Riley asked for more: the comp is a drawing with
     "Veggie Grill" in every row, and the real names are longer than that
     and were ellipsing two words in. 380 fits a real restaurant name and
     its mains pill on one line and still leaves most of the map.

     THE WIDTH IS A VARIABLE BECAUSE TWO RULES NEED IT. .peek starts where
     this card ends, and it had the old 320 written into its `left` as a
     number -- so widening the list did not move the panel, it just made
     the panel cover the list. */
  width: var(--mapcard); max-width: calc(100% - 52px);
  display: flex; flex-direction: column; min-height: 0;
  background: var(--card); border-right: 0; border-radius: 20px;
  box-shadow: 0 22px 54px -18px rgba(20, 30, 6, .30),
              0 2px 10px rgba(20, 30, 6, .08);
  overflow: hidden; }
.mapside-top { padding: 16px 16px 10px; border-bottom: 0; }

/* The search field is a filled pill, not a ruled line: on a white card
   floating over a map an underline reads as a stray hairline. */
.mapside-top .seek { display: flex; align-items: center; gap: 9px;
                     padding: 0 15px; border-radius: var(--pill);
                     background: var(--band); }
.mapside-top .seek svg { flex: 0 0 auto; color: var(--ink-3); }
.mapside-top .seek input { flex: 1; min-width: 0; border: 0; outline: 0;
                           background: none; padding: 10px 0;
                           font-family: var(--font-brand);
                           font-weight: var(--font-weight-book);
                           font-size: 14px; color: var(--ink);
                           -webkit-appearance: none; }
.mapside-top .seek input::placeholder { color: var(--ink-3); opacity: 1; }
.mapside-top .seek input::-webkit-search-cancel-button { display: none; }

/* THE CUISINE CHIPS, ported from the city page rather than rewritten --
   same .pill class, same pressed state, same wording. Screen 04 sets them
   in Medium where screen 06 leaves the city page's in Book, and cuts the
   fourth one off at the card's edge, so the row scrolls rather than
   wrapping: a chip row that wraps to three lines is a third of this card. */
.mapchips { display: flex; gap: 7px; padding: 10px 16px 12px;
            overflow-x: auto; overscroll-behavior-x: contain;
            scrollbar-width: none; -ms-overflow-style: none; }
.mapchips::-webkit-scrollbar { display: none; }
.mapchips .pill { flex: 0 0 auto; font-size: 13px; padding: 7px 15px;
                  font-weight: var(--font-weight-medium); }

.mapside-list { flex: 1; min-height: 0; overflow-y: auto;
                overscroll-behavior: contain; padding: 0 0 8px; }

/* THE ROW: name and distance left, one filled pill right ---------------- */
.mrow { display: flex; align-items: center; gap: var(--s3);
        grid-template-columns: none; padding: 13px 16px;
        border-bottom: 0; cursor: pointer; }
.mrow + .mrow { border-top: 1px solid var(--rule-soft); }
.mrow:hover, .mrow.lit { background: var(--accent-wash); }
.mrow > span { flex: 1; min-width: 0; }
.mrow-n { display: block; font-family: var(--font-brand);
          font-weight: var(--font-weight-medium); font-size: 15px;
          letter-spacing: -0.012em; line-height: 1.3; color: var(--ink);
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mrow-m { display: block; margin-top: 2px; font-family: var(--font-brand);
          font-weight: var(--font-weight-book); font-size: 12px;
          line-height: 1.3; color: var(--ink-3);
          font-variant-numeric: lining-nums tabular-nums;
          overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* THE COUNT IS THE PILL NOW, on the right, where the comp draws it. It
   still says MAINS and not "Dishes": the number is dish_kind.count()[0],
   sides and drinks excluded, and the drawn word would misstate it -- the
   same call the venue and city pages already made. */
.mrow b { flex: 0 0 auto; text-align: center; font-family: var(--font-brand);
          font-weight: var(--font-weight-medium); font-size: 12.5px;
          letter-spacing: 0; line-height: 1; color: #fff;
          background: var(--ink); border-radius: var(--pill);
          padding: 8px 13px; white-space: nowrap;
          font-variant-numeric: lining-nums tabular-nums; }
.mrow.none b { color: var(--ink-3); background: var(--band); opacity: 1; }

.mapside-note { margin: 0; padding: 0 16px 8px; font-family: var(--font-brand);
                font-weight: var(--font-weight-book); font-size: 11.5px;
                line-height: 1.4; color: var(--ink-3); }
.mapside-note .note-sub { display: block; }
/* The offer to move the map: the search no longer moves it on its own, so
   "340 more outside this view" and "Chicago, IL: 1,800 places" each carry
   the one button that does. */
.mapside-note .note-go { font: inherit; font-weight: var(--font-weight-medium);
                         color: var(--ink); background: none; border: 0;
                         padding: 0 0 0 6px; cursor: pointer;
                         text-decoration: underline;
                         text-underline-offset: 2px; }
.mapside-note .note-go:hover { color: var(--yes); }
/* The divider above the matches listed from outside the view. */
.mrow-sep { padding: 10px 16px 4px; font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 11.5px;
            color: var(--ink-3); border-top: 1px solid var(--rule); }

/* ---- SCREEN 03: the restaurant, opened over the map ------------------- */
/* A rounded card inset from the stage, clear of the list on the left and
   showing map on every other side. It used to be a full-bleed panel with
   a square edge that filled the stage completely; the comp keeps the map
   visible around it, which is the whole argument for the panel existing --
   you have not left the view you set up. */
.peek { top: 24px; right: 24px; bottom: 24px;
        left: calc(var(--mapgap) + var(--mapcard) + 22px);
        border-radius: 20px; border-left: 0; overflow: hidden;
        background: var(--card);
        box-shadow: 0 26px 64px -20px rgba(20, 30, 6, .38),
                    0 2px 10px rgba(20, 30, 6, .08); }
.peek-top { border-bottom: 0; padding: 14px 20px 4px; }
/* CLOSE, TOP RIGHT, WITH A CROSS. The old control was "<< Map", first in
   the row, because the panel covered the map completely and going back was
   the only thing it could mean. It does not cover the map any more -- the
   comp leaves it visible on three sides -- so the control is what the comp
   draws and where it draws it. */
/* FULL PAGE AND CLOSE ARE ONE PAIR AND ARE NOW SET AS ONE.
   They sit side by side and said different things about themselves: Close
   had been restyled here into 14px Arboria Medium in the brand green,
   while Full page was still picking up the pre-redesign control rule
   further up the file -- 12px, weight 600, --ink-2, a different family.
   Two adjacent controls at two sizes in two greys reads as one of them
   being broken. One rule, both controls, and only what differs between
   them (their order, and the glyph size) is said separately. */
.peek-top .back, .peek-top a {
  order: 1; font-family: var(--font-brand);
  font-weight: var(--font-weight-medium); font-size: 14px;
  color: var(--ink); display: inline-flex; align-items: center;
  gap: 8px; padding: 6px 6px; border-radius: 8px; }
/* No "Full page" link any more (Riley: remove it), so Close takes the gap. */
.peek-top .back { order: 2; margin-left: auto; }
.peek-top .back i, .peek-top a i { font-style: normal; font-size: 16px;
                                   line-height: 1; }
.peek-top .back:hover, .peek-top a:hover { background: var(--accent-wash);
                                           color: var(--ink); }
.peek-top b { order: 0; font-size: 0; }        /* the name is in the frame */

/* THE PHONE GETS A SHEET, NOT A STACK. The list used to sit under a
   58vh map in the page flow, which on a 390x844 phone left room for one
   row and a half of the thing the reader came for. Riley: "can the
   results part at the bottom take up more space so i can see 2 and a half
   venues in the list." So the map fills the screen and the list is a
   sheet over its bottom edge: --sheet tall at rest (the search pill, the
   chips and about two and a half rows), dragged by its grip up to nearly
   the whole screen, and the list scrolls inside it. mappage.html sets
   the height in px while a drag is live and snaps it on release. */
.mapside-grip { display: none; }
@media (max-width: 860px) {
  .page-map { overflow: hidden; }
  .maproot { display: block; height: auto; flex: 1 1 auto;
             --sheet: min(340px, 54vh); }
  .mapstage { position: absolute; inset: 0; order: 0; height: auto; }
  .mapside { position: absolute; order: 0; left: 0; right: 0; top: auto;
             bottom: 0; width: auto; max-width: none;
             height: var(--sheet); border-top: 0;
             border-radius: 18px 18px 0 0;
             box-shadow: 0 -12px 34px -16px rgba(20, 30, 6, .34),
                         0 -1px 0 var(--rule);
             transition: height .22s ease; }
  .mapside.dragging { transition: none; }
  .mapside-grip { display: flex; justify-content: center; align-items: center;
                  flex: 0 0 auto; height: 22px; cursor: grab;
                  touch-action: none; }
  .mapside-grip i { display: block; width: 38px; height: 4px;
                    border-radius: 2px; background: var(--rule); }
  .mapside-top { padding-top: 6px; }
  /* 16px so iOS Safari does not zoom into the field (see .find2 input). */
  .mapside-top .seek input { font-size: 16px; }
  .mapside-list { max-height: none; overflow-y: auto; }
  .peek { display: none; }
}

/* ---- the link footer on /cities.html ---------------------------------
   HappyCow's "Popular Top Vegan & Vegetarian Cities": a heading, then
   columns of plain text links, dozens of them. It is a linking surface
   and it should read as one -- quiet, dense, no cards, no rules between
   entries, nothing competing with the ranking above it.

   CSS columns rather than a grid, because the number of links changes
   every time a city is crawled and a grid would have to be told how many
   rows to make. break-inside: avoid keeps a link off a column break. */
/* NO RULE ABOVE IT, AND ROOM THAT READS AS ONE PAGE (Riley, 2026-10-09):
   the cards' last row, then the heading, at the gap between two sections
   rather than 200px and a line. */
.citylinks { max-width: 1120px; margin: 0 auto; padding: 0 0 var(--s6); }
.citylinks > .d-head { margin-top: 0; }
.citylinks .d-body { max-width: var(--measure); margin: var(--s3) 0 var(--s6); }
/* The global reset is `a { color: inherit; text-decoration: none }`, which is
   right for nav and cards, where the whole element is obviously clickable.
   In running prose it makes a link invisible: "see other places in Silver
   Lake" read as a sentence, not an offer. Anything inside body copy gets the
   underline back. */
.d-body a, .lede a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.d-body a:hover, .lede a:hover { color: var(--accent-deep); }
.citylinks .eyebrow { margin: var(--s6) 0 var(--s3); color: var(--ink-3); }
.linkwall { columns: 3; column-gap: var(--s7); margin: 0; padding: 0;
            list-style: none; }
.linkwall li { break-inside: avoid; padding: 4px 0; }
.linkwall a { font-family: var(--font-brand);
              font-weight: var(--font-weight-book); font-size: 14.5px;
              line-height: 1.5; color: var(--ink-2); }
.linkwall a:hover { color: var(--ink); text-decoration: underline;
                    text-underline-offset: 3px; }
@media (max-width: 900px) { .linkwall { columns: 2; column-gap: var(--s5); } }
@media (max-width: 480px) { .linkwall { columns: 1; } }

/* THE 11-20 TABLE MUST NOT SCROLL THE PAGE. Four columns of names and
   numbers do not fit across 390px, and a table that does not fit widens
   the document -- so the whole page scrolled sideways on a phone and every
   other section on it was 58px short of its right edge. The table keeps
   its shape and scrolls inside its own box instead, which is what a wide
   table is supposed to do. */
.restwrap { overflow-x: auto; overscroll-behavior-x: contain;
            -webkit-overflow-scrolling: touch; margin: var(--s4) 0 0; }
.restwrap .restlist { margin: 0; min-width: 30em; }

@media (max-width: 560px) {
  /* The centred lede is 19px Medium and runs to seven lines on a phone,
     which is a paragraph where the drawn screen has a caption. It steps
     down with the column, the way the rest of the type scale does. */
  .band.rankhead .lede { font-size: 16px; }
}

/* ======================================================================
   SIGN UP
   ----------------------------------------------------------------------
   The pill in the masthead was drawn on all six comps and had nothing
   behind it. This is what it opens: one field, one button, one sentence
   saying what it does and does not do. There is no drawn screen for it,
   so it is built out of the parts the rest of the site already uses --
   the same white card, the same 20px radius as the map's floating list,
   the same filled pill as every other primary control, the same two
   Arboria weights.

   NO PASSWORD FIELD. There is nothing on this site to log in to yet, so
   an account would be a login with nothing behind it. See
   functions/signup/ for the same argument from the server's end.
   ====================================================================== */
.joinbox { border: 0; padding: 0; background: none;
           max-width: min(420px, calc(100vw - 32px)); }
.joinbox::backdrop { background: rgba(12, 18, 4, .48);
                     -webkit-backdrop-filter: blur(3px);
                     backdrop-filter: blur(3px); }
.join { position: relative; background: var(--card); border-radius: 20px;
        padding: 30px 30px 26px;
        box-shadow: 0 30px 70px -24px rgba(20, 30, 6, .45),
                    0 2px 10px rgba(20, 30, 6, .10); }
.join h2 { margin: 0 0 8px; font-family: var(--font-brand);
           font-weight: var(--font-weight-medium); font-size: 25px;
           letter-spacing: -0.028em; line-height: 1.15; color: var(--ink); }
.join > p { margin: 0 0 20px; font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 14px;
            line-height: 1.5; color: var(--ink-2); }
/* the close control, sized to the 44px touch target rather than to the
   glyph inside it */
.join-x { position: absolute; top: 10px; right: 12px; width: 36px;
          height: 36px; border: 0; background: none; cursor: pointer;
          font-size: 24px; line-height: 1; color: var(--ink-3);
          border-radius: 50%; }
.join-x:hover { color: var(--ink); background: var(--band); }
.join-f { display: block; margin: 0 0 14px; }
.join-f span { display: block; margin-bottom: 6px;
               font-family: var(--font-brand);
               font-weight: var(--font-weight-medium); font-size: 12px;
               color: var(--ink); }
.join-f input { display: block; width: 100%; box-sizing: border-box;
                padding: 12px 16px; border: 1px solid var(--rule);
                border-radius: var(--pill); background: var(--card);
                font-family: var(--font-brand);
                font-weight: var(--font-weight-book); font-size: 15px;
                color: var(--ink); -webkit-appearance: none; }
.join-f input:focus { outline: 0; border-color: var(--ink);
                      box-shadow: 0 0 0 3px var(--accent-wash); }
.join-f input::placeholder { color: var(--ink-3); opacity: 1; }
/* THE HONEYPOT. Off-screen rather than display:none, because some bots
   skip what is hidden and fill what is merely out of view. It is out of
   the tab order and out of the accessibility tree, so nobody using a
   keyboard or a screen reader can reach it by accident and get refused. */
.join-hp { position: absolute; left: -9999px; width: 1px; height: 1px;
           opacity: 0; }
.join-go { display: block; width: 100%; border: 0; cursor: pointer;
           padding: 13px 20px; border-radius: var(--pill);
           background: var(--ink); color: #fff;
           font-family: var(--font-brand);
           font-weight: var(--font-weight-medium); font-size: 15px;
           line-height: 1; transition: background .14s ease; }
.join-go:hover { background: var(--accent-deep); }
.join-go[disabled] { opacity: .6; cursor: default; }
.join-msg { margin: 0 0 12px; min-height: 0; font-family: var(--font-brand);
            font-weight: var(--font-weight-book); font-size: 13px;
            line-height: 1.4; }
.join-msg:empty { display: none; }
.join-msg.good { color: var(--ink); }
.join-msg.bad { color: var(--clay); }
.join-fine { margin: 14px 0 0; font-family: var(--font-brand);
             font-weight: var(--font-weight-book); font-size: 11.5px;
             line-height: 1.45; color: var(--ink-3); }
/* THE PILL'S CURSOR, both states, and nowhere else says anything about
   it. `.is-soon` is what a page carries before signup.js finds the pill;
   the script removes it, and the pointer follows. */
.bar nav .cta { cursor: pointer; }
.bar nav .cta.is-soon { cursor: not-allowed; }

/* ---- FOOD AND DRINKS, AS TWO LISTS -------------------------------------
   Riley: "they need to be a separate tab or something on the site but it
   is actually useful information to have."

   The first attempt was an <h3>Drinks</h3> dropped into the middle of the
   menu. It reads fine at a restaurant with a six-line beer list and not at
   all at Nick + Stef's, whose cellar is longer than most venues' entire
   menus: a divider still makes somebody who came for dinner scroll past
   every bottle to be sure the food has ended.

   THE SHAPE IS THE COMP'S LEGEND ROW. vegeats-07 draws the verdict key as
   a row of pills with the live one filled in the brand green and the rest
   outlined, and that is already the site's way of saying "this one, not
   those". So the tab is that row: no new colour, no new radius, no third
   weight -- .mtab is .jump's pill with the masthead .cta's fill on the one
   that is selected.

   NOTHING HERE HIDES ANYTHING WITHOUT `.tabbed`. Both lists are in the
   HTML and menutabs.js is the only thing that ever adds that class, so a
   page whose script does not arrive shows the whole menu with a heading
   over each half, and the pills stay what the markup says they are --
   links that jump. A stylesheet that hid the drinks on its own would put
   half a restaurant behind a script that might not load. */
/* ONE BAR: the Food/Drinks toggle and the tier pills on a single line.
   They were two stacked rows saying related things, which read as two
   separate decisions rather than "which list, then which part of it". */
.menubar { display: flex; align-items: center; flex-wrap: wrap;
           gap: var(--s2) var(--s3); padding: var(--s5) 0 0; }
.menubar .jump { padding: 0; }
/* Only the live panel's pills, from the FIRST PAINT. Keyed off `.js` on
   <html>, set by one inline line in the head, rather than off `.tabbed`,
   which menutabs.js adds after load -- that timing was the flicker. With
   no JavaScript neither class is ever set and both rows show, which is
   right, because both lists are on screen then too. */
.jump[data-for] { display: flex; }
.js .jump[data-for] { display: none; }
.js .jump[data-for].is-on { display: flex; }

/* THE TOGGLE IS ONE TRACK WITH TWO HALVES, not two pills. Food and drinks
   are a single choice in two states; drawn as separate pills they read like
   the tier filters beside them, which genuinely are independent. */
.mtabs { position: relative; display: inline-flex; padding: 3px; gap: 3px;
         background: var(--band); border-radius: var(--pill); }
/* THE SLIDING GREEN, modelled on `.find2 .hl` in the homepage search bar.
   It starts at the FULL width of the track, so the first paint is a green
   pill rather than a grey one that turns green a moment later; menutabs.js
   then measures the live half and animates it down onto that. The same
   transition carries it across when the reader switches, instead of the
   fill disappearing from one half and appearing on the other. */
.mhl { position: absolute; z-index: 0; top: 3px; bottom: 3px;
       left: 3px; width: calc(50% - 3px);
       border-radius: var(--pill); background: var(--accent);
       transition: left .34s cubic-bezier(.32,.72,.28,1); }
/* THE ARRIVAL. Riley asked for the green to "render in as green and shrink
   to only green on the highlighted size so that we don't see it flash in".
   Done as a CSS animation rather than the JavaScript measurement this
   started as: the end state is what the stylesheet already says, so a
   browser that never runs the script -- or runs a cached copy of it --
   still lands on the correct half instead of sitting at full width.
   Only when Food is live, which is the default. Landing on Drinks means
   the reader has already switched, and animating then would be a second
   thing moving during a deliberate action. */
.mtabs[data-on="food"] .mhl {
  animation: mhl-in .5s cubic-bezier(.32,.72,.28,1) both;
}
@keyframes mhl-in {
  from { width: calc(100% - 6px); }
  to   { width: calc(50% - 3px); }
}
/* THE INACTIVE LABEL HAS TO SURVIVE THE INTRO. While the panel is still
   wide it covers BOTH halves, so "Drinks" -- which is --ink-2, a dark
   olive -- spends about 300ms as dark text on dark green and simply
   disappears. Measured: the panel is 191px wide across a 223px track at
   the first sample, so the whole control looks like a solid green bar
   with its labels missing. That is what Riley screenshotted and called a
   glitch, and it was: not in the mechanism, in the reading of it.
   So the inactive label starts white, where it is legible against the
   green, and darkens on the same curve as the green retreats off it. The
   icon follows because it strokes from currentColor. */
.mtabs[data-on="food"] .mtab:not(.is-on) {
  animation: mtab-ink .5s cubic-bezier(.32,.72,.28,1) both;
}
@keyframes mtab-ink {
  /* The flip is at 12%, not halfway. The width animation uses an ease-out
     curve, so the green is off the second half almost immediately --
     measured at 133px of a 223px track by 125ms. Holding the label white
     to 60% left it white on WHITE for a couple of hundred milliseconds,
     which is the same illegibility as before with the colours swapped. */
  from { color: #fff; }
  12%  { color: #fff; }
  to   { color: var(--ink-2); }
}
@media (prefers-reduced-motion: reduce) {
  .mtabs[data-on="food"] .mtab:not(.is-on) { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .mtabs[data-on="food"] .mhl { animation: none; }
}
/* WHICH HALF, DECIDED IN CSS FROM THE SERVER-RENDERED CLASS. The first
   version sized this panel from JavaScript measurements, which meant that
   until menutabs.js ran it sat at the full width of the track -- a solid
   green bar with the labels lost inside it, exactly what Riley
   screenshotted. Nothing that is correct on the first paint should wait
   for a script to be told so.
   Halves are equal so the geometry is knowable without measuring: `Food
   144` and `Drinks 3` are different widths, and a segmented control reads
   better evenly split anyway. */
.mtabs[data-on="drinks"] .mhl { left: 50%; }
/* The labels ride above it. */
.mtab { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) { .mhl { transition: none; } }
.mtab { flex: 1 1 0; justify-content: center;
        display: inline-flex; align-items: center; gap: 6px;
        font-family: var(--font-brand); font-weight: var(--font-weight-medium);
        font-size: 14px; color: var(--ink-2);
        border: 0; border-radius: var(--pill);
        padding: 7px 13px 8px; white-space: nowrap; cursor: pointer;
        transition: background .14s ease, color .14s ease; }
.mtab .ti { width: 16px; height: 16px; flex: 0 0 auto; }
.mtab:hover { color: var(--ink-body); }
/* The count is data, not a word: same treatment the tier heading gives its
   own "480 Dishes". It is the number of rows in the panel this tab opens
   and it is generated from that list, never from a second count. */
/* THE COUNT FOLLOWS ITS TAB'S INK. It used to set --ink-3 outright, which
   meant it did not take part in the intro: while the green panel was still
   over the second half, "Drinks" was white and legible but the "3" beside
   it was dark olive on dark green and vanished for the first frames.
   currentColor with an opacity gives the same quieter grey against white
   while inheriting whatever the tab is doing. */
.mtab em { font-style: normal; color: currentColor; opacity: .62;
           font-variant-numeric: lining-nums tabular-nums; }

/* THE SELECTED STATE ONLY EXISTS ONCE THE TAB DOES. Filled-in while both
   lists are on screen, the pill would be announcing a choice the page has
   not made -- the comps' filled pill means "you are looking at this one",
   and without the script you are looking at both. */
.menucol.tabbed /* THE FILL CHANGES WITH THE STATE. Food is the site's own olive; drinks
   take the blue already used for the shared-fryer swatch, so it is not a
   new colour invented for this control. */
/* The green is the SLIDING PANEL behind the labels (.mhl above), not a
   background on the tab itself. If the tab carried its own fill, the new
   half would turn green instantly while the panel was still travelling to
   it, and you would see both. The live tab only changes its ink. */
/* Both halves sit over the same track; only the live one is over green. */
.mtab.is-on { background: none; color: #fff; }
.mtab.is-on em { color: #fff; opacity: .78; }
.mtab.is-on:hover { color: #fff; }
/* The `.tabbed` class lands on .menucol when menutabs.js runs, and these
   rules used to change the count's opacity from .78 to .72 at that moment.
   A tenth of an opacity is invisible on its own; what was not invisible was
   the restyle it forced on an element with a colour transition. Same value
   either way now, so the class change is inert here. */
.menucol.tabbed .mtab.is-on em { color: #fff; opacity: .78; }
/* The keyboard has to be able to see where it is. The roving tabindex in
   menutabs.js means arrow keys move the focus between the two, and on the
   filled pill the browser's own outline is dark green on dark green. */
.mtab:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.menucol.tabbed .mtab.is-on:focus-visible { outline-color: var(--ink);
                                            outline-offset: 3px; }

/* Same reasoning as the pill rows above: hide the closed panel before the
   first paint, not after menutabs.js runs. The page used to render with
   both lists open -- on a 148-dish menu that is a very tall page -- and
   then collapse, which threw the map and the hours up the screen. */
.js .menucol .menu-panel { display: none; }
.js .menucol .menu-panel.is-on { display: block; }
/* Keyed off `.js` + the server-written `tabbed`, so the closed panel is
   closed in the first frame. Off `.tabbed` alone this waited for the
   script and the reader saw both lists first. */
.js .menucol.tabbed .menu-panel { display: none; }
.js .menucol.tabbed .menu-panel.is-on { display: block; }
/* SAFETY NET. `.js` is set by the inline head line, which cannot know
   whether menutabs.js later loaded. If it 404s or is blocked, the tabs are
   still ordinary anchors -- this makes the one they point at open. */
.js .menucol.tabbed .menu-panel:target { display: block; }

/* The panel heading is the no-script reader's divider: the two lists run
   one after the other and something has to say where the food stops. Sized
   like the menu's own section headings rather than like a tier, because it
   sits above "Vegan as it Comes" and is not competing with it. */
.panel-h { margin: var(--s8) 0 0; font-family: var(--font-brand);
           font-weight: var(--font-weight-medium); font-size: 16px;
           line-height: 1.4; color: var(--ink);
           border-bottom: 1px solid var(--rule); padding-bottom: var(--s2); }
/* Once the tab is on the page it says the same word, and the heading would
   be the page saying "Drinks" twice in two inches. Hidden, not removed:
   menutabs.js points each panel's aria-labelledby at its tab, and heading
   navigation still wants something here. */
.js .menucol.tabbed .panel-h { position: absolute; width: 1px; height: 1px;
                           margin: 0; padding: 0; overflow: hidden;
                           clip-path: inset(50%); white-space: nowrap;
                           border: 0; }
/* One list, no second: "Food" over the only menu on the page is a label
   for nothing, and so is "Drinks" at a bar with no food. Riley: "in places without drinks, i awkwardly see the
   food label with underline at the top which is weird and unnecessary."
   Hidden the same way, so it still names the menu to heading navigation.
   Off :has() rather than a builder class so the pages already built drop
   it without a re-render. */
.menucol:not(:has(#menu-drinks)) #menu-food > .panel-h,
.menucol:not(:has(#menu-food)) #menu-drinks > .panel-h {
  position: absolute; width: 1px; height: 1px; margin: 0; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* On a phone the two pills sit on one line at any name we would put in
   them; what they must not do is push the column wider than the screen,
   which is what a nowrap row does when the count runs to four figures. */
@media (max-width: 640px) {
  .mtab { padding: 7px 12px 8px; }
}

/* ---- DIET FILTERS (sidedish/diet_ui.py, dietfilter.js) -------------------
   The menu's own tags, under the dish name. A CLAIM is a filled chip in the
   page's accent wash; an OPTION ("Can be made gluten-free") is outlined and
   quieter, so the two never read as the same statement. Same pill, size and
   weight as the .fry / .mod chips on the name row. */
.dish-diet { display: flex; flex-wrap: wrap; gap: 6px; margin: var(--s2) 0 0; }
.dtag { font-family: var(--font-brand); font-weight: var(--font-weight-book);
        font-size: 12px; line-height: 1.4; letter-spacing: 0;
        color: var(--accent-deep); background: var(--accent-wash);
        padding: 3px 10px; border-radius: var(--pill); white-space: nowrap; }
.dtag.opt { color: var(--ink-2); background: none;
            box-shadow: inset 0 0 0 1px var(--rule); }
/* The filter row: the .jump pill's shape, pressed like the city screen's
   .pill. Hidden without JavaScript, where it would be buttons that do
   nothing; the chips on the dishes and the caution still show. */
.dietbar { display: none; }
.js .dietbar { display: flex; flex-wrap: wrap; align-items: center;
               gap: var(--s2); padding: var(--s4) 0 0; }
.dchip { display: inline-flex; align-items: center; gap: 6px;
         font-family: var(--font-brand); font-weight: var(--font-weight-medium);
         font-size: 13.5px; color: var(--ink-body); background: var(--card);
         border: 1px solid var(--rule); border-radius: var(--pill);
         padding: 7px 13px 8px; cursor: pointer; white-space: nowrap;
         transition: background .14s ease, border-color .14s ease; }
.dchip:hover { border-color: var(--ink-3); }
.dchip em { font-style: normal; opacity: .62;
            font-variant-numeric: lining-nums tabular-nums; }
.dchip[aria-pressed="true"] { background: var(--band-deep);
                              border-color: var(--band-deep); color: var(--ink); }
.dchip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.dopt { display: inline-flex; align-items: center; gap: 6px;
        font-family: var(--font-brand); font-size: 13.5px; color: var(--ink-2);
        cursor: pointer; }
.dopt input { margin: 0; accent-color: var(--accent); }
.diet-note { margin: var(--s2) 0 0; max-width: var(--measure);
             font-family: var(--font-brand); font-size: 12.5px;
             color: var(--ink-3); }
.diet-empty { margin: var(--s6) 0 0; font-family: var(--font-brand);
              font-size: 15px; color: var(--ink-body); }
.dish.dt-off, .dt-hide { display: none !important; }

/* ---- HOW TO ORDER VEGAN HERE (tools/build_venue_pages.guide_html) --------
   Guidance from published vegan guides, printed above the menu and outside
   it. Set BELOW the menu in weight: the same muted ink and left rule the
   kitchen notice uses, no card, no colour, so it reads as a note beside the
   answer rather than as more of the answer. No .dish markup lives in here,
   which is what keeps it out of every count and out of dietfilter.js. */
.guide { margin: var(--s4) 0 var(--s2); max-width: var(--measure);
         padding: var(--s2) 0 var(--s2) var(--s4);
         border-left: 3px solid var(--rule); }
.guide .d-label { margin: 0 0 var(--s2); }
.guide-list { list-style: none; margin: 0; padding: 0;
              display: flex; flex-direction: column; gap: var(--s2); }
.guide-list li { font-family: var(--font-brand);
                 font-weight: var(--font-weight-book);
                 font-size: 14px; line-height: 1.5; color: var(--ink-3);
                 overflow-wrap: anywhere; }
.guide-t { font-weight: var(--font-weight-medium); color: var(--ink); }
.guide-t::after { content: ":"; }
.guide-src { margin: var(--s2) 0 0; font-family: var(--font-brand);
             font-size: 12.5px; color: var(--ink-3); }

/* ---- the legal line in every footer ----------------------------------
   Written by sidedish.seo.legal_foot() on built pages and copied by hand
   into the few pages build_site.py copies verbatim. Links to the Terms,
   the Privacy Policy and the privacy choices, plus the copyright and
   database notice. Lane legal_pages, scratchpad/legal_pages/NOTES.md. */
.legal-foot { margin: var(--s3) 0 0; font-family: var(--font-brand);
              font-size: 12.5px; line-height: 1.55; color: var(--ink-3);
              font-weight: var(--font-weight-book); }
.legal-foot a { color: var(--ink-2); text-decoration: underline;
                text-underline-offset: 2px; text-decoration-thickness: 1px; }
.legal-foot a:hover { color: var(--ink); }
.legal-foot .lf-links { display: block; margin-top: 4px; }
.legal-foot .lf-links a { margin-right: 14px; white-space: nowrap; }
/* The landing photograph's legal line: white on the scene, small, last. */
/* No rule above it: the generic footer border drew a white line across the photograph (Riley 2026-09-21). */
/* A little room under the line (Riley 2026-10-14: "it'd be nice to have a little bit, not too much";
   ac837a5561 had taken the original 18px to 0). */
.scene-legal { position: relative; z-index: 6; padding: 0 22px calc(14px + env(safe-area-inset-bottom, 0px)); margin-top: 0; border-top: 0;
               text-align: center; font-family: var(--font-brand);
               font-size: 12px; line-height: 1.6; color: var(--scene-dim);
               text-shadow: 0 1px 14px rgba(0,0,0,.6); }
.scene-legal nav { display: flex; flex-wrap: wrap; justify-content: center;
                   gap: 2px 14px; }
.scene-legal a { color: var(--scene-ink); text-decoration: underline;
                 text-underline-offset: 2px; text-decoration-thickness: 1px;
                 opacity: .85; }
.scene-legal a:hover { opacity: 1; }
/* THE CREDIT READS AS ONE OF THE LINKS (Riley 2026-10-08): same colour, same
   opacity -- it was .8 and dimmed against the links' --scene-ink at .85.
   align-items:center puts it on the links' optical centre instead of their
   baseline, which is what shows in landscape where they share one row. */
.scene-legal span { color: var(--scene-ink); opacity: .85; }
.scene-legal nav { align-items: center; }
/* The map's side panel has no footer; the links close the panel. */
.legal-foot.map-legal { margin: 0; padding: 8px 16px 12px; font-size: 12px;
                        border-top: 1px solid var(--rule); }
.legal-foot.map-legal a { margin-right: 12px; white-space: nowrap; }
.join-fine a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* ======================================================================
   THE SEARCH PAGE'S BAR IS THE HOMEPAGE'S BAR (2026-09-29)
   ======================================================================
   Riley: "the where should be in the search bar like on the homepage,
   make the search bar look more like the homepage" and "the weird thin
   line under the centred on hermosa beach should be removed."

   Same .find2 markup and rules as the landing; only what the setting
   changes is here. The landing's pill floats on a photograph, so its
   glass and its deep drop shadow are for contrast against leaves. On
   paper the glass reads as nothing, so at rest the pill is a quiet band
   with a hairline, and the white highlight is what lifts off it. */
.findbar { border-bottom: 0; }
.find2.onpage { width: min(760px, 100%); background: var(--band);
                -webkit-backdrop-filter: none; backdrop-filter: none;
                box-shadow: inset 0 0 0 1px var(--rule); --where: 32%; }
.find2.onpage .hl { box-shadow: 0 4px 14px -6px rgba(20,30,6,.28),
                                0 0 0 1px var(--rule); }
.find2.onpage .wheredrop { width: min(340px, 100%); max-height: 360px; }

/* THE REFINE ROW: two controls under the bar, quiet until used. */
.refine { display: flex; align-items: center; flex-wrap: wrap;
          gap: var(--s2) var(--s4); margin-top: var(--s3); }
.rchip { display: inline-flex; align-items: center; gap: 7px;
         font-family: var(--font-brand); font-size: 13.5px;
         font-weight: var(--font-weight-book); color: var(--ink);
         background: var(--card); border: 1px solid var(--rule);
         border-radius: var(--pill); padding: 7px 14px; cursor: pointer;
         white-space: nowrap;
         transition: background .13s ease, border-color .13s ease; }
.rchip:hover { border-color: var(--ink-3); }
.rchip .caret { opacity: .6; transition: transform .15s ease; }
.rchip[aria-expanded="true"] .caret { transform: rotate(180deg); }
/* A chosen time is a filter in force, and says so in the one green. */
.rchip.on { background: var(--ink); border-color: var(--ink); color: #fff;
            font-weight: var(--font-weight-medium); }
.rchip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* THE HOURS PANEL. Google Maps' shape: three choices, the last one opens
   a day and a time. Hung under the chip the way Where's list hangs. */
.hours { position: relative; }
.hourspop { position: absolute; top: calc(100% + 8px); left: 0; z-index: 25;
            width: 292px; background: #fff; border-radius: 16px;
            padding: 8px 0 12px;
            box-shadow: 0 24px 60px -20px rgba(20,30,6,.45),
                        0 2px 8px rgba(20,30,6,.10); }
.hourspop[hidden] { display: none; }
.hopt { display: flex; align-items: center; gap: 12px; cursor: pointer;
        padding: 10px 20px; font-family: var(--font-brand); font-size: 14px;
        font-weight: var(--font-weight-book); color: var(--ink); }
.hopt:hover { background: var(--accent-wash); }
.hopt input { width: 17px; height: 17px; margin: 0; accent-color: var(--ink);
              cursor: pointer; }
.hopt input:checked + span { font-weight: var(--font-weight-medium); }
.hat { display: flex; gap: 8px; padding: 2px 20px 6px 49px; }
/* The native arrow sat flush against the box's right edge. Drawn here
   instead, with the same room on the right as the text has on the left. */
.hat select { flex: 1; min-width: 0; font-family: var(--font-brand);
              font-size: 13.5px; color: var(--ink);
              border: 1px solid var(--rule); border-radius: 10px;
              padding: 7px 32px 7px 12px; cursor: pointer;
              -webkit-appearance: none; appearance: none;
              background: var(--card) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%233e5215' stroke-width='2.6' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 12px center; }
.hnote { margin: 6px 20px 0; padding-top: 10px;
         border-top: 1px solid var(--rule-soft); font-family: var(--font-brand);
         font-size: 12px; line-height: 1.4; color: var(--ink-3); }

/* THE FRYER IS A SWITCH. Riley: "the shared fryer isn't clear that it's a
   toggle." A track and a knob is the one shape everybody reads as on/off;
   the words beside it say what "on" means and never change. */
.rswitch { display: inline-flex; align-items: center; gap: 10px;
           cursor: pointer; font-family: var(--font-brand); font-size: 13.5px;
           font-weight: var(--font-weight-book); color: var(--ink);
           user-select: none; }
.rswitch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.rswitch .track { position: relative; flex: 0 0 auto; width: 36px;
                  height: 21px; border-radius: 999px;
                  /* NOT --rule-heavy: that token IS the brand green, the
                     same as the "on" fill, so the switch read as on in
                     both states and only the knob moved. Off is grey. */
                  background: #cfd1c6;
                  transition: background .18s ease; }
.rswitch .knob { position: absolute; top: 2.5px; left: 2.5px; width: 16px;
                 height: 16px; border-radius: 50%; background: #fff;
                 box-shadow: 0 1px 3px rgba(0,0,0,.25);
                 transition: transform .18s cubic-bezier(.32,.72,.28,1); }
.rswitch input:checked + .track { background: var(--ink); }
.rswitch input:checked + .track .knob { transform: translateX(15px); }
.rswitch input:focus-visible + .track { outline: 2px solid var(--ink);
                                        outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .rswitch .track, .rswitch .knob, .rchip .caret { transition: none; }
}
/* Dishes to try instead, in an empty list. */
.nothing .trychips { display: flex; flex-wrap: wrap; justify-content: center;
                     gap: var(--s2); margin-top: var(--s4); }
/* "Zoom out to see them", in an empty list. */
.nothing .plain { font-family: var(--font-brand); border: 0; background: none;
                  color: var(--ink); cursor: pointer; font-size: inherit;
                  font-weight: var(--font-weight-medium); padding: 0;
                  text-decoration: underline; text-underline-offset: 3px; }
@media (max-width: 620px) {
  .find2.onpage { background: #fff; box-shadow: inset 0 0 0 1px var(--rule); }
  .hourspop { width: min(292px, calc(100vw - 44px)); }
}

/* THE SEARCH PAGE, TIGHTENED (2026-09-29). Riley: "the gap between the
   search bar stuff and [the count] with the line feels like it's not
   polished. there's a lot of white space on this page."

   Measured at 1440: 26px under the refine row, 24px more above the count,
   then a 2px green rule, then the first card; the bar started at 48px and
   the list at 32px, and the map sat 24px lower than the count. Now: one
   left edge for bar, count and cards; the count sits close under the
   controls with no rule of its own (each card already has its hairline);
   the map's top lines up with the count. */
.page-results .findbar .shell { padding-bottom: var(--s2); }
.page-results .count { padding: var(--s3) 0 var(--s2); margin: 0; }
.page-results .hit:first-child { border-top: 0; }
@media (min-width: 1021px) {
  .page-results .split { padding: 0 var(--s7) var(--s8); gap: var(--s6); }
  .page-results .mappane { margin-top: var(--s3);
    height: calc(100vh - var(--barh) - var(--s3) - var(--s4));
    height: calc(100dvh - var(--barh) - var(--s3) - var(--s4)); }
}
/* THE TYPEABLE WHERE (app/site/wherebox.js). The field is an input now,
   so it takes a text cursor, and the list lights the row Enter will take. */
.find2 .fld-where { cursor: text; }
.wheredrop button.act { background: var(--accent-wash); }
.wd-none { padding: 12px 22px; font-family: var(--font-brand); font-size: 13px;
           color: var(--ink-3); }
/* THE BRANDED DAY / TIME PICKERS (brandSelect in app/template.html): a
   button like the old select, and a list drawn like the Where list. */
.hat .bsel { position: relative; flex: 1; min-width: 0; }
.bsel-btn { width: 100%; display: flex; align-items: center;
            justify-content: space-between; gap: 8px;
            font-family: var(--font-brand); font-size: 13.5px;
            color: var(--ink); background: var(--card);
            border: 1px solid var(--rule); border-radius: 10px;
            padding: 7px 12px; cursor: pointer; text-align: left;
            transition: border-color .13s ease; }
.bsel-btn:hover, .bsel-btn[aria-expanded="true"] { border-color: var(--ink-3); }
.bsel-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.bsel-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bsel-btn svg { flex: 0 0 auto; opacity: .7; transition: transform .15s ease; }
.bsel-btn[aria-expanded="true"] svg { transform: rotate(180deg); }
.bsel-list { position: absolute; top: calc(100% + 6px); left: 0; right: 0;
             z-index: 30; max-height: 228px; overflow-y: auto;
             overscroll-behavior: contain; background: #fff;
             border-radius: 12px; padding: 5px 0;
             box-shadow: 0 18px 44px -16px rgba(20,30,6,.42),
                         0 2px 8px rgba(20,30,6,.10); }
.bsel-list[hidden] { display: none; }
.bsel-list button { display: block; width: 100%; text-align: left; border: 0;
                    background: none; cursor: pointer; padding: 8px 14px;
                    font-family: var(--font-brand); font-size: 13.5px;
                    color: var(--ink); font-variant-numeric: tabular-nums; }
.bsel-list button:hover, .bsel-list button.act { background: var(--accent-wash); }
.bsel-list button[aria-selected="true"] { font-weight: var(--font-weight-medium);
                                          background: #e8ecdc; }


/* THE TOP BAND (Airbnb's shape; see the note in app/template.html).
   The masthead and the search pill in one sticky band: white into the
   faintest warm grey, a hairline under it. Everything below is white page. */
:is(.page-results, .page-band) .topband { position: sticky; top: 0; z-index: 30;
  background: linear-gradient(#ffffff, #f7f7f4);
  border-bottom: 1px solid var(--rule); }
.page-results .topband header.bar { padding-top: 18px; padding-bottom: 12px;
                                    background: none; }
:is(.page-results, .page-band) .topband .findbar { position: static; background: none;
                                  border: 0; }
:is(.page-results, .page-band) .topband .findbar .shell { max-width: 1440px;
  padding: 4px var(--s7) 14px; display: flex; flex-direction: column;
  align-items: center; }
/* The pill: white at rest, a soft lift, a hairline -- the way Airbnb's
   floats on its band. */
:is(.page-results, .page-band) .find2.onpage { width: min(760px, 100%); background: #fff;
  box-shadow: 0 3px 12px rgba(20,30,6,.08), 0 0 0 1px var(--rule); }
:is(.page-results, .page-band) .find2.onpage[data-on] { background: #ececE6; }
:is(.page-results, .page-band) .refine { justify-content: center; }
/* The map sits under the band, not beside the bar. */
@media (min-width: 1021px) {
  .page-results .mappane { top: calc(var(--barh) + var(--s4));
    margin-top: var(--s4);
    height: calc(100vh - var(--barh) - var(--s4) * 2);
    height: calc(100dvh - var(--barh) - var(--s4) * 2); }
}
@media (max-width: 1020px) {
  :is(.page-results, .page-band) .topband { position: relative; }
  :is(.page-results, .page-band) .topband .findbar .shell { padding: 4px var(--s5) 12px; }
  :is(.page-results, .page-band) .topband .bar { background: none; }
}

/* THE BAND COLLAPSES ON SCROLL, matched to Riley's Airbnb recording
   (JS at the foot of template.html). Airbnb MORPHS one pill rather than
   cross-fading two: the fields' words vanish first (~100ms), the empty pill
   shrinks and rises into the masthead row (~300ms), and the one-line pill
   is simply there when it lands. Played back, the order is the same:

     compact on   words out 0-100ms | pill scales up+in 0-300ms | then the
                  pill fades as the mini pill appears at 220ms
     compact off  mini pill out at once | pill grows back 0-300ms | words
                  back in from 150ms

   The band is FIXED over the page on desktop (Airbnb's too), with the page
   padded by its open height, so folding it never moves the results under
   the reader -- in flow, the browser re-anchored the scroll and the band
   snapped shut again the moment it opened. */
:is(.page-results, .page-band) .findcollapse { display: grid; grid-template-rows: 1fr;
  transition: grid-template-rows .3s cubic-bezier(.2,.8,.2,1); }
:is(.page-results, .page-band) .findcollapse > .findbar { min-height: 0; }
:is(.page-results, .page-band) .topband.compact .findcollapse { grid-template-rows: 0fr; }
/* NOT clipped while it folds: the pill has to rise OUT of this row into
   the masthead, and an overflow clip cut it off halfway up -- a blank beat,
   then the mini pill, which read as vanishing rather than morphing. */

:is(.page-results, .page-band) .find2.onpage { transform-origin: 50% 0;
  transition: transform .3s cubic-bezier(.2,.8,.2,1), opacity .1s ease; }
:is(.page-results, .page-band) .find2.onpage > * { transition: opacity .12s ease .15s; }

/* Lands on the mini pill: ~13px from the top, ~a third as wide. */
:is(.page-results, .page-band) .topband.compact .find2.onpage {
  transform: translateY(-58px) scale(.34, .86); opacity: 0;
  visibility: hidden; pointer-events: none;
  transition: transform .3s cubic-bezier(.2,.8,.2,1), opacity .08s ease .24s,
              visibility 0s linear .32s; }
:is(.page-results, .page-band) .topband.compact .find2.onpage > * {
  opacity: 0; transition: opacity .1s ease; }

.minipill { display: none; }
@media (min-width: 1021px) {
  :is(.page-results, .page-band) .topband { position: fixed; top: 0; left: 0; right: 0; }
  :is(.page-results, .page-band) body { padding-top: var(--band0, 190px); }
  :is(.page-results, .page-band) .minipill {
    display: flex; align-items: center; position: absolute; left: 50%;
    top: 12px; height: 44px; padding: 0 6px 0 22px;
    background: #fff; border: 0; border-radius: var(--pill); cursor: pointer;
    box-shadow: 0 3px 12px rgba(20,30,6,.10), 0 0 0 1px var(--rule);
    font-family: var(--font-brand); font-size: 13.5px;
    font-weight: var(--font-weight-medium); color: var(--ink);
    opacity: 0; pointer-events: none; transform: translate(-50%, 6px);
    transition: opacity .06s ease, transform .06s ease; }
  :is(.page-results, .page-band) .topband.compact .minipill {
    opacity: 1; pointer-events: auto; transform: translate(-50%, 0);
    transition: opacity .1s ease .2s, transform .16s ease .2s; }
  .minipill .mp-where, .minipill .mp-mood { padding: 0 16px; max-width: 200px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .minipill .mp-where { padding-left: 0; }
  .minipill .mp-mood { border-left: 1px solid var(--rule); }
  .minipill .mp-mood.ph { color: var(--ink-3);
    font-weight: var(--font-weight-book); }
  .minipill .mp-go { width: 32px; height: 32px; border-radius: 50%;
    background: var(--ink); color: #fff; display: inline-flex;
    align-items: center; justify-content: center; margin-left: 4px; }
  .minipill:hover { box-shadow: 0 5px 16px rgba(20,30,6,.16), 0 0 0 1px var(--rule); }
  .minipill:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
}
@media (prefers-reduced-motion: reduce) {
  :is(.page-results, .page-band) .findcollapse, :is(.page-results, .page-band) .find2.onpage,
  :is(.page-results, .page-band) .find2.onpage > *, :is(.page-results, .page-band) .refine,
  :is(.page-results, .page-band) .minipill,
  :is(.page-results, .page-band) .topband.compact .find2.onpage,
  :is(.page-results, .page-band) .topband.compact .find2.onpage > *,
  :is(.page-results, .page-band) .topband.compact .refine,
  :is(.page-results, .page-band) .topband.compact .minipill { transition: none; }
}

/* COMPACT BY DEFAULT (Airbnb's results page; JS in template.html). The
   masthead row carries the one-line pill; the filter row sits under it;
   the full bar only opens on a click, over a dimmed page. */
:is(.page-results, .page-band) .refinebar { padding: 2px var(--s7) 14px; }
:is(.page-results, .page-band) .refinebar .refine { margin-top: 0; justify-content: center; }
:is(.page-results, .page-band) .topband.compact .findcollapse .shell { padding-bottom: 0; }
.bandscrim { display: none; }
@media (min-width: 1021px) {
  .page-results .topband header.bar { padding-top: 16px; padding-bottom: 16px;
                                      align-items: center; }
  /* A bigger pill, Airbnb's proportion: the values in medium weight, the
     mood's placeholder quieter. */
  :is(.page-results, .page-band) .minipill { top: 14px; height: 50px; padding-left: 26px;
                            font-size: 14.5px; }
  :is(.page-results, .page-band) .minipill .mp-go { width: 36px; height: 36px; }
  /* The page is padded by the band's COMPACT height (--band0, set while it
     is compact), so opening the bar lays it over the page instead of
     pushing results down; the map is pinned to the same line. */
  .page-results .mappane { top: calc(var(--band0, 130px) + var(--s4));
    height: calc(100vh - var(--band0, 130px) - var(--s4) * 2);
    height: calc(100dvh - var(--band0, 130px) - var(--s4) * 2); }
  .page-results .hit { scroll-margin-top: calc(var(--band0, 130px) + var(--s4)); }
  .bandscrim { display: block; position: fixed; inset: 0; z-index: 29;
    background: rgba(20,25,10,.28); opacity: 0; pointer-events: none;
    transition: opacity .25s ease; }
  :is(.page-results, .page-band) .topband:not(.compact) ~ .bandscrim { opacity: 1;
    pointer-events: auto; }
}
/* ONE CENTRE LINE FOR THE MASTHEAD ROW. Riley: "it doesn't feel like the
   veg eats logo is centered vertically with the rest of the nav info."
   Measured: Sign Up and the links centred at 35.5px, the pill at 39, the
   wordmark's box at 35.5 -- and its letters read LOWER than the box,
   because the g's descender hangs the ink down to 54px. The row is now
   exactly the pill's height so everything centres on the pill's line, and
   the wordmark is lifted by its descender so the LETTERS sit on it. */
/* SUPERSEDED 2026-10-14, Riley: "everything is slightly moved up in the nav
   bar compared to the other pages". Measured at 1440px: the search page's
   wordmark ink sat 11px and its links 6px above every other page's, because
   this row was 78px tall with a -5px lift while the site-wide masthead is
   26px over 24px. The search masthead now IS the site-wide one, and the pill
   centres on it the way the venue page's does. */
@media (min-width: 1021px) {
  .page-results .topband header.bar { padding-top: 26px; padding-bottom: 24px; }
  .page-results .minipill { top: calc((var(--hdrh, 89px) - 50px) / 2); }
}
/* The folded bar is invisible but still laid out, and without this its
   shell sat on top of the Hours chip and swallowed the click. */
:is(.page-results, .page-band) .topband.compact .findcollapse,
:is(.page-results, .page-band) .topband.compact .findcollapse * { pointer-events: none; }
/* Nothing in the band animates until the page has settled (see the note on
   .topband.preload in template.html). */
:is(.page-results, .page-band) .topband.preload, :is(.page-results, .page-band) .topband.preload *,
:is(.page-results, .page-band) .topband.preload ~ .bandscrim { transition: none !important; }
/* COMPACT IS A DESKTOP STATE. The class is in the markup from the first
   frame (so desktop never plays the fold on load), which on a phone hid the
   full bar for the few hundred ms before the script took the class off --
   an empty band, then the bar popping in. Below the desktop width the class
   changes nothing. */
@media (max-width: 1020px) {
  :is(.page-results, .page-band) .topband.compact .findcollapse { grid-template-rows: 1fr; }
  :is(.page-results, .page-band) .topband.compact .find2.onpage { opacity: 1; transform: none;
    visibility: visible; pointer-events: auto; }
  :is(.page-results, .page-band) .topband.compact .find2.onpage > * { opacity: 1; }
  :is(.page-results, .page-band) .topband.compact .findcollapse,
  :is(.page-results, .page-band) .topband.compact .findcollapse * { pointer-events: auto; }
}

/* A VENUE'S WEEK, as a floating panel (toggleWeek in template.html) rather
   than an inline table that pushed the list down. */
.hopen { position: relative; display: inline-flex; }
.opener[aria-expanded="true"] .caret { display: inline-block;
                                       transform: rotate(180deg); }
/* Modelled on Airbnb's price-details panel (Riley's recording): 24px of
   room, 20px corners, a wide SOFT shadow -- the thing that lifts it off the
   busy list behind -- a title with a close button, 14px rows, and it grows
   out of the status line it belongs to (~90% -> 100%, 220ms). */
.hrspop { position: absolute; top: calc(100% + 10px); left: -20px; z-index: 22;
  min-width: 260px; background: #fff; border-radius: 20px;
  padding: 18px 24px 16px;
  box-shadow: 0 6px 20px rgba(0,0,0,.2);
  transform-origin: 28px 0;
  animation: hrsIn .22s cubic-bezier(.2,.8,.2,1); }
.hrspop.leaving { animation: hrsOut .14s ease-in forwards; }
@keyframes hrsIn { from { opacity: 0; transform: scale(.9) translateY(-6px); }
                   to   { opacity: 1; transform: none; } }
@keyframes hrsOut { to { opacity: 0; transform: scale(.94) translateY(-4px); } }
@media (prefers-reduced-motion: reduce) {
  .hrspop, .hrspop.leaving { animation: none; }
}
.hrs-head { display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px; }
.hrs-head b { font-family: var(--font-brand); font-size: 16px;
  font-weight: var(--font-weight-medium); color: var(--ink); }
.hrs-x { width: 28px; height: 28px; margin-right: -8px; border: 0;
  border-radius: 50%; background: none; color: var(--ink); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; }
.hrs-x:hover { background: var(--band); }
.hrspop table.hours { width: 100%; border-collapse: collapse; margin: 0; }
/* The same note inside the floating panel. The rail's copy inherits
   .d-meta from the page; this one is written by openstate.js and has only
   its own class, so it carries the type here. */
.hrspop .hours-unread { font-family: var(--font-brand); font-size: 12.5px;
  line-height: 1.45; color: var(--ink-3); margin: 8px 0 0; }
.hrspop table.hours th, .hrspop table.hours td { padding: 6px 0;
  font-family: var(--font-brand); font-size: 14px; text-align: left;
  color: var(--ink-2); font-weight: var(--font-weight-book);
  border: 0; white-space: nowrap; }
.hrspop table.hours th { padding-right: 24px; }
.hrspop table.hours td { text-align: right; }
.hrspop table.hours tr.today th, .hrspop table.hours tr.today td {
  color: var(--ink); font-weight: var(--font-weight-medium); }
/* THE POPUP FROST IS NOT HERE. It lives in venue.css, and the reason is
   delivery rather than design: a Cloudflare cache rule gives /style.css a
   four-hour browser TTL that `_headers` does not override, so a change to
   this file reaches nobody until it expires or the page's `?v=` hash
   moves. venue.css is served no-cache. When the city pages get their own
   sheet the rule goes there too; see sidedish/seo.py `_css_version` for
   why the hash in the URL is the real mechanism. */
/* The frost over a venue's rows while its week is open (toggleWeek). */
.hit { position: relative; }
.weekfrost { position: absolute; left: -12px; right: -12px; z-index: 21;
  background: rgba(255,255,255,.55);
  -webkit-backdrop-filter: blur(6px) saturate(1.1);
  backdrop-filter: blur(6px) saturate(1.1);
  border-radius: 12px;
  animation: frostIn .22s ease-out; }
.weekfrost.leaving { animation: frostOut .16s ease-in forwards; }
@keyframes frostIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes frostOut { to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .weekfrost, .weekfrost.leaving { animation: none; }
}

/* VENUES ARE SEPARATED BY SPACE, NOT A RULE. Riley: "not sure if i like the
   line between the venues ... maybe just space would be the best thing so
   it's clear without being loud." The gap is about twice the space
   between dishes inside a card, which is what makes a card read as one
   group without a line to say so. */
.page-results .hit { border-bottom: 0; padding: var(--s4) 0 0;
                     margin-bottom: 40px; }   /* was 64px: "a little too much" */
.page-results .hit:last-child { margin-bottom: var(--s5); }
/* What the dish is, leading its description line (dishForm in template). */
.dwhy .dform { font-weight: var(--font-weight-medium); }
.dwhy .sep { color: var(--ink-3); margin: 0 2px; }

/* THE LEGAL LINKS, SMALLER (Riley, 2026-09-29): Terms, Privacy, Your
   privacy choices, Consumer health data -- the same 11px wherever they sit:
   the search page's footer, the map's sidebar, the homepage's photograph. */
.legal-foot .lf-links a, .legal-foot.map-legal, .scene-legal nav {
  font-size: 11px; }
.legal-foot .lf-links a, .legal-foot.map-legal a { margin-right: 11px; }
/* Centred under the map's list (Riley, 2026-09-29). Flex with a gap rather
   than a trailing margin, so the row's centre is the links' centre. */
.legal-foot.map-legal { display: flex; flex-wrap: wrap; justify-content: center;
                        gap: 4px 12px; text-align: center; }
.legal-foot.map-legal a { margin-right: 0; }

/* THE CLOSE BUTTON FLOATS; IT NO LONGER HAS A ROW. Riley: "too much padding
   above the venue name on the container on the map page." Measured: 52px
   of row holding only Close, then the page's own 16px, before the name. The
   control now sits in the panel's top-right corner over the page, and the
   name starts 20px down; the title row keeps clear of the button. */
.peek { position: absolute; }
.peek-top { position: absolute; top: 16px; right: 16px; z-index: 3;
            padding: 0; border: 0; background: none; }
/* Just an x, no word and no pill (Riley: "it's strange that the close
   button says close and is in a separate pill"). A 36px round target,
   invisible until hovered, centred on the venue name's line. */
.peek-top .back { width: 42px; height: 42px; padding: 0; gap: 0;
                  justify-content: center; border-radius: 50%;
                  background: none; box-shadow: none; color: var(--ink); }
.peek-top .back:hover { background: var(--band); }
.peek-top .back:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.embed .band { padding-top: 36px; }   /* 20 was too tight (Riley); 68 too loose */
.embed .titlerow { padding-right: 84px; }

/* MORE ROOM ON THE VENUE PAGE, DESKTOP (Riley, 2026-09-29): 80px a side
   instead of 48. Scoped to the page that holds a venue title (the pages have
   no page class of their own), never to the map's embedded preview, which has
   its own padding. The CONTENT only -- the masthead keeps the site-wide
   position ("that shouldn't have affected the nav"). */
@media (min-width: 1021px) {
  html:not(.embed) .shell:has(h1.vtitle),
  html:not(.embed) .shell.usp-shell { padding-left: 80px; padding-right: 80px; }
}

/* THE SEARCH NAV ON A VENUE PAGE (app/site/searchnav.js). The venue's
   masthead keeps its own height; the pill centres on it; and the filter row
   waits for a click -- Riley: "sans the filters which we can show if the
   user clicks on the search bar." */
@media (min-width: 1021px) {
  .page-band .minipill { top: calc((var(--hdrh, 89px) - 50px) / 2); }
  .page-band .topband.compact .refinebar { display: none; }
  .page-band .topband .findbar .shell { padding-top: 0; }
}

/* THE SEARCH NAV ON A VENUE PAGE, ON A PHONE (searchnav.js, `sdn-phone`;
   Riley, 2026-10-14: "on the venue page on mobile webapp i want the search
   bar to show collapsed like it does on the search page after a search").
   The pill itself is the search page's folded pill (the shared
   `:is(.page-results, .sdn-phone)` rules above and in the 640px block
   below). What is the venue page's own: the band is fixed with the page
   padded by its folded height (--pband, measured by the script); at the top
   of the page the masthead shows with the pill as a row under it, and past
   the top (`sdn-up`) the masthead slides up so only the pill stays pinned,
   which is the search page's folded band. Open, the full box and the filter
   row sit over a frosted page. */
@media (max-width: 640px) {
  html.sdn-phone .topband { position: fixed; top: 0; left: 0; right: 0; z-index: 30;
    transition: transform .28s cubic-bezier(.2,.8,.2,1); }
  html.sdn-phone body { padding-top: var(--pband, 130px); }
  html.sdn-phone .topband.compact.sdn-up { transform: translateY(calc(-1 * var(--hdrh, 66px))); }
  html.sdn-phone .topband.compact .findcollapse,
  html.sdn-phone .topband.compact .refinebar { display: none; }
  html.sdn-phone .topband .findbar .shell { padding-top: 0; }
  html.sdn-phone .bandscrim { display: block; position: fixed; inset: 0; z-index: 29;
    background: rgba(255,255,255,.55);
    -webkit-backdrop-filter: blur(6px) saturate(1.1);
    backdrop-filter: blur(6px) saturate(1.1);
    opacity: 0; pointer-events: none; transition: opacity .25s ease; }
  html.sdn-phone .topband:not(.compact) ~ .bandscrim { opacity: 1; pointer-events: auto; }
}
@media (max-width: 640px) and (prefers-reduced-motion: reduce) {
  html.sdn-phone .topband, html.sdn-phone .bandscrim { transition: none; }
}

/* THE PAGE BEHIND THE OPEN SEARCH BAR IS FROSTED, not dimmed (Riley,
   2026-09-29: "like what we do when a user clicks hours") -- the same wash
   and blur as .weekfrost, on the search page and the venue pages alike. */
@media (min-width: 1021px) {
  .bandscrim { background: rgba(255,255,255,.55);
    -webkit-backdrop-filter: blur(6px) saturate(1.1);
    backdrop-filter: blur(6px) saturate(1.1); }
}

/* The cities page's "vegeats / Best vegan cities in the US" trail is hidden
   but keeps its room, so the heading does not move up (Riley, 2026-09-29).
   The breadcrumb JSON-LD in the head still describes the page to search
   engines. */
.rankhead .crumbs { visibility: hidden; }
/* ON A PHONE THE ROOM GOES TOO (Riley, 2026-10-09: "reduce the padding at the
   top of the cities page"): the hidden trail's 33px and most of the band's
   padding, so the heading sits just under the masthead. Desktop keeps both. */
@media (max-width: 700px) {
  .band.rankhead { padding-top: var(--s2); }
  .rankhead .crumbs { display: none; }
}

/* NO YELP RATINGS ON THE CITY PAGES (Riley, 2026-09-29; the builder no
   longer prints them -- tools/build_city_pages.py). This covers pages built
   before that change, until they are rebuilt: the rating in each row and the
   two sorts that ordered by it. Not the venue page, which carries a venue
   title and is its own decision. */
.shell:not(:has(h1.vtitle)) .vline .rate { display: none; }
#sort option[value="rating"], #sort option[value="reviews"] { display: none; }

/* THE WHOLE CITY CARD OPENS THE CITY (Riley, 2026-09-29). The Discover pill
   stays the one real link -- one focus stop, one name for screen readers --
   and its hit area is stretched over the card, so a click on the photograph
   or the name goes where the pill goes. */
.ccard { position: relative; transition: transform .18s ease, box-shadow .18s ease; }
.ccard .ccard-go::after { content: ""; position: absolute; inset: 0;
                          border-radius: inherit; }
.ccard:hover { transform: translateY(-2px); }
.ccard:hover .ccard-shot img { filter: brightness(1.04); }
@media (prefers-reduced-motion: reduce) { .ccard { transition: none; } }

/* THE RANKING'S SOURCE, AS A FOOTNOTE (Riley, 2026-09-29: "make this
   citation look better placed"). It was a full-width paragraph run flush
   left under a centred grid, reading like body copy. Now: centred under the
   cards, a measure you can read in one sweep, set off by a short rule, small
   and quiet -- the page's fine print, which is what a citation is. */
.limits { margin: var(--s8) auto 0; max-width: 640px; text-align: center; }
.limits::before { content: ""; display: block; width: 48px; height: 1px;
                  margin: 0 auto var(--s4); background: var(--rule-heavy);
                  opacity: .35; }
.limits .srcs { margin: var(--s2) 0 0; font-size: 12.5px; line-height: 1.65;
                color: var(--ink-3); }
.limits .srcs b { color: var(--ink-2); font-weight: var(--font-weight-medium); }

/* VENUE PAGE: no breadcrumb, less air above the name (Riley, 2026-09-29).
   "Cities / Los Angeles, CA" is hidden -- the breadcrumb JSON-LD in the head
   still describes the page to search engines -- and the band's 64px top
   padding comes down to 40, so the name sits 40px under the nav instead of
   108. Scoped to the page with a venue title; the map's preview has its own. */
html:not(.embed) .band:has(h1.vtitle) .crumbs { display: none; }
html:not(.embed) .band:has(h1.vtitle) { padding-top: 40px; }

/* NO YELP DATA ANYWHERE (Riley, 2026-09-29). The builders no longer print
   the rating, price or "Liked by vegans"; this hides them on pages built
   before that, until they are rebuilt. */
.vline .rate, .vline .money, .vline .liked { display: none; }

/* ==== THE VENUE PAGE, TIGHTENED (Riley, 2026-09-29) =====================
   "the hours should show on this page like they do on the search page",
   "i see the hours but it's not clear", "maybe we move the map and hours
   up? there's a lot of blank space on the right side". */

/* The status reads like the search page's (openstate.js paints it), at the
   size of the rest of the line rather than the search card's 13px. */
.vline .opener { font-family: inherit; font-size: inherit;
                 font-weight: var(--font-weight-medium); }
/* No separator dot after the last item a reader can SEE: the Yelp items that
   follow on older builds are hidden, and left a dot hanging at the end. */
.vline > *:not(:has(~ :not(.rate, .money, .liked)))::after { content: none; }

/* The map and the hours rise to the top: on desktop the name block and the
   menu share the left column and the rail spans both rows, so it starts
   level with the venue name instead of under a band of empty page. */
@media (min-width: 1021px) {
  html:not(.embed) .shell:has(> .band h1.vtitle) {
    display: grid; grid-template-columns: minmax(0, 1fr) 470px;
    /* auto 1fr: the rail spans both rows, so when the menu is SHORTER than
       the rail (Drinks with 5 rows) the spare height must go to the menu
       row. With implicit rows it was shared with row 1 and opened a 107px
       gap above the Food/Drinks toggle (Riley, 2026-10-14, Bodhi). */
    grid-template-rows: auto 1fr;
    column-gap: 64px; align-items: start; }
  html:not(.embed) .shell:has(> .band h1.vtitle) > .band { grid-column: 1; grid-row: 1; }
  html:not(.embed) .shell:has(> .band h1.vtitle) > .with-rail { display: contents; }
  html:not(.embed) .shell:has(> .band h1.vtitle) .with-rail > .menucol { grid-column: 1; grid-row: 2; }
  html:not(.embed) .shell:has(> .band h1.vtitle) .with-rail > .rail {
    grid-column: 2; grid-row: 1 / span 2; margin-top: 40px; }
  /* Sticky under the fixed nav, not behind it. */
  .page-band .rail { top: calc(var(--band0, 90px) + 24px); }
}

/* The week in the rail, titled and drawn like the hours panel. */
.rail-block:has(> table.hours)::before { content: "Hours"; display: block;
  font-family: var(--font-brand); font-size: 16px;
  font-weight: var(--font-weight-medium); color: var(--ink);
  margin: 0 0 var(--s2); }
.rail table.hours th, .rail table.hours td { font-size: 14px; padding: 7px 0; }
.rail table.hours td { text-align: right; }
.rail table.hours tr.today th, .rail table.hours tr.today td {
  font-weight: var(--font-weight-medium); color: var(--ink); }

/* the search page while the server answers a new question: the old list
   stays, quietly dimmed, rather than flashing empty (2026-10-05) */
#results.is-asking { opacity: .55; transition: opacity .15s ease .12s; }
.loadcap { margin: 18px 0 8px; text-align: center; font-size: 14px; color: var(--muted, #6e7566); }

/* "Closed for good?" under a restaurant page's hours (feedback.js). Quiet:
   a line of small sans text with the same pill as every other answer. */
.closed-ask { display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
              margin: var(--s3) 0 0; font-family: var(--sans); font-size: 12px;
              color: var(--ink-3); }
.closed-ask .closed-thanks { color: var(--ink-3); }

/* The dish suggestions (app/site/dishsuggest.js): the Where panel, hung
   under the Mood field and a little narrower -- dish names are short. */
.dishdrop { width: min(260px, 100%); }

/* ==== THE PHONE PASS (2026-10-06) =======================================
   Riley: "i feel like we designed for desktop only without actually
   accounting for and keeping in mind the mobile experience which is likely
   to be most popular". Audited at 390px on every page: no page scrolls
   sideways, but taps were desktop-sized (the masthead links 30px tall, the
   hours opener 21px, the dish votes 23x21, every footer link 18px), cards
   ran a screen and a half each, the city list spent a quarter of the
   width on the number, and a wrapped meta line ended on a stray bullet.
   One block, last in the file, so it is the phone's final word. */

/* TAPS. Apple and Google both ask for ~44px; the visible size stays, the
   padding grows and an equal negative margin gives the space back, so
   nothing moves. Coarse pointers only: a mouse needs none of it. */
@media (pointer: coarse) {
  .bar nav a:not(.cta), .bar .mark { padding-top: 10px; padding-bottom: 10px;
                                      margin-top: -6px; margin-bottom: -6px; }
  /* The filled pill keeps its drawn size; an invisible ring takes the tap. */
  .bar nav .cta { position: relative; }
  .bar nav .cta::after { content: ""; position: absolute; inset: -6px -4px; }
  .legal-foot a, .scene-legal a { display: inline-block; padding: 12px 0; margin: -10px 0; }
  .opener { padding: 11px 0; margin: -11px 0; }
  .more { padding-top: 10px; padding-bottom: 10px; }
  .hit-name a { display: inline-block; padding: 8px 0; margin: -8px 0; }
  .vb { padding: 12px 10px; margin: -8px -3px; }
  .acct-link { padding: 10px 2px; margin: -10px -2px; }
  .maplibregl-ctrl.maplibregl-ctrl-group button { width: 40px; height: 40px; }
  /* NOT the Discover pill: .ccard-go::after already stretches over the
     whole card, and positioning the pill here (to grow its tap ring) made it
     the ::after's containing block -- on every phone the photo and the name
     stopped opening the city (Riley, 2026-10-10). */
}

@media (max-width: 640px) {
  /* A wrapped meta line, with no bullet at either end of a line: on a
     phone the open/shut status always opens a line of its own (it is the
     item that wrapped), and the item before it drops its trailing bullet.
     (A clip-path that hid leading bullets also clipped the hours panel
     and put it under the Food/Drinks tabs.) */
  .vline:has(> .hopen, > .open-now, > .open-shut, > .js-open) { row-gap: 4px; }
  .vline:has(> .hopen, > .open-now, > .open-shut, > .js-open)::after {
    content: ""; flex-basis: 100%; height: 0; order: 1; }
  .vline > :is(.hopen, .open-now, .open-shut, .js-open),
  .vline > :is(.hopen, .open-now, .open-shut, .js-open) ~ * { order: 2; }
  .vline > *:has(+ .hopen)::after, .vline > *:has(+ .open-now)::after,
  .vline > *:has(+ .open-shut)::after, .vline > *:has(+ .js-open)::after { content: none; }

  /* SEARCH CARDS: a card was a screen and a half. Descriptions keep three
     lines (the venue page has the rest), rows tighten, the count column
     narrows. */
  .hit-top { grid-template-columns: 48px minmax(0, 1fr); gap: var(--s2); }
  .hit-num b { font-size: 26px; }
  .hit-num span { font-size: 9.5px; }
  .hit-name { font-size: 20px; }
  /* 12px, the app's row padding, not the 10px that was chosen when a
     hairline did the separating (VenueCard.swift: rows() .padding(.vertical, 12)). */
  .hit-dishes li { padding: var(--s3) 0; }
  .dwhy { font-size: 14.5px; line-height: 1.45; display: -webkit-box;
          -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
  /* The box keeps its full padding on a phone -- that is the size the app
     draws (14/11) and the one Riley approved there. */
  .page-results .hit { padding-top: var(--s3); margin-bottom: 24px; }

  /* THE VENUE PAGE: on a phone the three links are what the page is FOR
     (call, get there, see the site), so they are buttons, not underlines. */
  .acts { gap: var(--s2); }
  .acts a, .acts button { border: 1.5px solid var(--rule); border-radius: var(--pill);
                          padding: 10px 12px; min-height: 44px; box-sizing: border-box;
                          display: inline-flex; align-items: center; justify-content: center;
                          flex: 1 1 auto; }
  .acts a:hover, .acts button:hover { box-shadow: none; border-color: var(--ink-3); }
  .dish { padding: 14px 0; }
  .tier { padding-top: var(--s6); }

  /* THE CITY LIST: the number moves above the name instead of holding a
     column a quarter of the screen wide. */
  .vrow { grid-template-columns: minmax(0, 1fr); gap: var(--s2); padding: var(--s5) 0; }
  .count-block { display: flex; align-items: baseline; gap: 6px; padding-top: 0; }
  .count-block b { font-size: 22px; }
  /* "vegan<br>mains" on one line needs the space the <br> stood for; the
     pages are built ahead of time, so the words are redrawn here. */
  .count-block span { font-size: 0; }
  .count-block span::before { content: "vegan mains"; font-size: 13px; }
  /* iOS zooms the page into any field under 16px when it is tapped. */
  .picker, select, input, textarea { font-size: 16px; }

  /* THE CITY CARDS: a wide photo, not a near-square one. */
  .ccard-shot img, .ccard-noshot { aspect-ratio: 16 / 9; height: auto; object-fit: cover; }
}

/* A focus ring for the keyboard, not for a finger: the sign-up sheet
   focuses its close button on open and drew a blue ring round it. */
.acct-x:focus:not(:focus-visible), .join-x:focus:not(:focus-visible) { outline: none; }

/* THE PHONE'S SEARCH PILL (Riley, 2026-10-06: "go ahead"). On a results
   page the Where/Mood box and the filter row took a third of the screen
   above the first result. Compact on a phone means the one-line pill as its
   own row under the masthead, the filters under it, and the full box only
   when the pill is tapped (setCompact/compactAllowed in template.html).
   No morph here: the desktop's fold animates into the masthead row, which
   on a phone has no room for it. */
@media (max-width: 640px) {
  .page-results .topband.compact .findcollapse { display: none; }
  :is(.page-results, .sdn-phone) .topband.compact .minipill {
    display: flex; align-items: center; width: calc(100% - 32px); box-sizing: border-box;
    margin: 2px 16px 10px; height: 50px; padding: 0 5px 0 18px;
    background: #fff; border: 0; border-radius: var(--pill); cursor: pointer;
    box-shadow: 0 3px 12px rgba(20,30,6,.10), 0 0 0 1px var(--rule);
    font-family: var(--font-brand); font-size: 15px;
    font-weight: var(--font-weight-medium); color: var(--ink); text-align: left; }
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-where,
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-mood {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-where { flex: 0 1 auto; max-width: 45%;
    padding-right: 12px; }
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-mood { flex: 1 1 auto; padding-left: 12px;
    border-left: 1px solid var(--rule); }
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-mood.ph { color: var(--ink-3);
    font-weight: var(--font-weight-book); }
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-go { flex: 0 0 auto; width: 40px; height: 40px;
    border-radius: 50%; background: var(--ink); color: #fff; display: inline-flex;
    align-items: center; justify-content: center; margin-left: 6px; }
  :is(.page-results, .sdn-phone) .topband.compact .minipill .mp-go svg { width: 16px; height: 16px; }
  .page-results .refinebar { padding-top: 0; padding-bottom: 10px; }
}

/* ======================================================================
   PHONE FIXES (Riley, 2026-10-07)
   ======================================================================

   WHAT A PHONE BROWSER ACTUALLY DOES WITH THE SCREEN, measured on real
   iOS Safari (iPhone 16 Pro, 440x956pt) rather than reasoned about. A page
   whose html background is magenta, holding a 100svh box with a red strip
   at its top edge and a blue one at its bottom:

       0   -  62pt   MAGENTA   the html BACKGROUND COLOUR, painted by
                               Safari behind the status bar
       62  - 858pt   the page  (innerHeight = 796; red at 62, blue at 846)
       858 - 956pt   MAGENTA   the html background again, behind the
                               floating address bar

   Three things follow, and all three were guesses before the measurement.
   `env(safe-area-inset-top)` and `-bottom` are BOTH 0px, with or without
   viewport-fit=cover, because Safari's own chrome already occupies them.
   `theme-color` is not what tints those bands -- #3e5215 was set on every
   page and appeared nowhere on screen. And the page can never paint into
   them: the only thing a page controls up there is its ROOT BACKGROUND
   COLOUR. So "make it reach the clock" is one question -- does the page's
   edge resolve to the same colour as its root background? */

/* 1 + 2. THE HOMEPAGE REACHES THE TOP AND THE BOTTOM (Riley: "the homepage
   on mobile [is] not extending above to the top of the screen where the
   time and wifi things are", and "why it doesn't extend to the bottom
   below the search bar").

   It did not, and the seam was measurable: Safari paints those two bands
   #101f0e (`.page-home body`, line 144) and the photograph's first row
   came in at about rgb(19,41,2) with leaf highlights well above that, so
   a flat dark olive met a lit green leaf on a hard line 62pt down.

   The fix is to land the scene ON that colour instead of beside it: the
   scrim gains two short gradients that go opaque #101f0e at the extreme
   top and bottom edges, so the photograph dissolves into exactly what the
   browser is painting behind its own chrome and the page reads as one
   surface running under the clock. 44px is the whole of it -- enough to
   read as a fade and short enough that the wordmark (26px down) and the
   legal line keep a legible, slightly darker bed, which is the job the
   scrim's top and bottom stops already had. The width lives in --edge and
   is 0 everywhere else, so no desktop pixel moves. */
@media (max-width: 760px) {
  /* --edge WAS 44px HERE AND IS NOW 0, and the reason is a measurement.
     The note above says "the page can never paint into [the browser's
     bands]... the only thing a page controls up there is its ROOT
     BACKGROUND COLOUR", so the photograph was faded to flat #101f0e at
     both edges to meet the band seamlessly. MEASURED on the simulator
     2026-10-08 with a fixed-position probe: a `position: fixed; bottom: 0`
     element PAINTS THROUGH the band, behind Safari's toolbar, to the
     physical screen edge. innerHeight 796 = visualViewport 796 = 100dvh =
     100svh, safe-area-bottom 0px, yet the screen is 956pt -- so the 160pt
     of chrome is reachable by a FIXED element and not by a 100dvh block.
     Riley: "at the bottom there is just green but it should be the
     background image. same thing at the top of the homepage."
     So the photo is taken fixed (below) and the fade is unnecessary. */
  .scene { --edge: 0px; }
  /* THE PHOTOGRAPH COVERS THE WHOLE SCREEN, not just the 100dvh viewport.
     `.scene` is 100dvh and its ::before was `absolute`, so it stopped at
     the viewport edge and the bands fell back to body's flat #101f0e.
     Fixed + inset:0 reaches them. The -3% bleed and the parallax translate
     are dropped on a phone: with a fixed layer the bleed has nothing to
     hide and the transform would make .scene a containing block again. */
  /* THE CANVAS, NOT A LAYER. `position: fixed` resolves to the 796pt
     VIEWPORT, not the 956pt screen, so a fixed layer stops at the band too
     (tested on the simulator: the photo filled the page and both bands
     stayed flat green). What DOES reach them is the ROOT element's
     background, which Safari propagates to the page canvas -- and that
     propagation carries a background-IMAGE, not only a colour, which is
     the bit the earlier note missed. So the photograph is painted on
     <html> with background-attachment: fixed, and .scene::before is turned
     off on a phone so the same image is not painted twice at two scales. */
  /* FIXED + 100lvh, and both halves matter. Two wrong answers were tested
     on the simulator first, and the reasons are documented upstream:
       - ROOT BACKGROUND-IMAGE: does not reach the bands. Safari samples the
         root background-COLOR for the status area and ignores the image, so
         the page went flat green and the leaf vanished entirely.
       - position: fixed; inset: 0 : `inset: 0` resolves to the SMALL
         viewport (796pt here), so the layer stops exactly where the chrome
         begins - which is what the earlier "the page can never paint into
         them" note was really observing.
     100lvh is the LARGE viewport: the full 956pt screen, chrome included.
     With viewport-fit=cover already set, a fixed layer at 100lvh paints
     behind both the status bar and the toolbar.
     https://1ar.io/updates/safari-26-liquid-glass-web/ */
  /* THE RUNWAY. Riley: "maybe we just need to make the page bigger so it
     extends further?" -- and that is the one thing that works.
     EVERYTHING ELSE WAS TESTED ON THE SIMULATOR AND FAILED: a fixed layer
     at inset:0 (resolves to the SMALL viewport, 796pt), the same at 100lvh
     (836pt -- still 120pt short of the 956pt screen), a root
     background-image (Safari samples the background-COLOR and the photo
     vanished), and two fixed per-edge sample strips (ignored; the
     propagated canvas colour won).
     WHY THIS ONE WORKS: Safari only samples a flat colour for its bars at
     scroll position ZERO. Once the document is scrolled it composites real
     page pixels behind the status bar and the toolbar. So the document is
     made taller than the viewport and parked past the top of the runway.
     Measured: with 320px of runway at scrollY 160 the leaf renders behind
     both bars, no flat band at either edge.
     The runway is PHOTOGRAPH, not empty space, so a reader who scrolls
     into it sees more leaf rather than a blank reveal -- which is what
     makes this safe to ship rather than a trick that breaks on a flick. */
  .page-home body { position: relative; --runway: 160px;
                    padding-top: var(--runway); padding-bottom: var(--runway); }
  .page-home body::before {
    content: ""; position: absolute; inset: 0; z-index: 0;
    background: url(/field.jpg) 50% 50% / cover no-repeat; }
  /* THE SCRIM HAS TO COVER THE RUNWAY TOO, or the bands are LIGHTER than the
     page -- which is the colour difference Riley saw, and it is ours, not
     Safari's. `.scene::after` lays rgba(3,10,5,.40) across the top of the
     scene and .46 across its foot; the runway lives OUTSIDE .scene, so it
     was showing raw photograph while the page edge beside it was darkened.
     Measured: band rgb(28,45,22) against rgb(18,34,7) a few px below.
     These two strips carry the same two values at the same opacities, flat
     over the full runway, so the pixel at the top of the scene and the pixel
     above it are treated identically and the seam disappears. Heights are
     --runway exactly; a gradient would reintroduce an edge. */
  .page-home body::after {
    content: ""; position: absolute; left: 0; right: 0; z-index: 0;
    pointer-events: none; height: var(--runway); }
  .page-home body::after { top: 0; background: rgba(3, 10, 5, .40); }
  .page-home body > .runway-foot {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 0;
    height: var(--runway); background: rgba(3, 10, 5, .46);
    pointer-events: none; }
  /* .scene carries an opaque `background: #101f0e` (line ~149) which sat on
     top of the photograph now painted on body::before -- the leaf showed
     only in the runway strips. Transparent here so the one image shows
     through the whole page; the ::after scrim still does its darkening. */
  .page-home .scene { position: relative; z-index: 1; background: transparent; }
  .scene::before { display: none; }

  /* THE BANDS CAN ONLY EVER BE A SOLID COLOUR, AND SAFARI PICKS IT PER EDGE.
     Measured here: 100lvh is 836 and screen.height is 956, so 120pt at the
     top is beyond ANY css length -- no layer reaches it. What Safari does
     instead (webkit behaviour, see the links below) is hit-test a point 8px
     inside each edge, walk up to the first FIXED or STICKY ancestor, and
     take the first plain background-COLOR it finds; a background-image or
     gradient is skipped. So the photograph can never show up there, which
     is what the original note was really observing -- but the COLOUR is
     ours to choose, and it is chosen PER EDGE.
     Sampled off a simulator screenshot of the rendered page (scrim
     included, not the raw jpg): the photo meets the top band at #253225 and
     the bottom band at #152712, while both bands were painting #101f0e --
     far darker than either, which is the flat green Riley saw.
     These two strips exist only to be sampled: 10px tall, behind the photo
     (z-index 0 against its 1), so nothing is drawn over the artwork.
     https://1ar.io/updates/safari-26-liquid-glass-web/
     https://medium.com/@evkirkiles/coloring-the-webkit-browser-bars-28d75cd8cf7f */
  /* PER-EDGE STRIPS WERE TRIED HERE AND DO NOT WORK -- tested on the
     simulator, both bands stayed #101f0e. Two fixed 10px strips with
     background-color #253225 / #152712 were added at top and bottom; Safari
     ignored them and kept taking the propagated CANVAS colour, i.e. the
     `.page-home body` background at line 144. So the per-edge hit-test the
     articles describe did not reach a strip placed behind the photo here.
     ONE COLOUR FOR BOTH BANDS IS THE REAL CONSTRAINT. Rendered edges
     measured off a screenshot: top #253225, bottom #152712. #101f0e was
     darker than both, which is why the bands read as flat green against
     the leaf. The midpoint below is within ~8 per channel of each edge. */
}

/* 3. THE HOMEPAGE'S LEGAL LINE, ORGANISED (Riley: "organize the 2026 veg
   eats terms, privacy, your privacy choices, consumer health data so it's
   not so bulky ... not so noisy").

   NOTHING IS MERGED AND NOTHING IS HIDDEN, and that is a legal decision,
   not a design one. Two of the four links carry a conspicuousness duty and
   this site's own published policies are what say so:
     * /privacy.html section 13 promises the opt-out will be "linked from
       the footer of every page", and section 8 that readers "will be able
       to opt out with the Your privacy choices link on every page";
     * /health-data.html is the separate Consumer Health Data Privacy
       Notice that Washington's My Health My Data Act requires a regulated
       entity to link prominently from its home page.
   Privacy itself is the CalOPPA conspicuous link. So the lever here is
   density, not subtraction -- and a <details>, a "more" menu or a
   `display:none` would each take a required link out of plain sight.

   The shape is Riley's own: the credit on its own quiet line, the four
   required links on one line beneath it. That lets the type go back UP
   from the 10px squeeze earlier tonight -- four links at 11px with a 13px
   gap measure 300px against the 351px an iPhone SE offers, where five
   items on one row needed 422px -- so the required links are now the only
   thing on their row and are bigger than they were. */
@media (max-width: 760px) {
  .scene-legal { padding-left: 14px; padding-right: 14px; }
  .scene-legal nav { gap: 1px 13px; }
  .scene-legal nav a { font-size: 11px; }
  /* Own row on a narrow phone, but no longer dimmed or shrunk: Riley wants
     it reading as part of the same set as the links. */
  .scene-legal nav span { flex: 0 0 100%; font-size: 11px; opacity: .85;
                          margin-bottom: 1px; }
}

/* LANDSCAPE NEEDS THE PADDING BACK. padding-bottom:0 is right in portrait
   (Riley: "move it down more") but in landscape Safari adds a TAB BAR under
   its toolbar, the usable height collapses to a few hundred points, and the
   legal line ends up flush against the physical edge with nothing under it.
   One line, scoped to the orientation, so portrait keeps what was asked for.
   The credit also stops claiming its own row here: at this width it fits
   beside the links, which is where Riley wants it centred with them. */
@media (orientation: landscape) and (max-height: 560px) {
  .scene-legal { padding-bottom: 10px; }
  .scene-legal nav span { flex: 0 1 auto; margin-bottom: 0; }
}

/* ...and the same thinking on the pages that carry the long footer. The
   prose and all four links stay; what changes is that the paragraph stops
   being one 12.5px block of five lines (measured: 141px, 16.7% of a
   390x844 screen) and reads as a disclaimer with fine print under it. */
@media (max-width: 760px) {
  .legal-foot { font-size: 11.5px; line-height: 1.45; }
  .legal-foot .lf-links { display: flex; flex-wrap: wrap; gap: 2px 13px;
                          margin-top: 7px; }
  .legal-foot .lf-links a { margin-right: 0; }
}

/* 4. THE MAP'S VENUE LIST RUNS TO THE BOTTOM (Riley: "on the map page on
   mobile, can we also make sure the venue list extends to the bottom past
   the search bar").

   Two parts, and the first one is why the second is small. MEASURED in
   Safari on the live markup: `.mapside` t=456 b=796 and `#mlist` t=619
   b=796 against an innerHeight of 796 -- the sheet and the list already
   end exactly on the page's last pixel -- and the band below it is
   rgb(255,255,255), the same white as the sheet, so there is no seam to
   close. What was left was the legal links, which Riley had already taken
   off the sheet earlier tonight (kept below), and the one case where the
   insets are NOT zero: installed from the manifest (display: standalone)
   there is no address bar, env(safe-area-inset-bottom) becomes the home
   indicator, and the last row would sit under it. Same spelling as
   onboard.css and account.css use. */
@media (max-width: 860px) {
  .mapside-list { padding-bottom: calc(8px + var(--sab, env(safe-area-inset-bottom))); }
}

/* THE MAP'S VENUE LIST DROPS THE LEGAL LINKS ON A PHONE (Riley: "i
   don't think we need [them] on the venue list on the map page on
   mobile"). On a phone that panel is a sheet over the map, so the links
   sit between the reader and the thing they opened; on a desktop the
   panel is a column beside the map and they close it properly, which is
   why this is scoped and not a deletion. They remain reachable from
   every other page's footer, so nothing is lost. */
@media (max-width: 760px) {
  .legal-foot.map-legal { display: none; }
}

/* 5. NO BLUE RING LEFT ON THE SIGN UP PILL (Riley: "after clicking sign up
   and exiting out without signing up on mobile i'm seeing the button
   outlined in blue"). The ring is the browser's own :focus-visible ring --
   measured in Chrome on this pill as `outline: auto 1px rgb(0,95,204)` --
   and it is there because <dialog>.close() hands focus back to whatever
   opened it, which is a PROGRAMMATIC focus move and so draws the keyboard
   ring. It is removed in account.js by not taking the focus back when a
   POINTER opened the sheet, never by `outline: none`: a keyboard still
   opens the sheet, still gets the pill back, and still gets the ring.
   This rule only says what the ring looks like when it is right -- the UA
   blue is a system colour that belongs to no palette on this page. */
.bar nav .cta:focus-visible { outline: 2px solid var(--ink);
                              outline-offset: 3px; }
.bar.on-scene nav .cta:focus-visible { outline-color: #fff; }

/* ---- THE AVATAR SLOT, PAINTED FROM MEMORY (lane PROFILEFIX, 2026-10-14) ----
   seo.PILL_BOOT, an inline script before every masthead, sets html.sd-me
   from account.js's sd_pill cache while the page is still parsing, so a
   signed-in reader's first frame is already the avatar and never "Sign Up"
   (Riley: "show the sign up button briefly and then the default first
   initial ... and then finally the actual set profile pic"). These rules
   are account.css's .is-user avatar to the pixel -- that sheet is fetched
   by account.js, far too late for the first frame -- so when account.js
   swaps in the real avatar and drops the classes, nothing moves.
     sd-me    the 38px leaf circle; the "Sign Up" text is not drawn
     --sd-ini the initial, only for a reader with NO photo
     sd-ph    a photo: the circle stays plain until the picture has
              loaded and the boot puts the <img> in (never the initial)
     sd-pro   the Pro tab under the circle
   FLEX, NOT account.css's inline-grid: until the boot's <img> goes in, the
   pill still holds its "Sign Up" text node (font-size 0), and in a grid that
   node is a second row, which lifted the initial 9px above centre. */
html.sd-me .bar nav .cta, html.sd-me .bar nav .cta:hover,
html.sd-me .bar.on-scene nav .cta, html.sd-me .bar.on-scene nav .cta:hover {
  position: relative; width: 38px; height: 38px; min-width: 0; max-width: none; padding: 0;
  border: 0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; overflow: visible;
  background: var(--leaf-deep); color: transparent; font-size: 0; cursor: pointer;
  box-shadow: 0 0 0 2px #fff, 0 4px 12px -4px rgba(38, 51, 11, .5); }
html.sd-me .bar nav .cta::before {
  content: var(--sd-ini, ""); color: #fff;
  font: var(--font-weight-medium) 17px/1 var(--font-brand); display: block; transform: translateY(-0.032em); }
@supports (text-box-trim: trim-both) {
  html.sd-me .bar nav .cta::before { transform: none; text-box: trim-both cap alphabetic; }
}
html.sd-me .bar nav .cta::after { content: none; }
html.sd-me.sd-pro .bar nav .cta::after {
  content: "Pro"; position: absolute; inset: auto; left: 50%; top: calc(100% - 3px); transform: translateX(-50%);
  padding: 0 5px; border-radius: 6px; background: #e4a62a; color: #26330b;
  font: var(--font-weight-bold) 9px/13px var(--font-brand); letter-spacing: .03em; white-space: nowrap;
  box-shadow: 0 0 0 2px #fff; pointer-events: none; }
html.sd-me .bar nav .cta .av-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }

/* ======================================================================
   THE VENUE CARD, SHARED BY THE CITY PAGE AND THE SEARCH PAGE (lane
   SEARCHUI, 2026-10-14; Riley: "can you change how the search page looks
   so it's more like the results on the city pages?"). These rules were
   city.css's; both pages draw the same `.hit` card (tools/build_city_pages.py
   listing(), hit() in app/template.html), so they live here once instead
   of in two copies. The city page loads this sheet before city.css, as it
   always has, so nothing it relied on moved under it.

   `.dmore` is `.hit .dmore` here, not `.hit-dishes .dmore`: the search
   card's foot holds its "Show all N matching dishes" button beside the
   link and is not a list item. `.hit .dmore` still outranks
   `.hit-dishes li` on the city card, so its 6px top padding holds.
   ====================================================================== */

/* THE LIST AND THE MAP, ONE GEOMETRY. city.css's `.shell.cx` (lane DSN)
   and the search page's `.split` below read the same three numbers. */
:root{--pair-list:clamp(600px, calc(600px + (100vw - 1280px) * 0.25), 700px);
  --pair-map:clamp(460px, calc((100vw - 96px) * 0.38), 640px);
  --pair-gap:104px}

/* A CARD'S DISH LIST AS A SHORT EXCERPT OF THE VENUE PAGE (Riley: no tags
   on this page; "it will end up looking more like the venue page"). The
   venue band heading (.tier-h.d-title: brand face, medium, ink-body, tight
   tracking) scaled for a card, 22px desktop / 20px phone, 22px above (2px
   for the first) and 9px below; the venue dish row: the 11px tier dot by
   the venue page's own rule (a centred flex row, the dot pinned to the
   first line, margin-top 10px, margin-right 4px), the description below
   from the row's edge. No pills anywhere. */
.hit-dishes .dgroup{font-family:var(--font-brand);font-weight:var(--font-weight-medium,500);
  font-size:22px;line-height:1.12;letter-spacing:-0.024em;color:var(--ink-body,#1a1c16);
  text-transform:none;padding:22px 0 9px}
.hit-dishes .dgroup:first-child{padding-top:2px}
.hit-dishes .dline{display:flex;flex-wrap:nowrap;align-items:center;gap:12px}
.hit-dishes .dline .dot{align-self:flex-start;margin-top:10px;margin-right:4px;flex:0 0 auto}
.hit-dishes .dline .dname{display:block;min-width:0}
.hit-dishes .dwhy{margin-top:8px;font-size:15px;line-height:1.5;color:var(--ink-body,#1a1c16)}
@media (max-width:640px){
  .hit-dishes .dgroup{font-size:20px}
  .hit-dishes .dline{gap:8px}
  .hit-dishes .dwhy{font-size:14px}
}

/* "See all N vegan mains ->" at the foot of every card: the venue page's
   fold link type (.tier-fold > summary: 13px, 600, ink-2), as a link with a
   44px tap target. No underline in any state (Riley, 2026-10-13); the cue
   is colour and the arrow moving 2px right. */
.hit .dmore{padding:6px 0 0}
.hit .dmore a{display:inline-flex;align-items:center;gap:6px;min-height:44px;
  font-family:var(--sans);font-size:13px;font-weight:600;letter-spacing:.01em;
  color:var(--ink-2,#3e5215);text-decoration:none}
.hit .dmore a,.hit .dmore a span{text-decoration:none;border:0;box-shadow:none}
.hit .dmore a i{font-style:normal;transition:transform .12s ease}
.hit .dmore a:hover,.hit .dmore a:focus-visible{color:var(--accent-deep,#2c3b0f)}
.hit .dmore a:hover i,.hit .dmore a:focus-visible i{transform:translateX(2px)}
.hit .dmore a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* The venue name: never underlined; keyboard focus still gets a ring. */
.hit-name a,.hit-name a:hover,.hit-name a:focus,.hit-name a:active,.hit-name a:visited{
  text-decoration:none;border-bottom:0;box-shadow:none}
.hit-name a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
/* The count column breathes before the name, hours and cuisine (was
   city.css; the city header's rank column is sized to match it). */
@media (max-width:640px){ .hit-top{gap:16px} }
/* The foot: the expander (search only) and the venue link on one line. */
.hit .dmore{display:flex;flex-wrap:wrap;align-items:center;column-gap:24px}
.page-results .hit .dmore .more{margin:0;padding:0;min-height:44px;
  font-family:var(--sans);font-size:13px;font-weight:600;letter-spacing:.01em;
  color:var(--ink-2,#3e5215)}
.page-results .hit .dmore .more:hover{color:var(--accent-deep,#2c3b0f)}

/* THE SEARCH PAGE'S LIST AND MAP, >=1100px (lane SEARCHUI, Riley
   2026-10-14: "i want the map to be wider and expand on desktop to fill
   the space on large screens", and "i want the top and bottom padding
   above and below the map to be same size as the padding above the top
   venue in the list before the nav bar").

   THE LIST IS THE CITY PAGE'S COLUMN (--pair-list, 600 -> 700px), on the
   left margin; THE MAP TAKES EVERYTHING ELSE, so on a 2560 screen it is a
   map and not a 640px postcard in the middle of white.

   ONE NUMBER FOR THE AIR, --list-air: the band's foot to the first card's
   header (measured 32px: the count line's 12 + 8 and the card's own 12),
   the band's foot to the map's top, and the map's foot to the window's
   foot. The count line's top padding is derived from it rather than typed,
   so changing the air moves the list and the map together. --band0 is the
   compact band's height, set by the page. */
.page-results{--list-air:32px;--map-air:48px;--card-above:12px}
.page-results .count{padding:calc(var(--list-air) - var(--card-above) - var(--s2)) 0 var(--s2)}
@media (min-width:1100px){
  .page-results .split{max-width:none;margin:0;
    grid-template-columns:var(--pair-list) minmax(0, 1fr);
    column-gap:var(--pair-gap);justify-content:stretch;
    padding-left:48px;padding-right:48px;box-sizing:border-box}
  /* THE MAP'S AIR IS MEASURED TO WHAT THE EYE SEES (Riley, 2026-10-14:
     "it's still not matching how much padding is above the top venue"):
     --list-air reaches the first card's BOX, but its visible top -- the
     count numeral's cap -- sits 48px under the band. So the map gets its
     own 48px above and below, the city page's figure too. */
  .page-results .mappane{margin-top:var(--map-air);
    top:calc(var(--band0, 130px) + var(--map-air));
    height:calc(100vh - var(--band0, 130px) - 2 * var(--map-air));
    height:calc(100dvh - var(--band0, 130px) - 2 * var(--map-air))}
}
/* Venues separated as the city list separates them: 12 above, 24 below. */
.page-results .hit{padding:var(--card-above) 0 var(--card-below,24px);margin-bottom:0}

/* THE QUESTION BOX IS THE VENUE PAGE'S (Riley, 2026-10-14: "i want the ask
   question containers to match how it looks on the venue pages now"):
   `.dish-ask` as style.css's base plus venue.css's OPTION C draw it, value
   for value. White inside, a dashed grey edge, body ink, and one bold lead
   word (askLead in app/search.js, the port of sidedish/ask_lead.py). */
.hit .dask{margin:16px 0 3px;padding:11px 14px;border-radius:14px;
  background:#fff;border:1px dashed #c4c6bf;max-width:var(--measure);
  font-family:var(--font-brand);font-weight:var(--font-weight-book);
  font-size:13.5px;line-height:1.55;color:var(--ink-body,#1a1c16)}
.hit .dask b{color:var(--ink-body,#1a1c16);font-weight:var(--font-weight-medium,500)}

/* The city page's 24px phone gutter (city.css --city-gutter), so the two
   lists start on the same line on a phone. */
@media (max-width:700px){
  .page-results .split{padding-left:24px;padding-right:24px}
}

/* ======================================================================
   BUILD-YOUR-OWN PANELS ON A SEARCH CARD (lane SEARCHBLD, 2026-10-14;
   Riley: "can [you] show me the configuration for places like chipotle").
   The search page does not load venue.css, so the panel's rules are here
   too. THE BLOCK BETWEEN THE TWO MARKERS IS venue.css SECTION 16, VALUE
   FOR VALUE (the -final previews, docs/lanes/DSN-SETTLED.md), and
   tests/test_search_build_panel.py fails if the two copies drift. It is a
   copy and not a move because the venue pages are stamped with
   venue.css's hash and are rebuilt separately: moving the rules out of
   venue.css would leave a page holding the new venue.css and a cached old
   style.css with no pills at all. When the venue pages next rebuild,
   delete venue.css's copy and this paragraph.
   ====================================================================== */
/* -- venue.css 16 begins -- */
.bld{max-width:var(--measure,42em);font-family:var(--font-brand)}
.bld .dot{width:8px;height:8px;margin:0 6px 0 0;flex:0 0 auto}
.bsame p{margin:0;font-size:13.5px;line-height:1.5;color:var(--ink-3,#6e7566)}
.bsame b{font-weight:var(--font-weight-medium,500);color:var(--ink-2,#3e5215)}
.bld-a{margin:14px 0 3px;padding:19px 14px 19px 21px;border-radius:14px;
  background:var(--band,#f4f5f0)}
.bld-a.bsame{padding:14px 14px 14px 21px}
.bstep{display:grid;grid-template-columns:76px minmax(0,1fr);column-gap:10px;
  padding:0}
.bstep + .bstep{margin-top:22px}
.bk{font-size:12.5px;line-height:26px;font-weight:var(--font-weight-medium,500);
  color:var(--ink-3,#6e7566)}
.bchips{display:flex;flex-wrap:wrap;gap:6px}
.bc{display:inline-flex;align-items:center;padding:3px 10px 3px 8px;
  border-radius:999px;background:#fff;border:1px solid rgba(62,82,21,.12);
  font-size:13px;line-height:18px;color:var(--ink-body,#1a1c16)}
/* NOT VEGAN: the same pill, pushed back. Background, border and dot at 0.5;
   the text at 0.65 so it keeps 5.2:1 against the faded pill (a flat
   opacity:0.5 would leave it at 3.3:1). */
.bc-no{background:rgba(255,255,255,.5);border-color:rgba(62,82,21,.06);
  color:color-mix(in srgb,var(--ink-body,#1a1c16) 65%,transparent)}
.bc-no .dot{opacity:.5}
/* ASK: white pill, neutral grey dashed border, purple dot, muted "ask". */
.bc em{font-style:normal;margin-left:5px;font-size:11.5px;color:var(--ink-3,#6e7566)}
.bc-dash{border:1px dashed #d2d4cd}
/* "+N not vegan": phones only; build_fold.js opens the step on tap. */
.bc-more{display:none;font:inherit;font-size:13px;cursor:pointer}
.bfold{display:contents}
@media (max-width:700px){
  .bc-more{display:inline-flex;font-size:12.5px}
  .bfold{display:none}
  .bv.is-open .bfold{display:contents}
  .bv.is-open .bc-more{display:none}
  .bld-a{padding:16px 12px 16px 17px}
  .bld-a.bsame{padding:12px 12px 12px 17px}
  .bstep{grid-template-columns:62px minmax(0,1fr);column-gap:8px}
  .bk{font-size:12px}
  .bc{font-size:12.5px;padding:2px 9px 2px 7px}
}
/* -- venue.css 16 ends -- */
/* THE CARD'S CUT: the same pills, set tighter. A card is an excerpt, so
   the panel takes the question box's 16px above, steps 14px apart instead
   of 22, and the venue page's phone padding on every screen. */
.hit .bld-a{margin:16px 0 3px;padding:16px 12px 16px 17px}
.hit .bld-a.bsame{padding:12px 12px 12px 17px}
.hit .bstep + .bstep{margin-top:14px}
