/*
 * Flavour Cup — "Brew" (Claude Design 5a, identity 3A).
 *
 * Plain CSS via Propshaft, no build step.
 *
 * The palette is deliberately not "Vietnamese bakery": the old chilli red and
 * egg yolk only meant one cuisine, so both accents move onto roast — caramel
 * and amber — which bánh mì and milk tea both contain. Structure is unchanged
 * from earlier rounds: 3px ink stroke, offset shadow, and at most two coloured
 * objects per screen.
 *
 * Colour discipline, which the screens rely on:
 *   caramel  — every action, once per screen
 *   amber    — the winner, and only the winner
 */

:root {
  /* Brew */
  --ink: #17130F;              /* warm black, every stroke */
  --green: #0E4A3C;            /* steeped green — pandan, jasmine, matcha */
  --green-deep: #08322A;       /* deep steep — inset panels, results */
  --caramel: #B4531F;          /* every action, once per screen */
  --amber: #E0A23A;            /* the winner, and only the winner */
  --amber-lift: #F2C14E;       /* the logo cup only — never a UI surface */
  --condensed: #EDE0C4;        /* selected states */
  --oat: #F7F0E1;              /* cards, sheets, body */
  --canvas: #E8E1D1;           /* page behind the app, light surfaces */

  /* Derived tones */
  --on-green-mono: #9CC4B4;
  --on-green-body: #B9D6C9;
  --on-green-rule: #12594A;
  --on-green-ghost: #6FA48F;
  /* Dark enough to clear 4.5:1 on the *canvas* rows as well as the oat ones.
     At #6E6459 a tried row's meta measured 4.44 against --canvas — and a tried
     row's meta is where the word "Tried" is printed, so the one line the mark
     depends on was the one line that failed. */
  --on-oat-mono: #6A6055;
  --on-oat-body: #4A423A;
  --on-oat-rule: #C8BFAC;
  --on-caramel-body: #EFCDB4;
  --on-amber-mono: #8A5410;
  --on-condensed-mono: #7A6440;

  /* Particle fills for the pick payoff (6a). Still the brand palette — these
     are the roast, crumb and herb tones the food itself is made of, not a
     second scheme. See ParticleKit. */
  --roast: #5C3216;
  --bake: #C98A3C;
  --yolk: #E8C05A;
  --crust: #EAD3A0;
  --dough: #F0E4CB;
  --herb: #4E7A3A;

  --font-display: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Work Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --stroke: 3px;
  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-lg: 22px;
  --shadow-sm: 4px 4px 0 var(--ink);
  --shadow-md: 5px 5px 0 var(--ink);
  --shadow-lg: 6px 6px 0 var(--ink);

  /* The offset shadows are painted outside the border box but read as part of
     the object, so they sit *in* whatever gap follows: 12px under a card with a
     6px shadow looks like 6px, while 12px under an unshadowed one looks like
     12px. That is the whole reason the spacing reads unevenly from object to
     object — the numbers already agree, the shadows don't. These give the
     shadow its own room back, so a gap is measured from what the eye treats as
     the bottom of the object rather than from the border. */
  --drop-sm: 4px;
  --drop-md: 5px;
  --drop-lg: 6px;
}

* { box-sizing: border-box; }

/* Safari and Chrome on Android paint a translucent blue box over whatever you
   tap. On the duel that lands on a contender card — a 300px slab of oat — so
   the whole thing flashes blue at the exact moment the pick is being made, over
   the top of the amber payoff that is the designed response to that tap.
   Removing it costs nothing here because every pressable object in the app
   already answers a press itself: `:active` shrinks the offset shadow and
   translates the card into it. `:focus-visible` is untouched, so the keyboard
   ring is still there — and it never fired on touch to begin with. */
html { -webkit-tap-highlight-color: transparent; }

/* A press held a moment too long used to select the label instead of firing —
   on iOS that raises the text-selection callout over the primary CTA, which is
   the worst place in the app for it. Labels on controls are not prose and
   nobody copies them. Inputs and the note field are untouched. */
button,
label,
.chunky-btn,
.ghost-link,
.ghost-option,
.contender-card,
.save-pill,
.app-bar__link { -webkit-user-select: none; user-select: none; }

body {
  margin: 0;
  background: var(--green);
  color: var(--oat);
  font-family: var(--font-body);
  font-weight: 500;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

/* The podium and its follow-on screens sit a shade deeper. */
body.is-deep { background: var(--green-deep); }

a { color: inherit; text-decoration: none; }

:focus-visible {
  outline: var(--stroke) solid var(--amber);
  outline-offset: 2px;
}

/* button_to wraps its button in a form — let the button participate directly
   in the surrounding layout instead of the form. */
form.button_to { display: contents; }

/* ---------------------------------------------------------------- */
/* Shell                                                             */
/* ---------------------------------------------------------------- */

/* The green runs edge to edge under the notch and home indicator (see
   viewport-fit=cover in the layout); these insets keep content out from under
   them. They resolve to 0 everywhere that isn't an iPhone. */
.app {
  max-width: 430px;
  margin: 0 auto;
  padding:
    0
    calc(20px + env(safe-area-inset-right))
    calc(40px + env(safe-area-inset-bottom))
    calc(20px + env(safe-area-inset-left));
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* Both celebrations throw twelve shapes *outward* from a card, and on a phone
   the outer arcs land past both edges — which is the point of them. What they
   must not do is widen the document, and animating out to x=498 on a 375px
   screen does exactly that. The cost is not a stray scrollbar: a wider document
   widens the initial containing block, and the surface fixed to it is 08b's
   sheet. The sheet is `left/right: 0` capped at 430px, so against a 498px block
   it stops going full-bleed and renders 430 wide, inset 34px, with its own text
   running off the right of the screen — which reads as "the sheet is the wrong
   width" when the sheet never changed. It only shows up once the particles have
   flown, which is roughly when the sheet arrives, so the two look connected.

   `clip`, not `hidden`: hidden makes this a scroll container and takes the
   page's own scrolling with it, while `clip` against a visible y-axis creates
   no scroll container at all. It has to go on an element, not on `html` or
   `body` — overflow set on either of those propagates to the viewport and
   leaves the element itself `visible`, so the rule computes to `clip` and the
   document widens anyway.

   430px is the shell's own max-width, and that is the exact width at which this
   is free: at or below it `.app` spans the whole window, so the flood
   `.pick-burst` aims at the window edges (`calc(50% - 50vw)`, written to escape
   this very box) lands precisely on `.app`'s border box and loses nothing. One
   pixel wider and the shell is narrower than the window, where clipping would
   put back the band of green down each side that the inset removed. It is also
   the exact width at which the sheet is the one that suffers: only a window
   under 430px wants a full-bleed sheet, so only there does a widened containing
   block let it reach its cap instead. */
@media (max-width: 430px) {
  .app { overflow-x: clip; }
}

.app-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: calc(18px + env(safe-area-inset-top)) 0 6px;
  flex: none;
}

/* Mark and name are one object: the lockup from the brand doc, at header
   scale. The mark carries the ink stroke, so it wants a hair more room below
   the baseline than the type does — hence align-items rather than baseline. */
.wordmark {
  display: flex;
  align-items: center;
  gap: 9px;
  font: 800 17px/1 var(--font-display);
  letter-spacing: -.02em;
  color: var(--amber);
}

.wordmark__mark {
  height: 30px;
  width: auto;
  flex: none;
  /* The viewBox is tight to the mark, so this only guards the miter on the
     rotated straw's corner from being shaved at the top edge. */
  overflow: visible;
}

.app-bar__meta {
  display: flex;
  align-items: center;
  gap: 14px;
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .1em;
  color: var(--on-green-mono);
  text-transform: uppercase;
}

.app-bar__link {
  position: relative;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* The bar's type is 11px, so the link it draws is 11px tall — a quarter of the
   44px minimum, on the app's only way to an account. The hit area is grown with
   a pseudo-element rather than with padding, because padding here would grow the
   bar and push the whole screen down. Horizontal reach stops at 6px a side: the
   meta row's gap is 14px, and two overlapping hit areas are worse than a small
   one. */
.app-bar__link::after {
  content: "";
  position: absolute;
  inset: -17px -6px;
} 

.app-main {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.back-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 0 2px;
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--on-green-mono);
}
.back-link__arrow {
  flex: none;
  width: 34px;
  height: 34px;
  border: var(--stroke) solid var(--on-green-ghost);
  border-radius: 10px;
  color: var(--on-green-body);
  font: 700 16px/28px var(--font-body);
  text-align: center;
}

/* ---------------------------------------------------------------- */
/* Type                                                              */
/* ---------------------------------------------------------------- */

.display {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -.035em;
  line-height: .92;
  margin: 0;
  text-wrap: balance;
}
.display--xl { font-size: clamp(38px, 12vw, 48px); }
.display--lg { font-size: clamp(34px, 11vw, 44px); }
.display--md { font-size: 32px; letter-spacing: -.03em; }

.eyebrow {
  font: 700 11px/1 var(--font-mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  margin: 0;
}

.lede {
  font: 500 16px/1.45 var(--font-body);
  color: var(--on-green-body);
  margin: 11px 0 0;
  text-wrap: pretty;
}

/* The stand-in for a meta line we have nothing to write on. Whether we know a
   chain's suburb is a fact about our data, not about the place, and without
   this it decides how tall a card is — on the duel it made the two options
   268px and 242px, which on a screen asking which of two equals you prefer is
   the layout answering the question. See shared/_item_meta's `placeholder`. */
.meta-hold { display: block; }
.field-row__body .meta-hold { min-height: 11px; margin-top: 5px; }
.contender-card .meta-hold { min-height: 12px; }

.mono-meta {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--on-oat-mono);
  display: block;
}

/* Said once when every contender is a chain, in place of a chip on each. */
.brands-note {
  font: 500 13px/1.4 var(--font-body);
  color: var(--on-green-mono);
  margin: 10px 0 0;
}
.panel .brands-note { color: var(--on-oat-body); }
.panel--inset .brands-note { color: var(--on-green-mono); }

/* "+3" beside a suburb: this name covers more than one shop. */
.meta-more {
  display: inline-block;
  margin-left: 6px;
  padding: 2px 5px;
  border-radius: 5px;
  background: rgba(23, 19, 15, .1);
  font-size: .92em;
  letter-spacing: .02em;
}

/* The rotated stamp: "VOTING OPEN". */
.stamp {
  display: inline-block;
  font: 700 11px/1 var(--font-mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  background: var(--amber);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  transform: rotate(-2deg);
  box-shadow: 3px 3px 0 var(--ink);
}
.stamp--closed { background: var(--condensed); }

/* ---------------------------------------------------------------- */
/* Actions                                                           */
/* ---------------------------------------------------------------- */

/* The one caramel object on a screen. */
.chunky-btn {
  display: block;
  width: 100%;
  background: var(--caramel);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 18px;
  text-align: center;
  cursor: pointer;
  color: var(--oat);
  font: inherit;
  transition: transform .12s ease, box-shadow .12s ease;
}
.chunky-btn:hover { transform: translate(1px, 1px); box-shadow: 5px 5px 0 var(--ink); }
.chunky-btn:active { transform: translate(3px, 3px); box-shadow: 3px 3px 0 var(--ink); }
.chunky-btn__label {
  display: block;
  font: 800 23px/1 var(--font-display);
  letter-spacing: -.02em;
  color: var(--oat);
}
/* A glyph and its label are one line, centred together, so the button still
   reads as a single object rather than as an icon with text next to it. */
.chunky-btn__line { display: flex; align-items: center; justify-content: center; gap: 11px; }
.chunky-btn__icon { flex: none; width: 21px; height: 25px; }
.chunky-btn__sub {
  display: block;
  font: 500 13px/1.25 var(--font-body);
  color: var(--on-caramel-body);
  margin-top: 8px;
}

/* Every action stack in the app is one caramel button with quieter links under
   it, and the button's 6px shadow drops straight into the gap the ghost-link's
   padding is meant to provide — so the space under the loudest object on the
   screen reads as half of what it is, and tighter than the space under anything
   else. Given back here, once, rather than tuned into each stack's padding.
   `form.button_to` is `display: contents`, so the button is the stack's child
   whether it was rendered as a link or as a form. */
:where(.podium__actions, .standings__actions, .closed__actions,
       .nominate__actions, .signin__actions, .picker__list) .chunky-btn {
  margin-bottom: var(--drop-lg);
}

.ghost-link {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  width: 100%;
  background: none;
  border: none;
  padding: 12px 0;
  margin: 0;
  text-align: center;
  font: 600 15px/1 var(--font-body);
  color: var(--on-green-body);
  text-decoration: underline;
  text-underline-offset: 4px;
  cursor: pointer;
}
.ghost-link:hover { color: var(--oat); }
/* Every other pressable object in the app answers a press by moving into its
   own shadow, which is what pays for the tap highlight being off globally (see
   the `html` rule above). A ghost has no shadow to move into, so it answers
   with its colour instead — without this the escapes and every quiet link were
   the one class of control that gave nothing back on touch. */
.ghost-link:active { color: var(--oat); opacity: .6; }

.ghost-option {
  flex: 1;
  min-height: 44px;
  background: none;
  border: 2px solid var(--on-green-ghost);
  border-radius: 12px;
  padding: 13px 8px;
  font: 600 14px/1 var(--font-body);
  color: var(--on-green-body);
  cursor: pointer;
}
.ghost-option:hover { border-color: var(--amber); color: var(--oat); }
.ghost-option:active { border-color: var(--amber); color: var(--oat); opacity: .6; }

.escape-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 16px;
}
/* Generic button, used by the Devise screens. */
.btn {
  display: inline-block;
  background: var(--caramel);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: 13px 18px;
  font: 800 16px/1 var(--font-display);
  letter-spacing: -.01em;
  color: var(--oat);
  cursor: pointer;
}
.btn:hover { transform: translate(1px, 1px); box-shadow: 3px 3px 0 var(--ink); }
.btn--secondary { background: var(--oat); color: var(--ink); }
/* Google's button on 08b is ink, not caramel — the loud colour is saved for
   wherever the real commitment on a screen is. */
.btn--ink { background: var(--ink); color: var(--oat); border-radius: 18px; padding: 16px; font-size: 19px; }
.btn--block { display: block; width: 100%; text-align: center; }
/* Mark and label centre together as one object, the same construction as
   .chunky-btn__line — a provider button whose logo drifts to the far edge reads
   as two things in a box rather than one control. */
.btn__line { display: flex; align-items: center; justify-content: center; gap: 11px; }
/* Sized to the cap height of the 19px label beside it, so the G sits on the
   same optical line rather than looming over it. */
.btn__glyph { flex: none; width: 19px; height: 19px; }

/* ---------------------------------------------------------------- */
/* Panels                                                            */
/* ---------------------------------------------------------------- */

.panel {
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: 17px;
}
.panel--inset {
  background: var(--green-deep);
  color: var(--oat);
  border-radius: 20px;
  box-shadow: none;
  padding: 15px 16px;
}
.panel--condensed { background: var(--condensed); }
.panel__label {
  font: 700 11px/1 var(--font-mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--green);
  margin: 0;
}
.panel--inset .panel__label { color: var(--amber); }
.panel__title {
  font: 800 21px/1.05 var(--font-display);
  letter-spacing: -.02em;
  margin: 9px 0 0;
}
.panel__note {
  font: 500 13px/1.35 var(--font-body);
  color: var(--on-oat-body);
  margin: 7px 0 0;
}
.panel--inset .panel__note { color: var(--on-green-mono); }

.flash {
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-md);
  background: var(--condensed);
  color: var(--ink);
  padding: 12px 15px;
  margin: 12px 0 0;
  font: 500 14px/1.35 var(--font-body);
}
/* A notice is the app agreeing with you — it says a thing you asked for
   happened — so it takes the mint that already means "done" on a map pin, rather
   than --condensed, which is the app's "selected" and was saying nothing here.
   Alert keeps caramel: that one is the app disagreeing, and it is the only
   flash that ever needs to stop somebody. */
