/*
 * Zoom Zoom Vroom — the game's own skin. Everything else (header, start screen, AI badge,
 * tool panels, dialog shell, fonts, the table felt) is inherited from ../core/deckades.css
 * and is deliberately NOT restated here.
 *
 * ── The world: THE PRINTED GAME, on a pit lane at night ───────────────────────
 * Erika Schnatz's artwork ships with the game (the publisher's print PDFs in assets/), so
 * nothing here draws a card, a token or the racetrack — it places them. assets/cards/*.webp
 * are the nine printed faces plus the back, cut from the PDF's own TrimBox with half of the
 * white frame taken off; assets/board-a|b.webp are the two racetrack faces; assets/tokens/
 * are the eight punched tokens (five player discs, the gold speed bolt, the checkered lap
 * flag, the grey gas drop), cut out with a circular alpha.
 *
 * What is left for CSS is the TABLE the print sits on: dark asphalt over the shared felt,
 * lit by the fuel.
 *
 *   --dk-accent  #f2b13c  the amber of fuel — 9.9:1 on the shared panel (#120a22); it is the
 *                         one hue that means "gas" and "act", and nothing else borrows it.
 *   --dk-tint    asphalt  a cool blue-black wash over the shared felt.
 *   suit inks             SAMPLED from the printed cards (the band each racer is drawn on),
 *                         used only on suit labels in the log, the chooser and the AI panel.
 *   seat colours          sampled from the five printed player tokens, and they are the
 *                         racetrack's five columns in order — so seat slot 0 is the pink
 *                         lane on the board, 1 the orange, and so on.
 */

:root {
    --dk-accent:      #f2b13c;
    --dk-accent-soft: rgba(242, 177, 60, 0.35);
    --dk-tint:        rgba(34, 46, 72, 0.42);

    /* Glass button 2 (../core/glassButton.css): the pane tinted with fuel amber, cool white
       ink, the shadow pool asphalt. */
    --gb-tint:  242 177 60;
    --gb-ink:   #f3efe6;
    --gb-shade: 6 8 14;

    /* ── This game's own vocabulary. Namespaced; never a new --dk-* token. ── */
    --zz-ink:        #ece9e1;                      /* body text: warm white */
    --zz-dim:        #a9adb8;                      /* secondary text, cooled from the ink */
    --zz-asphalt:    #11151f;
    --zz-board:      #171b28;                      /* a pit board's face */
    --zz-board-hi:   #222838;
    --zz-panel:      rgba(14, 18, 28, 0.90);       /* tags, the tower, the chooser */
    --zz-panel-hi:   rgba(24, 30, 44, 0.96);
    --zz-line:       rgba(242, 177, 60, 0.20);
    --zz-gas:        #f2b13c;
    --zz-gas-deep:   #b8791a;
    --zz-danger:     #f08a6e;
    --zz-good:       #8fe0a2;

    /* The six racers and the 0 suit, sampled from the band each card is printed on
       (assets/cards/*.webp). Every one clears 5:1 on the shared panel except the 0 suit,
       whose card is navy — its label takes the light blue of its own checkers instead. */
    --zz-lichen:  #c88048;
    --zz-salty:   #c8e0e8;
    --zz-chippy:  #e85858;
    --zz-jelly:   #8088b8;
    --zz-beans:   #f8e0a8;
    --zz-slippy:  #70b868;
    --zz-zero:    #c8e0e8;

    /* ── THE START CARD, in this game's colours ────────────────────────────────
       The shared sheet paints the whole card from the --dk-start-* group, so the house's
       violet plate is a default and not a law (deckades.css §"the THIRD sanctioned
       exception"). This is the pit lane at night: asphalt lit from above by the fuel amber,
       every value taken from the art already on this table — the board's asphalt (--zz-board,
       sampled from assets/board-*.webp), the printed gold of the speed token (--zz-gas), and
       the warm white the cards are drawn on (--zz-ink). No .start-* selector is written. */
    --dk-start-surface:
        radial-gradient(130% 78% at 50% -8%, rgba(242, 177, 60, 0.17), rgba(242, 177, 60, 0) 56%),
        linear-gradient(180deg, #1b2131 0%, #141a27 46%, #0c1018 100%);
    --dk-start-edge:   rgba(242, 177, 60, 0.55);
    --dk-start-glow:   0 0 34px rgba(242, 177, 60, 0.20), 0 22px 64px rgba(0, 0, 0, 0.78);
    --dk-start-scrim:  transparent;               /* the cover is painted below, whole */
    --dk-start-blur:   none;                      /* …and it is art, not a frosted backdrop */

    --dk-start-ink:      var(--zz-ink);
    --dk-start-ink-dim:  #b9bfcc;                 /* labels: 7.4:1 on the card's mid-tone */
    --dk-start-ink-soft: #ded9cd;
    --dk-start-ink-fade: #8b93a3;                 /* hints: 4.6:1, the floor for small text */
    --dk-start-title-shadow: 0 0 26px rgba(242, 177, 60, 0.32);

    --dk-start-field:      rgba(8, 11, 18, 0.86);
    --dk-start-field-edge: rgba(242, 177, 60, 0.26);
    --dk-start-field-ink:  var(--zz-ink);
    --dk-start-secondary-edge: rgba(242, 177, 60, 0.30);
    --dk-start-swatch-edge:    rgba(255, 255, 255, 0.30);
    --dk-start-swatch-edge-on: #ffffff;           /* white reads on all five lane colours */
    --dk-start-lang-on:  var(--zz-gas);
    --dk-start-focus:    2px solid #f2b13c;
    --dk-start-focus-offset: 2px;

    /* The five seat colours, sampled from the printed player tokens — and they ARE the
       racetrack's five columns, left to right, so a seat's colour finds its own lane. */
    --zz-seat-0: #ef4b75;
    --zz-seat-1: #f05524;
    --zz-seat-2: #ddd81f;
    --zz-seat-3: #30beef;
    --zz-seat-4: #624d9f;
    /* …and the same five as CHANNELS, which is what the glass wants: `--gb-tint` is fed to
       rgba(var(--gb-tint) / a) for the came and the glow, so it cannot be a hex. */
    --zz-seat-0-rgb: 239 75 117;
    --zz-seat-1-rgb: 240 85 36;
    --zz-seat-2-rgb: 221 216 31;
    --zz-seat-3-rgb: 48 190 239;
    --zz-seat-4-rgb: 98 77 159;

    /* Card geometry is solved in JS and written back here, so one variable resizes every
       card on the table. The printed card is 2.25 x 3.5in, less half its white frame. */
    --card-w:  80px;
    --card-h:  127px;
    --card-scale: 1;
    --opp-scale: 0.58;                             /* face-down opponent backs: never read */
    --play-scale: 1.00;                            /* cards in the TRICK: the thing being read */
}

html { background: #0b0e14; }
html, body { height: 100%; }
/* The page may not scroll. `overflow: hidden` only stops the SCROLLBAR — the box is still
   programmatically scrollable, and focusing a control near the bottom of a 360px-tall phone
   scrolled the whole app up by 200px to bring the crawlable #intro block into view, taking
   the table with it. `clip` cannot be scrolled at all; the older keyword stays underneath it
   for browsers that do not know it. #intro is still laid out and still read by crawlers. */
html { overflow: hidden; overflow: clip; }
body {
    margin: 0;
    color: var(--zz-ink);
    font-family: var(--dk-font-ui);
    overflow: hidden;
    overflow: clip;
    touch-action: none;
    -webkit-user-select: none; user-select: none;
}
::selection { background: var(--dk-accent-soft); color: var(--zz-ink); }
:focus-visible { outline: 2px solid var(--dk-accent); outline-offset: 2px; }

/* ── The play surface: the SHARED FELT under an asphalt wash ────────────────────
   The generic cover (a checkered band and the five columns) is soft-light blended into the
   table photo, so it comes through the grain as a pattern in the cloth rather than a
   picture behind the cards. */
/* FIXED, not absolute: the page below the table (the crawlable #intro block) still makes the
   document scrollable, and a browser will scroll it to bring a focused control into view —
   which on a 360px-tall phone took the whole table 200px up the screen. A fixed layer cannot
   be scrolled out from under the player, whatever the document does. */
#game {
    position: fixed; inset: 0;
    overflow: hidden;
    touch-action: none;
    /* A WASH, not a scrim: the drawn felt and the shared table under it are both nearly black,
       so a dark overlay could only ever take the table further down. This lifts it instead —
       cool at the top, warmer towards the floor — and the vignette that keeps the eye in the
       middle is drawn by #board, which is where the middle actually is. */
    background:
        linear-gradient(rgba(104, 118, 158, 0.20), rgba(86, 78, 96, 0.13)),
        url('assets/cover-felt.svg') center 40% / cover no-repeat,
        var(--dk-table);
    background-blend-mode: normal, soft-light, normal;
}

/* Behind the start screen: the PRINTED COVER. It is a tall portrait picture and the start
   card sits over its middle, so it is scaled to FILL and pulled up — what is left around the
   card is then the racers rather than two slivers of sky. The card itself carries the title,
   so losing the cover's own wordmark to the crop costs nothing. The felt keeps the drawn
   pattern; the printed box art belongs here, not under the cards. */
/* The cloth across the HEAD of the card (start.js §menu flag, flag.js) — the title and the
   starting line stand on it, and it is gone before the settings begin. A negative z-index puts
   it above the card's own ground and below every word printed on it, so nothing in the shared
   contract is restyled; the card is already a stacking context (fitCard scales it), so the
   layer cannot escape it. The mask is what makes it a BAND: it fades out downward and at both
   ends, so it needs no edge of its own and the card's rounded corners stay the card's.
   Plain opacity, no blend mode — a blended layer re-rasterises everything under it on every
   frame [[mix-blend-mode-rerasterises-the-page]], and this one is animating. */
.zzv-menu-flag {
    position: absolute; left: 0; right: 0; top: 0; height: 46%;
    z-index: -1;
    opacity: 0.34;
    pointer-events: none;
    border-radius: var(--dk-start-radius) var(--dk-start-radius) 0 0;
    /* No CSS mask: the fade is painted into the canvas's own alpha (flag.js §fade). A masked
       layer that animates has to be re-composited by the page on every frame, and a
       two-gradient `mask-composite: intersect` is the most expensive way to ask for it. */
}

#start-screen {
    background:
        radial-gradient(120% 70% at 50% 42%, rgba(6, 8, 13, 0.92) 30%, rgba(6, 8, 13, 0.58) 78%),
        url('assets/cover.png') center 22% / cover no-repeat,
        #0b0e14;
}
.start-glass { width: 100%; }
.start-glass .gb { width: 100%; }
/* The colour picker is the SHARED contract (deckades.css §"Colour / faction selector"):
   .start-colors / .color-swatches / .color-swatch. Nothing is restated here — the only thing
   this game adds is the token artwork, set on each swatch from start.js. */

