/* ═══════════════════════════════════════════════════════════════════
   FORMULA DREAM — Paddock Pop shared design system
   Ported from the Claude Design build. Flat brights, black stamp
   icons, chunky rounded tiles. No gradients, no glows.
   ═══════════════════════════════════════════════════════════════════ */

:root {
  --ink:      #17150F;
  --paper:    #F3EFE4;
  --sand:     #E9E3D2;
  --card:     #FFFCF2;
  --tile:     #EDE7D6;
  --edge:     #D8D0BC;
  --muted:    #8A8171;

  --red:      #FF4A2E;
  --blue:     #2E77FF;
  --green:    #2FB457;
  --amber:    #FFC531;

  /* ── vertical rhythm ──────────────────────────────────────────────
     ONE scale, so the distance between two things says how related they
     are rather than what number was to hand when the rule was written.

     Before this the games ran on 6, 9, 11, 13, 14, 15, 20 and 22px picked
     a rule at a time, and it showed: the result screen put 72px between a
     panel and its own caption and 9px between that caption and the board,
     which reads as the caption belonging to the board. Spacing is the
     grouping — it is doing that job whether or not it was chosen.

       --s1  a hairline break inside one element
       --s2  items inside a group
       --s3  a group's own internal rhythm
       --s4  between two related groups
       --s5  between two SEPARATE groups   <- the one that does the work
       --s6  a section break                                            */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 18px;
  --s5: 28px;
  --s6: 40px;

  /* ── the same two accents, for TEXT on paper ──────────────────────
     An accent that is right as a fill is usually wrong as a letterform.
     Measured on --paper: --green is 2.35:1 and --red is 2.91:1, so a P1
     stamp, a DNF stamp and every red warning line were below AA — and the
     warning line is the only thing telling a player their word bounced.

     Hue and saturation are held; only lightness moves, and only as far as
     4.5:1 and no further, because an accent darker than it needs to be is a
     different brand. Use these ONLY for text sitting on a light surface.
     On the dark share card and on the dark-mode games the bright originals
     are already 6.8:1 and are the correct choice there. */
  --green-ink: #207C3C;   /* 4.55:1 on --paper, 5.10:1 on --card */
  --red-ink:   #D61D00;   /* 4.52:1 on --paper, 5.07:1 on --card */
  --pink:     #FF5CA8;
  --purple:   #7C5CFF;
  --lime:     #C8F04B;

  /* dark-mode games (Lights Out) */
  --d-bg:     #0C0B09;
  --d-surf:   #141310;
  --d-panel:  #201E17;
  --d-tile:   #2A2820;

  --display: 'Archivo', 'Arial Black', system-ui, sans-serif;
  --mono:    'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--sand);
  font-family: var(--display);
  color: var(--ink);
  overscroll-behavior: none;
}

body.dark { background: var(--d-bg); color: var(--paper); }

a { color: inherit; text-decoration: none; }
button { font-family: var(--display); cursor: pointer; }
input { font-family: var(--display); }

/* ── app shell ──────────────────────────────────────────────────── */
/* Every game declares `viewport-fit=cover`, which is what lets the paper
   colour run to the edges of a notched phone — and also what puts the
   bottom of the layout underneath the home indicator. Nothing was insetting
   for it, so the last row of anything sat in the gesture area: on Pit Word
   that was ENTER and ⌫, which is the whole bottom row of the keyboard.
   Reported as "the keyboard is too low down"; this is the half of it that
   only exists on a real handset, so no desktop viewport ever showed it. */
