/* The Valuation Challenge — Cannes stand game.
   Palette is the "Deep water" scheme from the design canvas. Type follows the
   house rule: system sans for everything, IBM Plex Mono as the one webfont, used
   only for eyebrows, tags and data readouts.

   Everything is sized for a phone held at arm's length under hall glare, so the
   floors are higher than a desk design would use: no body text under 14px, no
   tap target under 44px. */

:root {
  --bg:    #0B1721;
  --card:  #16242F;
  --tx:    #EAF0F3;
  --dim:   #8AA0AE;
  --ln:    rgba(255,255,255,.13);
  --ac:    #1D65AF;   /* primary action */
  --acl:   #68ADEF;   /* our estimate / likely range — one colour, one meaning */
  --gold:  #F0A44B;   /* the asking price, everywhere, always */
  --ok:    #1F9B6B;
  --mono:  'IBM Plex Mono', ui-monospace, Menlo, monospace;
  --sans:  -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --pad:   22px;
  --safe-b: env(safe-area-inset-bottom, 0px);
}

*, *::before, *::after { box-sizing: border-box; }

/* .idle and .offline set display, which would otherwise beat the UA rule for
   [hidden] and leave both overlays permanently on screen. */
[hidden] { display: none !important; }

html, body {
  margin: 0; height: 100%;
  background: var(--bg); color: var(--tx);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
}
/* pan-y, not manipulation: manipulation only kills the double-tap zoom, and a
   pinch still zoomed. Vertical scrolling stays (the board needs it); the slider
   sets touch-action: none of its own and is unaffected. */
body { touch-action: pan-y; }

/* Someone pinching at the stand selected the slider and got a highlight box
   around it mid-drag. Nothing in a 90-second game is worth selecting — except
   what people type. */