.flash--notice { background: var(--on-green-mono); }
.flash--alert { background: var(--caramel); color: var(--oat); }

.empty-state {
  font: 500 15px/1.5 var(--font-body);
  color: var(--on-green-body);
  padding: 20px 0;
}
/* An empty state that names a way out has to be one. Underlined rather than
   coloured: caramel is the screen's one action and "Add a place" already has
   it. */
.empty-state__link {
  color: var(--oat);
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* ---------------------------------------------------------------- */
/* 01 · Cold open                                                    */
/* ---------------------------------------------------------------- */

.cold-open {
  display: flex;
  flex-direction: column;
  gap: 14px;
  flex: 1;
  padding-top: 16px;
}
.cold-open__title { color: var(--oat); margin-top: 16px; }
.cold-open__cta { margin-top: auto; padding-top: 24px; }

/* Your #1 and the city's race, in that order. On a phone this is the same
   stack the shell would have made anyway; on desktop it is what lets them sit
   against each other instead of on two rows a tall hero has stretched. */
.cold-open__side {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.cold-open__side:empty { display: none; }

/* "Your #1", above the city's race — the same oat panel, because it is the
   same kind of object. Its name gets the race's type at full width rather
   than the two-up treatment, since there is only one of it. No amber: the
   winner on this screen is whoever the city has, and a second amber object
   would be two things claiming the word. */
.cold-open__yours-name {
  margin: 11px 0 0;
  text-align: left;
  font-size: 24px;
}

/* The end of the road for one person: every pair between the spots they've
   had is answered, so there is no run left to offer them. Ink on oat, sitting
   where the button's sub would be, because it is doing that sentence's job. */
.cold-open__settled {
  font: 500 15px/1.4 var(--font-body);
  color: var(--on-green-body);
  margin: 0 0 14px;
}

/* The #1 race, and its chevron split bar. */
.race__names {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 12px;
  margin-top: 11px;
}
/* Bottom-aligned on purpose (see `.race__names`): both names sit against the
   bar they are arguing over. `text-wrap: balance` is what keeps a wrapped name
   from reading as two — "CHICHA San / Chen" put one word on a line of its own
   beside a rival that fitted on one, which looks like two different facts. */
.race__name {
  flex: 1 1 0;
  min-width: 0;
  font: 800 19px/1.05 var(--font-display);
  letter-spacing: -.01em;
  overflow-wrap: break-word;
  text-wrap: balance;
  hyphens: none;
}
.race__name:last-child { text-align: right; }
.race__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
}
.race__status {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--on-oat-mono);
}
.race__note {
  font: 500 13px/1.4 var(--font-body);
  color: var(--on-oat-body);
  margin: 10px 0 0;
}

.split-bar {
  margin-top: 10px;
  height: 26px;
  border: var(--stroke) solid var(--ink);
  border-radius: 13px;
  overflow: hidden;
  background: var(--amber);
}
.split-bar__fill {
  height: 100%;
  background: var(--caramel);
  clip-path: polygon(0 0, 100% 0, calc(100% - 13px) 50%, 100% 100%, 0 100%);
}

/* Not enough evidence for a confident split: hatched on both sides, so the bar
   says "unsettled" rather than asserting a 50/50 measurement. */
.split-bar--provisional {
  background: repeating-linear-gradient(135deg, var(--amber) 0 7px, var(--condensed) 7px 14px);
}
.split-bar--provisional .split-bar__fill {
  background: repeating-linear-gradient(135deg, var(--caramel) 0 7px, #C9805A 7px 14px);
}

/* ---------------------------------------------------------------- */
/* 08c · Did we miss one?  ·  12 · Nominations                       */
/* ---------------------------------------------------------------- */

.nominate,
.nominating {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding-top: 10px;
}
.nominate__title,
.nominating .cold-open__title { color: var(--oat); }
.nominate__form { padding-top: 20px; }

/* Inputs sit straight on the green here, with no card around them. */
.field--on-green label { color: var(--on-green-mono); }
.field--on-green input[type="text"] {
  background: var(--oat);
  box-shadow: var(--shadow-md);
  font-size: 17px;
}

.nominate__next { margin-top: 20px; }
.nominate__next-body { font-size: 15px; line-height: 1.45; color: var(--on-green-body); }
.nominate__ticker {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--on-green-mono);
  margin: 12px 0 0;
}

.nominate__actions { margin-top: auto; padding-top: 24px; }
.nominate__footnote,
.nominating__remaining {
  text-align: center;
  font: 500 12px/1.4 var(--font-body);
  color: var(--on-green-mono);
  margin: 13px 0 0;
}

.nominating__form { padding: 22px 0 4px; }
.nominating__board { padding-top: 18px; }
.nominating__board-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.nominating__board-label { color: var(--amber); }
.nominating__progress {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--on-green-mono);
}

.nomination-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.nomination-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--green-deep);
  border: var(--stroke) solid var(--ink);
  border-radius: 14px;
  padding: 11px 14px;
}
.nomination-row__body { flex: 1; min-width: 0; }
.nomination-row__name {
  font: 800 18px/1 var(--font-display);
  color: var(--oat);
  overflow-wrap: anywhere;
}
.nomination-row__meta { color: var(--on-green-mono); margin-top: 5px; }
.nomination-row__count {
  flex: none;
  font: 700 13px/1 var(--font-mono);
  color: var(--amber);
  font-variant-numeric: tabular-nums;
}

/* Adding your name to a nomination somebody else made. A quiet pill on green,
   the same weight as the save control on a ranked row — the screen's one
   caramel is spent on Nominate, and this is the smaller of the two things you
   can do here. 44px so a thumb can hit it. */
.back-pill {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 44px;
  padding: 0 13px;
  background: none;
  border: 2px solid var(--on-green-ghost);
  border-radius: 999px;
  font: 700 12px/1 var(--font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--on-green-body);
  cursor: pointer;
}
.back-pill:hover { border-color: var(--amber); color: var(--oat); }
.back-pill:active { border-color: var(--amber); color: var(--oat); opacity: .6; }
/* Backed is a state, not a control. The row is already --green-deep, so this
   fills with the page's own green to read as sunk into the row rather than
   sitting on it — the inverse of the outlined thing you can still press. */
.back-pill--on {
  border-color: var(--on-green-rule);
  background: var(--green);
  color: var(--on-green-mono);
  cursor: default;
}

/* ---------------------------------------------------------------- */
/* 02 · Calibration                                                  */
/* ---------------------------------------------------------------- */

.calibration {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding-top: 10px;
}
.calibration__title { color: var(--oat); }
.calibration__intro .lede { font-size: 14px; margin-top: 8px; }

.calibration__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 9px;
  padding: 18px 0 20px;
}

/* Names only — price and record belong on the comparison card, not here. */
.calib-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
  min-height: 72px;
  padding: 10px 11px;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: background .12s ease, transform .12s ease, box-shadow .12s ease;
}
.calib-tile:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--ink); }

/* Selection is CSS-only so the form still works without JavaScript. */
.calib-tile__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  pointer-events: none;
}
.calib-tile:has(.calib-tile__input:checked) { background: var(--condensed); }
.calib-tile:has(.calib-tile__input:focus-visible) {
  outline: var(--stroke) solid var(--amber);
  outline-offset: 2px;
}

.calib-tile__name {
  font: 800 15px/1.02 var(--font-display);
  letter-spacing: -.01em;
  padding-right: 28px;
  overflow-wrap: anywhere;
}
.calib-tile .mono-meta { font-size: 10px; letter-spacing: .04em; }
.calib-tile:has(.calib-tile__input:checked) .mono-meta { color: var(--on-condensed-mono); }

.calib-tile__tick {
  position: absolute;
  top: 9px;
  right: 9px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--amber);
  font: 700 11px/22px var(--font-body);
  text-align: center;
  opacity: 0;
  transform: scale(.6);
  transition: opacity .12s ease, transform .12s ease;
}
.calib-tile:has(.calib-tile__input:checked) .calib-tile__tick { opacity: 1; transform: scale(1); }

/* "Add a missing spot" lives in the grid, where you notice the gap. */
.calib-tile--suggest {
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  min-height: 0;
  background: none;
  color: var(--on-green-body);
  border: var(--stroke) dashed var(--on-green-ghost);
  box-shadow: none;
  padding: 12px 14px;
}
.calib-tile--suggest .calib-tile__name { color: var(--on-green-body); padding-right: 0; }
.calib-tile__note {
  display: block;
  font: 500 11px/1.3 var(--font-body);
  color: var(--on-green-mono);
  margin-top: 4px;
}
.calib-tile__plus {
  flex: none;
  width: 34px;
  height: 34px;
  border: 2px solid var(--on-green-ghost);
  border-radius: 9px;
  color: var(--on-green-body);
  font: 700 18px/30px var(--font-body);
  text-align: center;
}

/* The sheet reads back what you've told us and sizes the run to it. */
.calib-sheet {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin: auto calc(-20px - env(safe-area-inset-right)) calc(-40px - env(safe-area-inset-bottom))
          calc(-20px - env(safe-area-inset-left));
  padding:
    14px
    calc(20px + env(safe-area-inset-right))
    calc(22px + env(safe-area-inset-bottom))
    calc(20px + env(safe-area-inset-left));
  background: var(--oat);
  color: var(--ink);
  border-top: var(--stroke) solid var(--ink);
  display: flex;
  align-items: center;
  gap: 14px;
}
.calib-sheet[data-state="thin"] { background: var(--condensed); flex-direction: column; align-items: stretch; }
.calib-sheet__body { flex: 1; min-width: 0; }

.calib-sheet__headline {
  font: 800 25px/1.05 var(--font-display);
  letter-spacing: -.02em;
  margin: 0 0 8px;
}
.calib-sheet__count {
  font: 700 12px/1 var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  margin: 0;
}
.calib-sheet__bar {
  margin-top: 8px;
  height: 16px;
  border: var(--stroke) solid var(--ink);
  border-radius: 8px;
  overflow: hidden;
  background: var(--canvas);
}
.calib-sheet__bar-fill {
  height: 100%;
  background: var(--green);
  clip-path: polygon(0 0, 100% 0, calc(100% - 8px) 50%, 100% 100%, 0 100%);
  transition: width .18s cubic-bezier(.2, .9, .2, 1);
}
.calib-sheet__note {
  font: 500 12px/1.4 var(--font-body);
  color: var(--on-oat-body);
  margin: 7px 0 0;
}
.calib-sheet[data-state="thin"] .calib-sheet__note { font-size: 14px; }

.calib-sheet__play {
  flex: none;
  width: auto;
  padding: 15px 18px;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}
.calib-sheet__play .chunky-btn__label { font-size: 17px; }
.calib-sheet[data-state="thin"] .calib-sheet__play { width: 100%; margin-top: 15px; }
.calib-sheet__play[disabled] {
  background: var(--canvas);
  color: var(--on-oat-mono);
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}
.calib-sheet__play[disabled] .chunky-btn__label { color: var(--on-oat-mono); }

/* ---------------------------------------------------------------- */
/* 13 · Topics — the picker                                          */
/* ---------------------------------------------------------------- */