.check-label { display: flex; flex-direction: column; gap: 1px; line-height: 1.25; }
.check-sub { font-size: 12px; font-weight: 500; opacity: .75; }

/* ── The asphalt oval the seats sit around ──────────────────────────────────── */
#board {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.05);
    box-shadow: inset 0 0 70px rgba(0, 0, 0, 0.34), 0 18px 60px rgba(0, 0, 0, 0.3);
    background:
        radial-gradient(ellipse at 50% 45%, rgba(242, 177, 60, 0.10), rgba(0, 0, 0, 0) 58%),
        radial-gradient(ellipse at 50% 50%, rgba(46, 56, 78, 0.42), rgba(16, 21, 33, 0.34) 72%);
    pointer-events: none;
}
/* ── THE LAP TRICK: THE FELT FLIES THE FLAG ────────────────────────────────────
   The sixth trick of a round is the one a lap is raced for, and it is the table that says so:
   the checkered weave lies INSIDE the oval, clipped to it, under the cards. Not on the page —
   a flag behind the hands and the racetrack is a wallpaper; on the felt it is the finish line
   the cards are being played onto. Still, and low-contrast, because it sits under the trick
   for as long as the trick lasts. The rim keeps its own tighter checker. */
/* The cloth itself is DRAWN (game.js §flag), because a flag that does not move is a picture
   of a flag. This is only its bed: the canvas fills the oval, sits under every card, and is
   clipped to the felt by the same radius. */
#board .lap-flag {
    position: absolute; inset: 0; width: 100%; height: 100%;
    border-radius: 50%;
    pointer-events: none;
    /* Faded at the rim rather than cut by it: the fold pushes cells past the ellipse, and a
       hard edge there reads as a clipped rectangle instead of as cloth lying on the felt. */
    -webkit-mask: radial-gradient(closest-side, #000 68%, rgba(0,0,0,.45) 88%, transparent 100%);
    mask: radial-gradient(closest-side, #000 68%, rgba(0,0,0,.45) 88%, transparent 100%);
    opacity: 0; transition: opacity 420ms ease;
}
#board.lap-trick .lap-flag { opacity: .34; }

#board.lap-trick::after {
    content: ''; position: absolute; inset: -3px; border-radius: 50%;
    border: 3px solid transparent;
    background: repeating-conic-gradient(rgba(236,233,225,.55) 0 7deg, rgba(20,22,30,.7) 7deg 14deg) border-box;
    -webkit-mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    opacity: .7;
}

#trick-zone, #seats, #hand { position: absolute; inset: 0; pointer-events: none; }
/* The trick sits ABOVE the fans: a played card never overlaps one (games/ZZV/test/layout.mjs
   asserts it), but its number chip straddles the card's outer edge and the seat's own fan
   comes down to meet that edge on a tight table. */
#trick-zone { z-index: 6; }
#hand { z-index: 3; }
#hand > *, #seats > * { pointer-events: auto; }
.play-zone { position: absolute; width: 0; height: 0; pointer-events: none; }

/* ── Cards: THE PRINTED FACES ──────────────────────────────────────────────────
   assets/cards/<suit>.webp and back.webp, 512 x 811 each. The box owns the geometry and the
   image fills it; nothing here draws a number or a name. */
.card {
    position: absolute;
    width: calc(var(--card-w) * var(--card-scale));
    height: calc(var(--card-h) * var(--card-scale));
    border-radius: calc(var(--card-w) * 0.055);
    background: #f4f2ec;
    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.45);
    overflow: hidden;
    transition: transform 180ms cubic-bezier(.22,.75,.3,1), box-shadow 140ms, filter 140ms;
    will-change: transform;
    touch-action: none;
}
.card .card-face, .card .card-back { position: absolute; inset: 0; }
.card.face-down .card-face { display: none; }
.card:not(.face-down) .card-back { display: none; }
.card img {
    display: block; width: 100%; height: 100%;
    object-fit: cover; pointer-events: none;
    -webkit-user-drag: none;
}

/* Opponent backs and trick cards scale through their own tokens; every JS geometry helper
   reads the same numbers back, so layout and paint cannot disagree. */
.card.opp  { --card-scale: var(--opp-scale); }
.card.play { --card-scale: var(--play-scale); }
.card.play.tuned { filter: brightness(.82); }

/* A card the human may act on now. Face down is always open, so every card is tappable on
   the human's turn; a card whose suit is already face up is dimmed but still answers. */