.app {
  user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.app input {
  user-select: text; -webkit-user-select: text;
  -webkit-touch-callout: default;
}

.app {
  position: relative;
  min-height: 100dvh;
  max-width: 520px;
  margin: 0 auto;
  display: flex; flex-direction: column;
}

.nb { white-space: nowrap; }

/* Screens ---------------------------------------------------------------- */
.screen { display: none; flex: 1; flex-direction: column; padding: 0 var(--pad); }
.app[data-screen="attract"] [data-for="attract"],
.app[data-screen="howto"]   [data-for="howto"],
.app[data-screen="play"]    [data-for="play"],
.app[data-screen="reveal"]  [data-for="reveal"],
.app[data-screen="final"]   [data-for="final"],
.app[data-screen="board"]   [data-for="board"] { display: flex; animation: rise .28s ease both; }
.app[data-screen="loading"] .boot { display: block; }
.boot { display: none; margin: auto; color: var(--dim); font: 400 14px var(--sans); text-align: center; }

@keyframes rise { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .screen, .rail-mark, .rail-band50, .rail-band80,
  .rail-radar i, .bullseye, .bullseye-dot::after,
  .boat-loading img, .boat-loading::after { animation: none !important; transition: none !important; }
  .boat.is-loading .boat-loading::after { display: none; }
  .boat.is-loading .boat-loading img { opacity: .34; }
  .rail-radar { display: none !important; }
}

/* Shared bits ------------------------------------------------------------- */
.eyebrow {
  font: 500 10.5px/1.2 var(--mono);
  letter-spacing: .15em; text-transform: uppercase; color: var(--dim);
}
.eyebrow-row { display: flex; align-items: center; gap: 9px; padding-top: max(18px, env(safe-area-inset-top)); }
.mark { height: 15px; width: auto; display: block; opacity: .6; }

.wordmark {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding-top: max(20px, env(safe-area-inset-top));
}
/* A masthead, so it behaves like one: same tab, back button returns you. The
   attract screen is the only place it appears, so no game is ever in progress
   when someone taps it.

   The negative margin is the point of the padding — it buys a 30px tall hit
   target on a 12px wordmark without moving a single pixel of the layout. */
.wordmark a {
  font: 600 12px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: #fff;
  display: inline-flex; align-items: center;
  text-decoration: none; padding: 9px 4px; margin: -9px -4px; border-radius: 4px;
}
.wordmark a:active { opacity: .65; }
.wordmark a:focus-visible { outline: 2px solid var(--acl); outline-offset: 2px; }

/* The mark above a screen's button — how-it-works still carries one. */
.mark-foot { height: 17px; opacity: .55; margin-bottom: 2px; }
/* The trailing accent dot is part of the wordmark, not decoration. */
.wm-dot {
  width: 5px; height: 5px; border-radius: 50%; background: var(--blue-dot, #1D65AF);
  margin-left: 7px; flex: none;
}
:root { --blue-dot: #3E8FE0; }   /* the brand blue lifted to clear AA on ink */

/* The mark, once it moves off the header, anchors the bottom-left of a screen. */

.lede { margin: 0; font: 400 15px/1.55 var(--sans); color: var(--dim); text-wrap: pretty; }

.cta {
  appearance: none; border: 0; width: 100%;
  min-height: 54px; padding: 0 20px; border-radius: 10px;
  background: var(--ac); color: #fff;
  font: 600 16px/1 var(--sans); font-family: var(--sans);
  display: flex; align-items: center; justify-content: center; gap: 10px;
  cursor: pointer; text-decoration: none;
  transition: background .15s ease, opacity .15s ease;
}
.cta:active { background: #154E88; }
.cta[disabled] { background: var(--card); color: var(--dim); }
.cta .arrow { font-size: 17px; }

.ghost {
  appearance: none; background: transparent; border: 1px solid var(--ln);
  color: var(--tx); border-radius: 9px; min-height: 44px; padding: 0 16px;
  font: 500 14px var(--sans); cursor: pointer;
}
.ghost:active { background: rgba(255,255,255,.05); }

.ask-tone { color: var(--gold); font-style: normal; }
.est-tone { color: var(--acl); font-style: normal; }

/* Attract ----------------------------------------------------------------- */
/* The photo is full-bleed: absolutely positioned children resolve against the
   padding box, so it escapes the screen's side padding without a wrapper. */
.attract-photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; z-index: 0;
}
/* The scrim is baked into the file; this only deepens the head of the frame,
   where the audience row sits over bright water. */
.attract-veil {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(180deg,
    rgba(11,23,33,.72) 0%, rgba(11,23,33,.46) 16%, rgba(11,23,33,.10) 32%,
    rgba(11,23,33,.06) 38%, rgba(11,23,33,.68) 52%, rgba(11,23,33,.92) 66%,
    var(--bg) 82%);
}
.app[data-screen="attract"] [data-for="attract"] > *:not(.attract-photo):not(.attract-veil) {
  position: relative; z-index: 1;
}

.audiences {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 9px; margin-top: 24px;
}
.aud {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; padding-right: 9px; border-right: 1px solid var(--ln);
}
.aud:last-child { padding-right: 0; border-right: 0; }
.aud svg { width: 23px; height: 23px; display: block; color: var(--acl); }
.aud-label {
  font: 600 8.2px/1.35 var(--mono);
  letter-spacing: .07em; text-transform: uppercase; color: #fff;
  min-height: 2.7em;   /* two lines, so all four descriptions start level */
}
.aud-desc { font: 400 10px/1.4 var(--sans); color: #A9BAC6; text-wrap: pretty; }

.attract-body { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 20px; padding: 24px 0 0; }
.hero {
  margin: 0; font: 700 clamp(42px, 14.5vw, 56px)/0.95 var(--sans);
  letter-spacing: -.03em; color: #fff;
  text-shadow: 0 2px 30px rgba(11,23,33,.55);
}
.hero em { font-style: normal; color: var(--acl); }
.lede { max-width: 22em; }

.attract-foot { display: flex; flex-direction: column; align-items: stretch; gap: 0; padding-bottom: calc(20px + var(--safe-b)); }

/* The game's name, centred in the open water between the audience row and the
   headline. It used to be an 11px plate down in the foot, under the lede and
   above the occasion bar, where it read as a caption rather than a title.

   ⚠ The auto margins are what put it in the middle. .attract-body is a column
   with justify-content: flex-end, so hero and lede sit at the bottom and the
   photograph's free space collects above them; auto margins absorb that space
   before justify-content is applied, so an equal share lands above and below
   this line. Setting a fixed margin instead pins it under the audience row and
   the gap reopens underneath it on a tall phone.

   Sized to stay clearly second to the 42-56px headline. The clamp's floor is
   what keeps "THE VALUATION CHALLENGE" — 23 tracked mono caps — on one line at
   360px; it wraps to two below that and still reads, being centred. */
.title-plate {
  /* align-self shrink-wraps the box to its text and centres it; the auto block
     margins do the vertical placement described above. */
  margin: auto 0; align-self: center; text-align: center;
  /* em padding so the box breathes by the same proportion at every size — a
     fixed 18px left the chip 3px from the column edge on a 320px phone. */
  padding: .8em 1.2em; border-radius: 11px;
  border: 1px solid rgba(255,255,255,.15);
  background: rgba(8,17,25,.40);
  -webkit-backdrop-filter: blur(15px) saturate(1.15);
  backdrop-filter: blur(15px) saturate(1.15);
  font: 600 clamp(12px, 3.9vw, 18px)/1.3 var(--mono);
  letter-spacing: .13em; text-transform: uppercase; color: #fff;
  text-wrap: balance;
}
/* ⚠ Without this the box is a 40%-opaque wash over a sharp marina and the title
   is HARDER to read than with no box at all — which is what a plain translucent
   panel did over the Southampton aerial. The blur is doing the work; the fill is
   only there to keep it from going milky. Every current browser has it, but a
   phone that does not must get a genuinely opaque ground rather than the wash. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .title-plate { background: rgba(8,17,25,.78); }
}

/* The occasion. Gold is the one place it appears outside the reveal, where it
   means the asking price — here it means "this is happening now". */
.occasion { display: flex; align-items: center; gap: 10px; margin: 18px 0 0; }
/* It inherited the plate's 26px when the plate sat above it in the foot. */
.attract-foot > .occasion:first-child { margin-top: 26px; }
.occasion-star { width: 13px; height: 13px; flex: none; fill: var(--gold); display: block; }
.occasion-live {
  font: 600 10.5px/1 var(--mono); letter-spacing: .16em;
  text-transform: uppercase; color: var(--gold); white-space: nowrap;
  /* --occ-fit is set by fitOccasion() when a show's name and dates are too long
     for a narrow phone. 1 is the designed setting and what nearly every phone
     gets; see the comment on that function. */
  font-size: calc(10.5px * var(--occ-fit, 1));
  letter-spacing: calc(.16em * var(--occ-track, 1));
}
.occasion-rule { width: 1px; height: 12px; background: rgba(255,255,255,.24); flex: none; display: block; }
.occasion-when {
  font: 500 10.5px/1 var(--mono); letter-spacing: .14em;
  text-transform: uppercase; color: #C3D0D9; white-space: nowrap;
  font-size: calc(10.5px * var(--occ-fit, 1));
  letter-spacing: calc(.14em * var(--occ-track, 1));
}

.attract-foot .cta {
  margin-top: 18px; min-height: 56px; padding: 0 16px;
  font-size: 13.5px; letter-spacing: .04em; text-transform: uppercase;
}
.attract-foot .cta span:first-child { white-space: nowrap; }

.board-tease {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  margin: 15px 0 0; padding: 6px 0; color: var(--dim);
}
.board-tease svg { width: 15px; height: 15px; flex: none; display: block; }
/* Desktop and tablet get told, once, where this actually belongs. */
.mobile-note { display: none; margin: 14px 0 0; font: 400 12px/1.4 var(--sans); color: var(--dim); text-align: center; }
@media (pointer: fine) and (min-width: 800px) {
  .mobile-note { display: block; }
}

.board-tease span {
  font: 500 10.5px/1 var(--mono); letter-spacing: .12em;
  text-transform: uppercase; white-space: nowrap;
}

/* How it works ------------------------------------------------------------ */
.howto-foot { display: flex; flex-direction: column; align-items: stretch; gap: 12px; padding-bottom: calc(18px + var(--safe-b)); }
.howto-foot .mark-foot { align-self: flex-start; }
.howto-body { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 22px; }
.howto-lines { margin: 0; font: 700 clamp(30px, 9vw, 38px)/1.16 var(--sans); letter-spacing: -.02em; color: #fff; }

/* HUD --------------------------------------------------------------------- */
.hud {
  display: flex; align-items: center; justify-content: space-between;
  padding-top: max(16px, env(safe-area-inset-top)); padding-bottom: 14px;
}
.hud-left { display: flex; align-items: center; gap: 9px; }

/* The mark's letterforms sit flush on its bottom edge, and the bottom edge is
   exactly where an inline image's baseline is — so aligning on the baseline
   puts its "BW" on the same line as the label's caps, instead of centring two
   different cap heights against each other and calling it aligned. The height
   is chosen to match those caps: the mark is 63.7% letterform, and the label is
   ~0.7em of 10.5px mono. */
.hud-right { display: flex; align-items: baseline; gap: 9px; }
.hud-right .mark { height: 11.5px; }
.hud-label { font: 500 10.5px/1 var(--mono); letter-spacing: .15em; text-transform: uppercase; color: var(--dim); }
.hud-score { font: 600 11px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--tx); }
.dots { display: flex; gap: 5px; }
.dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--ln); display: block; }
.dots i.done { background: var(--acl); }
.dots i.now  { background: var(--tx); }

/* Boat card --------------------------------------------------------------- */
.boat {
  position: relative; margin: 0; border-radius: 12px; overflow: hidden;
  background: var(--card); border: 1px solid var(--ln);
  /* Grows into whatever the phone has spare rather than leaving a dead band
     between the specs and the slider. 4:3 is the floor, not the shape. */
  flex: 1 1 auto; min-height: min(46vh, 74vw); display: flex; align-items: flex-end;
}
.boat-photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; opacity: 0; transition: opacity .3s ease;
}
.boat.has-photo .boat-photo { opacity: 1; }
.boat::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(11,23,33,0) 34%, rgba(11,23,33,.82) 100%);
  opacity: 0; transition: opacity .3s ease;
}
.boat.has-photo::after { opacity: 1; }