.fd-app {
  max-width: 470px;
  min-height: 100dvh;
  margin: 0 auto;
  background: var(--paper);
  display: flex;
  flex-direction: column;
  position: relative;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
body.dark .fd-app { background: var(--d-surf); }

/* ── type ───────────────────────────────────────────────────────── */
.fd-display {
  font-weight: 900;
  font-stretch: 125%;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  line-height: 0.95;
}
.fd-mono {
  font-family: var(--mono);
  font-weight: 700;
  letter-spacing: 0.1em;
}
.fd-label {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* Available to a screen reader, invisible on screen. The build had no such
   utility, which is why colour ended up carrying meaning on its own in more
   than one place — the fastest day in Grid Rush's form strip was purple and
   nothing else, so a player who cannot separate the hues, or who is not looking
   at all, had no way to know which row it was.

   `clip-path` rather than `display: none` or `visibility: hidden`, both of
   which remove the text from the accessibility tree as well as the screen.
   `white-space: nowrap` stops a one-word insert wrapping the line it sits in
   back to a second row before it is clipped. */
.fd-sr {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ── header ─────────────────────────────────────────────────────── */
.fd-head {
  padding: 18px 16px 8px;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
/* The chips are nowrap and the title has no min-width, so below ~300px the chip
   group was pushed past the screen edge and every game scrolled sideways
   (measured: 20px over on a folded Galaxy Fold, 280px).

   ⚠ SCOPED TO <300px ON PURPOSE. An unscoped `flex-wrap: wrap` also wraps at
   320px, where the row still fits — costing 20px of header height and pushing
   the launch button from y=567 to y=587, i.e. below the fold on a 568px-tall
   iPhone SE. Fixing the narrowest phone must not cost the second-narrowest its
   primary action. Ellipsising the title was the other option and was rejected:
   `line-height: 1` clips its descenders. */
@media (max-width: 300px) {
  .fd-head { flex-wrap: wrap; }
}
.fd-back {
  width: 34px; height: 34px;
  border-radius: 999px;
  border: 2px solid var(--ink);
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; font-weight: 800;
  flex-shrink: 0;
}
body.dark .fd-back { border-color: var(--paper); color: var(--paper); }

.fd-badge {
  width: 30px; height: 30px;
  border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
/* The badge repeats the icon on the tile the player just pressed, so it is
   the first thing to go when the row runs out of room. */
@media (max-width: 358px) { .fd-badge { display: none; } }

/* ── identity block ───────────────────────────────────────────────
   Title over a mono sub-line. The block is the only thing in the header
   allowed to flex, so the action buttons keep their size at every width and
   a long name truncates instead of pushing them off the screen.

   `min-width: 0` is load-bearing — without it a flex item refuses to shrink
   below its content and the ellipsis never engages. */
.fd-id {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
}
.fd-title {
  font-size: 16px; line-height: 1.1; margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
@media (max-width: 340px) { .fd-title { font-size: 14.5px; } }

/* State, not actions: what day this is and how the run is going. Reading
   matter, so it is quiet — the buttons beside it are the loud things. */
.fd-meta {
  display: flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-weight: 700;
  font-size: 9px; letter-spacing: 0.1em;
  white-space: nowrap; overflow: hidden;
  min-height: 12px;
}
/* 0.55 measured 3.89:1 against --paper — under AA for 9px text, and this is
   the line carrying the day number. Quiet had been taken one step past legible.
   0.68 is 6.0:1 and still reads as the sub-line it is.

   Raising an opacity is safe in every palette: it moves the text further from
   whatever is behind it, never closer, so the dark-bodied games gain by the
   same change rather than needing their own. */
.fd-meta > span { opacity: 0.68; }
/* The run is the one bright thing in the sub-line, because it is the one
   thing the player stands to lose. */
.fd-meta .fd-run {
  opacity: 1; color: var(--ink);
  border-radius: 999px; padding: 2px 7px;
  font-size: 8.5px; letter-spacing: 0.08em;
  /* Never let it be the thing that gets cut. A pill reading "4-DAY" is worse
     than no pill: it looks like a bug, and the word it lost is the one that
     made it mean anything. */
  flex-shrink: 0; white-space: nowrap;
}
.fd-meta .fd-chip { padding: 2px 7px; font-size: 8.5px; border-width: 1px; }

/* ── header actions ──────────────────────────────────────────────
   Archive, leaderboard, help. All the same size, always in the same order,
   so the one a player wants is in the same place in every game. */
.fd-acts {
  margin-left: auto; flex-shrink: 0;
  display: flex; align-items: center; gap: 6px;
}
/* ⚠ INHERITED, NOT HARDCODED. These were `var(--ink)` — #17150F — which is
   correct on paper and invisible on every dark game. Three of the eight set
   their own near-black background under a `body.gr` / `body.pb` / `body.sl`
   class rather than `body.dark`, so the one override that existed missed all
   of them: measured 1.2:1 against #0B0C0E on Grid Rush. The archive and the
   leaderboard were reachable only by a player who knew to press a circle they
   could not see.

   Inheriting is what makes it right in all eight without eight patches. Every
   game already declares a text colour on <body>; a control that borrows it can
   never be the one thing on the page in the wrong palette. */
.fd-act {
  width: 30px; height: 30px; padding: 0; flex-shrink: 0;
  border-radius: 999px; border: 1.5px solid currentColor;
  background: transparent; color: inherit;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-weight: 700; font-size: 13px;
  /* kills the double-tap-zoom delay on the smallest targets in the app */
  touch-action: manipulation;
}
.fd-act:active { transform: scale(0.92); }
/* Filled while the archive is showing a day that is not today. */
.fd-act.on { color: var(--ink); }
@media (max-width: 320px) { .fd-acts { gap: 4px; } }

/* ── tap targets on the header controls ───────────────────────────
   The circles draw at 30px and the back arrow at 34px. Measured 2026-08-02,
   the effective hit area was 28×28 and 32×32 — which passes WCAG 2.5.8 AA
   (a 24px circle centred on each clears its neighbour) but sits well under
   the 44px both Apple and Android ask for. On the archive and help buttons
   that matters more than it looks: they are the two smallest controls in the
   app and they sit in the corner the thumb reaches for last.

   ⚠ GROWN WITH AN OVERLAY, NOT BY RESIZING THE BUTTONS. The header has no
   horizontal slack to give — measured across all seven games that use it at
   280/320/360/390px, `.fd-title` already truncates at 360px on Grid Recall
   and Radio Gaga. Every pixel the action group takes comes straight off the
   game's own name, so the drawn size must not change.

   Vertical is free: the header measures 58–60px tall, so 44 fits inside it
   without moving anything.

   Horizontal is capped at half the gap on purpose. Two 44px areas whose
   centres are 36px apart overlap by 8px, and a tap in the overlap goes to
   whichever button comes last in the DOM — so pushing for the full 44 would
   hand some archive taps to help. A small target beats a stolen one. */
.fd-act, .fd-back { position: relative; }
.fd-act::after, .fd-back::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  height: 44px;
  width: calc(100% + 6px);
}
/* The back arrow has no neighbour within 10px, so it takes the full 44. */
.fd-back::after { width: 44px; }
@media (max-width: 320px) { .fd-act::after { width: calc(100% + 4px); } }

.fd-chip {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 0.08em;
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  padding: 5px 9px;
  white-space: nowrap;
}
body.dark .fd-chip { border-color: var(--paper); }
.fd-chip.solid { border: none; color: var(--ink); }

/* Help affordance. Once a game ships a how-to-play sheet, a first-time
   player dismisses it and has no way back to the rules — this is that way. */
.fd-help {
  width: 26px; height: 26px; padding: 0;
  border-radius: 999px;
  border: 1.5px solid var(--ink);
  background: transparent; color: var(--ink);
  font-family: var(--mono); font-weight: 700; font-size: 12px;
  flex-shrink: 0;
}
body.dark .fd-help { border-color: var(--paper); color: var(--paper); }

/* ── how to play sheet ──────────────────────────────────────────── */
.fd-scrim {
  position: fixed; inset: 0; z-index: 900;
  background: rgba(23, 21, 15, 0.58);
  display: flex; align-items: flex-end; justify-content: center;
  padding: 14px;
  animation: fd-fade 0.18s ease both;
}
.fd-sheet {
  width: 100%; max-width: 442px;
  max-height: calc(100dvh - 28px); overflow-y: auto;
  background: var(--card); color: var(--ink);
  border: 2px solid var(--ink); border-radius: 24px;
  padding: 18px 17px 15px;
  display: flex; flex-direction: column; gap: 13px;
  animation: fd-sheet-in 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.05) both;
}
body.dark .fd-sheet { background: var(--d-panel); color: var(--paper); border-color: var(--d-tile); }

/* ── sheets on a desktop frame ──────────────────────────────────────
   A sheet is a bottom sheet because that is where a thumb is. With a mouse it
   is a dialog, and the same rules read wrong: audited at 1440x900, the record
   sheet was a 442px strip pinned to the bottom of a 998px card, and the scrim
   at 0.58 left the lobby headline behind it fully legible and competing for
   the eye. Centred, wider, and dimmed enough that the thing behind it is
   context rather than content. */
@media (min-width: 900px) and (pointer: fine) {
  .fd-scrim { align-items: center; background: rgba(23, 21, 15, 0.74); }
  .fd-sheet { max-width: 560px; max-height: calc(100dvh - 72px); }
}
/* The sheet takes focus on open so Escape and screen readers land in the right
   place, but a ring around the whole dialog tells a sighted user nothing they
   cannot already see. Everything interactive inside it still rings on Tab. */
.fd-sheet:focus, .fd-sheet:focus-visible { outline: none; }

.fd-sheet-head { display: flex; align-items: center; gap: 9px; }
.fd-sheet-head .fd-badge { width: 26px; height: 26px; border-radius: 8px; }
.fd-sheet-title { font-size: 15px; line-height: 1; }
/* 0.5 measured 3.43:1 on --card. Same reasoning as .fd-meta above. */
.fd-sheet-kicker {
  margin-left: auto; font-family: var(--mono); font-weight: 700;
  font-size: 9px; letter-spacing: 0.14em; opacity: 0.72;
}

/* Steps are genuinely ordered — you look, then they vanish, then you rebuild —
   so the numbers carry information rather than decorate.

   Sized to be READ AT A GLANCE, not studied. A rules sheet full of prose is a
   sheet nobody reads, so each step is one short line at a size you can take in
   without focusing, and anything longer belongs in the game itself. */
.fd-steps { display: flex; flex-direction: column; gap: 9px; }
.fd-step { display: flex; gap: 10px; align-items: center; }
.fd-step-n {
  flex-shrink: 0; width: 22px; height: 22px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  color: var(--ink);
}
.fd-step-t { font-size: 15px; font-weight: 700; line-height: 1.25; }
.fd-step-t b { font-weight: 900; }

/* The one-line summary above the steps: what the game is, in six words. */
.fd-sheet-lede {
  font-weight: 900; font-stretch: 125%; text-transform: uppercase;
  font-size: 19px; line-height: 1.05; letter-spacing: -0.01em;
}

.fd-sheet-rule { height: 1.5px; background: currentColor; opacity: 0.14; border: 0; margin: 1px 0; }

@keyframes fd-fade     { from { opacity: 0 } to { opacity: 1 } }
@keyframes fd-sheet-in { from { opacity: 0; transform: translateY(26px) } to { opacity: 1; transform: none } }

/* ── the archive calendar ─────────────────────────────────────────
   Lifted out of Pit Word, where it shipped first, and made shared.

   Deliberately no new visual language: a day you solved is a green tile, a
   day you lost is a dark tile, a day still open is an empty tile with an ink
   border. Those are the first three colours Pit Word teaches, and the same
   three every other game uses for the same three ideas, so the calendar is
   legible before anyone explains it. */
.fd-cal { display: flex; flex-direction: column; gap: 12px; }
.fd-cal-nav { display: flex; align-items: center; gap: 8px; }
.fd-cal-step {
  width: 34px; height: 34px; flex-shrink: 0; padding: 0;
  border: 1.5px solid var(--ink); border-radius: 9px;
  background: var(--card); color: var(--ink);
  font-size: 17px; font-weight: 700; line-height: 1;
  touch-action: manipulation;
}
.fd-cal-step[disabled] { opacity: 0.25; }
.fd-cal-month {
  flex: 1; text-align: center; font-family: var(--mono);
  font-size: 11px; font-weight: 700; letter-spacing: 0.14em;
}
.fd-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
.fd-cal-dow {
  font-family: var(--mono); font-size: 8px; font-weight: 700;
  letter-spacing: 0.06em; opacity: 0.45; text-align: center; padding-bottom: 2px;
}
.fd-cal-day {
  aspect-ratio: 1/1; padding: 0; border-radius: 9px;
  border: 2px solid var(--edge); background: var(--tile); color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-stretch: 110%; font-size: 12px;
  touch-action: manipulation;
}
/* Playable and untouched: an empty board row waiting to be typed into. */
.fd-cal-day.open,   .fd-cal-sw.open   { background: var(--card); border-color: var(--ink); }
.fd-cal-day.won,    .fd-cal-sw.won    { background: #2FB457; border-color: #2FB457; color: #FFFCF2; }
.fd-cal-day.lost,   .fd-cal-sw.lost   { background: #2A2820; border-color: #2A2820; color: #FFFCF2; }
/* For the games that record a score rather than a verdict — a day with a
   result on it, no better or worse than any other. */
.fd-cal-day.played, .fd-cal-sw.played { background: var(--amber); border-color: var(--amber); color: var(--ink); }
/* Outside launch..today. Not hidden — an empty cell would read as a broken
   grid — but visibly not a thing you can press. */
.fd-cal-day.void { opacity: 0.2; border-style: dashed; }
.fd-cal-day.here { box-shadow: 0 0 0 2px var(--card), 0 0 0 4px var(--red); }
.fd-cal-day:not([disabled]):active { transform: scale(0.93); }
.fd-cal-legend {
  display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;
  font-family: var(--mono); font-size: 8.5px; font-weight: 600;
  letter-spacing: 0.06em; opacity: 0.7;
}
.fd-cal-legend > span { display: flex; align-items: center; gap: 5px; }
/* A swatch shares the day cell's COLOURS but not its class. Reusing
   `.fd-cal-day` for the key meant `querySelectorAll('.fd-cal-day')` returned
   three legend swatches along with the month, so anything counting cells
   counted them too — and a <span> with no date on it failed the "every cell
   has a label" check. The colours still cannot drift: the state rules above
   name both selectors. */
.fd-cal-sw {
  width: 11px; height: 11px; border-radius: 3px; flex-shrink: 0;
  border: 1.5px solid var(--edge); background: var(--tile);
}
.fd-cal-note { font-size: 11px; font-weight: 600; line-height: 1.4; opacity: 0.7; text-align: center; }

/* Playing a day that is not today. A quiet band, not an alarm — the archive
   is a normal way to use the game, not an error state. */
.fd-archive-bar {
  display: flex; align-items: center; gap: 9px;
  background: var(--tile); border: 1.5px dashed var(--ink); border-radius: 11px;
  padding: 7px 11px; font-family: var(--mono);
  font-size: 9px; font-weight: 700; letter-spacing: 0.1em;
}
body.dark .fd-archive-bar { background: var(--d-tile); border-color: var(--muted); }
.fd-archive-bar button {
  margin-left: auto; border: none; background: none; padding: 4px 0 4px 8px;
  font: inherit; color: var(--red); text-decoration: underline; letter-spacing: 0.1em;
}

/* ── the leaderboard ──────────────────────────────────────────────
   A whole screen, and dressed in the game it belongs to.

   It used to be a cream bottom sheet, identical in all eight games, sitting
   over half of a game the player was not playing. Two things were wrong with
   that. A board is somewhere you go, not something that interrupts you — so
   it is a screen now, and only the rows scroll. And a cream card is Pit
   Word's world; landing it on Guess the Driver's card table or Grid Rush's
   black looked like a dialog from another app.

   So the structure is fixed and the SKIN varies. Every game gets the same
   ranked rows in the same places, and a set of `--b-*` tokens carried down
   from the scrim's `data-game` decides the surface, the accent, the shape of
   a row and the shape of the rank marker. One rule block per game, below.

   What does NOT vary: the player's own row is the only filled one. That is
   still the only reason anyone opens this screen. Pole gets a ring in the
   accent, never a fill, so it cannot compete with it. No medals, no avatars,
   no podium illustration.

   Deliberately not `.fd-sheet`: Grid Rush, Pit Board and Guess the Driver all
   restyle that class for their own how-to, and would have repainted the board
   with it. */

/* Defaults. A game with no skin below still gets a legible board — the
   Paddock Pop cream, which is what every unstyled surface in this build is. */
.fd-scrim.full {
  --b-surf: var(--paper);  --b-surround: var(--sand);
  --b-panel: var(--tile);  --b-ink: var(--ink);
  --b-line: var(--edge);   --b-acc: var(--red);   --b-on: var(--card);
  --b-warn: #FF4A2E;
  /* ⚠ TWO ACCENTS, ON PURPOSE. `--b-acc` is the accent as a FILL — the active
     tab, the player's row, the badge — where it sits under dark text and the
     contrast is the fill's problem, not the colour's. `--b-acc-t` is the same
     accent as small TEXT or a hairline, where it has to carry itself against
     the surface.

     On the three cream skins the game's own colour cannot: measured against
     #F3EFE4, Pit Word's red is 2.9:1, Radio Gaga's pink 2.5:1 and Grid
     Recall's purple 3.8:1, all under the 4.5:1 that 9.5px type needs. Each of
     those three darkens its own hue rather than borrowing a neutral, so the
     screen still reads as the game. The five dark skins need no such help and
     leave this pointing at `--b-acc`. */
  --b-acc-t: var(--b-acc);
  --b-row-fill: var(--card); --b-row-bc: var(--edge); --b-row-bw: 1.5px; --b-row-r: 12px;
  --b-chip-fill: var(--tile); --b-chip-bc: var(--edge); --b-chip-bw: 1.5px;
  --b-chip-r: 9px; --b-chip-ink: inherit;

  padding: 0; align-items: stretch; justify-content: center;
  background: var(--b-surround);
}
.fd-board {
  width: 100%; max-width: 470px; height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--b-surf); color: var(--b-ink);
  padding: calc(env(safe-area-inset-top, 0px) + 16px) 0 env(safe-area-inset-bottom, 0px);
  animation: fd-board-in 0.26s cubic-bezier(0.2, 0.9, 0.3, 1.02) both;
}
.fd-board:focus, .fd-board:focus-visible { outline: none; }
@keyframes fd-board-in { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }

/* ── board masthead ───────────────────────────────────────────────
   The title gets its own line rather than sharing one with the controls.
   Sharing meant LEADERBOARD had 262px to sit in at 390px and 192px at 320px,
   which is narrower than the word. On its own line it has the full column and
   can be the size a screen title should be. */
.fd-board-top { flex-shrink: 0; padding: 0 18px 16px; }
.fd-board-top-row { display: flex; align-items: center; margin-bottom: 14px; }
.fd-board-x {
  width: 36px; height: 36px; padding: 0; flex-shrink: 0;
  border-radius: 999px; border: 1.5px solid var(--b-line);
  background: transparent; color: var(--b-ink);
  font-size: 17px; font-weight: 800; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  touch-action: manipulation;
}
.fd-board-x:active { transform: scale(0.92); }
/* The shared badge hides below 358px because the GAME header runs out of room
   there. This one has a row to itself with only the back button on it, so the
   width that forces the header's hand does not apply — and the badge is the
   only thing on the screen carrying the game's mark. */
.fd-board .fd-board-badge {
  display: flex; margin-left: auto;
  width: 34px; height: 34px; border-radius: 10px;
}
.fd-board-eyebrow {
  font-family: var(--mono); font-weight: 700; font-size: 9.5px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--b-acc-t);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Sized to leave slack, not to fit exactly. At 8.4vw the word measured the
   full width of its column at every handset size — flush and good-looking, and
   luck: Archivo is a webfont, and the moment it fails to load the fallback is
   Arial Black, which is wider. It overflowed at 280px even WITH the font. This
   keeps ~8% in hand at every width, which the fallback fits inside. */
.fd-board-title { margin: 5px 0 0; font-size: clamp(23px, 7.7vw, 32px); line-height: 0.92; }

/* ── board body ───────────────────────────────────────────────────
   Tabs pinned, rows scrolling. `min-height: 0` on both is what lets the
   scroller actually shrink inside the flex column instead of pushing the
   screen taller than the viewport. */
.fd-lb { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.fd-lb-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 0 18px 24px; -webkit-overflow-scrolling: touch;
}

.fd-board .fd-lb-tabs {
  display: flex; flex-shrink: 0; align-self: stretch;
  margin: 0 18px 16px; border: 1.5px solid var(--b-line);
}
.fd-board .fd-lb-tabs button {
  flex: 1 1 0; padding: 11px 8px; font-size: 10px;
  background: transparent; color: var(--b-ink); opacity: 0.68;
}
.fd-board .fd-lb-tabs button.on { background: var(--b-acc); color: var(--b-on); opacity: 1; }

/* `min-height: 100%` costs nothing when there are rows — they are already
   taller than the scroller — and is what lets an empty state centre itself in
   the space instead of hanging off the top of a screen-high void. */
.fd-lb-pane { display: flex; flex-direction: column; gap: 6px; min-height: 100%; }

/* Column key and rows share their grid exactly, so PLAYER sits over the names
   and the unit sits over the numbers at every width. */
.fd-board .fd-lb-head {
  display: grid; grid-template-columns: 36px 1fr auto; gap: 12px;
  padding: 2px 12px 8px;
  font-family: var(--mono); font-weight: 700; font-size: 8.5px;
  /* 0.45 measured 2.9:1 on the cream skins. A column key is small type and
     still has to be read. */
  letter-spacing: 0.12em; opacity: 0.62;
}
.fd-board .fd-lb-row {
  display: grid; grid-template-columns: 36px 1fr auto; gap: 12px;
  align-items: center; padding: 10px 12px;
  border-radius: var(--b-row-r);
  background: var(--b-row-fill);
  border: var(--b-row-bw) solid var(--b-row-bc);
}
/* The player's own row. The only filled thing on the screen. */
.fd-board .fd-lb-row.me {
  background: var(--b-acc); color: var(--b-on); border-color: var(--b-acc);
}
/* The key's cells inherit the key's own type. They are scoped separately from
   the row's cells on purpose: sharing one rule sized PLAYER and GUESSES like
   the names underneath them, and the column key stopped reading as a key. */
.fd-board .fd-lb-head .fd-lb-rank { text-align: center; }

/* Inside a row the rank is a marker, not a number in a column: the shape is
   where each skin speaks. 36px square keeps it a real target-sized object and
   gives the row a 58px height nobody has to aim at. */
.fd-board .fd-lb-row .fd-lb-rank {
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-weight: 700; font-size: 12.5px;
  border-radius: var(--b-chip-r);
  background: var(--b-chip-fill);
  border: var(--b-chip-bw) solid var(--b-chip-bc);
  color: var(--b-chip-ink);
}
/* Pole. A ring, never a fill — the fill belongs to the player's row. Only ever
   on ONE row: see soloPole() in fd.js for why a shared pole gets no ring. */
.fd-board .fd-lb-row .fd-lb-rank.p1 {
  background: transparent; border: 1.5px solid var(--b-acc-t); color: var(--b-acc-t);
}
/* Punched out of the row in the SURFACE colour. It was --b-on, which is the
   text colour that sits ON the accent — near-black on five of the eight — so
   the marker came out dark type on a dark plate: 2.8:1 on Radio Gaga. The
   surface is the one colour guaranteed to carry the accent, since every other
   marker on the screen is already sitting on it. */
.fd-board .fd-lb-row.me .fd-lb-rank,
.fd-board .fd-lb-row.me .fd-lb-rank.p1 {
  background: var(--b-surf); border-color: transparent; color: var(--b-acc-t);
}
.fd-board .fd-lb-row .fd-lb-name {
  font-weight: 800; font-stretch: 110%; font-size: 14.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fd-board .fd-lb-row .fd-lb-score { font-family: var(--mono); font-weight: 700; font-size: 14px; }

/* Where the player stands, at the size the fact deserves now there is room. */
.fd-board .fd-lb-you {
  display: flex; align-items: baseline; gap: 10px;
  padding: 12px 14px; margin-bottom: 12px;
  border-radius: 12px; background: var(--b-panel);
  border: 1.5px solid var(--b-line);
}
.fd-lb-you-k, .fd-lb-you-of {
  font-family: var(--mono); font-weight: 700; font-size: 9.5px;
  letter-spacing: 0.12em; opacity: 0.68;
}
.fd-lb-you-of { margin-left: auto; }
.fd-board .fd-lb-you b {
  font-weight: 900; font-stretch: 125%; font-size: 26px;
  line-height: 1; letter-spacing: -0.01em; color: var(--b-acc-t);
}

/* A skin's own device, drawn above the first row. Off unless a game claims it;
   Grid Recall is the one that does. */
.fd-lb-mark { display: none; }

/* Says what the rows below it are before anyone reads them as real. Dashed,
   like every other "this is provisional" band in the build, and deliberately
   NOT skinned — a warning that takes the game's colours stops looking like a
   warning, and this one is the difference between a mock and a lie. */
.fd-lb-banner {
  border: 1.5px dashed var(--b-warn); border-radius: 12px;
  padding: 9px 11px; margin-bottom: 12px;
  font-size: 11px; font-weight: 600; line-height: 1.4;
}
.fd-lb-banner b {
  display: block; font-family: var(--mono); font-size: 9px;
  letter-spacing: 0.14em; color: var(--b-warn); margin-bottom: 3px;
}
.fd-lb-load {
  text-align: center; padding: 46px 0;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; opacity: 0.5;
}
.fd-lb-empty { text-align: center; padding: 8px; margin: auto; max-width: 32ch; }
.fd-lb-empty-t { font-weight: 900; font-stretch: 125%; text-transform: uppercase; font-size: 21px; }
.fd-lb-empty-p { font-size: 12.5px; font-weight: 600; line-height: 1.5; opacity: 0.75; margin-top: 7px; }

/* ── the eight skins ──────────────────────────────────────────────
   Each one is read off the game it belongs to, not invented for it: the
   surface is the game's own background, the accent is the colour that game
   already uses for its primary action, and the rank marker is the shape that
   game already draws. Same specificity throughout, so nothing here can win by
   accident — order is not load-bearing. */

/* Pit Word — cream paper, letter tiles, the red ENTER key. */
.fd-scrim.full[data-game="pit-word"] {
  --b-acc-t: #C22A12;
  --b-surf: #F3EFE4; --b-surround: #E9E3D2; --b-panel: #EDE7D6;
  --b-ink: #17150F; --b-line: #D8D0BC; --b-acc: #FF4A2E; --b-on: #17150F;
  --b-row-fill: #FFFCF2; --b-row-bc: #D8D0BC; --b-row-bw: 1.5px; --b-row-r: 12px;
  --b-chip-fill: #EDE7D6; --b-chip-bc: #D8D0BC; --b-chip-bw: 2px; --b-chip-r: 9px;
}

/* Grid Rush — the black screen and the acid green it counts in. */
.fd-scrim.full[data-game="grid-rush"] {
  --b-surf: #0B0C0E; --b-surround: #06070880; --b-panel: #14161A;
  --b-ink: #F2F5F7; --b-line: #2A2F38; --b-acc: #16DB65; --b-on: #06180D;
  --b-warn: #FF7A62;
  --b-row-fill: #14161A; --b-row-bc: #2A2F38; --b-row-bw: 1.5px; --b-row-r: 10px;
  --b-chip-fill: transparent; --b-chip-bc: #2A2F38; --b-chip-bw: 1.5px; --b-chip-r: 5px;
}

/* Grid Recall — painted tarmac boxes under a start line. Both are the game's
   own signature, so the rank marker is a grid box and the board opens on the
   line the grid starts from. */
.fd-scrim.full[data-game="grid-recall"] {
  --b-acc-t: #5B3FD9;
  --b-surf: #F3EFE4; --b-surround: #E9E3D2; --b-panel: #E9E3D2;
  --b-ink: #17150F; --b-line: #D6CEB8; --b-acc: #6A45E8; --b-on: #F7F4FF;
  --b-row-fill: #E9E3D2; --b-row-bc: #D6CEB8; --b-row-bw: 1.5px; --b-row-r: 10px;
  --b-chip-fill: #D2C9B0; --b-chip-bc: transparent; --b-chip-bw: 0px; --b-chip-r: 8px;
}
/* ⚠ The skin attribute lives on the SCRIM, not the board — it has to, because
   the surround colour outside the 470px column reads from it too. A
   `.fd-board[data-game=…]` selector matches nothing. */
.fd-scrim.full[data-game="grid-recall"] .fd-lb-mark {
  display: block; padding-bottom: 5px; margin-bottom: 12px;
  border-bottom: 3px solid var(--b-ink);
}
.fd-scrim.full[data-game="grid-recall"] .fd-lb-mark::before {
  content: 'START';
  font-family: var(--mono); font-weight: 700; font-size: 9px;
  letter-spacing: 0.14em; opacity: 0.5;
}

/* Pit Board — the pit wall. Dark rows, and the position read as an LED digit
   in the same green the game counts its remaining drivers in. */
.fd-scrim.full[data-game="pit-board"] {
  --b-surf: #101216; --b-surround: #08090C80; --b-panel: #191D23;
  --b-ink: #F0F3F6; --b-line: #2C333C; --b-acc: #23C552; --b-on: #06180D;
  --b-warn: #FF7A62;
  --b-row-fill: #1F242B; --b-row-bc: transparent; --b-row-bw: 1.5px; --b-row-r: 11px;
  --b-chip-fill: transparent; --b-chip-bc: transparent; --b-chip-bw: 0px;
  --b-chip-r: 0px; --b-chip-ink: #23C552;
}
/* The LED has no plate, so the shared "invert the marker on the player's row"
   rule gave it one: a hard black square with a zero radius, which read as a
   rendering fault rather than a highlight. An unlit digit on the lit row says
   the same thing and stays a digit. */
.fd-scrim.full[data-game="pit-board"] .fd-lb-row.me .fd-lb-rank {
  background: transparent; color: var(--b-on);
}

/* Lights Out — the gantry. Round lamps, unlit until pole lights one. */
.fd-scrim.full[data-game="lights-out"] {
  --b-surf: #0C0B09; --b-surround: #05050480; --b-panel: #1A1814;
  --b-ink: #F3EFE4; --b-line: #2A2820; --b-acc: #2FB457; --b-on: #0C0B09;
  --b-warn: #FF7A62;
  --b-row-fill: #1A1814; --b-row-bc: #2A2820; --b-row-bw: 1.5px; --b-row-r: 14px;
  --b-chip-fill: #26241C; --b-chip-bc: transparent; --b-chip-bw: 0px; --b-chip-r: 999px;
}

/* Radio Gaga — thick black outlines on cream and the pink of a live clip. */
.fd-scrim.full[data-game="radio-gaga"] {
  --b-acc-t: #B02463;
  --b-surf: #F3EFE4; --b-surround: #E9E3D2; --b-panel: #FFFCF2;
  --b-ink: #17150F; --b-line: #17150F; --b-acc: #FF5CA8; --b-on: #17150F;
  --b-row-fill: #FFFCF2; --b-row-bc: #17150F; --b-row-bw: 2px; --b-row-r: 13px;
  --b-chip-fill: transparent; --b-chip-bc: #17150F; --b-chip-bw: 2px; --b-chip-r: 999px;
}

/* Steady Lap — the dark cockpit, and the lime of the button that starts it. */
.fd-scrim.full[data-game="steady-lap"] {
  --b-surf: #141310; --b-surround: #0A0A0880; --b-panel: #201E17;
  --b-ink: #F3EFE4; --b-line: #35322A; --b-acc: #C8F04B; --b-on: #17150F;
  --b-warn: #FF7A62;
  --b-row-fill: #201E17; --b-row-bc: #35322A; --b-row-bw: 1.5px; --b-row-r: 12px;
  --b-chip-fill: #2A2820; --b-chip-bc: transparent; --b-chip-bw: 0px; --b-chip-r: 5px;
}

/* Guess the Driver — the card table. Felt, gold rules, and the rank on the
   back of a claret card. */
.fd-scrim.full[data-game="guess-the-driver"] {
  --b-surf: #073B27; --b-surround: #052B1C; --b-panel: #0A5334;
  --b-ink: #FBF6E9; --b-line: #1E7A52; --b-acc: #D4AF37; --b-on: #052B1C;
  /* Pole is a ring with no fill, so it sits on the ROW felt rather than the
     surface, and #D4AF37 measured 3.8:1 there. A lighter gold, still gold. */
  --b-acc-t: #E8C766;
  --b-warn: #F0A5A5;
  --b-row-fill: #0B5D3B; --b-row-bc: #1E7A52; --b-row-bw: 1.5px; --b-row-r: 10px;
  --b-chip-fill: #9E1B32; --b-chip-bc: #D4AF37; --b-chip-bw: 1px;
  --b-chip-r: 5px; --b-chip-ink: #FBF6E9;
}

/* ── settings ───────────────────────────────────────────────────── */
.fd-set { display: flex; flex-direction: column; }
/* Rules go on the TOP of every row after the first, not the bottom of every
   row but the last. `:last-of-type` looked equivalent and is not: the footer
   under these rows is a <div> too, so the last ROW was never the last div of
   its type and kept a rule that then floated above the footer with nothing
   under it. */
.fd-set-row {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 0;
}
.fd-set-row + .fd-set-row {
  border-top: 1.5px solid currentColor;
  border-top-color: color-mix(in srgb, currentColor 14%, transparent);
}
.fd-set-txt { flex: 1; min-width: 0; }
.fd-set-label { font-weight: 800; font-size: 14.5px; letter-spacing: -0.01em; }
.fd-set-note { font-size: 12.5px; font-weight: 600; line-height: 1.4; opacity: 0.62; margin-top: 2px; }
/* A row with nothing to press must not look like one that has. */
.fd-set-row.static .fd-set-label { opacity: 0.75; }

/* The switch reads as a switch at a glance: a track that fills, and a knob
   that travels. Colour alone would leave it ambiguous to anyone who cannot
   tell the two states apart by hue. */
.fd-set-switch {
  flex: none; width: 52px; height: 31px; border-radius: 999px;
  border: 2px solid currentColor; background: transparent;
  padding: 0; position: relative; cursor: pointer;
  opacity: 0.45; transition: opacity 140ms ease, background-color 140ms ease;
}
.fd-set-switch i {
  position: absolute; top: 2.5px; left: 2.5px;
  width: 22px; height: 22px; border-radius: 999px;
  background: currentColor; display: block;
  transition: transform 160ms cubic-bezier(0.3, 1.4, 0.5, 1);
}
.fd-set-switch.on {
  opacity: 1;
  background: var(--fd-on, currentColor);
  border-color: var(--fd-on, currentColor);
}
.fd-set-switch.on i { transform: translateX(21px); background: var(--ink); }
.fd-set-switch:active { transform: scale(0.94); }
/* 0.62, not 0.45. Measured 4.22:1 at 0.45 against the Grid Rush surface, which
   is under the 4.5 floor for a line a player is meant to read — it is the only
   place the sheet says these switches are global rather than per game. An
   opacity-dimmed label is the usual way this fails; 0.62 clears it. */
.fd-set-foot {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; opacity: 0.62; padding-top: 14px; text-align: center;
}
@media (prefers-reduced-motion: reduce) {
  /* The knob still travels, it just stops springing. Removing the movement
     altogether would leave the two states told apart by opacity alone. */
  .fd-set-switch i { transition-duration: 1ms; }
}

/* ── body ───────────────────────────────────────────────────────── */
/* `flex: 1` (basis 0) with `min-height: 0` let the body shrink below its
   own content. Because .fd-app is min-height:100dvh the overflow did not
   extend scrollHeight either, so on a short phone the content was clipped
   AND unreachable — you could not scroll to it. `1 0 auto` still grows to
   fill a tall screen, but never shrinks below what is inside it. */
.fd-body {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 4px 16px 28px;
}

.fd-panel {
  background: var(--card);
  border: 2px solid var(--ink);
  border-radius: 16px;
  padding: 13px 14px;
}
body.dark .fd-panel { background: var(--d-panel); border: none; }

/* ── buttons ────────────────────────────────────────────────────── */
.fd-btn {
  border: none;
  border-radius: 999px;
  padding: 15px;
  font-weight: 800;
  font-stretch: 125%;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 14px;
  background: var(--ink);
  color: var(--paper);
  width: 100%;
}
body.dark .fd-btn { background: var(--paper); color: var(--d-surf); }

.fd-btn.ghost {
  background: transparent;
  border: 2px solid var(--ink);
  color: var(--ink);
}
body.dark .fd-btn.ghost { border-color: var(--paper); color: var(--paper); }

.fd-btn.big { padding: 17px; font-size: 16px; font-weight: 900; }
.fd-btn:active { transform: scale(0.985); }
.fd-btn:disabled { opacity: 0.45; cursor: default; transform: none; }

/* ── segmented toggle ───────────────────────────────────────────── */
.fd-seg {
  display: inline-flex;
  border: 2px solid var(--ink);
  border-radius: 999px;
  overflow: hidden;
}
.fd-seg button {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 10.5px;
  letter-spacing: 0.12em;
  padding: 7px 14px;
  border: none;
  background: var(--paper);
  color: var(--ink);
}
.fd-seg button.on { background: var(--ink); color: var(--paper); }

/* ── result stamp ───────────────────────────────────────────────── */
.fd-stamp {
  border: 3px solid currentColor;
  border-radius: 12px;
  padding: 6px 14px;
  font-weight: 900;
  font-stretch: 125%;
  text-transform: uppercase;
  font-size: 19px;
  transform: rotate(-5deg);
  display: inline-block;
  animation: fd-stamp 0.5s cubic-bezier(0.2,0.9,0.3,1.15) both;
}

.fd-result {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 11px;
  animation: fd-rise 0.4s ease both;
}
.fd-result .fd-foot {
  width: 100%;
  max-width: 300px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.fd-next {
  text-align: center;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.1em;
  opacity: 0.65;
}
.fd-home-link {
  text-align: center;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-decoration: underline;
  padding: 4px;
}

/* ── grid share preview ─────────────────────────────────────────── */
.fd-grid-preview {
  font-size: 17px;
  letter-spacing: 3px;
  text-align: center;
  line-height: 1.5;
}

/* ── motion ─────────────────────────────────────────────────────── */
@keyframes fd-rise  { from { opacity:0; transform:translateY(12px) } to { opacity:1; transform:none } }
@keyframes fd-stamp { 0%{opacity:0;transform:scale(1.4) rotate(-14deg)} 60%{opacity:1;transform:scale(0.96) rotate(-4deg)} 100%{opacity:1;transform:scale(1) rotate(-5deg)} }
@keyframes fd-flip  { 0%{transform:rotateX(90deg)} 100%{transform:rotateX(0)} }
@keyframes fd-flipin{ 0%{transform:rotateY(88deg)} 100%{transform:rotateY(0)} }
@keyframes fd-shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-6px)} 40%{transform:translateX(6px)} 60%{transform:translateX(-4px)} 80%{transform:translateX(4px)} }
@keyframes fd-pop   { 0%{transform:scale(1)} 50%{transform:scale(1.08)} 100%{transform:scale(1)} }
@keyframes fd-blink { 50% { opacity: 0 } }
@keyframes fd-eq    { 0%,100%{transform:scaleY(0.4)} 50%{transform:scaleY(1)} }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── focus visibility (keyboard) ──────────────────────────────────
   `currentColor`, not a named colour, and the reason is semantics rather than
   contrast. This used to be `var(--blue)` (#2E77FF) with a `body.dark`
   override to amber — an override Grid Rush, Pit Board and Steady Lap never
   matched, because all three set their own `body.gr` / `.pb` / `.sl` palettes
   instead. So a focused cell in Grid Rush rang in #2E77FF while `--gr-blue` is
   #2E7BFF, and in that game blue means exactly one thing: Reverse Grid, the
   roles are inverted. The ring measured 4.99:1 and cleared every contrast
   floor; it just said the wrong word (SOT §13.9).

   The replacement is a plain neutral, per ground. NOT `currentColor`, which
   was tried first and measured worse than what it replaced: the ring is drawn
   2px OUTSIDE the element, so on a filled CTA — dark ink on a bright accent —
   it inherits the ink and lands on the dark page at **1.08:1**, which is not a
   focus ring at all. What the ring has to contrast with is the page behind it,
   never the element it surrounds.

   Measured 2026-08-02 on the first focusable control of all nine pages:
   near-black on the four light grounds 15.9–17.8:1, near-white on the four
   dark ones 14–16:1. Guess the Driver keeps its own gold ring (6.00:1) — its
   surface is neither, and gold means nothing in that game.

   Neutral is also the point semantically: white and near-black are the two
   colours no game has given a rule to. */
:root { --fd-focus: #17150F; }
body.dark, body.gr, body.pb, body.sl { --fd-focus: #F2F5F7; }
:focus-visible {
  outline: 3px solid var(--fd-focus);
  outline-offset: 2px;
}

/* ── dev bar ────────────────────────────────────────────────────── */
/* keeps the dev bar from sitting on top of the result footer */
body.has-dev .fd-app { padding-bottom: 26px; }

.fd-dev {
  position: fixed;
  bottom: 0; left: 50%;
  transform: translateX(-50%);
  z-index: 999;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.08em;
  background: var(--ink);
  color: var(--paper);
  padding: 4px 10px;
  border-radius: 8px 8px 0 0;
  opacity: 0.55;
}
.fd-dev a { text-decoration: underline; }