.picker { display: flex; flex-direction: column; flex: 1; padding-top: 16px; }
.picker__title { color: var(--oat); }
.picker__list {
  list-style: none;
  margin: 0;
  padding: 20px 0 0;
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.topic-row {
  display: block;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: 17px 18px;
  transition: transform .12s ease, box-shadow .12s ease;
}
.topic-row:hover { transform: translate(1px, 1px); box-shadow: var(--shadow-sm); }
.topic-row__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.topic-row__name {
  font: 800 27px/0.98 var(--font-display);
  letter-spacing: -.025em;
  overflow-wrap: anywhere;
}
.topic-row__status {
  flex: none;
  font: 700 10px/1 var(--font-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--on-oat-mono);
}
.topic-row__note {
  display: block;
  font: 500 14px/1.4 var(--font-body);
  color: var(--on-oat-body);
  margin-top: 9px;
}

/* Not yet voting: drops back to field green. */
.topic-row--nominating {
  background: var(--green-deep);
  color: var(--oat);
  box-shadow: none;
}
.topic-row--nominating .topic-row__name { font-size: 22px; }
.topic-row--nominating .topic-row__status { color: var(--amber); }
.topic-row--nominating .topic-row__note { color: var(--on-green-mono); }

/* Finished: an outline, not an object. */
.topic-row--closed {
  background: none;
  color: var(--on-green-body);
  border-style: dashed;
  border-color: var(--on-green-ghost);
  box-shadow: none;
}
.topic-row--closed .topic-row__name { font-size: 20px; color: var(--on-green-body); }
.topic-row--closed .topic-row__status,
.topic-row--closed .topic-row__note { color: var(--on-green-mono); }

/* ---------------------------------------------------------------- */
/* 09 · Round closed — the result                                    */
/* ---------------------------------------------------------------- */

.closed { display: flex; flex-direction: column; flex: 1; padding-top: 16px; }
.closed__title { color: var(--oat); margin-top: 16px; }
/* Amber, because it labels the winner and nothing else on this screen is. It
   takes the top margin the winner card used to own so the two read as one
   object — the caption belongs to the card under it, not to the lede above. */
.closed__crown { color: var(--amber); margin-top: 26px; }
.closed__winner { margin-top: 8px; }
.closed__actions { margin-top: auto; padding-top: 28px; }

/* The next argument, offered rather than pushed. */
.closed__next {
  display: block;
  margin-top: 14px;
  padding: 14px 16px;
  border: var(--stroke) dashed var(--on-green-ghost);
  border-radius: 18px;
}
.closed__next-label {
  display: block;
  font: 700 10px/1 var(--font-mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--amber);
}
.closed__next-name {
  display: block;
  margin-top: 6px;
  font: 800 20px/1.05 var(--font-display);
  letter-spacing: -.02em;
  color: var(--oat);
}

/* ---------------------------------------------------------------- */
/* 03 · The duel                                                     */
/* ---------------------------------------------------------------- */

.duel {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding-top: 8px;
}

/* The exit sits above the pips, where calibration's back link sits, so the two
   screens in a run have their way out in the same place. */
.duel__exit { padding-bottom: 12px; }
.duel__exit-body { min-width: 0; }
.duel__exit-note {
  display: block;
  margin-top: 4px;
  letter-spacing: .04em;
  text-transform: none;
  color: var(--on-green-ghost);
}

.run-progress {
  display: flex;
  align-items: center;
  gap: 7px;
}
.run-progress__step {
  flex: 1;
  height: 8px;
  border-radius: 4px;
  border: 2px solid var(--ink);
  background: var(--green-deep);
  overflow: hidden;
}
/* The fill is its own element so the round you just finished can snap full
   under the burst (6a, 0.50s) instead of simply being amber on arrival. */
.run-progress__fill {
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: left center;
}
.run-progress__step--done .run-progress__fill { background: var(--amber); }
.run-progress__step--current .run-progress__fill { background: var(--caramel); }
.run-progress__step--snap .run-progress__fill {
  animation: ymPipFill .4s .5s cubic-bezier(.2, .9, .2, 1) both;
}
.run-progress__count {
  flex: none;
  margin-left: 8px;
  font: 700 11px/1 var(--font-mono);
  letter-spacing: .08em;
  color: var(--on-green-mono);
}

/* The round and the payoff share this box: the burst lays over the pair that
   comes next rather than replacing it. */
.duel__stage {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
}
.duel__round {
  flex: 1;
  display: flex;
  flex-direction: column;
}
/* The next pair slides in from the right as the amber clears.

   Nothing hidden is pressable — nothing at all, contender cards included. The
   design's "skippable by momentum" used to be literal: the payoff passed taps
   through and the cards opted back in, so a tap aimed at the amber recorded a
   vote on a pair that was still off-screen. A blind vote is worse than a
   swallowed tap, and it's indistinguishable from one at the time. The payoff
   takes the tap itself now and skips, which reaches the same pair a frame
   later with the answer still yours to give.

   ymUnblock holds the round inert for the beat and hands it back the instant
   the payoff clears, so there's no state to clean up. */
.duel__round--next {
  animation:
    ymSlideNext .32s var(--burst-beat) cubic-bezier(.2, .9, .2, 1) both,
    ymUnblock var(--burst-beat) step-end both;
}

/* Skipping the payoff (pick_burst_controller) releases the round on the spot:
   `animation: none` drops both the slide and the gate, so the pair is where it
   belongs and pressable again in the same frame the amber disappears. */
body.payoff-skipped .duel__round--next { animation: none; }

.duel__ask {
  text-align: center;
  padding: 26px 0 0;
}
.duel__question {
  font: 800 clamp(30px, 9vw, 36px)/0.96 var(--font-display);
  letter-spacing: -.03em;
  color: var(--oat);
  margin: 0;
  text-wrap: balance;
}
/* Grid rather than flex, and `1fr auto 1fr` rather than two flex children:
   `flex: 1` still yields to a card's min-content height, so the option that
   happened to know its suburb came out 26px taller than the one that didn't.
   On a screen asking which of two equals you prefer, the taller card is the
   louder card — the layout was answering the question. Two `1fr` rows are the
   same height whatever is in them. */
.matchup {
  flex: 1;
  display: grid;
  grid-template-rows: 1fr auto 1fr;
  gap: 14px;
  padding-top: 22px;
}
.matchup__or {
  flex: none;
  text-align: center;
  font: 800 20px/1 var(--font-display);
  letter-spacing: -.02em;
  color: var(--amber);
}

/* No caramel here on purpose — the cards are the buttons. */
.contender-card {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  margin: 0;
  padding: 24px;
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: var(--oat);
  border: var(--stroke) solid var(--ink);
  border-radius: 26px;
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease;
}
.contender-card:hover { transform: translate(1px, 1px); box-shadow: var(--shadow-md); }
.contender-card:active { transform: translate(3px, 3px); box-shadow: 3px 3px 0 var(--ink); }

.contender-card__name {
  display: block;
  font: 800 clamp(30px, 9vw, 40px)/0.94 var(--font-display);
  letter-spacing: -.035em;
  text-wrap: balance;
}
.contender-card__meta {
  display: block;
  font: 500 12px/1 var(--font-mono);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--on-oat-mono);
}

/* ---------------------------------------------------------------- */
/* 6a · The pick payoff                                              */
/* ---------------------------------------------------------------- */

/* Arcade between rounds, and the only motion in the MVP. Game-like, not
   gamified: no points, no coins, no XP — the reward is the physics. Nothing
   earned means nothing to inflate later.

   It takes every tap, and every tap means the same thing: skip. A fast voter
   still outruns the beat, but they land on the next pair and answer it in the
   open rather than voting blind through the amber. */

/* The two numbers that set the pace, and the only two worth touching.

   `--burst-beat` is how long the payoff is on screen before it clears and the
   next pair slides in. The design specifies 1.5s; we hold it longer. The entry
   choreography is deliberately *not* scaled with it — the punch, the stamp and
   the shake are tuned physics that land inside the first second, and stretching
   them makes the whole thing feel underwater rather than slower.

   `--burst-drift` stretches the particle flights only, so the field is still
   moving as the beat ends instead of leaving a frozen frame sitting there.
   Raise the beat and this wants to go with it. */
:root {
  --burst-beat: 3.5s;
  --burst-drift: 1.8;
}

.pick-burst {
  position: absolute;
  /* Out to the screen edges: the flood is the loudest possible statement of
     "amber means the winner", and a 20px margin of green would undercut it.

     Sideways there are two things in the way, not one: the shell's gutter, and
     — on any window wider than the shell — the auto margin that centres it.
     `50% - 50vw` is both at once, because 50% is half the stage and 50vw is
     half the window, so it measures the distance rather than restating the
     shell's numbers. On a phone that lands on the same -20px it always was;
     past 430px it keeps reaching, which a hand-kept constant did not — desktop
     flooded the 1040px column and left a band of green down each side. */
  inset: 0
         calc(50% - 50vw)
         calc(-40px - env(safe-area-inset-bottom))
         calc(50% - 50vw);
  z-index: 5;
  overflow: hidden;
  /* The overlay is the hit target for its whole beat, and the only thing under
     it that could take a tap is the pair it's covering. ymRevealOut ends on
     `visibility: hidden`, which stops hit-testing as well as drawing, so the
     round is reachable again the moment the amber is gone — no gate to undo. */
  pointer-events: auto;
  color: var(--ink);
  /* 0.34s — the screen nudges 2px, felt more than seen, and the only moment the
     frame itself moves. Then the whole thing clears at --burst-beat. */
  animation:
    ymShake .3s .34s cubic-bezier(.36, .07, .19, .97) both,
    ymRevealOut .3s var(--burst-beat) ease-in both;
}

/* The last pick of a run plays over the podium, which has no stage to sit in.
   Same rule, higher stakes: a tap through this one would fire Share from behind
   the amber. */
.pick-burst--full {
  position: fixed;
  inset: 0;
  z-index: 30;
}

/* 0.05s — amber floods out of the tap as a circle. */
.pick-burst__flood {
  position: absolute;
  inset: 0;
  background: var(--amber);
  animation: ymFlood .5s cubic-bezier(.2, .9, .2, 1) both;
}
.pick-burst__ring {
  position: absolute;
  left: 50%;
  top: 46%;
  width: 200px;
  height: 200px;
  margin: -100px 0 0 -100px;
  border: 14px solid var(--ink);
  border-radius: 50%;
  animation: ymRing .65s .1s cubic-bezier(.2, .9, .2, 1) both;
}

/* Centred, but never under the stamp or over the next-round card. The stamp is
   pinned at 22% and the padding here is a percentage too, so the two track each
   other instead of colliding on a short or wide viewport. */
.pick-burst__stack {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: 30% 32px 100px;
}

/* The shop you passed over gets a name and an apology rather than being
   deleted — you chose between two real places, not two buttons. */
.pick-burst__loser {
  margin: 0;
  font: 700 12px/1.3 var(--font-mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--on-amber-mono);
  text-align: center;
  animation: ymFadeUp .3s .18s ease-out both;
}

/* 0.12s — the winner punches to centre, overshoots to 109%, and settles at a
   1.5° tilt so it looks placed rather than aligned. */
.pick-burst__card {
  width: 100%;
  background: var(--oat);
  border: var(--stroke) solid var(--ink);
  border-radius: 26px;
  box-shadow: 8px 8px 0 var(--ink);
  padding: 26px 24px;
  text-align: center;
  animation: ymPunch .55s .12s cubic-bezier(.2, .9, .2, 1) both;
}
.pick-burst__name {
  margin: 0;
  font: 800 clamp(32px, 10vw, 44px)/0.92 var(--font-display);
  letter-spacing: -.035em;
  overflow-wrap: anywhere;
}
.pick-burst__meta { display: block; margin-top: 12px; }

/* 0.50s — the chip flies in from the right. It counts agreements with the city,
   which is information you'd want anyway; when you disagree it says so, and
   that's the more interesting outcome, so it gets the same fanfare. */