.boat-noimg { position: absolute; top: 16px; left: 16px; right: 16px; display: block; }
.boat.has-photo .boat-noimg, .boat.is-loading .boat-noimg { display: none; }

/* Waiting on the photo. The card keeps its full size and the boat's name, so
   nothing moves when the image lands — only the ground behind it changes.
   The mark and a slow pass of light: the house version of a skeleton. */
.boat-loading { display: none; }
.boat.is-loading .boat-loading {
  position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center;
  background: var(--card);
}
.boat.is-loading .boat-loading img {
  height: 26px; width: auto; animation: boatPulse 1.9s ease-in-out infinite;
}
.boat.is-loading .boat-loading::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 48%;
  background: linear-gradient(90deg, transparent, rgba(104,173,239,.11), transparent);
  animation: boatSweep 1.6s ease-in-out infinite;
}
@keyframes boatPulse { 0%, 100% { opacity: .2; } 50% { opacity: .48; } }
@keyframes boatSweep { from { transform: translateX(-120%); } to { transform: translateX(330%); } }

.boat-name { position: relative; z-index: 1; padding: 16px; display: flex; flex-direction: column; gap: 2px; }
.boat.has-photo .boat-name { padding: 18px 16px 15px; }
.boat-make {
  font: 500 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; color: var(--acl);
}
.boat-model {
  font: 700 clamp(24px, 7.2vw, 30px)/1.08 var(--sans); letter-spacing: -.02em; color: #fff;
}
/* No photo: the type IS the card. No border, no grey box, no broken-image
   apology -- just a specification large enough to price at arm's length. */