.card.playable { cursor: pointer; }
.card.playable:hover { filter: brightness(1.1); box-shadow: 0 0 0 2px var(--dk-accent), 0 4px 14px rgba(0,0,0,.55); }
.card.blocked img { filter: brightness(0.62) saturate(0.6); }
.card.blocked::after {
    content: ''; position: absolute; left: 10%; right: 10%; top: 50%; height: 2px;
    background: rgba(240, 138, 110, 0.9); transform: rotate(-22deg);
    box-shadow: 0 0 6px rgba(0,0,0,.6);
}
.card.chosen { box-shadow: 0 0 0 2px var(--dk-accent), 0 0 22px var(--dk-accent-soft), 0 4px 14px rgba(0,0,0,.55); }
.card.just-played { box-shadow: 0 0 0 2px var(--dk-accent), 0 4px 14px rgba(0,0,0,.55); }
/* The card that took the trick, and everything it beat (the shape Wizard resolves a trick
   in). The dim is what makes the glow read at a glance — a lit card among four normal ones
   is a much weaker signal — and both are put on the HALVES of a torn card, whose wrapper is
   a transparent box that a filter or a shadow would have nothing to act on. */
.card.won-glow { box-shadow: 0 0 0 2px #ffe58a, 0 0 22px rgba(255, 229, 138, .85), 0 6px 18px rgba(0,0,0,.55); transform-origin: center; z-index: 80; }
.card.lost-dim { filter: brightness(0.5) saturate(0.65); }
.card.torn.won-glow .tear-half { filter: drop-shadow(0 0 5px rgba(255, 229, 138, .95)) drop-shadow(0 3px 7px rgba(2,4,9,.72)); }
.card.torn.lost-dim { filter: none; }
.card.torn.lost-dim .tear-half { filter: brightness(0.5) saturate(0.65) drop-shadow(0 3px 7px rgba(2,4,9,.72)); }
.final.faded { opacity: .4; }
.card.won-glow, .card.lost-dim, .final.faded { transition: box-shadow 220ms ease, filter 220ms ease, opacity 220ms ease; }

/* The audible refusal: an illegal tap shakes the card and the status line says why. The
   shake runs on the FACE, not the card: the card's own transform is its position. */
.card.shake .card-face, .card.shake .card-back { animation: zz-shake 360ms cubic-bezier(.36,.07,.19,.97) both; }
@keyframes zz-shake {
    10%, 90% { transform: translateX(-2px); }
    20%, 80% { transform: translateX(3px); }
    30%, 50%, 70% { transform: translateX(-5px); }
    40%, 60% { transform: translateX(5px); }
}

/* ── THE PUNCHED TOKENS ───────────────────────────────────────────────────────
   assets/tokens/*.webp, cut from the publisher's token sheet with a circular alpha. Three
   of them carry meaning on this table: the grey GAS drop, the gold SPEED bolt and the
   checkered LAP flag; the five player discs are the seats. */
.gas, .bolt, .flag, .disc {
    display: inline-block; flex: none;
    width: 14px; height: 14px;
    background: center / contain no-repeat;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}
.gas  { background-image: url('assets/tokens/gas.webp'); }
.bolt { background-image: url('assets/tokens/speed.webp'); }
.flag { background-image: url('assets/tokens/lap.webp'); width: 15px; height: 15px; }
.gas.lit { filter: drop-shadow(0 0 5px var(--dk-accent-soft)) drop-shadow(0 1px 2px rgba(0,0,0,.55)); }
.gas.ghost-drop { position: absolute; width: 15px; height: 15px; z-index: 905; }
.gas.dead { filter: grayscale(1) brightness(.5); opacity: .55; }
.flag.empty { filter: grayscale(1) brightness(.6); opacity: .40; }
.gas-row { display: inline-flex; align-items: center; gap: 3px; min-height: 14px; }
.gas-row.empty::before {
    content: ''; width: 13px; height: 13px; border-radius: 50%;
    border: 1px dashed rgba(242, 177, 60, 0.4); box-sizing: border-box;
}

/* THE GAS ON A PLAYED CARD. The drops are the reason one card beats another, so they are
   bigger than the ones in a seat's bay and they sit on a dark pill — the printed art is busy
   and light, and grey tokens laid straight on a cartoon lose themselves in it. */
.card-gas {
    position: absolute; left: 50%; bottom: 20%;
    transform: translateX(-50%);
    display: flex; gap: 3px; flex-wrap: nowrap; justify-content: center;
    padding: 4px 6px;
    border-radius: 999px;
    background: rgba(9, 12, 20, 0.78);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(242, 177, 60, 0.38);
    pointer-events: none; z-index: 3;
}
.card-gas .gas {
    /* --gas-on-card is solved in measure(): the biggest drop that still lets FIVE of them —
       the cap on a card — sit in one row inside the card's width. */
    width: var(--gas-on-card, 15px); height: var(--gas-on-card, 15px);
}

/* ── The middle: the pool and the removed pile ──────────────────────────────── */
#middle.hidden { display: none; }
/* THE MIDDLE, ON ITS OWN GROUND. What lies in the middle is a number the player counts, and on
   the lap trick it would otherwise be counted against a checkered cloth. So it brings a plain
   patch of asphalt with it — a disc that fades out before it has an edge — and it sits ABOVE
   the flag, which is drawn inside #board and therefore under everything on the table. */
#middle {
    position: absolute; transform: translate(-50%, -50%);
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    padding: var(--mid-pad-y, 15px) var(--mid-pad-x, 26px);
    pointer-events: none; z-index: 3;
    background: radial-gradient(closest-side, rgba(9, 12, 19, 0.93) 46%, rgba(9, 12, 19, 0.74) 74%, rgba(9, 12, 19, 0) 100%);
}
/* EVERY number here is written by the solver (game.js §MID), because the ring is grown to clear
   this box: sized in the sheet instead, the middle outgrew the room the table had reserved for
   it and printed itself over the cards on either side. */
.pool {
    display: flex; flex-wrap: wrap; justify-content: center; align-content: center;
    gap: var(--pool-gap, 6px);
    width: var(--pool-w, 108px); min-height: var(--pool-h, 52px); padding: var(--pool-pad, 10px);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(242, 177, 60, 0.12), rgba(0,0,0,0) 70%);
    box-shadow: inset 0 0 0 1px rgba(242, 177, 60, 0.20);
}
.pool .gas { width: var(--gas-mid, 18px); height: var(--gas-mid, 18px); }
/* THE CHIP: the same middle, read as a number, for a table with no width to spend on a disc
   of drops. One drop says what the number counts; the number says how many. */
.pool.chip {
    width: auto; min-height: 0; border-radius: 999px;
    gap: 5px; padding: var(--mid-pad-y, 6px) var(--mid-pad-x, 10px);
    box-shadow: inset 0 0 0 1px rgba(242, 177, 60, 0.28);
}
.pool.chip b {
    font-family: var(--dk-font-display); font-weight: 800; line-height: 1;
    font-size: var(--mid-num, 16px); color: var(--zz-gas);
    font-variant-numeric: lining-nums tabular-nums;
}
.pool.chip.lit { background: radial-gradient(circle, rgba(242, 177, 60, 0.18), rgba(0,0,0,0) 78%); }
body.narrow #middle { gap: 3px; padding: 4px 6px; }
body.narrow .removed { font-size: 10px; gap: 3px; }
/* ── A TAG ON A NARROW TABLE ───────────────────────────────────────────────────
   Same facts, a third of the width: the gas is a number instead of a row of drops, a long
   name is cut, and the human's own tag drops the name altogether — it is standing beside
   their own hand, which is the one seat nobody has to be told the name of. */