.pick-burst__streak {
  margin: 0;
  max-width: 100%;
  font: 700 11px/1.5 var(--font-mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  text-align: center;
  text-wrap: balance;
  background: var(--ink);
  color: var(--amber);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  animation: ymChipIn .45s .5s cubic-bezier(.2, .9, .2, 1) both;
}

/* 0.30s — eases down from 122% and settles at 99%. A placed label, not a slam. */
.pick-burst__stamp {
  position: absolute;
  left: 50%;
  top: 22%;
  margin: 0;
  font: 800 30px/1 var(--font-display);
  letter-spacing: -.02em;
  text-transform: uppercase;
  color: var(--oat);
  background: var(--caramel);
  border: var(--stroke) solid var(--ink);
  border-radius: 12px;
  padding: 11px 18px;
  box-shadow: var(--shadow-sm);
  white-space: nowrap;
  animation: ymStampIn .46s .3s cubic-bezier(.25, .8, .3, 1) both;
}

/* 0.16s — twelve particles launch from behind the card on the same twelve arcs
   every time. Only the shapes change by topic (see ParticleKit), which is what
   makes a new food topic four shapes and no new motion work. */
.pick-burst__particles {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 34%;
  height: 0;
}
/* The launch order is the design's, unchanged. Only the flight time is scaled,
   by --burst-drift, so the field is still in the air when the beat ends. */
.pick-burst__particle {
  position: absolute;
  animation-timing-function: cubic-bezier(.3, .5, .5, 1);
  animation-fill-mode: both;
}
.pick-burst__particle:nth-child(1)  { left: 20%; rotate: 0deg;   animation-name: ymPearlL;  animation-duration: calc(1.25s * var(--burst-drift)); animation-delay: .16s; }
.pick-burst__particle:nth-child(2)  { left: 34%; rotate: 7deg;   animation-name: ymPearlL;  animation-duration: calc(1.35s * var(--burst-drift)); animation-delay: .24s; }
.pick-burst__particle:nth-child(3)  { left: 44%; rotate: 14deg;  animation-name: ymPearlUp; animation-duration: calc(1.4s * var(--burst-drift));  animation-delay: .1s; }
.pick-burst__particle:nth-child(4)  { left: 52%; rotate: 21deg;  animation-name: ymPearlUp; animation-duration: calc(1.2s * var(--burst-drift));  animation-delay: .3s; }
.pick-burst__particle:nth-child(5)  { left: 60%; rotate: -4deg;  animation-name: ymPearlR;  animation-duration: calc(1.3s * var(--burst-drift));  animation-delay: .2s; }
.pick-burst__particle:nth-child(6)  { left: 70%; rotate: -11deg; animation-name: ymPearlR;  animation-duration: calc(1.45s * var(--burst-drift)); animation-delay: .34s; }
.pick-burst__particle:nth-child(7)  { left: 28%; rotate: 18deg;  animation-name: ymPearlL;  animation-duration: calc(1.5s * var(--burst-drift));  animation-delay: .4s; }
.pick-burst__particle:nth-child(8)  { left: 64%; rotate: -1deg;  animation-name: ymPearlR;  animation-duration: calc(1.55s * var(--burst-drift)); animation-delay: .46s; }
.pick-burst__particle:nth-child(9)  { left: 48%; rotate: 8deg;   animation-name: ymPearlUp; animation-duration: calc(1.15s * var(--burst-drift)); animation-delay: .5s; }
.pick-burst__particle:nth-child(10) { left: 38%; rotate: 15deg;  animation-name: ymPearlL;  animation-duration: calc(1.28s * var(--burst-drift)); animation-delay: .56s; }
.pick-burst__particle:nth-child(11) { left: 56%; rotate: -22deg; animation-name: ymPearlR;  animation-duration: calc(1.38s * var(--burst-drift)); animation-delay: .62s; }
.pick-burst__particle:nth-child(12) { left: 74%; rotate: 5deg;   animation-name: ymPearlUp; animation-duration: calc(1.42s * var(--burst-drift)); animation-delay: .68s; }

/* No tap needed — the run keeps its own pace — but one is allowed, because this
   is drawn like every other pressable card in the app and looking pressable
   while doing nothing is its own kind of broken. It's the *named* way out; the
   rest of the payoff does the same thing silently. */
.pick-burst__next {
  position: absolute;
  left: 24px;
  right: 24px;
  /* The payoff bleeds to the physical bottom of the screen in both variants —
     the in-run one cancels the shell's padding, the podium's is fixed and never
     had it — so 28px alone puts this under the home indicator. The shell clears
     it with the same inset; nothing inside here inherits that. */
  bottom: calc(28px + env(safe-area-inset-bottom));
  margin: 0;
  display: block;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: 18px;
  box-shadow: var(--shadow-md);
  padding: 14px 16px;
  text-align: center;
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease;
  /* `backwards`, not `both`: this is the one animated element that is also
     pressable, and a forwards fill would pin `transform` for good and swallow
     the press. The fade's end state is the resting state anyway, so dropping
     the forwards half costs nothing and hands `transform` back to :active. */
  animation: ymFadeUp .34s .72s ease-out backwards;
}
.pick-burst__next:hover { transform: translate(1px, 1px); box-shadow: var(--shadow-sm); }
.pick-burst__next:active { transform: translate(3px, 3px); box-shadow: 2px 2px 0 var(--ink); }
.pick-burst__next-label {
  display: block;
  font: 800 19px/1.2 var(--font-display);
  letter-spacing: -.02em;
}

/* The countdown. It runs on the same clock as the payoff itself — no delay, so
   it reads true even though the card it sits in doesn't fade in until 0.72s —
   and it empties exactly as the next pair starts sliding in.

   Ink on rule, because caramel is spent on SETTLED and amber means the winner.
   Built like the run's own progress pips so it reads as the same family of
   object: this is time, not another score. */
.pick-burst__timer {
  display: block;
  height: 8px;
  margin-top: 11px;
  border: 2px solid var(--ink);
  border-radius: 4px;
  background: var(--on-oat-rule);
  overflow: hidden;
}
.pick-burst__timer-fill {
  display: block;
  height: 100%;
  background: var(--ink);
  animation: ymCountdown var(--burst-beat) linear both;
}

/* ---------------------------------------------------------------- */
/* 05 · Your podium                                                  */
/* ---------------------------------------------------------------- */

.podium { display: flex; flex-direction: column; flex: 1; padding-top: 10px; }
/* The way out has the top line to itself; the title has the next one. A row
   rather than an overlay, so a long topic name and a 44px chip can never end up
   on top of each other. */
.podium__header { display: grid; grid-template-columns: 1fr auto; align-items: center; }
.podium__skip { grid-column: 2; grid-row: 1; justify-self: end; }
.podium__title { grid-column: 1 / -1; color: var(--oat); margin-top: 6px; }

/* The podium's own build. It is the suspense, and it is the second of the two
   sequences the MVP keeps: third, then second, then your #1 last.

   The design's 3.2s version stacked three cards fading up in order, which is a
   list arriving quickly — you could read all three names off the screen before
   the last one had finished moving, so the count-up was a rendering order
   rather than a reveal. This one *sets the stage before it fills it*: three
   sealed, hatched slots rise into place with their numbers already on them, so
   you can see the shape of the result while you still can't read it, and then
   they unseal one at a time from the bottom.

     0.20  the eyebrow fades up
     0.35  "Your podium" wipes up
     0.70  three sealed slots rise, bottom-up (0.70 / 0.85 / 1.00)
     1.70  third unseals — the cover wipes off, left to right
     2.45  second unseals
     2.95  the #1 rattles: it's the only one still shut, and it holds that
           beat alone for six tenths of a second
     3.55  the #1 unseals — the cover blows off, amber flares behind the card,
           the card punches, the stack takes the kick, and the topic's twelve
           shapes fly
     4.30  everything below the top three
     4.55  the crowd panel and the actions
     4.90  rest (--podium-build)

   Everything is scoped under .podium on purpose. The same ranking partial is
   what a shared link renders (screen D1, `.doc`), and replaying somebody else's
   reveal at a stranger who followed a link is not a payoff, it's a delay.

   --podium-cue is when the build starts. Zero when you arrive at the podium
   cold — a revisit, or a completed run reopened — and the length of the pick
   payoff when you got here by finishing a run, so the sequence begins as the
   amber clears rather than playing invisibly underneath it.

   --podium-build is how long it runs from that cue. Both are declared on the
   body rather than on .podium because the sign-in sheet (08b) is a sibling of
   .podium, not a child, and it schedules itself four seconds after the build
   ends — against these two numbers rather than a literal, so lengthening the
   build moves the ask with it. Nothing outside .podium and .signin-sheet reads
   them, so putting them on the body costs nothing: a shared link renders the
   same ranking partial with no .podium around it and is unaffected. */
body { --podium-cue: 0s; --podium-build: 4.9s; }
body.has-payoff { --burst-beat: 2s; --podium-cue: var(--burst-beat); }
/* Skipping the payoff starts the build now. The delays are recomputed against
   the same start time, so it picks up where waiting would have had it rather
   than restarting — this rule must outrank the one above it, hence the order. */
body.payoff-skipped { --podium-cue: 0s; }
/* And skipping the build itself puts the cue further in the past than the build
   is long, so every delay has already elapsed and every animation opens on its
   final frame. Same trick as payoff-skipped, at a different number. It outranks
   both rules above it on specificity, so it holds whichever way you arrived. */
body:has(.podium__skip-toggle:checked) { --podium-cue: -6s; }

/* The five moments the rest of the section hangs off. Held here rather than
   repeated inline so the sequence can be re-timed in one place — and so the
   flare and the particles, which are pinned to the winner rather than part of
   any card, can read the same instant the #1 card does. */
.podium {
  --slot-cue: calc(var(--podium-cue) + .7s);
  --third-cue: calc(var(--podium-cue) + 1.7s);
  --second-cue: calc(var(--podium-cue) + 2.45s);
  --first-cue: calc(var(--podium-cue) + 3.55s);
  --rest-cue: calc(var(--podium-cue) + 4.3s);
}

.podium__title { animation: ymWipeUp .4s calc(var(--podium-cue) + .35s) cubic-bezier(.2, .9, .2, 1) both; }

/* The way out of the build. It renders in its final state — an escape that
   fades in is an escape you can't take yet — and it retires at the end of the
   build, opacity and hit-testing together, because a control that skips
   something already finished is a button that does nothing. The duration is
   the whole clock, cue included, so it waits out a payoff too. */
.podium__skip { animation: ymRetire calc(var(--podium-cue) + var(--podium-build)) linear both; }
body:has(.podium__skip-toggle:checked) .podium__skip { display: none; }

/* Visually hidden but focusable and checkable, the same as 08b's dismiss: the
   label is the control, and the keyboard reaches the checkbox directly. */
.podium__skip-toggle {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
}
.podium__skip-label {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 12px;
  font: 700 11px/1 var(--font-mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--on-green-mono);
  border: 2px solid var(--on-green-rule);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color .12s ease, border-color .12s ease;
}
.podium__skip-label:hover { color: var(--oat); border-color: var(--on-green-ghost); }
.podium__skip-toggle:focus-visible + .podium__skip-label {
  outline: var(--stroke) solid var(--amber);
  outline-offset: 2px;
}

/* By position, not by DOM order, so a two-item ranking still counts upward.
   Third and second only have to arrive — their unsealing is the cover's job,
   below. Your #1 is one animation from its first frame to its last (rise,
   hold, rattle, punch) rather than four: a second animation on the same
   element would fill its own opening frame backwards over the first one and
   flatten it, which is what used to quietly cancel this card's rise. */
.podium .place:nth-child(3) { animation: ymRise .5s var(--slot-cue) cubic-bezier(.2, .9, .2, 1) both; }
.podium .place:nth-child(2) { animation: ymRise .5s calc(var(--slot-cue) + .15s) cubic-bezier(.2, .9, .2, 1) both; }
.podium .place:nth-child(1) { animation: ymCrown 3.35s calc(var(--slot-cue) + .3s) cubic-bezier(.2, .9, .2, 1) both; }

/* The kick. The whole stack takes the winner's landing, which is the only
   thing on this screen that moves anything but itself — and it stops at the
   podium rather than the app frame, because the frame moving is the pick
   payoff's one trick and it doesn't get borrowed. */
.podium .podium__places { position: relative; z-index: 1; animation: ymKick .5s var(--first-cue) cubic-bezier(.2, .9, .2, 1) both; }

.podium .also-ranked { animation: ymFadeUp .35s var(--rest-cue) cubic-bezier(.2, .9, .2, 1) both; }
.podium .empty-state { animation: ymFadeUp .35s var(--slot-cue) cubic-bezier(.2, .9, .2, 1) both; }

/* The bottom block lands together at 4.55s and takes .35s, which is where
   --podium-build's 4.9s comes from. */
.podium .crowd-panel,
.podium .podium__actions { animation: ymFadeUp .35s calc(var(--rest-cue) + .25s) cubic-bezier(.2, .9, .2, 1) both; }

/* list-style and margin come off because the cards are the list; the inline
   padding has to go with them or the ol's default 40px indent pushes the
   podium off the column's left edge while its right edge stays put. */
.podium__places {
  display: flex;
  flex-direction: column;
  gap: 10px;
  list-style: none;
  margin: 0;
  padding: 20px 0 0;
}

.place {
  display: flex;
  align-items: baseline;
  gap: 12px;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: 20px;
  box-shadow: var(--shadow-sm);
  padding: 15px;
}
.place--first {
  background: var(--amber);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: 18px;
}

.place__rank {
  flex: none;
  font: 800 26px/1 var(--font-display);
  color: var(--on-oat-mono);
}
.place--first .place__rank { font-size: 34px; color: var(--on-amber-mono); }
.place__body { flex: 1; min-width: 0; }
.place__name {
  font: 800 25px/0.98 var(--font-display);
  letter-spacing: -.025em;
  overflow-wrap: anywhere;
}
.place--first .place__name { font-size: 32px; letter-spacing: -.03em; }
.place__meta { margin-top: 6px; }
.place--first .place__meta { color: var(--on-amber-mono); margin-top: 7px; }

/* The seal. Every slot lands shut: a hatched, empty plinth in the exact
   silhouette of the card underneath it, using the same hatch the app already
   spends on "not enough evidence to say" — which is precisely what a sealed
   place is. It's a pseudo-element, so the shared-link version of this partial
   never grows one and neither does screen 09's winner card.

   An absolute inset is measured from the padding box, so -3px puts the cover's
   border box exactly on the card's — same rectangle, same inherited radius,
   first place's larger one included, and its ink border lands on the card's
   own rather than a pixel inside it, where the amber underneath showed as a
   hairline. No second set of numbers to keep in step either. */
.podium .place { position: relative; }
.podium .place::before {
  content: "";
  position: absolute;
  inset: -3px;
  z-index: 1;
  border-radius: inherit;
  border: var(--stroke) solid var(--ink);
  background: repeating-linear-gradient(135deg, var(--green-deep) 0 9px, var(--on-green-rule) 9px 18px);
  animation: ymUnseal .5s var(--third-cue) cubic-bezier(.55, 0, .2, 1) both;
}
.podium .place:nth-child(2)::before { animation-delay: var(--second-cue); }
/* Your #1's cover doesn't wipe, it blows off. */
.podium .place:nth-child(1)::before {
  animation: ymUnsealPop .55s var(--first-cue) cubic-bezier(.3, .8, .3, 1) both;
}

/* The rank numeral rides *over* the seal, so a shut slot still says which one
   it is — that's what makes three hatched panels read as a podium rather than
   as a page that hasn't loaded. It's oat while it sits on the hatch and settles
   into its resting colour as the cover leaves; the keyframe names only the
   opening colour, so the closing one is whatever the card would have been. */
.podium .place__rank {
  position: relative;
  z-index: 2;
  animation: ymRankLit .25s var(--third-cue) ease both;
}
.podium .place:nth-child(2) .place__rank { animation-delay: var(--second-cue); }
.podium .place:nth-child(1) .place__rank { animation-delay: var(--first-cue); }

/* The stage: the flare and the shapes are pinned to the winner's card and take
   no layout, so the three places sit exactly where they always did. */
.podium__stage { position: relative; }

/* The hit. An amber ring leaves the winner's card and washes out past the
   edges of the screen — the payoff's ymRing, which is the same idea one screen
   earlier, and a flat expanding stroke rather than a soft glow because there
   isn't a soft gradient anywhere else in this app.

   It rides *over* the places rather than behind them: a flash behind an opaque
   card is a flash you can't see, and a wave crossing the two cards below is
   what makes the top one look like it landed on them. Amber is the winner and
   only the winner, and this comes off the one card allowed to wear it. */
.podium__flare {
  position: absolute;
  left: 50%;
  top: 64px;
  width: 190px;
  height: 190px;
  margin: -95px 0 0 -95px;
  z-index: 3;
  pointer-events: none;
  border: 10px solid var(--amber);
  border-radius: 50%;
  animation: ymFlare .7s var(--first-cue) cubic-bezier(.2, .9, .2, 1) both;
}

/* Twelve shapes out from behind the winning card, launched on twelve fixed
   arcs. Same contract as the payoff's: the arcs, delays and rotations live
   here and are identical on every topic; only the shapes change, and they come
   from the same ParticleKit. These fall — the payoff throws its field up a
   full screen, this one bursts from a card near the top of the page and rains
   down past the fold. */
.podium__particles {
  position: absolute;
  left: 0;
  right: 0;
  top: 64px;
  height: 0;
  z-index: 2;
  pointer-events: none;
}
.podium__particle {
  position: absolute;
  animation-timing-function: cubic-bezier(.3, .5, .5, 1);
  animation-fill-mode: both;
}
.podium__particle:nth-child(1)  { left: 16%; rotate: 0deg;   animation-name: ymTossL;  animation-duration: 1.5s;  animation-delay: calc(var(--first-cue) + .02s); }
.podium__particle:nth-child(2)  { left: 30%; rotate: 7deg;   animation-name: ymTossL;  animation-duration: 1.62s; animation-delay: calc(var(--first-cue) + .1s); }
.podium__particle:nth-child(3)  { left: 42%; rotate: 14deg;  animation-name: ymTossUp; animation-duration: 1.7s;  animation-delay: calc(var(--first-cue) + .04s); }
.podium__particle:nth-child(4)  { left: 52%; rotate: 21deg;  animation-name: ymTossUp; animation-duration: 1.44s; animation-delay: calc(var(--first-cue) + .16s); }
.podium__particle:nth-child(5)  { left: 62%; rotate: -4deg;  animation-name: ymTossR;  animation-duration: 1.56s; animation-delay: calc(var(--first-cue) + .08s); }
.podium__particle:nth-child(6)  { left: 74%; rotate: -11deg; animation-name: ymTossR;  animation-duration: 1.74s; animation-delay: calc(var(--first-cue) + .18s); }
.podium__particle:nth-child(7)  { left: 24%; rotate: 18deg;  animation-name: ymTossL;  animation-duration: 1.8s;  animation-delay: calc(var(--first-cue) + .24s); }
.podium__particle:nth-child(8)  { left: 68%; rotate: -1deg;  animation-name: ymTossR;  animation-duration: 1.86s; animation-delay: calc(var(--first-cue) + .28s); }
.podium__particle:nth-child(9)  { left: 47%; rotate: 8deg;   animation-name: ymTossUp; animation-duration: 1.38s; animation-delay: calc(var(--first-cue) + .3s); }
.podium__particle:nth-child(10) { left: 36%; rotate: 15deg;  animation-name: ymTossL;  animation-duration: 1.54s; animation-delay: calc(var(--first-cue) + .34s); }
.podium__particle:nth-child(11) { left: 58%; rotate: -22deg; animation-name: ymTossR;  animation-duration: 1.66s; animation-delay: calc(var(--first-cue) + .38s); }
.podium__particle:nth-child(12) { left: 80%; rotate: 5deg;   animation-name: ymTossUp; animation-duration: 1.7s;  animation-delay: calc(var(--first-cue) + .42s); }

/* Everything below the top three, for runs that ranked more than three. */
.also-ranked {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.also-ranked__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 4px;
  border-bottom: 1px solid var(--on-green-rule);
  font: 500 14px/1.3 var(--font-body);
  color: var(--on-green-body);
}
.also-ranked__row:last-child { border-bottom: none; }
.also-ranked__rank {
  flex: none;
  width: 26px;
  font: 800 15px/1 var(--font-display);
  color: var(--on-green-mono);
}
.also-ranked__name { flex: 1; min-width: 0; color: var(--oat); font-weight: 600; }

.crowd-panel {
  background: var(--green);
  border: var(--stroke) solid var(--ink);
  border-radius: 18px;
  padding: 16px;
  margin-top: 18px;
}
body:not(.is-deep) .crowd-panel { background: var(--green-deep); }
.crowd-panel__headline { font: 500 16px/1.45 var(--font-body); color: var(--oat); margin: 0; }
.crowd-panel__detail { font: 500 14px/1.4 var(--font-body); color: var(--on-green-mono); margin: 8px 0 0; }
.crowd-panel__detail b { color: var(--oat); font-weight: 700; }
/* The save on the city's #1. The pill carries a 2px shadow and the panel's
   padding follows it, so the gap above is the only one that needs stating. */
.crowd-panel__save { margin-top: 12px; }

/* No `margin-top: auto` any more: the actions aren't the last thing on the
   screen now that the field follows them, and pinning them to the bottom of a
   viewport-height column opened a hole between the crowd panel and Share on
   any screen taller than the result. */
.podium__actions { padding-top: 24px; }

/* --- The field, under the podium -------------------------------- */
/* The crowd's whole order with your own footprints on it. It is the reason
   there is no "See the standings" link and no screen 14 any more: the ranking
   and the list of places left to go were always the same list, and the only
   interesting fact — that their #2 is somewhere you've never been — only
   exists when the two are printed together.

   Rows you've had are drawn on --canvas with no shadow, so they sit back into
   the page; rows you haven't keep the full oat card and the offset shadow.
   That tonal step is what makes the opportunities the loud objects, which is a
   job for the layout rather than for a label — the "Not tried" tag only names
   what the card is already doing. No third colour: caramel is still spent once
   on Share, and amber is still the winner and only the winner. */
.field { margin-top: 30px; border-top: 2px solid var(--on-green-rule); padding-top: 22px; }
.field__title { font: 800 24px/1 var(--font-display); letter-spacing: -.02em; color: var(--oat); margin: 0; }
.field__meta {
  font: 500 11px/1.4 var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--on-green-mono);
  margin: 8px 0 0;
}
.field__meta b { color: var(--amber); font-weight: 700; }
/* Margins rather than `gap`, so the rhythm survives the switch to columns on
   desktop — `gap` governs nothing in block layout, and one spacing rule beats
   two that have to be kept in step. The shadowed rows carry the extra so that
   every visual gap in the list is `--field-gap`, whether the card above it
   throws a shadow into that gap or not. */