.boat:not(.has-photo):not(.is-loading) {
  flex: 0 0 auto; min-height: 0; align-items: flex-start; flex-direction: column;
  background: transparent; border: 0; border-radius: 0; padding: 8px 0 4px; gap: 14px;
}
.boat:not(.has-photo):not(.is-loading) .boat-noimg { position: static; }
.boat:not(.has-photo):not(.is-loading) .boat-name { padding: 0; }
.boat:not(.has-photo):not(.is-loading) .boat-model { font-size: clamp(38px, 12vw, 54px); line-height: 1.02; }
.boat:not(.has-photo):not(.is-loading) .boat-make { font-size: 12px; }

.specs { display: flex; margin: 0; padding: 14px 2px 2px; gap: 0; }
.specs > div { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.specs dt {
  font: 500 9.5px/1 var(--mono); letter-spacing: .15em; text-transform: uppercase; color: var(--dim);
}
.specs dd { margin: 0; font: 600 15px/1.1 var(--sans); color: var(--tx); }

/* Guess ------------------------------------------------------------------- */
.guess { margin-top: auto; padding: 18px 0 calc(18px + var(--safe-b)); display: flex; flex-direction: column; gap: 10px; }
.guess-readout { display: flex; align-items: center; justify-content: center; gap: 14px; padding-top: 2px; }
.guess-readout output {
  flex: 1; text-align: center;
  font: 700 clamp(30px, 9vw, 38px)/1 var(--sans); letter-spacing: -.02em;
  color: #fff; font-variant-numeric: tabular-nums;
}
.nudge {
  appearance: none; flex: none; width: 44px; height: 44px; border-radius: 50%;
  background: var(--card); border: 1px solid var(--ln); color: var(--tx);
  font: 400 21px/1 var(--sans); cursor: pointer;
}
.nudge:active { background: rgba(255,255,255,.08); }

.slider { position: relative; height: 46px; display: flex; align-items: center; touch-action: none; cursor: pointer; }
.slider-track { position: relative; height: 6px; width: 100%; border-radius: 3px; background: rgba(255,255,255,.1); overflow: hidden; }
.slider-fill { position: absolute; inset: 0 auto 0 0; width: 50%; background: var(--ac); border-radius: 3px; }
.slider-thumb {
  position: absolute; top: 50%; left: 50%;
  width: 30px; height: 30px; margin: -15px 0 0 -15px; border-radius: 50%;
  background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,.45); pointer-events: none;
}
.slider:focus-visible { outline: 2px solid var(--acl); outline-offset: 6px; border-radius: 6px; }
.ticks { position: relative; height: 12px; font: 400 10px/1 var(--mono); color: var(--dim); letter-spacing: .06em; }
.ticks span { position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap; }
.ticks span:first-child { transform: none; }
.ticks span:last-child { transform: none; }

/* Reveal ------------------------------------------------------------------ */
/* ⚠ THE RESULT SCREENS RUN A LARGER TYPE SCALE THAN THE REST OF THE GAME, and
   that is deliberate. Reported back from Cannes: the reveal, result and board
   were too small to read on a stand. They are the screens people actually stop
   and study — and, unlike the attract and play screens, they had the room: the
   reveal carried an empty band between the score and the footer on every phone
   we checked. Nothing on these three screens is now under 11px.

   Keep it that way. If something here gets "tidied" back down to match the
   play screen, the complaint comes back. */