.gas-row.count { display: inline-flex; align-items: center; gap: 3px; }
.gas-row.count b { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--zz-gas); }
.gas-row.count.empty b { color: var(--zz-dim); }
body.narrow .seat .nm { max-width: 7ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body.narrow .seat.mine .seat-name { display: none; }
/* Speed and laps are ALREADY on the strip under the header when the racetrack cannot dock —
   every seat, every number, one row. A second copy of them in each tag is what made a tag on
   this table 45% of the table's width. The tank is not up there, so the tank stays here. */
body.narrow .seat .seat-row + .seat-row { display: none; }

/* A narrow table reads the COUNT; the drops are how it reads it at a glance. */
body.narrow .pool-label { font-size: 12px; }
body.narrow .pool-label b { font-size: 15px; }
body.narrow .removed { font-size: 10px; }
.pool.lit { box-shadow: inset 0 0 0 1px rgba(242, 177, 60, 0.45), 0 0 30px rgba(242,177,60,.18); }
.pool-label { font-size: 11px; color: var(--zz-dim); letter-spacing: .04em; text-transform: uppercase; }
.pool-label b { color: var(--zz-gas); font-weight: 700; }
.removed { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--zz-dim); }

/* ── The final number, beside its card ─────────────────────────────────────────
   One number, big, in the seat's own colour: what this card is worth. The arithmetic behind
   it (printed + gas + speed) is on the card itself — the gas is lying on it and the speed is
   on the racetrack — so spelling the sum out beside it was a third copy of the same facts.
   A card the 1 has zeroed is TORN, which says more than a struck-through number could. */
/* ── THE FINAL NUMBER: a glass plate on the card ───────────────────────────────
   What this card is worth, standing in the middle of the printed art it belongs to. The
   material is the platform's own glass (core/glassButton.css §.gb-plate — "a score that
   floats up over the table"), tinted with the seat's own colour through `--gb-tint`, which is
   exactly the case that component describes. The pane FROSTS what is behind it instead of
   covering it, so the number reads over a donut or a waterfall without a slab of asphalt
   between them.

   The wrap does the placing (.final) and the pane does the reading (.final .gb-plate): a
   pill sized to its digits, never a token sized to a guess. */
.final {
    position: absolute; transform: translate(-50%, -50%);
    --gb-tint: var(--seat-rgb, 255 255 255);
    z-index: 4; pointer-events: none;
    transition: transform 200ms;
}
.final .gb-plate {
    /* Sized to the digits and no further, and the number CENTRED in it. The display face
       (Cormorant Garamond) draws OLD-STYLE figures by default — 0 1 2 sit at x-height, 3 4 5 7 9
       hang below the baseline — so no padding could centre every number: the ink moved by a
       tenth of an em from one digit to the next. LINING figures all stand cap-height on the
       baseline, and with them an even padding centres the ink to half a pixel (measured at
       40px, every digit and 10-15). */
    min-height: 0; padding: 0.1em 0.2em; gap: 0;
    min-width: 1.22em;
    align-items: center; justify-content: center;
    /* The glass everywhere else on the platform sits over a DARK table, where frosting is
       what makes it read. This one sits on printed card art — a pale sky, a yellow van — and
       frost alone left a coloured numeral floating in a bright pane. So the pane dims what is
       behind it as well as blurring it: same lip, same came, same specular strip, a darker
       room behind the glass. The rgba layer under the tint is the same job done where
       backdrop-filter is not available. */
    background:
        linear-gradient(-75deg,
            rgba(255, 255, 255, 0.05) 0%,
            rgba(255, 255, 255, 0.17) 50%,
            rgba(255, 255, 255, 0.05) 100%),
        rgba(var(--gb-tint) / 0.12),
        rgba(6, 10, 17, 0.26);
    -webkit-backdrop-filter: blur(7px) saturate(1.15) brightness(0.62);
    backdrop-filter: blur(7px) saturate(1.15) brightness(0.62);
    font-family: var(--dk-font-display); font-weight: 800; line-height: 1;
    font-size: var(--final-size, 30px);
    /* The seat's colour, LIFTED a quarter of the way to white. The five lane colours were
       chosen to read on the table's dark furniture, and four of them do the same on a dimmed
       pane — but the purple is the darkest of them, and a dark violet numeral on dark glass is
       a numeral you have to look for. The lift costs the yellow and the cyan nothing and makes
       the purple a lilac, so all five read at the same glance. */
    color: color-mix(in srgb, var(--seat, var(--zz-ink)) 76%, #fff);
    text-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 6px rgba(0, 0, 0, .7);
    font-variant-numeric: lining-nums tabular-nums;
}
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
    .final .gb-plate { background-color: rgba(6, 10, 17, 0.72); }
}
/* The pool under a number is a contact shadow, not a cast one: it has no room to spread on a
   card, and at full strength it read as a smudge under the plate. */
.final .gb-shadow { left: 12%; right: 12%; top: 40%; bottom: -14%; opacity: .7; filter: blur(4px); }

/* The one in front, for as long as it is: a touch bigger, and the came goes green — the glass
   says which number is winning without a second colour anywhere else on the table. */
.final.lead { transform: translate(-50%, -50%) scale(1.12); --gb-tint: 143 224 162; }

/* A TIGHT table (five seats, no room to dock the racetrack) lays the tag out on one line:
   the name, the gas, the speed and the flags, side by side. Nothing is dropped — the tag is
   a quarter as tall, which is what lets it stand beside a long row instead of on it. */
/* …every tag but MY OWN: mine stands in the gutter beside my hand, where the room is a
   narrow column and a one-line tag would not fit at all. */
body.tight .seat:not(.mine) { flex-direction: row; align-items: center; gap: 7px; padding: 4px 9px 5px; }
body.tight .seat:not(.mine) .seat-row { gap: 5px; }
body.tight .seat:not(.mine) .gas-row .gas { width: 9px; height: 9px; }
body.tight .seat:not(.mine) .seat-name { gap: 4px; }

/* ── Seats: the name tags with each seat's gas ──────────────────────────────── */
.seat {
    position: absolute;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 5px 10px 6px;
    border-radius: 10px;
    background: var(--zz-panel);
    border: 1px solid rgba(255, 255, 255, 0.07);
    border-top: 2px solid var(--seat, var(--zz-dim));
    font-size: 12.5px; line-height: 1.25;
    white-space: nowrap;
    transition: border-color 180ms, box-shadow 180ms, background 180ms;
}
.seat.turn {
    box-shadow: 0 0 0 1px var(--dk-accent-soft), 0 6px 18px rgba(0, 0, 0, 0.35);
    background: var(--zz-panel-hi);
}
.seat.turn .seat-name::after { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--dk-accent); margin-left: 6px; vertical-align: middle; box-shadow: 0 0 8px var(--dk-accent); }
.seat-name { font-weight: 600; color: var(--zz-ink); display: flex; align-items: center; gap: 6px; }
.seat-dot {
    width: 15px; height: 15px; flex: none;
    background: center / contain no-repeat var(--disc);
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}
.seat-row { display: flex; align-items: center; gap: 8px; color: var(--zz-dim); font-size: 11px; }
.seat-chip { display: inline-flex; align-items: center; gap: 3px; font-variant-numeric: tabular-nums; }
.seat-chip b { color: var(--zz-ink); font-weight: 700; }
/* ── THE RACETRACK ────────────────────────────────────────────────────────────
   Docked, and on a board face: the printed board (assets/board-a|b.webp) with a speed token
   on each seat's space and the lap tokens in the flag row — the picture the game is played
   around. On a short or portrait table the board comes
   off the table and the seats carry their speed tokens instead, which is exactly what the
   rulebook's "My Favorite Carrera" box describes. */
#tower {
    position: absolute;
    background: var(--zz-panel);
    border: 1px solid rgba(255,255,255,.07);
    border-radius: 12px;
    box-shadow: 0 10px 30px rgba(0,0,0,.4);
    padding: 8px;
    box-sizing: border-box;
    display: flex; flex-direction: column; gap: 6px; align-items: center;
    z-index: 5;
    overflow: hidden;
}
.tower-title { font-family: var(--dk-font-display); font-size: 13px; letter-spacing: .12em; color: var(--zz-dim); text-align: center; text-transform: uppercase; }
.tower-pool { font-size: 11px; color: var(--zz-dim); text-align: center; display: flex; align-items: center; justify-content: center; gap: 5px; flex-wrap: wrap; }