.field__list { list-style: none; margin: 0; padding: 16px 0 0; display: flex; flex-direction: column; --field-gap: 8px; }
.field-row { margin-bottom: var(--field-gap); }
.field-row:not(.field-row--had) { margin-bottom: calc(var(--field-gap) + var(--drop-sm)); }
.field__list > .field-row:last-child { margin-bottom: 0; }

.field-row {
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: 18px;
  box-shadow: var(--shadow-sm);
  padding: 11px 14px;
}
.field-row--had {
  background: var(--canvas);
  border-color: var(--on-oat-rule);
  box-shadow: none;
  color: var(--on-oat-body);
}
/* The crowd's #1, carried over from screen 08's leader row so the same list
   reads the same on both screens. It sits under `--had` on purpose: whether
   you've been is the more useful fact about a row you're looking at on the
   podium, and the rank numeral already says which one is first. */
.field-row--leader:not(.field-row--had) { background: var(--condensed); box-shadow: var(--shadow-md); }
.field-row--leader .field-row__rank { font-size: 24px; }
.field-row--leader .field-row__name { font-size: 18px; }
.field-row--leader:not(.field-row--had) .field-row__meta { color: var(--on-condensed-mono); }
.field-row__head { display: flex; align-items: center; gap: 11px; }
.field-row__rank { flex: none; width: 24px; font: 800 20px/1 var(--font-display); }
.field-row__body { flex: 1; min-width: 0; }
.field-row__name { font: 800 17px/1 var(--font-display); overflow-wrap: anywhere; }
.field-row--had .field-row__name { color: var(--on-oat-body); }
.field-row__meta { margin-top: 5px; }

.field-row__flag {
  flex: none;
  background: var(--green);
  color: var(--oat);
  border-radius: 6px;
  padding: 5px 7px;
  font: 700 9px/1 var(--font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
}
/* The last badge left in the row, and the only one still filled. It appears
   only for a reader with no history of their own, where nothing else on the row
   is claiming that corner — and amber on green is the winner saying so, which
   is the one thing amber is for.

   Tried / Not tried used to live here too, in solid green. Both are gone: the
   fill was the app's own background colour punched into an oat card, it was the
   heaviest object in the row, and it sat on the state that says the least.
   "Tried" rides the suburb line now (see .mono-meta__mark and
   docs/decisions/lists.md), and "Not tried" says nothing a full card wasn't
   already saying. */
.field-row__flag--leading { color: var(--amber); }

/* The whole answer to "never heard of it". We hold a name, a suburb and a
   record, so inventing a description would be inventing; a map is the honest
   hand-off, and the link says which one it is before it's tapped. */
/* The 44px is the tap target, not the spacing. Centring 13px of text in a 44px
   box leaves ~15px of dead air under the words, which lands on top of the card's
   own 11px of bottom padding and makes every row with a map link look bottom
   heavy against its own top edge. Keep the target and let it *be* the padding:
   the card gives up its bottom padding to the link, so the tap area still
   reaches the full 44px and stays inside the card, while the space under the
   words comes back to roughly what sits above the rank. */
.field-row__lookup {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font: 700 13px/1 var(--font-body);
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 4px;
}
.field-row:has(.field-row__lookup) { padding-bottom: 0; }
.field-row__lookup:hover { color: var(--caramel); }
/* The arrow is outside the link's own word, so it needs the space the underline
   would otherwise swallow. */
.field-row__lookup span { margin-left: 5px; text-decoration: none; }

/* It arrives with the rest of the bottom block rather than before it: the
   reveal is the screen until it has finished. */
.podium .field { animation: ymFadeUp .35s calc(var(--rest-cue) + .25s) cubic-bezier(.2, .9, .2, 1) both; }

/* ---------------------------------------------------------------- */
/* 08b · The sign-in sheet, over the podium                          */
/* ---------------------------------------------------------------- */

/* Not a destination — it rises over screen 05 and goes away again, and it is
   the only thing in the app that asks for anything. It is deliberately late:
   the result is read first, and then the ask arrives on top of a card worth
   keeping. Nothing is gated behind it, so dismissing costs nothing.

   Only a guest ever sees it, and only here, so it can never appear mid-run.

   The timing is not a number of its own: the design says the sheet rises about
   four seconds after the build settles, so that is what it says. Written
   against the build's own two variables rather than a literal, it follows
   everything that moves them — it waits behind the amber when you arrived by
   finishing a run, moves up when the payoff is skipped, moves back when the
   build is lengthened, and lands three seconds later when someone skips the
   build outright rather than arriving the instant they did. */
.signin-sheet { --signin-cue: calc(var(--podium-cue) + var(--podium-build) + 4s); }

/* Nothing hidden is pressable, the other way round from the payoff's gate: the
   scrim covers the whole screen from load, and the podium's own actions sit
   directly under where the sheet lands. A tap already travelling towards
   "Share your top 3" must not be caught by a sheet that arrived under it, so
   the whole thing stays inert until it has finished rising. pointer-events is
   inherited, so the one rule covers the card and its buttons. */
.signin-sheet {
  pointer-events: auto;
  animation: ymUnblock calc(var(--signin-cue) + .42s) step-end both;
}

/* "Keep your list" — the same sheet, arriving as the answer to a tap rather
   than on a clock, so all three animations come off.

   It is not a shortcut. The gate above exists because 08b lands on top of live
   controls *while they are still reachable*; a sheet rendered whole has no
   in-flight window for a tap to fall into, which makes "nothing hidden is
   pressable" true by construction rather than by a timer — the same argument
   the save strip makes, on the same strip of screen. It also keeps motion.md's
   two-sequences rule intact: a third arrival sequence would be a third
   sequence.

   The cue itself has to go too, not just the motion. `--signin-cue` is written
   against `--podium-cue` and `--podium-build`, which only exist on the podium's
   body — on the list screen the calc() is invalid, and an invalid delay is not
   "no delay". */
.signin-sheet--instant,
.signin-sheet--instant .signin-sheet__scrim,
.signin-sheet--instant .signin-sheet__card { animation: none; }

/* Visually hidden but still focusable and still checkable: this is the whole
   dismiss mechanism, and its labels are the scrim and "Not now". Keyboard
   reaches it directly, where it reads as its label's text.

   Fixed rather than absolute, and that is load-bearing. The sheet is the last
   thing in the document, so a statically-placed box inside it sits below the
   podium and the entire field list — and activating a label focuses its
   control, which the browser then scrolls into view. "Not now" is on a card
   welded to the bottom of the viewport, so dismissing it threw the page to the
   bottom of the field. Pinned to the viewport there is nothing to scroll to.
   The card and scrim are siblings, so the ~ rules below don't care. */
.signin-sheet__dismiss {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
}
.signin-sheet__dismiss:checked ~ .signin-sheet__scrim,
.signin-sheet__dismiss:checked ~ .signin-sheet__card { display: none; }
.signin-sheet__dismiss:focus-visible ~ .signin-sheet__card {
  outline: var(--stroke) solid var(--amber);
  outline-offset: -6px;
}

/* Dims the podium without hiding it — the ranking stays legible behind the
   ask, which is the point of arriving after it was read. */
.signin-sheet__scrim {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: rgb(8 20 16 / .55);
  cursor: pointer;
  animation: ymFadeIn .3s var(--signin-cue) ease both;
}

.signin-sheet__card {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 21;
  margin: 0 auto;
  max-width: 430px;
  background: var(--oat);
  color: var(--ink);
  border-top: var(--stroke) solid var(--ink);
  border-radius: 28px 28px 0 0;
  padding:
    20px
    calc(24px + env(safe-area-inset-right))
    calc(26px + env(safe-area-inset-bottom))
    calc(24px + env(safe-area-inset-left));
  /* A short screen or a long #1 name must not push "Not now" off the top with
     nothing to scroll — the way out has to stay reachable. */
  max-height: 88dvh;
  overflow-y: auto;
  animation: ymSheetRise .42s var(--signin-cue) cubic-bezier(.2, .9, .2, 1) both;
}

/* The grab handle says "this slides away", which is the honest promise here. */
.signin-sheet__grip {
  display: block;
  width: 46px;
  height: 5px;
  border-radius: 3px;
  background: var(--on-oat-rule);
  margin: 0 auto 16px;
}

.signin-sheet__title {
  font: 800 31px/0.98 var(--font-display);
  letter-spacing: -.03em;
  margin: 0;
}
.signin-sheet__lede {
  font: 500 15px/1.45 var(--font-body);
  color: var(--on-oat-body);
  margin: 10px 0 0;
}
/* Your #1, named in the middle of the ask. It steps up to full ink while the
   sentence around it stays body colour, because the name is the specific thing
   being kept and the rest is the explanation of what keeping means. No amber:
   the winner's colour is spent on the podium behind the scrim, and a second
   amber object on top of it would compete with the card it is pointing at. */
.signin-sheet__lede b { font-weight: 800; color: var(--ink); }

/* The benefits list is shared with the full 08b screen, which sits on green —
   on oat the body colour has to come back down to ink. Scoped through the card
   so it outranks .signin__benefits li regardless of which section of this file
   comes first. */
.signin-sheet__benefits { margin-top: 18px; }
.signin-sheet__card .signin__benefits li { color: var(--ink); }

.signin-sheet__card .btn--ink { margin-top: 20px; }
.signin-sheet__email { margin-top: 10px; padding: 15px; font: 700 16px/1 var(--font-body); }

/* Ghost, not caramel: the one loud object on this sheet is the way in. */
.signin-sheet__not-now {
  display: block;
  padding: 15px 0 0;
  text-align: center;
  font: 600 14px/1 var(--font-body);
  color: var(--on-oat-mono);
  text-decoration: underline;
  text-underline-offset: 4px;
  cursor: pointer;
}

/* ---------------------------------------------------------------- */
/* 08 · Standings                                                    */
/* ---------------------------------------------------------------- */

/* The list itself is shared/_ranked_list, styled with the podium's field in
   the 05 section — the crowd's order reads the same wherever it appears, so
   there is one component rather than two that drift. What is left here is the
   page around it. */
.standings { display: flex; flex-direction: column; flex: 1; padding-top: 8px; }
.standings__title { color: var(--oat); }
.standings__meta {
  font: 500 11px/1 var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--on-green-mono);
  margin: 8px 0 0;
}
/* The one number on the page, and it is topic-level: whether the ranking
   means anything yet, not how any single row did. */