.reveal-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding-top: max(16px, env(safe-area-inset-top)); padding-bottom: 14px;
}
.reveal-boat {
  font: 600 16px/1.3 var(--sans); color: var(--tx);
  flex: 1 1 auto; min-width: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.reveal-head .hud-label { flex: none; white-space: nowrap; }

.bullseye {
  display: flex; align-items: center; gap: 9px;
  align-self: flex-start; margin-bottom: 12px;
  padding: 8px 13px; border-radius: 6px;
  background: rgba(31,155,107,.16); border: 1px solid rgba(31,155,107,.4);
  font: 600 12.5px/1 var(--mono); letter-spacing: .16em; text-transform: uppercase; color: #4CC392;
  animation: pop .42s cubic-bezier(.2,1.5,.4,1) both;
}
.bullseye-dot { position: relative; width: 8px; height: 8px; border-radius: 50%; background: #4CC392; flex: none; }
.bullseye-dot::after {
  content: ""; position: absolute; inset: -3px; border-radius: 50%;
  border: 1px solid #4CC392; animation: ring 1.6s ease-out infinite;
}
@keyframes pop { 0% { transform: scale(.6); opacity: 0; } 58% { transform: scale(1.08); opacity: 1; } 100% { transform: scale(1); } }

/* Rings out from the guess mark: the "you hit it" beat, on the rail itself. */
.rail-radar { position: absolute; top: 72px; left: 0; width: 0; height: 0; display: none; }
.rail-mark.hit .rail-radar { display: block; }
.rail-radar i {
  position: absolute; top: -13px; left: -13px; width: 26px; height: 26px;
  border-radius: 50%; border: 1.5px solid #4CC392;
  animation: radar 2.1s cubic-bezier(.2,.6,.3,1) infinite;
}
.rail-radar i:nth-child(2) { animation-delay: .5s; }
.rail-radar i:nth-child(3) { animation-delay: 1s; }
@keyframes radar { 0% { transform: scale(.26); opacity: .9; } 65% { opacity: .3; } 100% { transform: scale(2.9); opacity: 0; } }
@keyframes ring { 0% { transform: scale(.5); opacity: .9; } 100% { transform: scale(2.2); opacity: 0; } }

.rail-mark.hit .rail-flag { background: #4CC392; }
.rail-mark.hit .rail-tag { color: #4CC392; }

.verdict { display: flex; flex-direction: column; gap: 9px; align-items: flex-start; }
.chip {
  font: 600 12px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
  padding: 8px 12px; border-radius: 5px;
}
.chip.above { background: rgba(240,164,75,.16); color: var(--gold); }
.chip.below { background: rgba(31,155,107,.18); color: #4CC392; }
.chip.inside { background: rgba(104,173,239,.16); color: var(--acl); }
.verdict-line { margin: 0; font: 600 21.5px/1.32 var(--sans); letter-spacing: -.01em; color: #fff; text-wrap: pretty; }

/* The rail: log-scaled, three marks, two bands. The 50% "likely range" is the
   solid one because the 80% band is ±38% at the median and reads as noise. */
/* ⚠ Every offset in this block is measured from the rail's top and they move
   TOGETHER. The tags grew from 10.5px to 12.5px, so the rail grew 150-132 to
   keep the 'worth' and 'your guess' tags from touching underneath it. */
.rail { position: relative; height: 150px; margin: 26px 0 2px; }
.rail-axis { position: absolute; left: 0; right: 0; top: 72px; height: 1px; background: var(--ln); }
.rail-band80 {
  position: absolute; top: 71px; height: 3px; border-radius: 2px;
  background: rgba(104,173,239,.22);
  transform-origin: left center; animation: grow .5s .1s ease both;
}
/* End caps, so the 80% whisker reads as a measured extent rather than a smudge. */
.rail-band80::before, .rail-band80::after {
  content: ""; position: absolute; top: -4px; width: 1px; height: 11px;
  background: rgba(104,173,239,.4);
}
.rail-band80::before { left: 0; }
.rail-band80::after { right: 0; }
.rail-band50 {
  position: absolute; top: 65px; height: 15px; border-radius: 4px;
  background: rgba(104,173,239,.34); border: 1px solid rgba(104,173,239,.7);
  transform-origin: left center; animation: grow .5s .18s ease both;
}
@keyframes grow { from { transform: scaleX(.02); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }

.rail-mark { position: absolute; top: 0; height: 150px; width: 0; animation: rise .4s .3s ease both; }
.rail-flag { position: absolute; left: -1px; width: 2px; border-radius: 1px; }
.rail-tag {
  position: absolute; white-space: nowrap;
  font: 600 12.5px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase;
}
/* asking sits above the axis, worth below it, your guess below that */
.rail-ask .rail-flag { top: 44px; height: 32px; background: var(--gold); }
.rail-ask .rail-tag  { top: 16px; color: var(--gold); transform: translateX(-50%); }
.rail-est .rail-flag { top: 68px; height: 32px; background: var(--acl); }
.rail-est .rail-tag  { top: 106px; color: var(--acl); transform: translateX(-50%); }
.rail-guess .rail-flag { top: 68px; height: 20px; background: var(--dim); }
.rail-guess .rail-tag  { top: 128px; color: var(--dim); transform: translateX(-50%); }
.rail-mark.edge-l .rail-tag { transform: none; left: 0; }
.rail-mark.edge-r .rail-tag { transform: none; right: 0; left: auto; }

.rail-key { margin: 8px 0 0; font: 400 13px/1.5 var(--sans); color: var(--dim); }
.rail-key b { font-weight: 600; }

.gap-line { margin: 18px 0 0; font: 400 16px/1.5 var(--sans); color: var(--dim); text-wrap: pretty; }

/* ⚠ A GRID WITH THE VALUES PUSHED TO THE BOTTOM, not a flex row. At the larger
   label size "ON THE MARKET" and "PRICE CHANGES" take two lines while
   "COMPARABLES" takes one, so in a plain row the three numbers sat at three
   different heights. Equal columns and space-between put the values on one
   line as the labels wrap above them, at any width. */
.colour { display: grid; grid-template-columns: repeat(3, 1fr); margin: 16px 0 0; padding: 0; gap: 0 14px; }
.colour > div { display: flex; flex-direction: column; justify-content: space-between; gap: 6px; }
.colour dt { font: 500 11px/1.25 var(--mono); letter-spacing: .11em; text-transform: uppercase; color: var(--dim); }
.colour dd { margin: 0; font: 600 16px/1.1 var(--sans); color: var(--tx); }

.score-row {
  margin-top: 18px; padding: 14px 16px; border-radius: 10px; background: var(--card);
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
}
.score-row > div { display: flex; flex-direction: column; gap: 5px; }
.score-err { font: 600 19px/1 var(--sans); color: var(--tx); }
.score-pts { font: 700 30px/1 var(--sans); color: var(--dim); font-variant-numeric: tabular-nums; }
.score-pts.good { color: var(--acl); }
.score-pts.fair { color: var(--tx); }

.reveal-foot { margin-top: auto; padding: 18px 0 calc(18px + var(--safe-b)); display: flex; flex-direction: column; gap: 12px; }
.credit { margin: 0; display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
.credit a, .stamp { font: 400 12px/1.45 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.credit a { color: var(--acl); text-decoration: none; border-bottom: 1px solid rgba(104,173,239,.4); }
.stamp { color: var(--dim); }

/* Final ------------------------------------------------------------------- */
.final-head { padding-top: max(18px, env(safe-area-inset-top)); display: flex; flex-direction: column; gap: 10px; }
.final-head .eyebrow-row { padding-top: 0; justify-content: space-between; }
.final-total { margin: 0; display: flex; align-items: baseline; gap: 8px; }
.final-total span { font: 700 54px/1 var(--sans); letter-spacing: -.03em; color: #fff; font-variant-numeric: tabular-nums; }
.final-total small { font: 500 16.5px/1 var(--mono); color: var(--dim); }

.recap { list-style: none; margin: 20px 0 0; padding: 0; display: flex; flex-direction: column; }
.recap li {
  display: grid; grid-template-columns: 8px 1fr auto auto; align-items: center; gap: 12px;
  padding: 13px 0; border-bottom: 1px solid var(--ln);
}
.recap i { width: 7px; height: 7px; border-radius: 50%; display: block; }
.recap .rc-name { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.recap .rc-name b { font: 600 16px/1.2 var(--sans); color: var(--tx); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recap .rc-name small { font: 400 12px/1.25 var(--mono); letter-spacing: .08em; color: var(--dim); }
.recap .rc-err { font: 500 12.5px/1 var(--mono); color: var(--dim); }
.recap .rc-pts { font: 700 19px/1 var(--sans); color: var(--tx); font-variant-numeric: tabular-nums; }

.final-gap { margin: 20px 0 0; font: 400 16px/1.55 var(--sans); color: var(--dim); text-wrap: pretty; }
.final-gap b { color: var(--tx); font-weight: 600; }
.final-stamp { margin: 14px 0 0; font: 400 11.5px/1.45 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--dim); opacity: .85; }

.join { margin-top: auto; padding: 22px 0 calc(18px + var(--safe-b)); display: flex; flex-direction: column; gap: 11px; }
.join input[type="text"], .join input[type="email"] {
  appearance: none; width: 100%; min-height: 50px; padding: 0 15px;
  background: var(--card); border: 1px solid var(--ln); border-radius: 9px;
  color: var(--tx); font: 500 16px var(--sans);
}
.join input::placeholder { color: var(--dim); }
.join input:focus { outline: none; border-color: var(--ac); }
/* Normally empty and taking no space at all. On the rare visitor Cloudflare
   wants to challenge, it becomes a real widget above the join button rather than
   something that has to fight for room. */
.turnstile-slot:empty { display: none; }
.turnstile-slot { margin: 2px 0; }

.consent { display: flex; gap: 11px; align-items: flex-start; cursor: pointer; padding: 2px 0; }
.consent input { appearance: none; flex: none; width: 22px; height: 22px; margin: 1px 0 0; border: 1px solid var(--dim); border-radius: 5px; background: transparent; cursor: pointer; }
.consent input:checked { background: var(--ac); border-color: var(--ac); }
.consent input:checked::after { content: "✓"; display: block; text-align: center; color: #fff; font-size: 14px; line-height: 20px; }
.consent span { font: 400 14px/1.5 var(--sans); color: var(--dim); }
.consent a { color: var(--acl); white-space: nowrap; }

/* Board ------------------------------------------------------------------- */
.board-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-top: max(18px, env(safe-area-inset-top)); padding-bottom: 4px;
}
.board-head h2 { margin: 0; font: 700 29px/1 var(--sans); letter-spacing: -.02em; color: #fff; }
.board-head .hud-label { white-space: nowrap; }
.board { list-style: none; margin: 14px 0 0; padding: 0; }
.board li {
  display: grid; grid-template-columns: 29px 1fr auto auto; align-items: center; gap: 12px;
  padding: 11px 10px; margin: 0 -10px; border-radius: 8px;
  border-bottom: 1px solid var(--ln);
}
.board li.me { background: rgba(104,173,239,.12); border-bottom-color: transparent; }
.board .bd-rank { font: 500 13.5px/1 var(--mono); color: var(--dim); }
.board .bd-name { font: 600 17px/1.2 var(--sans); color: var(--tx); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.bd-plays {
  font: 500 11.5px/1 var(--mono); font-style: normal; color: var(--dim);
  margin-left: 7px; letter-spacing: .04em;
}
.board li.me .bd-plays { color: var(--acl); opacity: .8; }
.board .bd-err { font: 400 12px/1 var(--mono); color: var(--dim); }
.board .bd-pts { font: 700 19px/1 var(--sans); color: var(--tx); font-variant-numeric: tabular-nums; }
.board li.me .bd-name, .board li.me .bd-pts, .board li.me .bd-rank { color: var(--acl); }

/* Where someone landed when they did not land on the board. A row of their own
   rather than a sentence: they came to see their name against everyone else's,
   and "23rd of 87" is still that. */
.board-note { margin: 14px 0 0; font: 400 14.5px/1.5 var(--sans); color: var(--dim); }
.board-note b { color: var(--tx); font-weight: 600; }
.board li.off-board {
  margin-top: 10px; border-bottom: 0;
  border-top: 1px dashed var(--ln); border-radius: 0 0 8px 8px;
}

.offramp {
  margin: 26px 0 calc(20px + var(--safe-b)); padding: 20px 18px;
  border: 1px solid var(--ln); border-radius: 12px; background: var(--card);
  display: flex; flex-direction: column; gap: 10px;
}
.offramp-lead { margin: 0; font: 600 20px/1.32 var(--sans); letter-spacing: -.01em; color: #fff; text-wrap: pretty; }
.offramp-body { margin: 0 0 6px; font: 400 15px/1.55 var(--sans); color: var(--dim); text-wrap: pretty; }
.offramp-minor { display: flex; gap: 10px; }
.offramp-minor .ghost { flex: 1; }

/* Overlays ---------------------------------------------------------------- */
/* The warning bar must not sit on top of the primary action. */
.app.idle-on { padding-bottom: 74px; }
.idle {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  padding: 14px var(--pad) calc(14px + var(--safe-b));
  background: rgba(11,23,33,.96); border-top: 1px solid var(--ln);
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  font: 500 13.5px/1.3 var(--sans); color: var(--tx);
}
.toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(10px + var(--safe-b)); z-index: 26;
  width: min(calc(100% - 24px), 496px); margin: 0;
  padding: 12px 14px; border-radius: 9px;
  background: var(--card); border: 1px solid var(--ln);
  font: 500 13px/1.35 var(--sans); color: var(--tx); text-align: center;
  box-shadow: 0 4px 18px rgba(0,0,0,.4);
  animation: toast-in .22s ease both;
}
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.offline {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(10px + var(--safe-b)); z-index: 25;
  width: min(calc(100% - 24px), 496px);
  padding: 10px 13px; border-radius: 9px;
  background: rgba(22,36,47,.97); border: 1px solid var(--ln);
  display: flex; align-items: center; gap: 9px;
  font: 400 12.5px/1.35 var(--sans); color: var(--dim);
  box-shadow: 0 4px 18px rgba(0,0,0,.4);
}
.offline .dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--gold); animation: pulse 1.8s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: .35; } 50% { opacity: 1; } }

/* Landscape / tablet: the same build, wider gutters, capped column. */
@media (min-width: 700px) and (orientation: landscape) {
  .app { max-width: 620px; }
  :root { --pad: 34px; }
}

/* Desktop. The column is unchanged — this only fills the room around it, with
   the same aerial blurred well past recognition so it reads as depth rather
   than a second photograph competing with the sharp one on the attract screen.
   The file is already cached and precached, so it costs no bytes.

   --show-photo is set by app.js per show, and set to `none` when that show's
   aerial is missing. The colour under it is not decoration: body is transparent
   inside this query, so without it a show with no photo would leave the room
   around the column showing the browser's own canvas. */
@media (min-width: 900px) {
  html::before {
    content: ""; position: fixed; inset: -6%; z-index: 0;
    background: var(--bg) var(--show-photo, none) center 32% / cover no-repeat;
    filter: blur(26px) saturate(.75) brightness(.5);
  }
  body { background: transparent; }
  .app {
    max-width: 640px;
    /* The screens themselves are transparent apart from attract, so without
       this the blurred backdrop would show through the round and board screens. */
    background: var(--bg);
    box-shadow: 0 0 0 1px rgba(255,255,255,.07), 0 24px 70px rgba(0,0,0,.55);
  }
}

/* ⚠ SHORT PHONES GET THE SAME TYPE AND LESS AIR.
   ⚠⚠ AND THIS BLOCK MUST STAY AT THE END OF THE FILE. A media query adds no
   specificity, so sitting above the Final and Board sections it lost every
   overlap to the plain rules below it — silently, and only on the screens it
   was not obviously changing. It was written there first and did nothing.
   The larger scale added 126px to the reveal, and on a 375x667 screen — iPhone
   SE 2/3, 6/7/8, all still common — that pushed "Next boat" below the fold on a
   screen that fitted exactly before. Scrolling to advance is a bad thing to ask
   of someone standing at a stand holding a drink.

   Everything given back here is SPACING, not size. The point of the change was
   legibility; a phone that cannot afford the air can still afford the type. */
@media (max-height: 740px) {
  .reveal-head { padding-top: max(10px, env(safe-area-inset-top)); padding-bottom: 8px; }
  .bullseye { margin-bottom: 8px; padding: 7px 12px; }
  .chip { padding: 7px 11px; }
  .verdict { gap: 7px; }
  .rail { height: 126px; margin: 12px 0 2px; }
  .rail-axis, .rail-radar { top: 60px; }
  .rail-band80 { top: 59px; }
  .rail-band50 { top: 53px; }
  .rail-mark { height: 126px; }
  .rail-ask .rail-flag { top: 34px; height: 28px; }
  .rail-ask .rail-tag { top: 8px; }
  .rail-est .rail-flag { top: 58px; height: 28px; }
  .rail-est .rail-tag { top: 92px; }
  .rail-guess .rail-flag { top: 58px; height: 18px; }
  .rail-guess .rail-tag { top: 112px; }
  .rail-key { margin: 4px 0 0; }
  .gap-line { margin: 8px 0 0; }
  .colour { margin: 10px 0 0; }
  .score-row { margin-top: 10px; padding: 11px 14px; }
  .reveal-foot { padding: 12px 0 calc(10px + var(--safe-b)); gap: 8px; }

  /* The result and board screens have always scrolled on a short phone — the
     join form and ten board rows cannot fit and were never meant to. The larger
     type still must not push the join button or the offramp further down than
     they already were, so they give back the same kind of air. */
  .final-head { padding-top: max(10px, env(safe-area-inset-top)); gap: 8px; }
  .final-total span { font-size: 46px; }
  .recap { margin: 12px 0 0; }
  .recap li { padding: 10px 0; }
  .final-gap { margin: 12px 0 0; }
  .final-stamp { margin: 10px 0 0; }
  .consent { padding: 0; }
  .join { padding: 14px 0 calc(10px + var(--safe-b)); gap: 9px; }

  .board-head { padding-top: max(10px, env(safe-area-inset-top)); }
  .board { margin: 10px 0 0; }
  .board li { padding: 9px 10px; }
  .board-note { margin: 10px 0 0; }
  .offramp { margin: 16px 0 calc(14px + var(--safe-b)); padding: 16px 16px; gap: 8px; }
}