/* The board, and the tokens ON it: every token is placed in PERCENT of the image box, from
   one table of coordinates measured off the print (game.js BOARD). */
.board-wrap { position: relative; width: 100%; line-height: 0; margin-bottom: 19px; }
.board-wrap img.board { width: 100%; height: auto; border-radius: 6px; display: block; }
.board-tok {
    position: absolute; transform: translate(-50%, -50%); aspect-ratio: 1;
    background: center / contain no-repeat;
    filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.55));
    transition: top 420ms cubic-bezier(.22,.75,.3,1), left 420ms cubic-bezier(.22,.75,.3,1);
}
.board-tok.speed { background-image: url('assets/tokens/speed.webp'); }
.board-tok.lap   { background-image: url('assets/tokens/lap.webp'); }
/* WHICH LANE IS MINE, under the plate — never on it. The wrap sets line-height 0 for the
   image, so this needs its own line box or the text collapses onto the board's bottom row. */
.board-you {
    position: absolute; transform: translate(-50%, 0);
    font-size: 9px; line-height: 13px; letter-spacing: .08em; text-transform: uppercase;
    color: var(--zz-gas);
    text-shadow: 0 1px 3px rgba(0,0,0,.9);
    pointer-events: none;
}

/* Carrera / the strip: a seat's speed is the tokens in front of it. */
.tower-seats { display: flex; flex-direction: column; gap: 5px; width: 100%; }
.tower-seat {
    display: flex; align-items: center; gap: 6px;
    padding: 4px 6px; border-radius: 8px;
    background: color-mix(in srgb, var(--seat) 14%, transparent);
    border-left: 3px solid var(--seat);
    min-width: 0;
}
.tower-seat .tower-name { flex: 1; font-size: 11px; color: var(--zz-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tower-seat .tower-name.you { color: var(--zz-ink); font-weight: 600; }
.tower-toks { display: flex; gap: 2px; align-items: center; min-height: 14px; }
.tower-toks .bolt { width: 12px; height: 12px; }
.tower-speed { font-family: var(--dk-font-display); font-size: 14px; font-weight: 700; color: var(--zz-ink); min-width: 20px; text-align: right; }

/* The strip: the same seats in a row under the header. */
#tower.strip { flex-direction: row; align-items: stretch; gap: 6px; padding: 6px 8px; border-radius: 10px; }
#tower.strip .tower-title, #tower.strip .board-wrap { display: none; }
#tower.strip .tower-seats { flex-direction: row; gap: 6px; }
/* One line per seat in the strip: the name, its speed, its flags. The bolts themselves do
   not fit — the number says the same thing. */
#tower.strip .tower-seat { flex: 1; flex-direction: row; align-items: center; gap: 5px; border-left: 0; border-top: 2px solid var(--seat); padding: 3px 7px; min-width: 0; }
#tower.strip .speed-toks { display: none; }
#tower.strip .tower-speed { font-size: 13px; min-width: 0; }
#tower.strip .tower-pool { flex: none; flex-direction: column; gap: 2px; padding: 0 6px; border-left: 1px solid rgba(255,255,255,.08); }
#tower.strip .tower-pool span { display: inline-flex; align-items: center; gap: 3px; }
/* The lap tokens still to be raced for — the pile beside the board, as tokens. */
.lap-pile { display: inline-flex; flex-wrap: wrap; justify-content: center; gap: 4px; }
.lap-pile .flag { width: 19px; height: 19px; }
#tower.strip .lap-pile .flag { width: 13px; height: 13px; }
.strip-mid { color: var(--zz-ink); }
/* On a phone the strip already carries every seat's speed and laps, so the tag drops that
   line and keeps the name and the gas — a smaller tag also has somewhere to go. */
body.compact .seat .seat-row + .seat-row { display: none; }


/* ── The play chooser ───────────────────────────────────────────────────────────
   Above the human's hand: FACE UP with a gas stepper and the live sum, and TUNE UP. */
#chooser {
    position: absolute; z-index: 130;                 /* above the status line while a choice is open */
    display: flex; flex-direction: column; gap: 8px;
    padding: 10px 12px 12px;
    border-radius: 14px;
    background: var(--zz-panel-hi);
    border: 1px solid var(--zz-line);
    box-shadow: 0 16px 40px rgba(0,0,0,.55);
    width: 268px; box-sizing: border-box;
    animation: zz-pop 200ms cubic-bezier(.22,.75,.3,1) both;
}
#chooser.hidden { display: none; }
@keyframes zz-pop { from { opacity: 0; transform: translateY(10px) scale(.96); } to { opacity: 1; transform: none; } }
.ch-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ch-title { font-family: var(--dk-font-display); font-size: 16px; font-weight: 700; color: var(--zz-ink); }
.ch-title .suit-ink { margin-left: 4px; }
.ch-close { width: 44px; height: 44px; margin: -8px -10px -8px 0; border: 0; background: transparent; color: var(--zz-dim); font-size: 22px; cursor: pointer; touch-action: manipulation; border-radius: 10px; }
.ch-close:hover { color: var(--zz-ink); background: rgba(255,255,255,.06); }
.ch-why { font-size: 12.5px; color: var(--zz-danger); line-height: 1.3; }
.ch-hint { font-size: 12px; color: var(--zz-dim); line-height: 1.3; }
.ch-row { display: flex; flex-direction: column; gap: 6px; }
.ch-stepper { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ch-stepper .ch-label { font-size: 12px; color: var(--zz-dim); text-transform: uppercase; letter-spacing: .06em; }
.ch-step { display: inline-flex; align-items: center; gap: 4px; }
.ch-step button {
    width: 44px; height: 44px; border-radius: 10px; border: 1px solid rgba(255,255,255,.1);
    background: rgba(255,255,255,.06); color: var(--zz-ink); font-size: 22px; line-height: 1;
    cursor: pointer; touch-action: manipulation;
}
.ch-step button:hover:not(:disabled) { background: var(--dk-accent-soft); }
.ch-step button:disabled { opacity: .3; cursor: default; }
.ch-step .ch-n { min-width: 40px; text-align: center; display: inline-flex; align-items: center; justify-content: center; gap: 4px; font-family: var(--dk-font-display); font-size: 20px; font-weight: 700; }
.ch-sum { display: flex; align-items: baseline; justify-content: center; gap: 6px; font-size: 13px; color: var(--zz-dim); font-variant-numeric: tabular-nums; padding: 2px 0; }
.ch-sum b { font-family: var(--dk-font-display); font-size: 24px; color: var(--dk-accent); font-weight: 700; }
.ch-sum .zeroed { text-decoration: line-through; color: var(--zz-danger); }
.ch-effects { display: flex; gap: 6px; }
.ch-effects button {
    flex: 1; min-height: 44px; border-radius: 10px; border: 1px solid rgba(255,255,255,.1);
    background: rgba(255,255,255,.05); color: var(--zz-ink); font: inherit; font-size: 12.5px; cursor: pointer; touch-action: manipulation;
}
.ch-effects button.on { border-color: var(--dk-accent); background: var(--dk-accent-soft); }
.ch-effects button:disabled { opacity: .35; cursor: default; }
.ch-actions { display: flex; gap: 8px; }
.ch-actions .gb-wrap { flex: 1; }
.ch-actions .gb { width: 100%; }

/* ── The header's glass tools ────────────────────────────────────────────────── */
.hdr-tool svg { width: 18px; height: 18px; display: block; }

/* ── Status line ─────────────────────────────────────────────────────────────── */
#status-msg em { font-style: normal; color: var(--dk-accent); }

/* ── Game end ───────────────────────────────────────────────────────────────── */
#game-over-line { font-size: 14px; color: var(--zz-dim); margin: 2px 0 12px; }
/* There is no second place: the winner's row, then the others under a quiet label. */
.go-also {
    font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
    color: var(--zz-dim); text-align: left; margin: 12px 0 5px;
}
.go-row {
    display: flex; align-items: center; gap: 10px;
    padding: 7px 12px; border-radius: 8px; margin-bottom: 4px;
    font-size: 14px; background: rgba(255,255,255,0.04);
}
.go-row.winner { background: var(--dk-accent-soft); box-shadow: inset 0 0 0 1px var(--dk-accent); }
.go-row:not(.winner) { opacity: .82; }
.go-name  { flex: 1; text-align: left; }
.go-laps { display: inline-flex; gap: 4px; }
.go-gas { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--zz-dim); min-width: 44px; justify-content: flex-end; }
.modal-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }

/* Suit names in the log, the chooser and the AI panel carry their suit's ink. */
.suit-ink { font-weight: 600; white-space: nowrap; }
.suit-ink[data-suit="lichen"] { color: var(--zz-lichen); }
.suit-ink[data-suit="salty"]  { color: var(--zz-salty); }
.suit-ink[data-suit="chippy"] { color: var(--zz-chippy); }
.suit-ink[data-suit="jelly"]  { color: var(--zz-jelly); }
.suit-ink[data-suit="beans"]  { color: var(--zz-beans); }
.suit-ink[data-suit="slippy"] { color: var(--zz-slippy); }
.suit-ink[data-suit="zero"]   { color: var(--zz-zero); }
.log-gas { color: var(--zz-gas); font-weight: 600; }

/* What the seat could NOT do, under its ranked options: the mask, in words. Dim, small, and
   never the first thing read — it is there to be checked, not scanned. */
.eval-illegal {
    font-size: 11px; line-height: 1.4; color: var(--zz-dim);
    opacity: .8; margin: 2px 0 5px;
}
.eval-illegal .suit-ink { font-weight: 700; }

/* THE GO BUTTON. The shared glass component fills a primary pane with the game's own tint at
   34% and labels it near-white — which measures above 7:1 for the house VIOLET and 1.4:1 for
   fuel amber, because a bright hue makes a light pane. This game's filled pane is therefore
   lettered in the asphalt under it, which is also what a lit plate on a pit wall looks like:
   6.0:1, and the one loud button on the screen stays the loudest.
   (A class on the wrap — the shared selectors keep their own rules.) */
.zzv-go { --gb-ink: #1b1204; }
.zzv-go .gb-primary { text-shadow: 0 1px 0 rgba(255, 255, 255, .28); }

/* The launch stack: the two "who is playing" fields and the three buttons, filling the shared
   column so the fields line up with the settings opposite instead of floating in its middle. */
.zzv-start-launch {
    flex: 1 1 auto;
    display: flex; flex-direction: column; justify-content: flex-start;
    gap: 10px; width: 100%;
}
/* The launch buttons stand apart from the two settings above them: the gap is what says
   "these are decisions, that is the door". */
.zzv-start-launch .start-glass:first-of-type { margin-top: 44px; }

/* ── THE GRID WALK ─────────────────────────────────────────────────────────────
   A glass plate over the table for three seconds before the first card. The layer takes every
   pointer and dims nothing: the table it is introducing stays in view behind it. */
/* OVER THE TABLE, not over the screen. The table is not in the middle of the window — the
   hand takes the foot of it and the racetrack takes a side — so a plate centred on the viewport
   sits low and off to one side of the thing it is introducing. game.js hands it the table's own
   middle (`--wx`/`--wy`, geom.cx/cy). */
#zzv-welcome {
    position: fixed; inset: 0; z-index: 1190;
    opacity: 0; transition: opacity 320ms ease;
}
#zzv-welcome.show { opacity: 1; }
#zzv-welcome .zzv-welcome-plate {
    position: absolute;
    left: var(--wx, 50%); top: var(--wy, 50%);
    transform: translate(-50%, calc(-50% + 10px));
    transition: transform 420ms cubic-bezier(.22, .75, .3, 1);
}
#zzv-welcome.show .zzv-welcome-plate { transform: translate(-50%, -50%); }
#zzv-welcome .gb-plate {
    flex-direction: column; gap: 6px; padding: 20px 38px 22px; border-radius: 16px;
    max-width: min(92vw, 560px); text-align: center;
    background:
        linear-gradient(-75deg, rgba(255,255,255,.06) 0%, rgba(255,255,255,.18) 50%, rgba(255,255,255,.06) 100%),
        rgba(var(--gb-tint) / 0.10),
        rgba(var(--gb-shade) / 0.66);
    backdrop-filter: blur(10px) saturate(1.15); -webkit-backdrop-filter: blur(10px) saturate(1.15);
}
.zzv-welcome-title {
    font-family: var(--dk-font-display); font-weight: 700;
    font-size: 26px; line-height: 1.25; letter-spacing: .04em;
    color: var(--zz-ink); text-shadow: 0 0 18px var(--dk-accent-soft), 0 2px 4px rgba(2, 6, 20, .75);
}
.zzv-welcome-line { font-size: 15px; color: var(--zz-dim); }
@media (max-width: 620px) {
    .zzv-welcome-title { font-size: 19px; }
    #zzv-welcome .gb-plate { padding: 15px 22px 17px; }
}
@media (prefers-reduced-motion: reduce) {
    #zzv-welcome, #zzv-welcome .zzv-welcome-plate { transition: none; }
}

/* ── THE STARTING LINE ─────────────────────────────────────────────────────────
   Six racers on the grid, under the title, before anything is chosen. It is the one picture
   this game cannot be mistaken for another by — the suits ARE the characters, and a player
   who has met them once reads the table faster. Their own printed number rides with each,
   because that number is the whole card.

   The strip is the game's own furniture inside the shared card (`zzv-start-*`); no .start-*
   selector is written. The portraits are the publisher's, cut off their page by
   tools/cut_racers.py. */
.zzv-start-grid {
    position: relative;
    margin: 2px 0 20px;
    padding: 12px 6px 0;
}
/* The start line they are standing on: the checkered tape, woven, with the asphalt showing
   through at both ends so it reads as a line ACROSS the lane rather than a band. */
.zzv-start-grid::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 9px;
    background:
        repeating-linear-gradient(90deg, rgba(255,255,255,.14) 0 1px, rgba(0,0,0,.14) 1px 3px, rgba(255,255,255,0) 3px 6px),
        repeating-conic-gradient(rgba(236,233,225,.92) 0% 25%, rgba(10,12,18,.92) 0% 50%) 0 0 / 9px 9px;
    -webkit-mask: linear-gradient(90deg, transparent, #000 12% 88%, transparent);
    mask: linear-gradient(90deg, transparent, #000 12% 88%, transparent);
    opacity: .75;
}
.zzv-racers {
    display: grid; grid-template-columns: repeat(6, 1fr);
    align-items: end; gap: 4px;
}
.zzv-racer {
    position: relative;
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding-bottom: 11px;                       /* clears the start line */
    background: none; border: 0;
    font: inherit; color: inherit; cursor: default;
}
.zzv-racer img {
    width: 100%; max-width: 76px; height: 68px;
    object-fit: contain; object-position: bottom center;
    display: block;
    filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.55));
    transform-origin: 50% 100%;
    transition: transform 320ms cubic-bezier(.2, .8, .3, 1), filter 320ms ease;
}
/* THE CAPTION STANDS ON ITS OWN GROUND. Behind this row the card flies the checkered cloth,
   and a cloth is bright where the fold catches it — measured, a printed number in its own
   suit colour came out at 3.9:1 on the brightest square, under the 4.5:1 a 16px numeral
   needs. Dimming the cloth until that one element passed would have left no cloth at all
   (the arithmetic allows a light thread of 38, which is nothing), so the caption brings a
   patch of the card's own dark with it instead — a pad with no edge, which reads as the
   racer's plate and puts the number back at 8:1. */