.standings .field__list { padding-top: 16px; }

/* The sticky play button's containing block. It stretches, so on a topic too
   short to scroll the `margin-top: auto` below still drops the button to the
   foot of the screen rather than leaving it stranded mid-page above a gap. */
.standings__body { flex: 1; display: flex; flex-direction: column; }

/* The offer to argue with the order, welded to the bottom of the screen for as
   long as the order is on it. Sticky and not fixed, which is the whole
   difference: it is a shelf inside the page, so it travels no further than
   `.standings__body` and comes to rest in the flow above the ghost links
   instead of sitting on top of them at the foot of the read. Full-bleed by the
   same negative margins the calibration sheet uses, and green rather than oat,
   because an oat bar across the bottom of a list of oat cards reads as one more
   card that happens to be stuck.

   z-index 2 puts it over the rows it rides above and under everything that is
   entitled to the screen: the save strip (21), 08b's sheet (20/21) and the
   opened map (900/901). So a save covers it while the strip is up — nothing is
   ever half-covered and pressable. */
.standings__play {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin: auto calc(-20px - env(safe-area-inset-right)) 0
          calc(-20px - env(safe-area-inset-left));
  padding:
    12px
    calc(20px + env(safe-area-inset-right))
    calc(12px + env(safe-area-inset-bottom))
    calc(20px + env(safe-area-inset-left));
  background: var(--green);
  border-top: var(--stroke) solid var(--ink);
}

/* Shorter than the stack version, because this one is on the screen the whole
   way down the list rather than waiting at the end of it — the full 18px
   padding and 23px label took 116px of a phone, which is a fifth of the field
   it is meant to be an offer about. The shadow comes off with it: an offset
   shadow sits in the gap under its object, and there is no gap here. */
.chunky-btn--slim {
  padding: 13px;
  box-shadow: var(--shadow-sm);
}
.chunky-btn--slim:hover { box-shadow: 2px 2px 0 var(--ink); }
.chunky-btn--slim:active { box-shadow: 1px 1px 0 var(--ink); }
.chunky-btn--slim .chunky-btn__label { font-size: 19px; }
.chunky-btn--slim .chunky-btn__sub { margin-top: 5px; }

.standings__actions { margin-top: auto; padding-top: 20px; }

/* ---------------------------------------------------------------- */
/* The results map — a second view of the list above it              */
/* ---------------------------------------------------------------- */

/* Every rule here is downstream of one already written for the field rows:
   `--had` is quiet, the leader is amber and only while you haven't been, and
   everything else is an oat card with a shadow. The map states nothing the
   list doesn't; it only says where. See docs/decisions/results-map.md. */

.results-map { display: block; margin-top: 18px; }
/* `hidden` has to win — the section is only revealed once there is a map in it. */
.results-map[hidden] { display: none; }

/* The strip. Read-only at this size: it is a picture of the list, and the one
   thing you can do to it is make it bigger. */
.results-map__frame {
  position: relative;
  height: 112px;
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  background: var(--canvas);
}
.results-map__canvas { position: absolute; inset: 0; }

/* Held back rather than drained. Greyscale made the whole strip read as a
   disabled control; the tiles are still a backdrop, so they lose a third of
   their saturation and a little contrast and keep the rest — parks green,
   water blue, roads pale — under the oat wash below. */
.results-map__canvas .leaflet-tile-pane { filter: saturate(.68) contrast(.94); }
/* `.results-map__canvas` *is* the Leaflet container — L.map() puts the class on
   the element it is given — so this was written as a descendant selector and
   never matched a thing. The backdrop it was meant to set stayed Leaflet's own
   #ddd, which is what you see under a frame whose tiles haven't arrived yet:
   opening the map painted a band of flat grey where the oat should be. */
.results-map__canvas.leaflet-container { background: var(--canvas); font-family: var(--font-body); }
.results-map__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 450;
  pointer-events: none;
  background: var(--oat);
  opacity: .14;
  mix-blend-mode: multiply;
}

.results-map .leaflet-control-attribution {
  font: 400 8px/1.3 var(--font-mono);
  background: rgba(247, 240, 225, .84);
  color: var(--on-oat-mono);
  padding: 2px 5px;
}
.results-map .leaflet-control-attribution a { color: var(--on-oat-mono); }

/* Not caramel. The page spends its one action colour on "Disagree? Play." or on
   Share, and this only enlarges what is already on the screen. */
/* 44px, like every other target. These two are the whole way in and out of a
   full-screen map, and they were the last controls in the app still under it. */
.results-map__toggle,
.results-map__back {
  min-height: 44px;
  font: 700 10px/1 var(--font-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: 999px;
  padding: 10px 13px;
  box-shadow: 3px 3px 0 var(--ink);
  cursor: pointer;
}
.results-map__toggle { position: absolute; z-index: 600; right: 10px; bottom: 10px; }
.results-map__toggle:active,
.results-map__back:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }

/* The bar over an open map. Full screen means the page's own heading has gone,
   so the map names the argument it is drawing — otherwise a shared link lands
   somebody on twenty pins with nothing saying what they are pins of.

   Solid rather than a fade: a gradient put the title over moving tiles, so the
   contrast under it changed as the map was dragged and the type flickered
   between readable and not. A band the map cannot reach into is the whole
   point of a heading. */
.results-map__bar {
  position: absolute;
  z-index: 600;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px 12px;
  padding: 12px 14px;
  background: var(--green-deep);
  border-bottom: var(--stroke) solid var(--ink);
}
.results-map__bar[hidden] { display: none; }
.results-map__title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 800 17px/1 var(--font-display);
  letter-spacing: -.02em;
  color: var(--oat);
}

/* The key to the pins, on the bar's second line. The pins say one thing with
   colour and until this was here they never named it — the reader had to infer
   two greens from the cards, one tap at a time.

   On the bar rather than floating over the tiles, for the reason the heading is
   a solid band at all: the map cannot reach into it. A key over the canvas
   would sit on somebody's shop, drift across moving tiles as the map is
   dragged, and need a place in `layoutLabels` beside the labels it would be
   competing with. Here it needs none of that, and the bar's box already keeps
   labels off it.

   Oat on green, in the labels' type: it belongs to the same family of small
   captions the map draws, not to the heading's display face.

   Two elements, because the bar is a flex row and one element cannot be both
   halves of what is wanted: the outer is the line break, and the pill inside it
   is what has to stay the width of its own words. A pill given the break's
   `flex-basis: 100%` stretches the width of the bar and reads as an empty
   input. */
.map-legend { flex-basis: 100%; }
.map-legend__keys {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin: 0;
  padding: 6px 9px;
  list-style: none;
  width: fit-content;
  max-width: 100%;
  background: var(--oat);
  color: var(--ink);
  border: 2px solid var(--ink);
  border-radius: 999px;
  box-shadow: 2px 2px 0 var(--ink);
  font: 700 10px/1 var(--font-mono);
  letter-spacing: .04em;
  text-transform: uppercase;
}
.map-legend__key { display: flex; align-items: center; gap: 6px; }
/* The pin, at 12px, and exactly the pin: deep for an untried spot, mint for a
   tried one, flat both times. Colour is the whole of what a pin says now, so
   colour is the whole of what the key copies — a swatch wearing a shadow the
   pin beside it doesn't would be describing a map that isn't there. */
.map-legend__swatch {
  width: 12px;
  height: 12px;
  border: 2px solid var(--ink);
  border-radius: 50%;
}
.map-legend__swatch--untried { background: var(--green-deep); }
.map-legend__swatch--tried { background: var(--on-green-mono); }

/* Open. The frame leaves the flow and takes the screen; the list underneath is
   still there, and closing puts it straight back.

   The podium's field arrives on a transform (`ymFadeUp`), and a transformed
   ancestor is what `position: fixed` measures itself against — so the "full
   screen" map opened inside the field and covered the field.

   `is-settled` is set the first time the map opens and never removed. Tying it
   to `is-map-open` instead put the animation *back* on the way out, and with
   `both` fill and a five-second cue the reader closed the map onto an empty
   green screen and waited for the field to fade in a second time. The animation
   has always finished playing by the time anything here can be tapped, so
   dropping it for good costs nothing. */
.podium .field.is-settled { animation: none; transform: none; }
.is-map-open .results-map__frame {
  position: fixed;
  inset: 0;
  z-index: 900;
  height: auto;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.is-map-open .results-map__cards {
  position: fixed;
  z-index: 901;
  left: 0;
  right: 0;
  bottom: 0;
  /* Clear of the attribution, which is a licence condition and not decoration. */
  padding: 0 14px calc(26px + env(safe-area-inset-bottom));
}
/* The frame is out of the flow, so the strip's space would collapse and the
   list below it would jump. Hold the gap. */
.is-map-open .results-map { height: 112px; }

/* --- pins --- */

/* One axis of colour, and it is the same fact the list marks: whether you have
   been. Two steps of the app's own green — deep steep for a spot you haven't
   had, mint for one you have — so the map reads as one material at a glance and
   the difference is depth rather than hue.

   Dark is the loud one against a pale basemap, and it is on the spots you
   haven't been to. Colour is the whole of it: no pin throws a drop shadow, so
   the pale basemap keeps one layer of ink on it. Rank changes the shape and
   never the colour.

   The cards below still wear the list's oat and canvas; the green pair is the
   pins only. See docs/decisions/results-map.md. */
.map-pin {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  background: var(--green-deep);
  color: var(--on-green-mono);
  border: var(--stroke) solid var(--ink);
  border-radius: 9px;
  font: 800 15px/1 var(--font-display);
  cursor: pointer;
}
/* Been there. The lighter of the two greens, and the only difference between
   them: the list still drops its shadow on a row you've had, the map draws
   both pins flat. */
.map-pin--had {
  background: var(--on-green-mono);
  color: var(--ink);
}

/* Names for the top five, once the map is open and close enough in to have room
   (`is-labelled`). The pin carries the rank when it is a medal and the label
   carries it when it isn't, so the number appears exactly once either way.

   Oat whichever pin it hangs off. The pin is a few pixels away and already says
   whether you've been; a name is the one thing on this map that has to stay
   readable, so it doesn't take a turn at carrying state. */
.map-pin__label {
  display: none;
  position: absolute;
  left: calc(100% + 7px);
  top: 50%;
  translate: 0 -50%;
  max-width: 132px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: var(--oat);
  color: var(--ink);
  border: 2px solid var(--ink);
  border-radius: 6px;
  box-shadow: 2px 2px 0 var(--ink);
  padding: 5px 6px;
  font: 700 10px/1 var(--font-mono);
  letter-spacing: .04em;
  text-transform: uppercase;
  pointer-events: none;
}
.is-labelled .map-pin__label { display: block; }
/* Placed by the controller. `--left` is a label that had no room on the right;
   `--hidden` is one that had room on neither side without landing on a pin, the
   heading or the card. `visibility` rather than `display`, so the hidden ones
   can still be measured on the next pass and come back when there is room. */
.map-pin__label--left { left: auto; right: calc(100% + 7px); }
.map-pin__label--hidden { visibility: hidden; }

/* In the strip they are a scatter and not a set of targets: a topic whose field
   is twenty CBD shops has twenty pins inside 112px, and at full size that is a
   wall rather than a picture of where the topic lives. Small, flat, unpressable
   — and no labels, because 13px of dot has nowhere to put a word. */
.results-map:not(.is-map-open) .map-pin {
  pointer-events: none;
  width: 13px;
  height: 13px;
  border-width: 2px;
  border-radius: 50%;
  box-shadow: none;
  font-size: 0;
  scale: 1;
}
.results-map:not(.is-map-open) .map-pin__label { display: none; }

/* Selection is a state, not a sequence — it lands whole (motion.md). The ring is
   the app's focus amber, which is what "this one" already looks like everywhere
   else, and the small pins grow more than the big ones so both end up about as
   loud. Amber here is a ring on one pin at a time, not a fill, so it still says
   what it says everywhere else. */
/* Ranks 6 and below: the same pin, round and without a numeral. Not a smaller
   *class* of marker — every shop of every brand is one of these two, and both
   are targets. A map where every pin is numbered is a second ranking to read. */
.map-pin--plain {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  font-size: 0;
}

.map-pin.is-selected {
  box-shadow: 0 0 0 5px rgb(224 162 58 / .55);
  scale: 1.25;
  z-index: 1;
}
.map-pin--plain.is-selected { scale: 1.5; }
/* Selecting a brand rings all of its shops so you can see the chain; the one
   you actually tapped is the one that grows, so "which of these is the card
   about" never has to be guessed. */
.map-pin.is-selected:not(.is-tapped) { scale: 1; }
.map-pin--plain.is-selected:not(.is-tapped) { scale: 1; }

/* The selected brand's other shops. Small, quiet, and never pressable: they are
   the same brand as the pin, so there is nothing else to select — and they take
   that pin's colour, because they are the same row. */
/* --- the card under a selected pin --- */

/* Deliberately `.field-row`'s numbers, not numbers near them: same radius, same
   stroke, same shadow, same padding, same rank column. It is the row you tapped,
   shown where the map can put it. */
/* Only ever populated while the map is open — closing clears the selection —
   so the collapsed strip doesn't keep an empty box's worth of margin under it. */
.results-map__cards { margin-top: 10px; }
.results-map:not(.is-map-open) .results-map__cards { display: none; }
.map-card {
  display: block;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: 18px;
  box-shadow: var(--shadow-sm);
  padding: 11px 14px 0;
}
/* The badge throws a 3px shadow, so the gap beside it is measured from where
   the eye puts its edge rather than from its border (see --drop-sm). */
.map-card__head { display: flex; align-items: center; gap: calc(11px + var(--drop-sm)); }
.map-card[hidden] { display: none; }
/* Been there — the same demotion `.field-row--had` gives a row, so the card
   under the map and the row in the list are the same object twice. */
.map-card--had {
  background: var(--canvas);
  border-color: var(--on-oat-rule);
  box-shadow: none;
  color: var(--on-oat-body);
}
.map-card--had .map-card__name { color: var(--on-oat-body); }
/* The pin, in the card. Same green, same ink stroke, same offset shadow, same
   rounded square — and the same circle for a rank below the top five, so the
   badge is the shape of the thing you actually tapped. It carries its numeral
   either way: on the map the number is for the top five only, but a card is
   about one place and its rank is the point of it.

   This is the whole answer to "the green pin and the beige card look
   unrelated": the card doesn't change colour, it just contains the pin. */
.map-card__rank {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: var(--green-deep);
  color: var(--on-green-mono);
  border: var(--stroke) solid var(--ink);
  border-radius: 9px;
  box-shadow: 3px 3px 0 var(--ink);
  font: 800 15px/1 var(--font-display);
}
/* Round where the brand's pins are round, so the badge is the pin again. */
.map-card__rank--plain { border-radius: 50%; }
.map-card--had .map-card__rank {
  background: var(--on-green-mono);
  color: var(--ink);
  box-shadow: none;
}
.map-card__body { flex: 1; min-width: 0; display: block; }
.map-card__name { display: block; font: 800 17px/1 var(--font-display); overflow-wrap: anywhere; }
.map-card__meta { display: block; margin-top: 5px; }

/* The one thing a map can offer that we can't, so it gets the row's own
   `field-row__lookup` treatment: the 44px is the tap target rather than the
   spacing, and the card gives up its bottom padding so the words sit where the
   eye expects and the target still reaches the card's edge. */
.map-card__go {
  display: inline-flex;
  align-items: center;
  min-height: 42px;
  font: 700 13px/1 var(--font-body);
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 4px;
}
.map-card__go:hover { color: var(--caramel); }
.map-card__go span { margin-left: 5px; text-decoration: none; }
.map-card--had .map-card__go { color: var(--on-oat-body); }

/* The flags are gone from here for the same reason they went from the rows: a
   filled chip in the app's own background colour, on the state that says the
   least. "Tried" rides the card's meta line now — one fact, one place, and the
   card says it exactly the way the row beneath it does. See
   `.mono-meta__mark` and docs/decisions/lists.md. */

/* A selected row, so the list and the map agree about what you are looking at.
   The app's focus amber, because that is what "this one" already looks like
   everywhere else.

   One-way for now: tapping a pin lights its row, and the rows are not pressable
   themselves — no cursor, no handler. The list is the ranking and reads as a
   list; making twenty rows into map controls is a decision of its own. */
.field-row.is-selected { outline: var(--stroke) solid var(--amber); outline-offset: 2px; }

/* ---------------------------------------------------------------- */
/* Documents — share pages, Devise                                   */
/* ---------------------------------------------------------------- */

/* A shared link lands on the app, not on a document about it — design D1 puts
   public pages on the same green as everything else. */
.doc { display: flex; flex-direction: column; flex: 1; padding: 16px 0 0; }
.doc .eyebrow { color: var(--on-green-mono); }
.doc__title { color: var(--oat); }

/* 08b · Sign-in — the only auth screen in the MVP. */
.signin { display: flex; flex-direction: column; flex: 1; padding-top: 16px; }
.signin__title { color: var(--oat); }

.signin__benefits {
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.signin__benefits li {
  display: flex;
  align-items: center;
  gap: 11px;
  font: 500 15px/1.3 var(--font-body);
  color: var(--on-green-body);
}
.signin__num {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: var(--amber);
  border: 2px solid var(--ink);
  color: var(--ink);
  font: 700 13px/22px var(--font-body);
  text-align: center;
}

.signin__actions { margin-top: auto; padding-top: 28px; }

/* Email is one tap away, and it's a <details> so that stays true with no JS. */
.email-toggle { margin-top: 10px; }
.email-toggle__summary {
  list-style: none;
  display: block;
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: 18px;
  box-shadow: var(--shadow-sm);
  padding: 15px;
  text-align: center;
  font: 700 16px/1 var(--font-body);
  cursor: pointer;
}
.email-toggle__summary::-webkit-details-marker { display: none; }
.email-toggle__summary:hover { transform: translate(1px, 1px); box-shadow: 3px 3px 0 var(--ink); }
.email-toggle[open] .email-toggle__summary {
  border-radius: 18px 18px 0 0;
  box-shadow: none;
  border-bottom-width: 0;
}
/* Scoped so it outranks .form-card's own margin regardless of source order. */
.email-toggle .email-toggle__card { margin: 0; border-radius: 0 0 18px 18px; box-shadow: var(--shadow-sm); }

.field--check { display: flex; align-items: center; gap: 9px; }
.field--check label { margin: 0; text-transform: none; letter-spacing: 0; font-size: 14px; }

.form-card {
  background: var(--oat);
  color: var(--ink);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 22px;
  margin: 18px 0;
}
.form-card h1, .form-card h2 {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -.025em;
  margin: 0 0 14px;
}
.form-card h1 { font-size: 30px; }
.form-card h2 { font-size: 22px; }

.form-note {
  font: 500 14px/1.45 var(--font-body);
  color: var(--on-oat-body);
  margin: -6px 0 16px;
}

.field { margin-bottom: 14px; }
.field label {
  display: block;
  font: 700 11px/1 var(--font-mono);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--on-oat-mono);
  margin-bottom: 7px;
}
.field input[type="text"],
.field input[type="email"],
.field input[type="password"] {
  width: 100%;
  min-height: 44px;
  padding: 11px 13px;
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-sm);
  background: var(--canvas);
  font: 500 16px/1.2 var(--font-body);
  color: var(--ink);
}
.field_with_errors { display: contents; }
.field_with_errors input { border-color: var(--caramel); }