.zzv-racer .zzv-racer-cap {
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    padding: 3px 9px 4px;
    border-radius: 9px;
    background: radial-gradient(closest-side, rgba(8, 11, 18, 0.94) 55%, rgba(8, 11, 18, 0.72) 82%, rgba(8, 11, 18, 0) 100%);
}
/* The printed number, in the ink of the band that racer is drawn on. */
.zzv-racer b {
    font-family: var(--dk-font-display); font-weight: 700; font-size: 16px; line-height: 1;
    color: var(--racer, var(--zz-gas));
    text-shadow: 0 1px 4px rgba(0, 0, 0, .8);
}
.zzv-racer span {
    font-size: 10px; letter-spacing: .04em; line-height: 1.15;
    color: var(--dk-start-ink-dim);              /* 8.9:1 — a name is read, not glanced past */
    transition: color 220ms ease;
}
/* Meeting one: it leans in, its ink warms, and the lane under it lights. Hover AND focus —
   a keyboard or a screen reader reaches every racer, and on touch the whole row is legible
   without any of this. */
.zzv-racer::before {
    content: ''; position: absolute; left: 12%; right: 12%; bottom: 11px; height: 3px;
    border-radius: 2px;
    background: var(--racer, var(--zz-gas));
    opacity: 0; transform: scaleX(.3);
    transition: opacity 260ms ease, transform 260ms cubic-bezier(.2, .8, .3, 1);
}
.zzv-racer:hover img, .zzv-racer:focus-visible img { transform: translateY(-5px) scale(1.06); }
.zzv-racer:hover span, .zzv-racer:focus-visible span { color: var(--zz-ink); }
.zzv-racer:hover::before, .zzv-racer:focus-visible::before { opacity: .9; transform: scaleX(1); }
.zzv-racer:focus-visible { outline: none; }
.zzv-racer:focus-visible img { filter: drop-shadow(0 0 6px var(--racer)) drop-shadow(0 6px 8px rgba(0,0,0,.55)); }

/* THE ONE AUTHORED MOMENT: they roll onto the grid, left to right, and settle. Once, on the
   card being built — never again, because a start screen is looked at more than once. */
@keyframes zzvRollIn {
    from { opacity: 0; transform: translateX(-26px) rotate(-3deg); }
    to   { opacity: 1; transform: none; }
}
.zzv-racer { animation: zzvRollIn 520ms cubic-bezier(.16, .84, .32, 1) both; }
.zzv-racer:nth-child(1) { animation-delay: 40ms; }
.zzv-racer:nth-child(2) { animation-delay: 100ms; }
.zzv-racer:nth-child(3) { animation-delay: 160ms; }
.zzv-racer:nth-child(4) { animation-delay: 220ms; }
.zzv-racer:nth-child(5) { animation-delay: 280ms; }
.zzv-racer:nth-child(6) { animation-delay: 340ms; }

/* Short cards hide the names first, then the whole line: the shared sheet pins the card's
   padding to a literal at these heights, so furniture yields before the fields do. */
@media (max-height: 760px) {
    .zzv-start-grid { margin-bottom: 14px; }
    .zzv-racer img { height: 54px; }
    .zzv-racer span { display: none; }
}
@media (max-height: 620px) { .zzv-start-grid { display: none; } }
@media (prefers-reduced-motion: reduce) {
    .zzv-racer { animation: none; }
    .zzv-racer img, .zzv-racer span, .zzv-racer::before { transition: none; }
}

/* ── Meet the racers, inside the rules popup ──────────────────────────────────
   The publisher's own portraits (assets/racers, cut off their page by tools/cut_racers.py).
   A grid that reflows from three columns to two on a phone, and each figure carries the
   printed number and what the card does — the question a player actually opens this for. */
.rules-racers {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
    margin: 12px 0 14px;
}
.rules-racers .racer {
    margin: 0; padding: 8px 6px 7px;
    background: rgba(255,255,255,.04); border-radius: 10px;
    box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);
    text-align: center;
}
.rules-racers img { width: 100%; height: 72px; object-fit: contain; display: block; }
.rules-racers figcaption {
    margin-top: 5px; font-size: 11px; line-height: 1.35; color: var(--zz-ink);
}
.rules-racers figcaption b {
    font-family: var(--dk-font-display); font-size: 15px; margin-right: 3px;
    color: var(--zz-gas);
}
.rules-racers figcaption span { display: block; color: var(--zz-dim); font-size: 10px; }
@media (max-width: 520px) {
    .rules-racers { grid-template-columns: repeat(2, 1fr); gap: 8px; }
    .rules-racers img { height: 60px; }
}

/* ── The personal 0/5 card ─────────────────────────────────────────────────────
   It is NOT in the hand: the booklet puts it on the table in front of its owner, and its own
   3-player figure draws it lying beside the hand (p.10). So it is the last card of the seat's
   row, a little apart, face up for every seat — and the gold ring says it is not one of the
   six that were dealt. */
.card.zf {
    box-shadow: 0 0 0 2px var(--zz-gas), 0 6px 16px rgba(0,0,0,.5);
}
/* The card is set apart by POSITION (renderHands offsets it along the row) — a transform here
   would be overwritten by the inline one every card is placed with. */
.card.zf.playable:hover { filter: brightness(1.12); }
.card.zf.blocked { box-shadow: 0 0 0 2px rgba(242,177,60,.35), 0 4px 10px rgba(0,0,0,.45); opacity: .6; }
/* The discard beat: every card in the hand is a candidate and nothing else is. */
.card.discardable { box-shadow: 0 0 0 2px var(--zz-danger), 0 6px 16px rgba(0,0,0,.5); }

/* ── A 4 OR 5 TORN BY THE 1 ────────────────────────────────────────────────────
   Ported from games/Violet (§"a 9 torn by a 1"), where a 1 tears the 9 it beats. Here the 1
   turns every face-up 4 and 5 in the trick into a 0, which is the sharpest edge in these
   rules and used to happen silently. Now the card comes apart and STAYS apart until the
   trick is swept, so the table carries the reason for as long as the cards are on it.

   The wrapper is a card-shaped BOX and nothing else: the ground and the shadow move onto the
   two halves, or the gap the tear opens would be filled with card-coloured nothing instead
   of with the table. */
.card.torn {
    background: none; box-shadow: none; overflow: visible;
    transition: none;
}
.card.torn .tear-half {
    position: absolute; left: 0; top: 0;
    /* The raw edge, and the piece's own weight: a tight pale shadow for the fibres the tear
       has just exposed, and an offset blurred dark one because each half is now a separate
       scrap lying on the table at its own angle. */
    filter: drop-shadow(0 3px 7px rgba(2, 4, 9, .72))
            drop-shadow(0 0 .5px rgba(255, 244, 214, .5));
    transform: translate(var(--tx), var(--ty)) rotate(var(--tr));
    transition: transform 620ms cubic-bezier(.12, .82, .24, 1);
    will-change: transform;
}
/* Closed. The card is whole for exactly one frame before the rip — and whole again after it,
   because a card lying open in two pieces for the rest of the trick shows the table through
   its middle. What stays is the seam: two scraps of paper with their own edges and their own
   shadows, back together but no longer one card. */
.card.torn.sealed .tear-half { transform: none; }
.card.torn.closing .tear-half { transition: transform 340ms cubic-bezier(.3, .72, .28, 1); }

/* The fibres, lit along the line the tear is running down — the same points the clip uses,
   so the light is on the actual edge and not near it. */
.card.torn .tear-spark {
    position: absolute; inset: 0; width: 100%; height: 100%;
    pointer-events: none; overflow: visible;
    clip-path: inset(0 0 0 0);
    opacity: 1;
    transition: clip-path 300ms cubic-bezier(.45, .05, .6, .95), opacity 400ms ease;
}
.card.torn .tear-spark polyline {
    fill: none;
    stroke: rgba(255, 246, 224, .95);
    stroke-width: 1.8;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    filter: drop-shadow(0 0 4px rgba(242, 177, 60, .9));
}
.card.torn.drawing .tear-spark { clip-path: inset(0 0 100% 0); }
.card.torn.ripped .tear-spark { opacity: 0; }

/* ...and the paper that stays with neither half. */
.tear-fleck {
    position: absolute; width: 3px; height: 7px; border-radius: 1px;
    background: linear-gradient(180deg, rgba(255, 246, 224, .92), rgba(206, 190, 160, .5));
    pointer-events: none;
    animation: zz-fleck 720ms cubic-bezier(.2, .6, .35, 1) both;
}
@keyframes zz-fleck {
    0%   { opacity: 0; transform: none; }
    12%  { opacity: .95; }
    100% { opacity: 0; transform: translate(var(--fx), var(--fy)) rotate(var(--fr)); }
}

/* The card that did it. ONE pass of light across the 1 — the tear is the performance, and a
   second one beside it in the same 200ms means neither is read. */
.card.struck { overflow: hidden; }
.card.struck::after {
    content: ''; position: absolute; inset: -20% -60%;
    background: linear-gradient(104deg, rgba(255, 246, 224, 0) 42%,
        rgba(255, 246, 224, .55) 50%, rgba(255, 246, 224, 0) 58%);
    animation: zz-struck 460ms cubic-bezier(.3, .7, .35, 1) both;
    pointer-events: none;
}
@keyframes zz-struck {
    from { transform: translateX(-58%); }
    to   { transform: translateX(58%); }
}

/* Reduced motion keeps the RESULT — the card is in two pieces, which is the whole point —
   and drops the flight. */
@media (prefers-reduced-motion: reduce) {
    .card.torn .tear-half { transition: opacity 200ms ease; }
    .card.torn.sealed .tear-half { transform: translate(var(--tx), var(--ty)) rotate(var(--tr)); }
    .tear-fleck { display: none; }
    .card.torn .tear-spark { transition: clip-path 120ms linear, opacity 220ms ease; }
    .card.struck::after { animation: none; opacity: 0; }
}

/* ── Animation ghosts ───────────────────────────────────────────────────────── */
#anim-layer { position: absolute; inset: 0; pointer-events: none; z-index: 900; }
.ghost { position: absolute; z-index: 900; pointer-events: none; will-change: transform; }
.ghost .card { position: absolute; left: 0; top: 0; transition: none; }
.ghost-flag { position: absolute; z-index: 910; width: 22px; height: 16px; background: repeating-conic-gradient(#ece9e1 0 25%, #1a1d26 0 50%) 0 0 / 8px 8px; box-shadow: 0 2px 8px rgba(0,0,0,.6); pointer-events: none; }
/* Call-outs floated beside the seat that made them. */
.callout {
    position: absolute; z-index: 800;
    padding: 5px 12px; border-radius: 999px;
    background: var(--zz-panel-hi);
    border: 1px solid var(--dk-accent-soft);
    color: var(--dk-accent);
    font-family: var(--dk-font-display); font-size: 14px; font-weight: 700;
    letter-spacing: .03em; white-space: normal; max-width: 240px; text-align: center; line-height: 1.25;
    pointer-events: none;
    animation: zz-callout 1900ms ease-out forwards;
}
.callout.lap { font-size: 16px; border-color: var(--zz-good); color: var(--zz-good); }
@keyframes zz-callout {
    0%   { opacity: 0; transform: translateY(6px) scale(.92); }
    14%  { opacity: 1; transform: translateY(0) scale(1); }
    76%  { opacity: 1; }
    100% { opacity: 0; transform: translateY(-16px); }
}

/* ── The crawlable block ────────────────────────────────────────────────────── */
#intro {
    position: relative; margin-top: 100vh; padding: 24px 20px 40px;
    max-width: 68ch; color: var(--zz-dim); font-size: 14px; line-height: 1.55;
}
#intro h1 { font-family: var(--dk-font-display); color: var(--zz-ink); font-size: 22px; }
#intro a { color: var(--dk-accent); }

/* ── Mobile ─────────────────────────────────────────────────────────────────── */
/* The phone's chooser: one bar across the table, just above the hand. */
#chooser.bar {
    width: auto; flex-direction: row; align-items: center; gap: 10px;
    padding: 5px 10px; border-radius: 12px;
    height: 60px; box-sizing: border-box;          /* fixed: game.js places it from this */
}
#chooser.bar { flex-wrap: nowrap; }
#chooser.bar .ch-head { flex: 0 1 auto; min-width: 0; }
#chooser.bar .ch-title, #chooser.bar .ch-title .suit-ink { min-width: 0; }
#chooser.bar .ch-sum { white-space: nowrap; }
#chooser.bar .ch-title { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#chooser.bar .ch-close { width: 44px; height: 44px; margin: 0; flex: none; }
#chooser.bar .ch-row { flex: 1; flex-direction: row; align-items: center; justify-content: flex-end; gap: 10px; }
#chooser.bar .ch-hint, #chooser.bar .ch-why { display: none; }
#chooser.bar .ch-stepper .ch-label { display: none; }
#chooser.bar .ch-step button { width: 44px; height: 44px; }
#chooser.bar .ch-stepper { flex: none; }
#chooser.bar .ch-sum { flex: none; }
#chooser.bar .ch-actions { flex: 0 1 auto; min-width: 0; }
#chooser.bar .ch-actions .gb { white-space: nowrap; }
#chooser.bar .ch-actions { flex: 0 0 auto; gap: 6px; }
body.compact #chooser:not(.bar) { width: min(400px, calc(100vw - 12px)); padding: 6px 10px 8px; gap: 5px; }
body.compact .ch-row { flex-direction: row; align-items: center; justify-content: space-between; gap: 10px; }
body.compact .ch-sum { padding: 0; }
body.compact .ch-head { margin-bottom: -2px; }
body.compact .ch-title { font-size: 14px; }
body.compact .ch-hint, body.compact .ch-why { font-size: 11.5px; }
body.compact #tower.strip .strip-bottom { display: none; }
body.compact #tower.strip .strip-seat { padding: 1px 6px; }

@media (max-width: 700px), (max-height: 460px) {
    .seat { font-size: 11.5px; padding: 4px 8px 5px; }
    .seat-row { gap: 6px; font-size: 10.5px; }

    #chooser { width: 236px; padding: 8px 10px 10px; }
    .ch-sum b { font-size: 20px; }
    .tower-speed { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
    .card, .final, .seat { transition: none; }
    #chooser, .callout, .final.flash { animation: none; }
}

/* The Rules link in the start screen's launch column — sized to its own text, never a
   .start-play (the shared sheet makes those full width). */
.rules-link {
    align-self: center;
    display: inline-block;
    width: auto;
    margin-top: 4px;
    padding: 7px 16px;
    border: 1px solid var(--dk-accent-soft);
    border-radius: var(--dk-radius);
    background: transparent;
    color: var(--zz-ink);
    font-family: var(--dk-font-ui); font-size: 13px; font-weight: 600;
    text-align: center; text-decoration: none; white-space: nowrap;
    opacity: .85;
    transition: background .15s, opacity .15s;
}
.rules-link:hover { background: var(--dk-accent-soft); opacity: 1; }