.error-explanation {
  background: var(--caramel);
  color: var(--oat);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius-md);
  padding: 13px 16px;
  margin-bottom: 16px;
}
.error-explanation h2 { font-size: 16px; margin: 0 0 6px; }
.error-explanation ul { margin: 0; padding-left: 18px; }

.auth-links {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 4px 0 20px;
  font: 500 14px/1.4 var(--font-body);
  color: var(--on-green-body);
}
.auth-links a { text-decoration: underline; text-underline-offset: 3px; }

/* ---------------------------------------------------------------- */
/* Motion — one moment                                               */
/* ---------------------------------------------------------------- */

/* The MVP keeps two moments of motion, and they are the two the design kept:
   the pick payoff (6a), the second of arcade between two rounds of a duel, and
   the podium build (05), which is the result arriving. Everything else renders
   in its final state — no stamp drop on the cold open, no pulsing "Settle it",
   no split-bar seam, no growing standings bars. Motion earns its place by
   carrying information; decoration doesn't.

   The payoff's keyframes come first, and they run to this beat:

     0.00s  the card presses in 3px — the same press every card has
     0.05s  amber floods out of the tap as a circle
     0.12s  the winner punches to centre, settling at a 1.5° tilt
     0.16s  twelve particles launch on the same twelve arcs every time
     0.30s  SETTLED eases down from 122% and settles at 99%
     0.34s  the screen nudges 2px — the only time the frame itself moves
     0.50s  the streak chip flies in; the progress pip snaps full at 125%
     0.72s  the next-round card fades up, its countdown already part-drained
     …      the particles stay in the air, scaled by --burst-drift
     3.50s  the countdown empties and the next pair slides in from the right
            (--burst-beat; 1.5s in the design, held longer here — see the 6a
            section for why only the tail is stretched and never the entry) */

@keyframes ymFadeUp { from { transform: translateY(12px); opacity: 0; } }
/* The whole payoff collapses to this one under prefers-reduced-motion. */
@keyframes ymFadeIn { from { opacity: 0; } }
@keyframes ymFlood {
  from { clip-path: circle(0% at 50% 46%); }
  to { clip-path: circle(150% at 50% 46%); }
}
@keyframes ymRing {
  0% { transform: scale(.3); opacity: .9; border-width: 14px; }
  100% { transform: scale(2.4); opacity: 0; border-width: 2px; }
}
@keyframes ymPunch {
  0% { transform: scale(.72) rotate(-5deg); opacity: 0; }
  45% { transform: scale(1.09) rotate(1.5deg); opacity: 1; }
  70% { transform: scale(.97) rotate(-1deg); }
  100% { transform: scale(1) rotate(-1.5deg); }
}
@keyframes ymStampIn {
  0% { transform: translate(-50%, -34px) rotate(-9deg) scale(1.22); opacity: 0; }
  60% { transform: translate(-50%, 0) rotate(-5deg) scale(.99); opacity: 1; }
  100% { transform: translate(-50%, 0) rotate(-4deg) scale(1); opacity: 1; }
}
@keyframes ymChipIn {
  0% { transform: translateX(150px) rotate(8deg); opacity: 0; }
  60% { transform: translateX(-10px) rotate(-3deg); opacity: 1; }
  100% { transform: translateX(0) rotate(-2deg); opacity: 1; }
}
@keyframes ymShake {
  0% { transform: translate(0, 0); }
  20% { transform: translate(-2px, 1px); }
  45% { transform: translate(2px, -1px); }
  70% { transform: translate(-1px, 1px); }
  100% { transform: translate(0, 0); }
}
@keyframes ymPipFill {
  0% { transform: scaleX(0); }
  60% { transform: scaleX(1.25); }
  100% { transform: scaleX(1); }
}
@keyframes ymPearlL {
  0% { transform: translate(0, 0) scale(.4); opacity: 0; }
  10% { opacity: 1; }
  45% { transform: translate(-70px, -330px) scale(1) rotate(140deg); }
  100% { transform: translate(-140px, 120px) scale(.85) rotate(340deg); opacity: 0; }
}
@keyframes ymPearlR {
  0% { transform: translate(0, 0) scale(.4); opacity: 0; }
  10% { opacity: 1; }
  45% { transform: translate(70px, -350px) scale(1) rotate(-140deg); }
  100% { transform: translate(150px, 110px) scale(.85) rotate(-330deg); opacity: 0; }
}
@keyframes ymPearlUp {
  0% { transform: translate(0, 0) scale(.4); opacity: 0; }
  10% { opacity: 1; }
  45% { transform: translate(6px, -400px) scale(1.1) rotate(60deg); }
  100% { transform: translate(-14px, 140px) scale(.9) rotate(200deg); opacity: 0; }
}
@keyframes ymSlideNext {
  from { transform: translateX(110px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}
/* The only keyframe here that isn't physics: it's a clock face. */
@keyframes ymCountdown {
  from { width: 100%; }
  to { width: 0%; }
}
/* Nor is this one — it's a gate, and the only way to time one in plain CSS.
   pointer-events is a discrete property, so under step-end it holds `none` for
   the whole duration and flips at the end rather than interpolating. */
@keyframes ymUnblock {
  from { pointer-events: none; }
  to { pointer-events: auto; }
}

/* The podium's build (05). Sealed slots rise, then unseal from the bottom up,
   and your #1 opens last — see the beat sheet in the 05 section. */
@keyframes ymRise {
  from { transform: translateY(24px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
@keyframes ymWipeUp {
  from { clip-path: inset(100% 0 0 0); transform: translateY(10px); }
  to { clip-path: inset(0 0 0 0); transform: translateY(0); }
}
/* The seal wiping off, left to right — the same direction you read the name it
   uncovers. */
@keyframes ymUnseal {
  from { clip-path: inset(0 0 0 0); }
  to { clip-path: inset(0 0 0 100%); }
}
/* Your #1's doesn't wipe. It swells off the card and goes — and stays opaque
   while it does, because a lid you can see the winner through has already
   given the answer away. The clip is what removes it; the fade is only there
   to take the leaving edge off. */
@keyframes ymUnsealPop {
  0% { clip-path: inset(0 0 0 0); transform: scale(1); opacity: 1; }
  40% { clip-path: inset(0 0 0 0); transform: scale(1.06); opacity: 1; }
  92% { opacity: 1; }
  100% { clip-path: inset(0 0 0 100%); transform: scale(1.16); opacity: 0; }
}
/* Oat on the hatch, then whatever the card underneath wanted — naming only the
   opening colour leaves the closing one to the element, which is how one
   keyframe serves both the oat places and the amber one. */
@keyframes ymRankLit { from { color: var(--oat); } }
/* The whole life of the winning card in one pass, because two animations on one
   element fight over `transform`: rise (0–15%), wait (15–58%), rattle alone in
   the silence (58–76%), then punch open on the beat the cover leaves (76%). */
@keyframes ymCrown {
  0% { transform: translateY(24px) scale(1); opacity: 0; }
  15% { transform: translateY(0) scale(1); opacity: 1; }
  58% { transform: translate(0, 0) scale(1); opacity: 1; }
  61% { transform: translate(-3px, 0) scale(1); }
  64% { transform: translate(3px, 0) scale(1); }
  67% { transform: translate(-3px, 0) scale(1); }
  70% { transform: translate(2px, 0) scale(1); }
  73% { transform: translate(-2px, 0) scale(1); }
  76% { transform: translate(0, 0) scale(1); }
  84% { transform: translate(0, 0) scale(1.075); }
  91% { transform: translate(0, 0) scale(.98); }
  100% { transform: translate(0, 0) scale(1); }
}
/* The stack takes the landing. */
@keyframes ymKick {
  0% { transform: translateY(0); }
  22% { transform: translateY(-7px); }
  55% { transform: translateY(3px); }
  78% { transform: translateY(-2px); }
  100% { transform: translateY(0); }
}
/* Amber, once, off the one card allowed to wear it. */
@keyframes ymFlare {
  0% { transform: scale(.25); opacity: 0; border-width: 16px; }
  15% { opacity: .95; }
  100% { transform: scale(2.4); opacity: 0; border-width: 2px; }
}
/* The winner's twelve, out and then down past the fold. */
@keyframes ymTossL {
  0% { transform: translate(0, 0) scale(.35); opacity: 0; }
  12% { opacity: 1; }
  38% { transform: translate(-86px, -104px) scale(1) rotate(-130deg); }
  100% { transform: translate(-142px, 300px) scale(.9) rotate(-330deg); opacity: 0; }
}
@keyframes ymTossR {
  0% { transform: translate(0, 0) scale(.35); opacity: 0; }
  12% { opacity: 1; }
  38% { transform: translate(90px, -96px) scale(1) rotate(126deg); }
  100% { transform: translate(150px, 320px) scale(.9) rotate(330deg); opacity: 0; }
}
@keyframes ymTossUp {
  0% { transform: translate(0, 0) scale(.35); opacity: 0; }
  12% { opacity: 1; }
  38% { transform: translate(6px, -150px) scale(1.1) rotate(70deg); }
  100% { transform: translate(-12px, 330px) scale(.9) rotate(210deg); opacity: 0; }
}
/* The mirror of ymUnblock: a control that outlives its purpose is worse than
   one that leaves. Opacity and hit-testing go together at the end — the fade
   is the last 8% so the chip is fully live right up until it isn't. */
@keyframes ymRetire {
  0% { opacity: 1; pointer-events: auto; }
  92% { opacity: 1; pointer-events: auto; }
  100% { opacity: 0; pointer-events: none; }
}
/* 08b's sheet comes up from the bottom edge, the way a sheet does. It isn't a
   moment like the payoff or the podium — it's a surface arriving, so it gets
   one movement and no physics. */
@keyframes ymSheetRise {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
/* The same arrival on desktop, where the card is centred rather than docked —
   so its resting transform is a -50% offset it has to animate back to. */
@keyframes ymSheetFloatIn {
  from { transform: translateY(calc(-50% + 28px)); opacity: 0; }
  to { transform: translateY(-50%); opacity: 1; }
}
/* The payoff is a full-screen overlay, so this only fades it and then takes it
   out of hit-testing — the max-height it used to collapse was written for a
   banner and would crush the overlay to a strip. */
@keyframes ymRevealOut {
  from { opacity: 1; visibility: visible; }
  to { opacity: 0; visibility: hidden; }
}

/* Hover and press transitions stay: they're feedback on a direct tap, not a
   moment. Everything else collapses to a near-instant fade. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }

  /* 6a's own contract: the payoff collapses to a 120ms crossfade to the same
     frozen frame it would have landed on. The stamp and the streak chip still
     appear — they're the information. Only the shake, the pearls and the flood
     drop out, because those are the parts that are purely felt. */
  .pick-burst {
    animation:
      ymFadeIn .12s linear both,
      ymRevealOut .12s var(--burst-beat) linear both !important;
  }
  .pick-burst__flood { animation: none !important; }
  .pick-burst__ring {
    animation: none !important;
    opacity: .25;
    border-width: 5px;
  }
  .pick-burst__particles { display: none; }
  .pick-burst__loser,
  .pick-burst__next { animation: none !important; }
  /* The countdown keeps running. It's a clock, not a flourish — someone who has
     asked for less motion still needs to know when the screen will change out
     from under them, and a bar draining in place is not the kind of movement
     this setting is about. */
  .pick-burst__timer-fill {
    animation: ymCountdown var(--burst-beat) linear both !important;
  }
  .pick-burst__card {
    animation: none !important;
    transform: rotate(-1.5deg);
  }
  .pick-burst__streak {
    animation: none !important;
    transform: rotate(-2deg);
  }
  .pick-burst__stamp {
    animation: none !important;
    transform: translateX(-50%) rotate(-6deg);
  }
  .run-progress__step--snap .run-progress__fill { animation: none !important; }
  /* The slide-in goes, but the gate stays at full length — the round is behind
     opaque amber for just as long here, and a control you can't see must not be
     pressable whatever your motion preference. */
  .duel__round--next {
    animation: ymUnblock var(--burst-beat) step-end both !important;
  }

  /* Design 5c: "both kept sequences collapse to a 120ms fade". The podium
     arrives all at once rather than counting up — but it still waits for the
     payoff to clear, so nothing appears from behind the amber mid-fade. */
  .podium__title,
  .podium .place:nth-child(1),
  .podium .place:nth-child(2),
  .podium .place:nth-child(3),
  .podium .also-ranked,
  .podium .empty-state,
  .podium .crowd-panel,
  .podium .podium__actions {
    animation: ymFadeIn .12s var(--podium-cue) linear both !important;
  }

  /* Nothing is ever sealed here, so nothing unseals: the covers, the flare and
     the shapes are the felt half of the reveal, and what they were hiding is
     the information. Dropped outright rather than shortened — a cover that
     lingers for its delay and then vanishes would hide the names for a second
     and a half and call it accessibility.

     The skip goes with them. A 120ms fade has nothing to skip, and the sheet
     that follows it is already scheduled off the same numbers. */
  .podium .place::before,
  .podium__flare,
  .podium__particles,
  .podium__skip { display: none !important; }
  .podium .place__rank,
  .podium .podium__places { animation: none !important; }

  /* 08b fades in where it would have risen to. The four-second wait stays —
     that's the point of the sheet, not decoration — and so does the gate,
     shortened to match: a sheet you can't see yet must not take the tap you
     aimed at the podium underneath it, but it shouldn't sit there looking
     pressable and refusing either.

     Both are scoped away from `--instant`, and both had to be. That sheet has
     no arrival to shorten, and these two would have given it one it could never
     finish: `--signin-cue` is written against the podium's build variables, so
     on the list screen the delay is invalid — which left the card at
     `opacity: 0` behind a scrim, and `ymUnblock` holding the whole thing
     inert. A full-screen scrim over an invisible, untappable card is precisely
     what "reduced motion still gets the information" exists to stop, and it
     showed up only in a browser with the setting on. */
  .signin-sheet:not(.signin-sheet--instant) .signin-sheet__card {
    animation: ymFadeIn .12s var(--signin-cue) linear both !important;
  }
  .signin-sheet:not(.signin-sheet--instant) {
    animation: ymUnblock calc(var(--signin-cue) + .12s) step-end both !important;
  }
}

/* ---------------------------------------------------------------- */
/* Desktop (design D1/D2): the phone column widens, cards go side by  */
/* side, and the standings become the table a shared link deserves.   */
/* ---------------------------------------------------------------- */

@media (min-width: 900px) {
  .app { max-width: 1040px; padding: 0 32px 64px; }
  .app-bar { padding: 26px 0 10px; }
  .wordmark { font-size: 22px; gap: 12px; }
  .wordmark__mark { height: 38px; }

  .display--xl { font-size: 68px; line-height: .9; }
  .display--lg { font-size: 58px; }
  .lede { font-size: 18px; max-width: 620px; }

  /* Cold open: pitch on the left, the race and the rules on the right. */
  .cold-open {
    display: grid;
    grid-template-columns: 1.2fr 1fr;
    column-gap: 40px;
    row-gap: 20px;
    align-items: start;
    padding-top: 28px;
  }
  .cold-open__hero { grid-column: 1; grid-row: 1; }
  .cold-open__side { grid-column: 2; grid-row: 1 / span 2; }
  .cold-open__cta { grid-column: 1; grid-row: 2; margin-top: 0; padding-top: 8px; max-width: 420px; }

  /* Nominations: the form on the left, the running board on the right. */
  .nominating {
    display: grid;
    grid-template-columns: 1.1fr 1fr;
    column-gap: 40px;
    align-items: start;
    padding-top: 24px;
  }
  .nominating__hero { grid-column: 1; }
  .nominating__form { grid-column: 1; padding-top: 8px; max-width: 460px; }
  .nominating__board { grid-column: 2; grid-row: 1 / span 2; padding-top: 8px; }
  .nominate { max-width: 620px; }
  .nominate__actions { max-width: 420px; }

  /* Calibration: more tiles per row, and the sheet stops being a phone sheet. */
  .calibration { padding-top: 24px; }
  .calibration__grid { grid-template-columns: repeat(4, 1fr); gap: 12px; padding: 24px 0 28px; }
  .calibration__intro .lede { font-size: 17px; }
  .calib-tile { min-height: 88px; padding: 14px; }
  .calib-tile__name { font-size: 18px; }
  /* On a phone the sheet's negative margins exactly cancel the shell's padding,
     so it docks to all three edges. Neither half of that survived the jump to
     desktop: -32px only cancels `.app`'s own padding, not the gutter that
     centring it leaves either side, so a bar meant to be welded to the bottom
     of the window rendered 1040px wide with green down both flanks and a
     hard-cut edge — floating, not docked. And nothing cancelled the shell's
     64px of bottom padding, so it stuck 64px shy of the bottom.

     `calc(50% - 50vw)` is the same escape `.pick-burst`'s flood uses (the
     percentage resolves against the shell's content box, the vw against the
     window), which takes it to the window edges from any column width. The
     inline padding then walks the content back onto the column so the meter and
     Play stay in line with the tiles above them — floored at 32px for windows
     narrower than the column, where the shell is already full width. */
  .calib-sheet {
    margin: auto calc(50% - 50vw) -64px;
    padding: 18px max(32px, calc(50vw - 488px)) 24px;
    border-radius: 0;
  }
  .calib-sheet[data-state="thin"] { flex-direction: row; align-items: center; }
  .calib-sheet[data-state="thin"] .calib-sheet__play { width: auto; margin-top: 0; }

  /* Duel: the two cards face each other. */
  .duel { padding-top: 20px; }
  .duel__ask { padding-top: 30px; }
  .duel__question { font-size: 46px; }
  .matchup {
    flex-direction: row;
    align-items: stretch;
    gap: 20px;
    padding-top: 30px;
    min-height: 300px;
  }
  .matchup__or { align-self: center; font-size: 26px; }
  .contender-card__name { font-size: 46px; }
  .contender-card { padding: 36px; }
  .escape-row { max-width: 480px; margin-inline: auto; flex-wrap: nowrap; }
  .ghost-option { white-space: nowrap; }

  /* The winner stays a card, not a letterbox. Sideways there's nothing to say:
     the base rule measures its way to the window edge, so it already covers the
     wider gutter and the auto margin beside it.

     The bottom is still a number, and still only the in-run payoff's. The
     podium's is fixed to the viewport and has no shell padding under it:
     bleeding it -64px past the bottom edge takes the next-round card with it —
     same specificity as `.pick-burst--full` and later in the file, so it wins —
     and half of "Your podium" ends up below the fold. */
  .pick-burst:not(.pick-burst--full) { bottom: -64px; }
  /* That 30% is a share of the flood, and the flood is the whole window here
     rather than the column, so left alone it puts 432px above the winner on a
     1440 and sits the streak chip on top of the next-round card. The cap is
     what the column used to give: 30% of the widest the stage ever gets
     (1040 - 64). Below that width the percentage still governs. */
  .pick-burst__stack { padding: min(30%, 292px) 64px 120px; }
  .pick-burst__card { max-width: 520px; padding: 40px 32px; }
  .pick-burst__name { font-size: 52px; }
  .pick-burst__stamp { font-size: 36px; }
  /* Centred with auto margins rather than a translate, because the fade-up owns
     this element's transform. */
  .pick-burst__next { max-width: 520px; margin-inline: auto; }

  /* Podium: places on the left, the crowd panel and actions on the right. */
  .podium {
    display: grid;
    grid-template-columns: 1.1fr 1fr;
    column-gap: 40px;
    align-items: start;
    padding-top: 24px;
  }
  .podium__header { grid-column: 1 / -1; }
  /* The stage is the grid item now — the places and the also-ranked list are
     inside it, along with the flare and the shapes that are pinned to them. */
  .podium__stage { grid-column: 1; grid-row: 2; }
  /* One grid item, so the two things in it stack against each other instead of
     landing in separate rows — the row holding the crowd panel is as tall as the
     stage beside it, which used to strand the Share button ~200px below the
     panel with nothing in between. */
  .podium__side { grid-column: 2; grid-row: 2; }
  .crowd-panel { margin-top: 20px; }
  .podium__actions { margin-top: 0; padding-top: 16px; }
  /* The field runs the full width under both columns — it is a list of
     fourteen, and half a grid is where a list of fourteen goes to become a
     scroll. Two-up for the same reason the standings are. */
  .field { grid-column: 1 / -1; grid-row: 3; }
  /* Columns, not a grid. The rows are different heights by design — a suburb
     line and a map link each add one — and a grid
     quantises every row to the tallest card in it, so a plain name sitting
     beside one carrying all three gets a hole under it the size of the
     difference. `align-items: start` was what stopped the short card being
     *stretched* into a tall empty box, but it could only ever move the empty
     space, not remove it.

     Multicol has no rows to quantise: the list flows down column one and
     continues at the top of column two, so every card is exactly as tall as its
     own content. It also reads better for a ranking — 2..14 down the left, 15
     on — instead of the grid's zigzag. `break-inside` keeps a card whole, and
     the vertical rhythm has to come from margins because `gap` governs nothing
     in block layout. */
  .field__list { display: block; columns: 2; column-gap: 10px; --field-gap: 10px; }
  .field-row { break-inside: avoid; }
  .field-row--leader { column-span: all; }
  .place--first .place__name { font-size: 44px; }
  /* The winner's card is taller here, so both the ring that leaves it and the
     shapes that burst from behind it start lower. */
  .podium__flare { top: 76px; }
  .podium__particles { top: 76px; }

  /* 08b stops being a sheet here. A narrow card welded to the bottom edge of a
     wide screen is neither a sheet nor a dialog; on a pointer it should sit in
     the middle of the thing it's covering, with the ink border closed all the
     way round like every other card in the app. It still arrives from below —
     that's the same movement, just a shorter one. */
  .signin-sheet__card {
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    max-width: 460px;
    max-height: 84dvh;
    border: var(--stroke) solid var(--ink);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: 28px 32px 30px;
    animation-name: ymSheetFloatIn;
  }
  .signin-sheet__grip { display: none; }

  /* Standings: the full list is the content — it is what makes the page worth
     linking to. It goes two-up the same way the podium's field does; the rule
     that does that lives with .field, and the leader spans both columns. */
  .field-row--leader { grid-column: 1 / -1; }
  .standings__actions { max-width: 420px; }

  /* The strip keeps its job but not its phone proportions — 112px across a
     1040px page is a letterbox, and the pins end up sharing one band of it. */
  .results-map__frame { height: 220px; }
  .is-map-open .results-map { height: 220px; }
  .is-map-open .results-map__cards { max-width: 560px; right: auto; }

  .doc { max-width: 760px; }
  .form-card { max-width: 520px; }

  /* Sign-in: the pitch on the left, the two ways in on the right. */
  .signin {
    display: grid;
    grid-template-columns: 1.1fr 1fr;
    column-gap: 48px;
    align-items: start;
    padding-top: 32px;
  }
  .signin__actions { margin-top: 0; padding-top: 4px; max-width: 420px; }
}

/* The way out for someone who has had none of them. */
.calib-sheet__escape {
  flex-basis: 100%;
  order: 3;
  margin: 10px 0 0;
  text-align: center;
  font: 500 13px/1.3 var(--font-body);
  color: var(--on-oat-body);
}
.calib-sheet__escape a { text-decoration: underline; text-underline-offset: 3px; }
.calib-sheet { flex-wrap: wrap; }

/* Selected calibration tiles read as pressed in, not just tinted — a tonal
   shift alone was hard to scan across fourteen of them. */
.calib-tile:has(.calib-tile__input:checked) {
  background: var(--condensed);
  box-shadow: inset 3px 3px 0 rgba(23, 19, 15, .12);
  transform: translate(2px, 2px);
}
