/* STONIC — themed UI. The display font + colour vars below are the HOUSE PRINT SHOP
   defaults; themes.js overrides --disp / palette / @font-face per active theme.
   THE ONE MATERIAL RULE: every surface is INK ON PAPER. Over the film (HUD, banners,
   chevrons, tutorial) type is knockout — --white/--gold fill, ink stroke, var(--shadow)
   offset. On paper (any card) type is SOLID INK — zero text-stroke, tracking ≥ .04em.
   The only knockout on paper is the h1 STONIC marquee. */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* FIRST PAINT IS VINTAGE. The journey is pinned to the 1930s look, so the CSS
   defaults ARE that look — the loader must never flash another game's palette
   while scripts are still downloading. themes.js re-applies (a no-op for
   vintage) and dev ?theme= overrides still repaint on top. */
:root {
  /* WHERE THE INTERFACE MAY SAFELY START. On the web this is the device's own
     safe area. Inside Telegram, tg.js publishes --tg-top/--tg-bottom which
     ALREADY include the device inset plus Telegram's overlaid controls — so take
     the LARGER of the two rather than adding them, or the notch counts twice.
     Unset on the web, where max() collapses to env() and nothing changes. */
  --ui-top: max(env(safe-area-inset-top, 0px), var(--tg-top, 0px));
  --ui-bottom: max(env(safe-area-inset-bottom, 0px), var(--tg-bottom, 0px));
  --sky:    #8f8a7e;
  --sky-dk: #5a554b;
  --gold:   #e7e1d2;
  --gold-dk:#8a8377;
  --ink:    #1a1a18;          /* outline / shadow — INK, not navy */
  --white:  #f5f2ea;
  --green:  #7d786c;
  --red:    #3a352e;
  /* THE SAFE-AREA FILL. Both edge fades and the browser's own bars take these,
     so the strips above and below the picture read as more STAGE rather than a
     cream band. Both values are MEASURED off cc-stage.png's own edges: the top
     from its curtain/wings band, the bottom from its floorboards, which is why
     the fill and the painting meet without a seam. --page-bot defaults to
     --page, so the other seven themes are untouched by this. */
  --page:   #131312;          /* curtain tone, measured rgb(19,19,18) */
  --page-bot: #080808;        /* floorboard tone, measured rgb(8,8,8) */
  --card:   #f2eee4;          /* OPAQUE — a printed plate hides what is behind it */
  --card-border: 3px solid #1a1a18;
  --card-radius: 10px;
  --chip:   #f7f4ec;          /* OPAQUE — art linework must never strike through label type */
  --chip-text: #1a1a18;
  --chip-you: #1a1a18;
  --chip-you-text: #f5f2ea;
  --shadow: rgba(26,26,24,.35);
  --muted:  #4a463d;
  --body:   Georgia, "Iowan Old Style", "Times New Roman", serif;
  --btn:    linear-gradient(180deg, #efe9da, #b8b1a0);
  --btn-ghost: linear-gradient(180deg, #e6e0d1, #d5cebc);   /* flat-ish paper, not chrome */
  --overlay: radial-gradient(150% 120% at 50% 42%, rgba(245,242,234,.30), rgba(206,201,189,.72));
  --glow:   rgba(40,38,34,.26);
  --disp: "Vintage Display", "Ultra", Rockwell, Georgia, serif;
  /* print-shop tokens — mirrored into every themes.js palette */
  --accent-on-paper: #1a1a18;                 /* vintage: ink. sonic/comic/etc: their --gold */
  --stroke-card: 0px;                          /* vintage: 0. dark-card themes: 2px */
  --screen: #0b0b09;                           /* film black — endCard bg + iris (game.js reads activeStyle.screen) */
  --shadow-drop: rgba(26,26,24,.28);           /* hard offset box-shadows (cards, plates, chips) */
  --well: rgba(26,26,24,.5);                   /* meter track wells (non-vintage; vintage gauges use --card) */
  --rule-faint: rgba(26,26,24,.24);            /* ledger column rules, graduation ticks */
  --rule-half: rgba(26,26,24,.5);              /* leader crosshair */
  --dim-strong: rgba(26,26,24,.62);            /* tut spotlight */
  --dim-soft: rgba(26,26,24,.32);
  --dim-faint: rgba(26,26,24,.18);
  /* THE LOBBY MARGIN — strip, prop columns and deck all inset by this, so nothing
     sits on the frame's edge. On a phone it is a flat 18px; past 676px of width it
     grows so the furniture stays a phone-shaped column instead of stretching a
     50px-tall strip across a desktop. ONE number, so no edge can ever drift. */
  --lobby-pad: max(18px, calc((100vw - 640px) / 2));
  /* THE PROP SIZE — one box for every illustrated button, deck and side column
     alike, so the two columns can never read as different tiers by accident.
     Bounded on BOTH axes: in landscape the side column hangs above ROUTE in the
     deck, and a width-only size grew until the two collided. */
  /* The chip row was 107px of a 701px screen with 81px of art. Together with
     the plaques that gave a quarter of the lobby to furniture, and put the
     deck's top edge above the stage floor so the buttons stood on the pilots'
     feet. Still well over the 44px touch floor. */
  --prop-art: clamp(44px, min(13vw, 8.4vh), 62px);
  --paper-grain:
    repeating-linear-gradient(0deg,  rgba(26,26,24,.035) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgba(26,26,24,.022) 0 1px, transparent 1px 7px);
  color-scheme: light;
}
html, body {
  height: 100%; width: 100%;
  background: var(--page, #061634);
  color: var(--white);
  font-family: var(--disp);
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
  user-select: none; -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
/* Full-bleed stage: 100dvh tracks the DYNAMIC mobile viewport (iOS URL bar / chrome),
   so the game truly covers the screen; vh is the legacy fallback. The canvas fills the
   stage box (JS reads the stage's real rendered size — not window.innerHeight, which
   excludes iOS browser chrome). UI keeps itself in the safe zones via env() paddings. */
/* THE STAGE STOPS WHERE TELEGRAM'S CHROME BEGINS. We ask Telegram for
   fullscreen, which draws its Close/menu controls OVER our content — so the
   playfield must be inset by the insets tg.js publishes, or the marquee hides
   under the header and the canvas sizes itself taller than the visible area
   (which is why the cat and the backdrop rendered too large).
   --tg-* are UNSET on the web and fall back to 0px, so the web page computes
   byte-for-byte what it always did. One rule, both doorways. */
/* THE PICTURE FILLS THE SCREEN. An earlier attempt inset the STAGE by
   Telegram's insets, which letterboxed the artwork behind black bands top and
   bottom — wrong for a full-bleed game. The canvas always runs edge to edge; it
   is the UI that steps out of the way of Telegram's controls, via --ui-top and
   --ui-bottom. */
#stage { position: fixed; inset: 0; width: 100vw; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); }
#game, #filmfx { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#filmfx { z-index: 1; pointer-events: none; }
/* Safari's status/URL bars can't be painted (no Fullscreen API on iPhone) — instead the
   game DISSOLVES into them: top/bottom fades to var(--page), the same colour the bars are
   tinted with (theme-color meta tracks --page per zone), so phone + game read as one surface. */
/* On real devices the page extends UNDER the bars into the safe areas — a fixed-height fade
   ends up submerged beneath the bar with only its weak end visible. Stay SOLID through the
   safe-area strip, then fade across the visible 80px. env() = 0 where there are no insets.
   Fades target var(--page) — the ACTIVE style's page colour, live per zone/style — so the
   game area always matches its own current look (user call, round 8). Safari's own bars keep
   the boot tint (it samples once and never re-tints); that seam is outside our control. */
#stage::before, #stage::after {
  content: ""; position: absolute; left: 0; right: 0;
  z-index: 2; pointer-events: none;
  display: none;             /* desktop: no browser bars / notch to dissolve into — no fades */
}
/* Touch devices only (phones + tablets, incl. Dynamic-Island iPhones): hover:none +
   pointer:coarse is the standard primary-input test — mouse/trackpad desktops report
   hover:hover + pointer:fine and keep the full play area edge-to-edge. */
@media (hover: none) and (pointer: coarse) {
  #stage::before, #stage::after { display: block; }
}
/* The visible fade must READ during play on every style: 80px of linear falloff let
   bright sprites stay near-full brightness until ~40px from the edge, which looks like
   no gradient at all on dark themes. 120px + an eased mid-stop (color-mix, with a plain
   3-stop fallback for engines without it) dissolves sprites clearly before the edge.
   env() lives in height only; stops use calc(100% - Npx), which equals the safe-area
   inset exactly: solid through the bar strip, fade across the visible 120px. */
#stage::before {
  top: 0; height: calc(var(--fade, 120px) + var(--ui-top));
  background: linear-gradient(to bottom,
    var(--page), var(--page) calc(100% - var(--fade, 120px)), transparent);
  background: linear-gradient(to bottom,
    var(--page), var(--page) calc(100% - var(--fade, 120px)),
    color-mix(in srgb, var(--page) 55%, transparent) calc(100% - var(--fade, 120px) * 0.46),
    transparent);
}
#stage::after {
  bottom: 0; height: calc(var(--fade, 120px) + var(--ui-bottom));
  background: linear-gradient(to top,
    var(--page-bot, var(--page)), var(--page-bot, var(--page)) calc(100% - var(--fade, 120px)), transparent);
  background: linear-gradient(to top,
    var(--page-bot, var(--page)), var(--page-bot, var(--page)) calc(100% - var(--fade, 120px)),
    color-mix(in srgb, var(--page-bot, var(--page)) 55%, transparent) calc(100% - var(--fade, 120px) * 0.46),
    transparent);
}

/* WITH A CARD UP the veil (.overlay, z-20) used to bury these fades (z-2), so the
   bar strip met the dimmed scene on a hard line — the bands looked like flat fill
   with the dissolve gone. Lift them over the veil, and shorten the falloff to a
   short lip: the seam stays dissolved while a centred card is never reached. */
body.overlay-up #stage::before,
body.overlay-up #stage::after { z-index: 21; --fade: 30px; }

/* ---------- HUD (over-film: knockout type, var(--shadow) offsets) ---------- */
.hud {
  position: absolute; inset: 0; z-index: 5; pointer-events: none;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: max(16px, var(--ui-top)) 20px max(20px, calc(var(--ui-bottom) + 14px));
}
.hud-top, .hud-bottom { display: flex; justify-content: space-between; align-items: flex-start; }
.hud-label { font-size: 12px; letter-spacing: .16em; color: var(--gold); -webkit-text-stroke: 2px var(--ink); paint-order: stroke fill; text-shadow: 0 2px 0 var(--shadow); }
.hud-num { font-size: 40px; line-height: .92; transform: skewX(-6deg); display: inline-block;
  color: var(--white); -webkit-text-stroke: 3px var(--ink); paint-order: stroke fill; text-shadow: 0 4px 0 var(--shadow); }
.hud-num.small { font-size: 22px; -webkit-text-stroke: 2px var(--ink); }
.hud-num.gold { color: var(--gold); }
.hud-num .unit { font-size: .42em; margin-left: 1px; -webkit-text-stroke: 1.5px var(--ink); }

.hud-score, .hud-best { display: flex; flex-direction: column; gap: 1px; }
.hud-rings { display: flex; align-items: center; gap: 10px; }
.hud-rings.pop { animation: ringpop .28s cubic-bezier(.2,1.6,.4,1); }
.ring-icon {
  width: 30px; height: 30px; border-radius: 50%;
  border: 8px solid var(--gold);
  box-shadow: 0 0 0 3px var(--ink), inset 0 0 0 3px #fff3b0, 0 3px 0 var(--shadow);
  background: transparent; flex: 0 0 auto;
  position: relative; overflow: hidden;
}
/* the coin's glint sweeps INSIDE the ring — the old rotation spun a radially
   symmetric shape, so the only visible motion was its drop shadow orbiting */
.ring-icon::after {
  content: ""; position: absolute; inset: -8px;
  background: conic-gradient(rgba(255,255,255,.7) 0 14deg, transparent 14deg);
  animation: spin 2.4s linear infinite; will-change: transform;
}
.hud-boost { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.boost-meter { width: 110px; height: 12px; border-radius: 8px; background: var(--well); border: 3px solid var(--ink); overflow: hidden; }
.boost-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--gold), #ff8a1e); box-shadow: 0 0 12px rgba(255,160,40,.7); transition: width .08s linear; }

.zone-label {
  font-size: 16px; letter-spacing: .16em; text-align: center; align-self: center; flex: 1;
  color: var(--white); -webkit-text-stroke: 2.5px var(--ink); paint-order: stroke fill; text-shadow: 0 3px 0 var(--shadow);
  opacity: 0; transition: opacity .3s ease; transform: skewX(-6deg);
}
.zone-label.show { opacity: .95; }

.combo {
  align-self: center; font-size: 34px; transform: skewX(-8deg);
  color: var(--gold); -webkit-text-stroke: 3px var(--ink); paint-order: stroke fill;
  text-shadow: 0 4px 0 var(--shadow);
  opacity: 0; transition: opacity .12s ease;
}
.combo.show { opacity: 1; animation: pop .22s cubic-bezier(.2,1.7,.4,1); }
[data-theme="vintage"] .combo.show { animation: pop-steps .22s steps(2); }   /* pop carries the skew every frame; vintage (skew stripped) pops on twos */

/* ---------- ring-collect "+1" popups ---------- */
.ringpop-fx {
  position: absolute; z-index: 6; pointer-events: none;
  font-family: var(--disp); font-size: 26px; color: var(--gold);
  -webkit-text-stroke: 3px var(--ink); paint-order: stroke fill; transform: skewX(-6deg);
  animation: floatup .7s ease-out forwards; will-change: transform, opacity;
}
.ringpop-fx.idle { opacity: 0; animation: none; }
.ringpop-fx.rp2 { animation-name: floatup2; }

/* ---------- banner ---------- */
/* P1: the lower-third band — fight-scoped shouts print BELOW the keeper
   (the boss occupies ~4-44% of frame; a banner at 30% sat on its face). */
.banner.low { top: 58%; }
.banner {
  position: absolute; top: 30%; left: 0; right: 0; text-align: center; z-index: 4;
  font-size: clamp(30px, 10vw, 60px); transform: skewX(-7deg);
  color: var(--white); -webkit-text-stroke: 4px var(--ink); paint-order: stroke fill; text-shadow: 0 6px 0 var(--shadow);
  pointer-events: none; opacity: 0;
}
.banner.play { animation: bannerPop 2.1s ease forwards; }
.banner.play2 { animation: bannerPop2 2.1s ease forwards; }
.banner .sub-banner { display: block; font-size: .36em; letter-spacing: .12em; color: var(--gold); margin-top: 8px; -webkit-text-stroke: 2px var(--ink);
  max-width: 86vw; margin-left: auto; margin-right: auto; }   /* the chain line must never touch the frame edges */

/* ---------- boss bar ---------- */
.boss-ui { margin: 8px auto 0; width: min(86%, 360px); text-align: center; }
.boss-name { font-size: 16px; letter-spacing: .14em; color: var(--red); margin-bottom: 6px; transform: skewX(-6deg);
  -webkit-text-stroke: 2.5px var(--ink); paint-order: stroke fill; text-shadow: 0 3px 0 var(--shadow); }
.boss-bar { height: 14px; border-radius: 9px; background: var(--well); border: 3px solid var(--ink); overflow: hidden; }
.boss-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--red), var(--gold)); transition: width .1s linear; }

/* ---------- overlays (title / results) ---------- */
.overlay {
  position: absolute; inset: 0; z-index: 20; display: grid; place-items: center;
  background: var(--overlay);
  animation: fadein .25s ease;
}
/* The card prints its own paper: the film shader never grades z-20 (filmfx is z-1),
   so grain, hand-cut corners, the double ink rule and the printer's quads are CSS.
   Ornaments live in background (not pseudos): the card is its own scroll container
   and backgrounds stay glued to the box while absolute pseudos scroll away. */
/* ONE PLATE WIDTH for every panel: the cards used to shrink-to-fit, so MISSIONS
   printed 40px narrower than TOP PILOTS and the ledger rows overflowed the paper
   margin. A definite width gives the whole house one measure — and one content
   column (width minus the side padding) that plates, rows and lists all share. */
.overlay-card {
  text-align: center; width: min(356px, 92vw); padding: 22px 24px 24px;
  max-height: calc(100svh - 24px); overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: var(--card-border, 3px solid var(--ink));
  border-radius: 11px 9px 12px 9px / 9px 12px 9px 11px;   /* hand-cut plate corners */
  background-color: var(--card);
  background-image:
    radial-gradient(circle 3.5px at 11px 11px,                       var(--ink) 0 3px, transparent 3.5px),
    radial-gradient(circle 3.5px at calc(100% - 11px) 11px,          var(--ink) 0 3px, transparent 3.5px),
    radial-gradient(circle 3.5px at 11px calc(100% - 11px),          var(--ink) 0 3px, transparent 3.5px),
    radial-gradient(circle 3.5px at calc(100% - 11px) calc(100% - 11px), var(--ink) 0 3px, transparent 3.5px),
    var(--paper-grain);
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, repeat, repeat;
  box-shadow:
    inset 0 0 0 3px var(--card),      /* gap */
    inset 0 0 0 4.5px var(--ink),     /* the double ink rule — same geometry as .title-card */
    0 10px 0 var(--shadow-drop);
}

/* STONIC logo — the one knockout allowed on paper */
.overlay-card h1 {
  /* THE CARD IS A FIXED MEASURE, SO THE TITLE MUST BE TOO. 17vw overran it and
     the loader's STONIC was clipped at both ends; 14.6vw fixed that at PHONE
     widths but the 110px ceiling still overran on desktop — the card stops
     growing at 356px while vw keeps climbing, so at 1600px the title rendered
     110px / 537px wide inside a 302px column and read "STON". The ceiling has
     to be the column, not a guess: 56px is the largest that fits 302px. */
  font-family: var(--disp); font-size: clamp(38px, 14.6vw, 56px); line-height: .9;
  transform: skewX(-8deg); letter-spacing: .01em;
  color: var(--gold);
  -webkit-text-stroke: 6px var(--ink); paint-order: stroke fill;
  text-shadow: 0 8px 0 var(--shadow), 0 0 30px var(--glow);
  animation: logobounce 2.4s ease-in-out infinite;
}
/* T2 card title: solid ink on paper — no stroke, no skew, no shadow. 8vw keeps
   TODAY'S ROUTE on ONE line inside the card's measure (8.4vw broke it in two). */
.overlay-card h2 {
  font-family: var(--disp); font-size: clamp(28px, 8vw, 40px); line-height: 1.06;
  letter-spacing: .03em; color: var(--ink); text-wrap: balance;
}
/* the Oxford rule under every card title */
.overlay-card h2::after {
  content: ""; display: block; width: 72px; height: 5px;
  margin: 10px auto 0; border-top: 3px solid var(--ink); border-bottom: 1px solid var(--ink);
}
.overlay-card h2.win-title { color: var(--accent-on-paper); }
#deathScreen h2 { color: var(--red); }   /* danger red belongs to CRASHED alone */
.sub { font-size: 14px; letter-spacing: .26em; color: var(--sky); margin-top: 6px; text-transform: uppercase; -webkit-text-stroke: 1.5px var(--ink); paint-order: stroke fill; }
/* body copy: 1.9 leading floated two short lines apart with no paragraph to hold
   them; 1.5 sets them as one block, and balance stops the one-word orphan line */
.hint {
  font-size: 14px; line-height: 1.5; letter-spacing: .04em; text-wrap: balance;
  color: var(--muted); margin: 14px 0 12px;
  font-family: var(--body, system-ui, sans-serif); font-weight: 500;
}
.hint b { color: var(--accent-on-paper); font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.tiny { font-size: 12.5px; line-height: 1.45; color: var(--muted); margin-top: 12px; font-family: var(--body, system-ui, sans-serif); font-weight: 600; text-wrap: balance; }
.tiny b { color: var(--accent-on-paper); }

/* ---------- first-time tutorial freeze layer (over-film grammar) ---------- */
.tut { position: absolute; inset: 0; z-index: 8; pointer-events: none; }   /* pointer-events:none is LOAD-BEARING — canvas must keep receiving taps */
.tut-dim {   /* radial spotlight on the threat (dodge/orb) */
  position: absolute; inset: 0;
  background: radial-gradient(circle at var(--sx, 50%) var(--sy, 40%),
    transparent calc(var(--sr, 90px) * 0.8), var(--dim-soft) var(--sr, 90px), var(--dim-strong) calc(var(--sr, 90px) * 1.9));
  animation: fadein .2s ease;
}
.tut.lane .tut-dim {   /* full-height lane band (guardian tell) */
  background: linear-gradient(90deg,
    var(--dim-strong) calc(var(--lx0, 33%) - 6px), var(--dim-faint) var(--lx0, 33%),
    var(--dim-faint) var(--lx1, 66%), var(--dim-strong) calc(var(--lx1, 66%) + 6px));
}
.tut-card {
  position: absolute; top: 16%; left: 50%; transform: translateX(-50%);
  text-align: center; width: max-content; max-width: 86vw;
  animation: pop .35s cubic-bezier(.2,1.7,.4,1);
}
.tut-card h3 {
  font-family: var(--disp); font-size: clamp(28px, 8vw, 44px); color: var(--gold);
  transform: skewX(-6deg); -webkit-text-stroke: 3.5px var(--ink); paint-order: stroke fill;
  text-shadow: 0 5px 0 var(--shadow);
}
.tut-card p {
  margin-top: 8px; font-size: 15px; font-weight: 700; font-family: var(--body);
  color: var(--white); text-shadow: 0 2px 4px var(--shadow);
}
.tut-card p b { color: var(--gold); }
[data-theme="vintage"] .tut-card h3 { transform: none; }   /* vintage strips skew (flat 1930s title card) */
[data-theme="vintage"] .tut-card { animation: pop-steps-center .3s steps(2) both; }   /* pops on twos; keyframes must carry the translateX centering — fill 'both' holds the last frame forever */
.tut-hand {
  position: absolute; bottom: 22%; width: 76px; height: 76px;
  display: grid; place-items: center;
  font-family: var(--disp); font-size: 17px; color: var(--white);
  -webkit-text-stroke: 2px var(--ink); paint-order: stroke fill;
}
.tut-hand.l { left: 14%; } .tut-hand.r { right: 14%; }
.tut-ring {   /* pulsing tap ripple */
  position: absolute; inset: 0; border-radius: 50%;
  border: 4px solid var(--gold); box-shadow: 0 0 18px var(--glow);
  animation: tutring 1.1s ease-out infinite;
}
@keyframes tutring { 0% { transform: scale(.55); opacity: 1; } 70% { transform: scale(1.15); opacity: .25; } 100% { transform: scale(1.25); opacity: 0; } }

/* ---------- fading edge chevrons (early-runs tap affordance) ---------- */
.chev {
  position: absolute; top: 66%; transform: translateY(-50%); z-index: 5; pointer-events: none;
  line-height: 0;
  filter: drop-shadow(0 2px 0 var(--shadow));
  animation: chevpulse 1.4s steps(6) infinite;   /* on the exposure clock, not eased */
}
/* the LEFT hint points LEFT: mirror the glove (the SVG is drawn pointing right) */
.chev.l svg { transform: scaleX(-1); }
.chev.l { left: calc(10px + env(safe-area-inset-left, 0px)); }
.chev.r { right: calc(10px + env(safe-area-inset-right, 0px)); }
@keyframes chevpulse { 0%, 100% { transform: translateY(-50%) scale(1); } 50% { transform: translateY(-50%) scale(1.22); } }

/* results stats — a printed table: T3 ink numerals, T7 labels, ledger rules.
   THREE EQUAL COLUMNS: as a flex row the cells were as wide as their own labels,
   so the numerals landed at uneven intervals and the rules never fell on the
   thirds. A grid rules the table properly. */
.death-stats { display: grid; grid-template-columns: repeat(3, 1fr); align-items: end; margin: 16px 0 14px; }
.death-stats div { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.death-stats div + div { border-left: 1.5px solid var(--rule-faint); }
.death-stats span { font-size: 34px; line-height: 1.05; letter-spacing: .02em; color: var(--ink); }
.death-stats small { font-family: var(--body); font-size: 12px; letter-spacing: .1em; color: var(--muted); }
.new-best { font-size: 22px; letter-spacing: .04em; color: var(--accent-on-paper);
  -webkit-text-stroke: var(--stroke-card) var(--ink); paint-order: stroke fill;
  margin-bottom: 6px; animation: pop-steps .34s steps(2); }

/* ---------- the letterpress plate — primary action ----------
   THE BILLING LADDER. Plate width used to be whatever the label measured, so
   PRACTICE FLIGHT printed wider than FLY THE ROUTE and STOKE THE FURNACE bullied
   RESTART off its own card. Now the primary owns the card's full content column
   and the ghost steps in from both edges at a smaller size — the ladder reads the
   same on every panel, whatever the words happen to be. */
.play-btn {
  /* THE PLATE IS A FIXED SIZE. It used to be min-height, so any label long
     enough to wrap grew the box — POST THE CROSSING made the win card's
     primary 85px tall while every other primary was 58px, and stacked above a
     50px ghost and 44px links it read as three mismatched buttons. Fixed
     height + nowrap means a plate is the same plate on every screen; fitPlate()
     shrinks an over-long label to fit rather than letting it wrap or spill. */
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 100%; margin-left: auto; margin-right: auto;
  height: 58px; min-height: 58px; padding: 12px 22px; white-space: nowrap;
  font-family: var(--disp); font-size: clamp(19px, 5.4vw, 24px); letter-spacing: .08em;
  line-height: 1.14; color: var(--ink);
  background-image: var(--paper-grain), var(--btn);
  border: 3px solid var(--ink); border-radius: 12px;
  cursor: pointer; pointer-events: auto;
  box-shadow: 0 5px 0 var(--ink);
  transition: none;   /* a press is a discrete state change — steps(1), no easing */
}
.play-btn::before {   /* the double ink rule — the house signature, over any gradient */
  content: ""; position: absolute; inset: 3px; border: 1.5px solid var(--ink);
  border-radius: 7px; pointer-events: none;
}
.play-btn:active { transform: translateY(4px); box-shadow: 0 1px 0 var(--ink); }
/* not set in the press — but --card IS the card, so the plate used to vanish into
   the paper. --chip keeps a readable plate with the grain still printed on it. */
.play-btn[disabled] { background-color: var(--chip); background-image: var(--paper-grain); color: var(--muted); box-shadow: none; pointer-events: none; }
/* second billing: narrower, shorter, smaller type, single rule, shallower shadow */
.play-btn.ghost {
  /* THE SAME WIDTH AS THE PLATE ABOVE IT. This was 88%, so any card stacking a
     primary over a ghost — COPY LINK over SEND IT over CANCEL — printed three
     buttons in two widths, which reads as a mistake rather than a hierarchy.
     Ghosts are already distinguished by weight, fill and shadow; they do not
     need to be narrower as well. */
  width: 100%; height: 50px; min-height: 50px; padding: 11px 14px;
  font-size: clamp(15px, 4.2vw, 18px);
  border-width: 2.5px; border-radius: 11px;
  background-image: var(--paper-grain), var(--btn-ghost); box-shadow: 0 3px 0 var(--ink);
}
.play-btn.ghost::before { content: none; }
.play-btn.ghost:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--ink); }
.win-buttons { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-top: 16px; }
/* the quiet tier rides ONE row, on the win card exactly as on the death card */
.card-links { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }

/* the one hidden utility — never re-asserted, never outranked */
.hidden { display: none !important; }

/* ---------- keyframes ---------- */
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes ringpop { 0%{transform:scale(1)} 40%{transform:scale(1.35)} 100%{transform:scale(1)} }
@keyframes pop { 0%{transform:scale(.5) skewX(-8deg)} 60%{transform:scale(1.18) skewX(-8deg)} 100%{transform:scale(1) skewX(-8deg)} }
@keyframes floatup { 0%{opacity:0; transform:skewX(-6deg) translateY(8px) scale(.7)} 25%{opacity:1; transform:skewX(-6deg) translateY(-6px) scale(1.1)} 100%{opacity:0; transform:skewX(-6deg) translateY(-56px) scale(1)} }
@keyframes floatup2 { 0%{opacity:0; transform:skewX(-6deg) translateY(8px) scale(.7)} 25%{opacity:1; transform:skewX(-6deg) translateY(-6px) scale(1.1)} 100%{opacity:0; transform:skewX(-6deg) translateY(-56px) scale(1)} }
@keyframes logobounce { 0%,100%{transform:skewX(-8deg) translateY(0) rotate(-1deg)} 50%{transform:skewX(-8deg) translateY(-12px) rotate(1deg)} }
@keyframes fadein { from{opacity:0} to{opacity:1} }
@keyframes bannerPop { 0%{opacity:0; transform:skewX(-7deg) scale(.8) translateY(10px)} 16%{opacity:1; transform:skewX(-7deg) scale(1) translateY(0)} 78%{opacity:1; transform:skewX(-7deg) scale(1) translateY(0)} 100%{opacity:0; transform:skewX(-7deg) scale(1.05) translateY(-10px)} }
@keyframes bannerPop2 { 0%{opacity:0; transform:skewX(-7deg) scale(.8) translateY(10px)} 16%{opacity:1; transform:skewX(-7deg) scale(1) translateY(0)} 78%{opacity:1; transform:skewX(-7deg) scale(1) translateY(0)} 100%{opacity:0; transform:skewX(-7deg) scale(1.05) translateY(-10px)} }

/* ---------- task rows — the burn card, printed in-system ---------- */
.tasks-list { display: flex; flex-direction: column; gap: 8px; margin: 16px 0 0; }
.task-row { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 58px;
  text-align: left; padding: 10px 12px; cursor: pointer; pointer-events: auto;
  background-color: var(--chip); background-image: var(--paper-grain);
  color: var(--chip-text); border: 2.5px solid var(--ink);
  border-radius: 9px 7px 8px 7px / 7px 9px 7px 8px;
  box-shadow: 0 3px 0 var(--shadow-drop); font-family: var(--body); transition: none; }
.task-row:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--shadow-drop); }
.hint + .tasks-list { margin-top: 8px; }   /* the paragraph already left its own air */
/* on the revive card the burn row IS the primary action, so it prints as a plate
   (ink drop, 3px rule) instead of a list chip — the ghost below it is the exit */
#revivePanel .task-row { min-height: 62px; border-width: 3px; box-shadow: 0 4px 0 var(--ink); }
#revivePanel .task-row:active { box-shadow: 0 1px 0 var(--ink); }
#revivePanel .task-body b { font-size: 16.5px; }
.task-ic     { font-size: 20px; width: 26px; text-align: center; color: var(--chip-accent, var(--accent-on-paper)); flex: 0 0 auto; }
.task-body { flex: 1; display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.task-body b { font-family: var(--disp); font-size: 15px; letter-spacing: .06em;
               text-transform: uppercase; font-weight: 400; color: var(--chip-text); }
.task-body small { font-size: 11.5px; color: var(--muted); }
.task-reward { font-family: var(--disp); font-size: 12px; letter-spacing: .08em;
               color: var(--chip-accent, var(--accent-on-paper)); flex: 0 0 auto; white-space: nowrap; }
.task-row.validating { opacity: .75; }
.task-row.validating .task-reward { animation: taskpulse .9s steps(2) infinite; }  /* stepped blink, not a fade */
.task-row.done { border-color: var(--green); }
.task-row.done .task-reward, .task-row.done .task-ic { color: var(--green); }
@keyframes taskpulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }

/* ---------- Vintage Toon: flatter title-card UI ---------- */
[data-theme="vintage"] .overlay-card h1 { transform: none; animation: vintbob 2.6s ease-in-out infinite; text-shadow: 0 6px 0 var(--shadow); }
[data-theme="vintage"] .sub, [data-theme="vintage"] .hud-num,
[data-theme="vintage"] .banner, [data-theme="vintage"] .combo, [data-theme="vintage"] .zone-label { transform: none; }
[data-theme="vintage"] .chev { color: var(--ink); -webkit-text-stroke: 3px var(--white); text-shadow: none; }
@keyframes vintbob { 0%,100%{transform:translateY(0) rotate(-1deg)} 50%{transform:translateY(-8px) rotate(1deg)} }


/* — HUD: cockpit readouts (upright, flat print shadows) — */

/* — gauges: segmented cockpit meters (boost / boss) — */

/* targeting brackets: ONE pseudo, 8 corner gradients — on NON-clipping wrappers */

/* — zone banner: OVA episode title card (letterbox strip, thin rules, EPISODE numeral) — */

/* — overlays: static title-card logo, italic episode-end titles — */

/* fansub subtitles: bold sans, thick black outline (the VHS-fansub look) */
/* VCR OSD small print */

/* buttons: flat cel print — no shine, no pulse, hard offset shadow */

/* results stats + quest rows: anime-OS console list */

/* tutorial: bracket-framed cockpit callout, clean cut-in */

/* chevrons: flat print, no glow (pulse kept — tap affordance) */

/* A period production dims the house lights; it does not frost the glass —
   the blur lives ONLY off the flagship look (vintage must never pay for it). */
html:not([data-theme="vintage"]) .overlay { backdrop-filter: blur(2px); }

/* DEV style switch (local review only, ?dev=1) — exempt from the token law */

/* revive flow + death card: stacked plates, printed rhythm (16/10/8) */
#deathScreen .play-btn + .play-btn { margin-top: 10px; }
#deathScreen .rule-link.lg + .play-btn { margin-top: 10px; }
#deathScreen .revive-used + .rule-link { margin-top: 8px; }
#revivePanel .tasks-list + .play-btn { margin-top: 14px; }

/* ── quiet text control — the typeset rule-link (acts ⇒ underlined 2px/4px;
   informs ⇒ never underlined: .tiny/.fineprint/.revive-used) ── */
.rule-link { background: none; border: none; cursor: pointer; pointer-events: auto;
  min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  padding: 10px 14px; font-family: var(--disp); font-size: 15px; letter-spacing: .08em;
  line-height: 1.35; text-transform: uppercase; color: var(--accent-on-paper);
  -webkit-text-stroke: var(--stroke-card) var(--ink); paint-order: stroke fill;
  text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 2px; }
/* the rank pull is a SENTENCE, not a headline: at 17px it ran three underlined
   lines and out-shouted the plate under it. A measure + balance keeps it a quiet
   two-line pull, and a 1.5px rule stops three stacked underlines reading as bars. */
.rule-link.lg { display: block; width: 100%; margin: 4px auto 0; padding: 12px 14px;
  font-size: 15.5px; text-wrap: balance; text-underline-offset: 4px;
  text-decoration-thickness: 1.5px; animation: rankpop-steps .3s steps(2); }
.rule-link.sm { font-family: var(--body); font-size: 12.5px; letter-spacing: .14em; }
.rule-link:active { transform: translateY(1px); }                   /* discrete */
.rule-link[disabled] { color: var(--muted); text-decoration: none; pointer-events: none; }
@keyframes rankpop-steps { from { transform: scale(.7); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* SHARE's WEIGHT is a function of whether the run EARNED a brag (resolveBrag in
   game.js), not a fixed rung. THREE TIERS, ONE LOUD SLOT: quiet = this bare
   rule-link, ghost = .primary-share, plate = .share-plate. RESTART is still
   FIRST and instant in the reading order — on the plate tier it steps down to a
   ghost exactly as it already does for a loud revive, and in a letterpress
   billing ladder the biggest type wins the eye wherever it sits. A loud REVIVE
   always outranks a loud SHARE: two plates on one card is the shakedown look. */
#deathShare.primary-share {   /* same metrics as .play-btn.ghost — one ghost plate in the house */
  display: block; width: 88%; margin: 10px auto 0;
  min-height: 50px; padding: 11px 14px;
  border: 2.5px solid var(--ink); border-radius: 11px;
  background-image: var(--paper-grain), var(--btn-ghost);
  color: var(--ink); -webkit-text-stroke: 0;
  font-size: clamp(15px, 4.2vw, 18px); letter-spacing: .08em;
  text-decoration: none; box-shadow: 0 3px 0 var(--ink);
}
#deathShare.primary-share:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--ink); }

/* THE PLATE TIER — the full .play-btn metrics, shaped exactly like the ghost
   block above so no fourth control family enters the house: same paper, same
   double ink rule, same 5px ink drop, same discrete press. Because both the
   plate and .play-btn are width:100%, swapping the label for an
   acknowledgement can never reflow the card. */
#deathShare.share-plate {
  position: relative; display: block; width: 100%; margin: 10px auto 0;
  min-height: 58px; padding: 12px 22px;
  font-size: clamp(19px, 5.4vw, 24px); letter-spacing: .08em; line-height: 1.14;
  color: var(--ink); -webkit-text-stroke: 0;
  background-image: var(--paper-grain), var(--btn);
  border: 3px solid var(--ink); border-radius: 12px;
  text-decoration: none; box-shadow: 0 5px 0 var(--ink);
}
#deathShare.share-plate::before {   /* the double ink rule — the house signature */
  content: ""; position: absolute; inset: 3px; border: 1.5px solid var(--ink);
  border-radius: 7px; pointer-events: none;
}
#deathShare.share-plate:active { transform: translateY(4px); box-shadow: 0 1px 0 var(--ink); }
/* .primary-share (1,1,0) outranks .rule-link[disabled] (0,2,0) on colour, so a
   spent ghost-plate stayed full ink with its shadow and looked alive. Match the
   disabled treatment on BOTH share families. */
#deathShare.primary-share[disabled], #deathShare.rule-link[disabled] {
  color: var(--muted); box-shadow: none; opacity: .75;
}
#deathShare.share-plate[disabled] {
  background-color: var(--chip); background-image: var(--paper-grain);
  color: var(--muted); box-shadow: none;
}
/* the acknowledgement re-fires the house stamp, on twos like every card motion */
#deathShare.acked, #winShare.acked, #endShare.acked { animation: pop-steps .34s steps(2); }
#deathShare.acked2, #winShare.acked2, #endShare.acked2 { animation: pop-steps2 .34s steps(2); }

/* ── TOP PILOTS leaderboard — ledger rows, paper chips ── */
.lb-title { margin-bottom: 4px; }
/* the ledger rides the card's content column: at min(320px, 84vw) it was wider
   than the paper it printed on, so auto margins collapsed and every row leaned
   left and overran the plate's own margin. Its HEIGHT is measured in whole rows
   (pitch = row + gap) — a free vh cap sliced the last row in half behind the
   sticky YOU chip. 9 pitches, less the trailing gap, = 8 clean rows + yours. */
.lb-list { display: flex; flex-direction: column; gap: 5px; overflow-y: auto;
  --lb-pitch: 37px;
  max-height: min(46vh, calc(9 * var(--lb-pitch) - 5px));
  width: 100%; max-width: 320px; margin: 12px auto 4px; }
.lb-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; min-height: 32px;
  font-variant-numeric: lining-nums tabular-nums; font-feature-settings: 'lnum' 1, 'tnum' 1;
  border-radius: 8px 6px 7px 6px / 6px 8px 6px 7px;
  background-color: var(--chip); background-image: var(--paper-grain);
  border: 2px solid var(--ink); font-family: var(--body, system-ui, sans-serif);
  font-size: 13px; color: var(--chip-text); }
.lb-row .lb-rank { width: 30px; border-right: 1.5px solid var(--rule-faint); padding-right: 6px; margin-right: 2px;
  color: var(--accent-on-paper); font-weight: 800; flex: 0 0 auto; text-align: left; }
.lb-row .lb-name { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-row .lb-score { font-weight: 800; }
.lb-you { margin: 10px 0 2px; }
.lb-you input { width: min(240px, 100%); min-height: 46px; padding: 11px 12px; border-radius: 8px; text-align: center;
  border: 2.5px solid var(--ink); background: var(--chip); color: var(--chip-text);
  font-family: var(--disp); font-size: 15px; letter-spacing: .08em; outline: none; }
.lb-you input::placeholder { color: var(--muted); opacity: .6; }   /* the UA grey merged into the paper */
.lb-you input:focus { box-shadow: inset 0 0 0 2px var(--chip), inset 0 0 0 3.5px var(--ink); }   /* the house double rule as a focus ring */
#lbPanel .play-btn { margin-top: 12px; }
/* the CTA used to wrap to two lines and get clipped by Safari's toolbar */
#lbConnect { font-size: clamp(15px, 4.4vw, 20px); letter-spacing: .04em; white-space: nowrap; padding-left: 18px; padding-right: 18px; }
/* your row is what the eye hunts for: invert it, pin it in view, and give the
   inverted chip its own paper pinstripe */
/* the elision row: skipped ranks, printed as a run of leaders */
.lb-gap { justify-content: center; min-height: 20px; padding: 0 10px; opacity: .5;
  letter-spacing: .3em; pointer-events: none; border: 0; }
.lb-gap .lb-name { text-align: center; }
.lb-row.you {
  background: var(--chip-you, var(--gold)); color: var(--chip-you-text, var(--ink));
  border-color: var(--ink); position: sticky; bottom: 0;
  box-shadow: inset 0 0 0 2px var(--chip-you), inset 0 0 0 3.5px var(--chip-you-text), 0 2px 0 var(--shadow-drop);
}
.lb-row.you .lb-rank { border-right-color: var(--chip-you-text); color: var(--chip-you-text); }
.challenge-line {
  font-family: var(--disp); font-size: 17px; letter-spacing: .06em; margin-top: 8px;
  color: var(--accent-on-paper); -webkit-text-stroke: var(--stroke-card) var(--ink); paint-order: stroke fill;
}
.death-cause { font-size: .34em; letter-spacing: .1em; -webkit-text-stroke: 0; color: var(--muted); vertical-align: middle; }
.revive-used { font-family: var(--body, system-ui, sans-serif); font-size: 12px; letter-spacing: .08em;
  color: var(--muted); margin: 6px 0 0; }
/* the legal truth informs, it never competes: 600-weight ink at full size read as
   loud as the plate above it */
.trust { max-width: 32ch; margin: 10px auto 0; font-size: 11.5px; font-weight: 400; line-height: 1.45; opacity: .78; }
/* …YOUR row is an inverted ink chip, so its cells must flip back or they
   render black-on-black (equal specificity — this rule has to come after) */
[data-theme="vintage"] .lb-row.you .lb-rank,
[data-theme="vintage"] .lb-row.you .lb-name,
[data-theme="vintage"] .lb-row.you .lb-score { color: var(--chip-you-text); }

/* fine print — the period-honest home for technical truth (tx states, burn terms) */
.fineprint { display: inline-block; margin-top: 8px; font-size: 11.5px; line-height: 1.45;
  letter-spacing: .02em; opacity: .78; text-wrap: balance; }

/* long period labels must not shatter layouts */
.boss-name { white-space: nowrap; font-size: clamp(13px, 4.6vw, 20px); }

/* ── S2: the film's native grammar ─────────────────────────────────────── */
/* THE END — white-on-black studio ident, the only frame with no paper */
#endCard { background: var(--screen); }
.end-inner { text-align: center; padding: 0 22px; max-width: 100vw; }
/* 20vw ran THE END off both edges of the frame — a title card that doesn't fit
   the screen is not a title card. 15vw is the widest that stays inside the margins. */
.end-the { font-family: var(--disp); font-size: clamp(44px, 15vw, 120px); line-height: 1; color: var(--white); letter-spacing: .04em; }
.end-ident { font-family: var(--body, serif); font-size: 14px; letter-spacing: .3em; text-indent: .3em; color: var(--gold); margin-top: 20px; text-transform: uppercase; }
.end-tap { font-family: var(--body, serif); font-size: 12px; color: var(--gold-dk); margin-top: 38px; animation: endblink 1.6s steps(1) infinite; }
/* WIRE THE NEWS — the ident is the only card with no paper under it, so the
   quiet link prints in gold. It sits AFTER tap-to-continue and stops its own
   click, so tapping anywhere else on the card still deals the win card. */
.end-share { margin-top: 6px; color: var(--gold); -webkit-text-stroke: 0; }
@keyframes endblink { 0%,100% { opacity: .85; } 50% { opacity: .25; } }

/* Episode intertitle: a paper plate with a double ink rule, lantern-slide in */
.title-card {
  position: absolute; left: 50%; top: 26%; transform: translateX(-50%);
  z-index: 6; pointer-events: none; text-align: center;
  min-width: min(320px, 78vw); padding: 20px 30px 22px;
  background: var(--card, #f2eee4);
  border: var(--card-border, 3px solid var(--ink));
  border-radius: var(--card-radius, 10px);
  box-shadow: inset 0 0 0 3px var(--card, #f2eee4), inset 0 0 0 4.5px var(--ink), 0 8px 0 var(--shadow-drop);
  animation: slidein .28s steps(2) both;
}
.title-card.out { animation: slideout .24s steps(2) both; }
@keyframes slidein { from { opacity: 0; transform: translate(-50%, 5px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes slideout { from { opacity: 1; } to { opacity: 0; } }
.tc-eyebrow { font-family: var(--body, serif); font-size: 12.5px; letter-spacing: .3em; color: var(--muted); text-transform: uppercase; }
.tc-name { font-family: var(--disp); font-size: clamp(30px, 9vw, 46px); color: var(--ink); margin: 4px 0 2px; }
.tc-sub { font-family: var(--body, serif); font-style: italic; font-size: 14px; color: var(--muted); }
.tc-boss { display: none; }
.tc-type { display: contents; }   /* the plain card keeps its exact old layout */

/* THE IRIS TITLE CARD (founder pick) — the splice's black leader now carries
   a real Mickey-short opening: a drawn iris circle with a double ink ring,
   the chain's keeper peeking through as a silhouette, the episode type over
   it. Irises in on threes like a hand-cranked print. */
.title-card.leader {
  top: 50%; transform: translate(-50%, -50%);
  width: min(78vw, 330px); height: min(78vw, 330px);
  min-width: 0; padding: 0; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  border: 3px solid var(--ink);
  box-shadow: inset 0 0 0 5px var(--card, #f2eee4), inset 0 0 0 7px var(--ink),
              0 0 0 8px rgba(242, 238, 228, 0.12);
  animation: irisIn .4s steps(3) both;
}
.title-card.leader.out { animation: irisOut .24s steps(2) both; }
@keyframes irisIn { from { opacity: 0; transform: translate(-50%, -50%) scale(.18); }
                    to { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
@keyframes irisOut { from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
                     to { opacity: 0; transform: translate(-50%, -50%) scale(.18); } }
.title-card.leader .tc-boss {
  display: block; position: absolute; left: 50%; bottom: -34%;
  width: 60%; transform: translateX(-50%);
  filter: brightness(0); opacity: .45;   /* the keeper RISES into the iris — a shadow peeking, never eating the type */
}
.title-card.leader .tc-type { display: block; position: relative; padding: 0 26px; }
.title-card.leader .tc-eyebrow::before { content: "— "; letter-spacing: 0; }
.title-card.leader .tc-eyebrow::after { content: " —"; letter-spacing: 0; }
.title-card.leader .tc-name { font-size: clamp(34px, 11vw, 54px); }
.title-card.leader .tc-sub { max-width: 230px; margin: 0 auto; }

/* Academy film leader: countdown circle w/ clock wipe, crosshair, flutter */
.leader {
  position: relative; width: 140px; height: 140px; margin: 24px auto 8px;
  border-radius: 50%; border: 3px solid var(--ink);
  box-shadow: 0 0 0 6px var(--card, #f2eee4), 0 0 0 7.5px var(--ink);
  overflow: hidden;
}
.leader-sweep {
  position: absolute; inset: 0; border-radius: 50%;
}
.leader-cross { position: absolute; inset: 0; }
.leader-cross::before, .leader-cross::after {
  content: ""; position: absolute; background: var(--rule-half);
}
.leader-cross::before { left: 50%; top: 0; bottom: 0; width: 1.5px; }
.leader-cross::after { top: 50%; left: 0; right: 0; height: 1.5px; }
.leader-num {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--body); font-weight: 700;
  font-size: 60px; color: var(--ink);
}
#loadScreen .overlay-card { animation: flutter .34s steps(1) infinite; }
@keyframes flutter { 0%,100% { opacity: 1; } 55% { opacity: .93; } }

/* the HUD steps aside while the iris owns the frame */
.hud.dimmed { opacity: .12; transition: opacity .25s ease; }

/* ── S5: period print & the keep-the-mistakes motion pass ─────────────────
   Machine-perfect easing is a 2020s tell. UI motion steps like a hand-cranked
   print: cards deal in on twos, pops land in two frames, the score clunks over
   like an odometer wheel. */
.overlay:not(.hidden) .overlay-card { animation: cardIn .22s steps(2) both; }
/* the alternating pair: the death card re-triggers its deal-in at the iris
   hand-off with a class toggle — no reflow, no forced layout */
.overlay:not(.hidden) .overlay-card.deal2 { animation-name: cardIn2; }
@keyframes cardIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes cardIn2 { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
#distance.roll { animation: odoroll .28s steps(2); }
#distance.roll2 { animation: odoroll2 .28s steps(2); }
@keyframes odoroll { 0% { transform: skewX(-6deg) translateY(-3px); } 50% { transform: skewX(-6deg) translateY(2px); } 100% { transform: skewX(-6deg); } }
@keyframes odoroll2 { 0% { transform: skewX(-6deg) translateY(-3px); } 50% { transform: skewX(-6deg) translateY(2px); } 100% { transform: skewX(-6deg); } }
[data-theme="vintage"] #distance.roll { animation: odoroll-flat .28s steps(2); }
[data-theme="vintage"] #distance.roll2 { animation: odoroll-flat2 .28s steps(2); }
@keyframes odoroll-flat { 0% { transform: translateY(-3px); } 50% { transform: translateY(2px); } 100% { transform: none; } }
@keyframes odoroll-flat2 { 0% { transform: translateY(-3px); } 50% { transform: translateY(2px); } 100% { transform: none; } }
@keyframes pop-steps { from { transform: scale(.6); } to { transform: none; } }
/* the alternating twin: an acknowledgement label swap re-fires the stamp with a
   class toggle — no reflow, no void offsetWidth */
@keyframes pop-steps2 { from { transform: scale(.6); } to { transform: none; } }
@keyframes pop-steps-center { from { transform: translateX(-50%) scale(.6); } to { transform: translateX(-50%); } }
[data-theme="vintage"] .banner { animation-timing-function: steps(3); }
/* the logo bob breathes on sixes — a hand-held rostrum camera, not a tween */
[data-theme="vintage"] .overlay-card h1 { animation-timing-function: steps(6); }

/* ── S5 coherence fixes (blind panel round 2) ─────────────────────────────
   The keeper's name lives ONCE, in the top chip — the mid-screen DOM ghost
   read as a z-order bug to every judge who saw it. */
[data-theme="vintage"] .boss-name { display: none; }
/* Meters are printed gauges, not app pills: ink frame, paper gap, ink fill. */
[data-theme="vintage"] .boss-bar, [data-theme="vintage"] .boost-meter {
  border-radius: 3px; border: 3px solid var(--ink); background: var(--card, #efece3);
  padding: 2px; height: 18px; box-sizing: border-box; box-shadow: 0 3px 0 var(--shadow-drop);
}
[data-theme="vintage"] .boost-meter { height: 16px; }
[data-theme="vintage"] .boss-fill, [data-theme="vintage"] .boost-fill {
  background: var(--ink); box-shadow: none; border-radius: 1px; transition: width .15s steps(3);
}
/* Graduation ticks so an empty gauge still reads as a printed instrument. */
[data-theme="vintage"] .boss-bar, [data-theme="vintage"] .boost-meter {
  background-image: repeating-linear-gradient(90deg, transparent 0 9px, var(--rule-faint) 9px 10px);
}
/* REDLINE (M2): the STEAM track's last 15% is cross-hatched — a printed
   danger zone — and past it the whole instrument rattles on twos. */
[data-theme="vintage"] .boost-meter {
  background-image: repeating-linear-gradient(90deg, transparent 0 9px, var(--rule-faint) 9px 10px),
    repeating-linear-gradient(45deg, transparent 0 3px, var(--rule-faint) 3px 5px);
  background-size: 100% 100%, 15% 100%;
  background-position: 0 0, 100% 0;
  background-repeat: repeat, no-repeat;
}
.boost-meter.redline { animation: needlerattle .18s steps(2) infinite; }
@keyframes needlerattle { 0% { transform: translateX(-1px); } 100% { transform: translateX(1px); } }
/* The 6px stroke ate the slab face's letter gap — CRASHED read double-struck. */
[data-theme="vintage"] .overlay-card h1 { letter-spacing: .1em; -webkit-text-stroke-width: 5px; }

/* ── M3: THE VERDICT + THE ALBUM ──────────────────────────────────────────
   The verdict is a letterpress plate stamped over the keeper's fall — paper
   plate, double ink rule (the title-card material), steps() motion, no glow
   (§0.8). It waits half a beat so the KO card lands first. */
.verdict {
  position: absolute; left: 50%; top: 46%; z-index: 5; pointer-events: none;
  transform: translateX(-50%) rotate(-3deg);
  font-family: var(--disp); font-size: clamp(17px, 5vw, 30px); letter-spacing: .12em;
  color: var(--ink, #1a1410); text-transform: uppercase; text-align: center; white-space: nowrap;
  padding: 10px 20px 12px; max-width: 94vw;
  background: var(--card, #f2eee4);
  border: 3px solid var(--ink, #1a1410);
  border-radius: var(--card-radius, 6px);
  box-shadow: inset 0 0 0 2.5px var(--card, #f2eee4), inset 0 0 0 4px var(--ink, #1a1410), 0 6px 0 var(--shadow-drop);
  opacity: 0;
}
.verdict.play { animation: verdictstamp .3s steps(2) both; animation-delay: .45s; }
.verdict.play2 { animation: verdictstamp2 .3s steps(2) both; animation-delay: .45s; }
@keyframes verdictstamp2 { from { opacity: 0; transform: scale(1.6); } to { opacity: 1; transform: scale(1); } }
@keyframes verdictstamp {
  from { opacity: 0; transform: translateX(-50%) rotate(-3deg) scale(1.5); }
  to   { opacity: 1; transform: translateX(-50%) rotate(-3deg) scale(1); }
}

/* The album line — six printer's marks (● ◐ ✕ ○), small type under the
   subtitle / stats. Sorts from the case, not emoji. */
.album-line {
  font-family: var(--body, serif); font-size: 12px; letter-spacing: .28em; text-indent: .28em;
  color: var(--muted); margin-top: 6px; text-transform: uppercase;
}
[data-theme="vintage"] .album-line { color: var(--ink); opacity: .72; }

/* ── M4: TODAY'S ROUTE — the daily appointment ────────────────────────────
   The schedule card is set type only: the postmark is a rotated double ring
   with the route number inside (a rubber stamp, not an image). The board's
   TODAY · ALL-TIME line is one typeset rule — the live word underlined like
   a typeset link (§0.8), never a modern pill tab. */
.lb-tabs { display: flex; align-items: center; justify-content: center; gap: 2px; margin: 2px 0 0; }
.lb-tab {
  background: none; border: none; cursor: pointer; pointer-events: auto;
  min-height: 44px; display: inline-flex; align-items: center; padding: 6px 10px;
  font-family: var(--disp); font-size: 14px; letter-spacing: .1em; color: var(--accent-on-paper);
  -webkit-text-stroke: var(--stroke-card) var(--ink); paint-order: stroke fill; opacity: .62;
}
.lb-tab.on { opacity: 1; text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 2px; }
.lb-tab:active { transform: translateY(1px); }
.lb-tab-sep { font-family: var(--disp); font-size: 14px; color: var(--muted); }
.lb-subhead { margin: 2px 0 0; letter-spacing: .18em; text-transform: uppercase; }

.daily-post {
  width: 148px; height: 148px; margin: 16px auto 2px; border-radius: 50%;
  border: 2.5px solid var(--ink);
  box-shadow: inset 0 0 0 5px var(--card, #f2eee4), inset 0 0 0 6.5px var(--ink);
  display: grid; place-content: center; gap: 2px; transform: rotate(-7deg);
}
.daily-no {
  font-family: var(--disp); font-size: 34px; letter-spacing: .04em;
  color: var(--accent-on-paper); -webkit-text-stroke: var(--stroke-card) var(--ink); paint-order: stroke fill;
}
.daily-hint { margin: 14px 0 16px; }
#dailyStatus { color: var(--accent-on-paper); }   /* a status is not a challenge banner */
#dailyPanel .play-btn { margin-top: 10px; }
#dailyPodium { max-height: 24vh; margin-top: 4px; }

/* ── transition grammar: every user-dismissed overlay exits (dismiss() in
   game.js); being covered by a stacked panel is NOT a dismissal ── */
.overlay.closing { animation: fadeout .18s ease both; pointer-events: none; }
/* descendants re-assert pointer-events:auto (overlay hit-testing law), which
   would keep an invisible fading panel tappable — a dismissing overlay is inert */
.overlay.closing * { pointer-events: none !important; }
.overlay.closing .overlay-card { animation: cardOut .18s steps(2) both; }
@keyframes fadeout { to { opacity: 0 } }
@keyframes cardOut { to { opacity: 0; transform: translateY(4px) } }
/* no infinite animation may run on occluded elements (S6): show()/hide()
   keep body.overlay-up current; everything else infinite lives inside
   overlays and stops via display:none */
body.overlay-up .ring-icon::after, body.overlay-up .chev { animation-play-state: paused; }

/* ── H2b THE HOME STAGE — DOM props over the canvas scene ─────────────────
   The scene (backdrop, boards, spotlight, cat, curtains) is canvas-drawn and
   film-graded; #startScreen carries only set dressing. The container is
   pointer-transparent so tap-anywhere reaches the stage; every prop that
   acts re-asserts pointer-events. House dim lives in canvas layer 4 — the
   overlay wash and the blur are gone from this screen on every theme. */
#startScreen { display: block; background: none; animation: none; pointer-events: none; }
html:not([data-theme="vintage"]) #startScreen { backdrop-filter: none; }
#startScreen .marquee, #startScreen .cat-bubble, #startScreen .start-hint { pointer-events: none; }
#startScreen .challenge-line { display: none; }   /* text carrier for the bubble ladder */

/* the hanging marquee plate — title-card material, bobbing on sixes */
/* ---- THE CURTAIN CALL: the drawn logo, and the paper apron ------------------
   The painting occupies a panel whose exact top/bottom the canvas publishes as
   --stage-top / --stage-bot (one source of truth: if the pan-and-scan maths
   changes, the paper follows it without a second set of numbers). Below the
   panel is the apron: real paper, where every control lives. */
:root { --stage-top: 0%; --stage-bot: 100%; --cat-top: 42%; }

.marquee {
  position: absolute; top: calc(1.2% + var(--ui-top)); left: 0; right: 0; margin: 0 auto;
  width: fit-content; max-width: 68vw;
  padding: 10px 18px;                     /* air on all four sides, per the note */
  text-align: center; pointer-events: none; z-index: 3;
}
.cc-logo {
  display: block; width: min(62vw, 360px); height: auto;
  /* the drawn line is already inked; it only needs to sit ON the world, so a
     single hard letterpress drop, no glow, no stroke, no skew */
  filter: drop-shadow(0 3px 0 var(--shadow));
}
.album-line, .challenge-line { position: relative; z-index: 3; }

/* the apron: the paper the painting stands on. Portrait only — in landscape the
   stage is full-bleed and the controls sit over the painting's dark edges. */
.home-apron { display: none; }   /* retired: the painting is full-bleed now */
@media (min-aspect-ratio: 95/100) {
  /* LANDSCAPE: the painting is full-bleed, so there is no apron to sit on —
     the controls ride the painting's own dark floor above the plaque. */
  .home-apron { display: none; }


  .cc-logo { width: min(30vw, 340px); }
}
/* ---- THE PILOT'S STRIP + THE SIDE COLUMN ------------------------------------
 The reference carries identity and progress along the top edge. Ours does the
 same with REAL numbers, and when no wallet is attached the strip becomes one
 unmistakable call to action IN THE SAME SHAPE AND PLACE — connecting fills the
 furniture in rather than rearranging it. */
.pilot-strip {
/* THE LOBBY MARGIN: one inset, used by the strip and both prop columns, so
   nothing in the lobby ever touches the frame's edge. */
position: absolute; top: calc(11.5% + var(--ui-top));
left: calc(var(--lobby-pad) + env(safe-area-inset-left, 0px));
right: calc(var(--lobby-pad) + env(safe-area-inset-right, 0px));
display: flex; align-items: center; gap: 9px;
padding: 6px 10px; box-sizing: border-box;
background-color: rgba(20,18,16,.62);
border: 2px solid var(--ink);
border-radius: 11px 8px 10px 8px / 8px 11px 8px 10px;
pointer-events: none; z-index: 4;
}
.pilot-face {
flex: 0 0 auto; width: 38px; height: 38px;
background: center/contain no-repeat url('assets/vintage/cc-face.png');
}
.pilot-body { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 0; }
.pilot-name {
font-family: var(--disp); font-size: 13px; letter-spacing: .04em;
color: var(--paper, #f2eee4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pilot-stats {
font-family: var(--body); font-size: 11.5px; letter-spacing: .05em;
color: #cfc7b6; white-space: nowrap;
}
/* the default state: a real button, unmissable, but the same material family */
.pilot-cta {
margin-left: auto; flex: 0 0 auto; pointer-events: auto; cursor: pointer;
font-family: var(--disp); font-size: 11.5px; letter-spacing: .08em;
padding: 8px 12px; min-height: 38px; color: var(--ink);
/* PAPER, not the accent token: in vintage --accent-on-paper IS ink, so an accent
   fill produced dark type on a dark plate — invisible. A paper plate on the dark
   strip is unmissable AND the same material as PLAY, which is what "consistent
   with the connected state" has to mean. */
background-color: var(--chip, #f7f4ec); background-image: var(--paper-grain);
border: 2px solid var(--ink);
border-radius: 9px 7px 8px 7px / 7px 9px 7px 8px;
box-shadow: 0 3px 0 var(--ink); transition: none;
}
.pilot-cta:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--ink); }

/* the two side buttons hang under the strip, right-aligned to the SAME inset as
 ROUTE in the deck below — one number, so the two edges can never drift */
/* THE SIDE COLUMN IS GONE. Two chips hanging top-right and two more at the
   bottom read as two unrelated menus; all four now sit in one row on the deck.
   Anything still selecting .side-col is selecting nothing. */


.cat-bubble {
  /* Hangs off his CROWN, so it can never cover him. It needs no floor against the
     pilot strip: a max() would only shove it DOWN onto his head, and the two
     constraints genuinely have no solution when the crown rides above the strip.
     Room is guaranteed upstream instead — catH is capped so the crown always
     clears the strip plus this bubble (see CAT_HEAD_ROOM in game.js). */
  /* THE BUBBLE BELONGS TO THE PILOT, NOT THE WINDOW. This was left:50% — the
     centre of the VIEWPORT — while the cat stands wherever the painting's lit
     pool is, and now slides right when a friend arrives. Its tail pointed at
     empty boards about 68px from his mouth. --cat-cx has carried his real
     horizontal mark all along and was read by nothing. */
  position: absolute; left: var(--cat-cx, 50%); top: calc(var(--cat-top, 40%) - 14px);
  z-index: 4;                                   /* over the tag: the comic overlap */
  transition: left .18s linear;                 /* rides the slide, does not lag it */
  transform: translate(-50%, -100%);
  /* WIDTH, EXPLICITLY. An absolutely-positioned box with width:auto shrink-to-
     fits into whatever space is left of its `left` — so once the pilot moved to
     75% the bubble had 125px to live in and folded into a narrow column. It is
     sized by its own content now, capped so that a box centred on him still
     clears the frame. */
  width: max-content; max-width: min(46vw, 230px); padding: 8px 13px 9px;
  font-family: var(--disp); font-size: 13.5px; letter-spacing: .07em; line-height: 1.45;
  text-align: center; text-wrap: balance;  /* two even lines, never a one-word second line */
  color: var(--ink);   /* lines arrive typeset — "No." keeps its period case */
  background-color: var(--card); background-image: var(--paper-grain);
  border: 3px solid var(--ink);
  border-radius: 10px 8px 10px 9px / 8px 10px 9px 10px;
  box-shadow: 0 4px 0 var(--shadow-drop);
  animation: bubpop .22s steps(2) both;
}
.cat-bubble.bub2 { animation-name: bubpop2; }   /* alternating names restart the pop — no reflow */
.cat-bubble::after {   /* the tail: a rotated square pointing at the cat's mouth */
  content: ""; position: absolute; left: 50%; bottom: -8px;
  /* IT PAINTS UNDER THE TYPE. The tail is an opaque rotated square whose UPPER
     half sits inside the bubble, and as ::after it painted after the text — so
     that half did not "overlap" the letters, it ERASED them, in the bubble's own
     background colour. The founder photographed it eating an "a" and a "wh".

     z-index:-1 is the whole fix and it needs no new shape. Inside a stacking
     context the paint order is background, then negative-z children, then inline
     content — so the tail lands above the bubble's own background and below its
     text. The buried half is invisible (same colour as what it covers), the half
     hanging below still draws, and the letters are untouched. A triangle would
     also have worked, but it would have cost the rounded nib tip. */
  z-index: -1;
  width: 15px; height: 15px;
  /* TALLER, NOT WIDER. scaleY is applied AFTER the rotation (transforms read
     right to left), so it stretches the finished diamond in screen space
     rather than skewing the square before it turns. The diamond's shoulders
     sit exactly on the bubble's bottom edge and scaling about the centre
     leaves points at that height untouched — so the join keeps its width and
     only the point travels further down. */
  transform: translateX(-50%) scaleY(var(--tail-h, 1.5)) rotate(45deg);
  background-color: var(--card);
  border-right: 3px solid var(--ink); border-bottom: 3px solid var(--ink);
  border-radius: 0 6px 7px 6px;                    /* the nib, see .cat-tag::after */
}
/* scale-only pop: visibility must never ride an animation's end state */
@keyframes bubpop  { from { transform: translate(-50%, -100%) scale(.6); } to { transform: translate(-50%, -100%); } }
@keyframes bubpop2 { from { transform: translate(-50%, -100%) scale(.6); } to { transform: translate(-50%, -100%); } }

/* corner-column letterpress chips — paper chip material, fixed column spots */
/* the two corner columns: absolutely placed, but their chips are IN FLOW so a
   two-line chip pushes instead of overprinting; pointer-transparent between
   chips so tap-anywhere still reaches the stage */
/* ---- THE CONTROL DECK ------------------------------------------------------
   Founder's layout, mapped off the reference lobby: PLAY dead centre at the
   bottom, SOUND to its left, ROUTE to its right, and MISSIONS + TOP PILOTS
   stacked in a column that hangs directly above ROUTE. Buttons are PROPS: the
   illustrated object is the button, and a small caps label only names it, so the
   type can stay quiet while the object does the talking. */
.deck {
  position: absolute; left: 0; right: 0; bottom: calc(2.6% + var(--ui-bottom));
  /* CENTRED, NOT BOTTOM-ALIGNED. flex-end lined the plaque's bottom up with the
     bottom of the props' LABELS, so PLAY's optical centre floated above the two
     icons either side of it. Centring makes the three read as one row. */
  /* TWO ROWS: the ways to fly, then the chips. Was a single horizontal
     space-between of [MUSIC][play][ROUTE], which is why the other two chips had
     to live somewhere else entirely. */
  display: flex; flex-direction: column; align-items: stretch;
  gap: 10px; box-sizing: border-box;
  /* each side takes ITS OWN inset — one shared value applied to both would
     shift the deck sideways on a notched phone held in landscape */
  padding-left: calc(var(--lobby-pad) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--lobby-pad) + env(safe-area-inset-right, 0px));
  /* ABOVE THE CHAIR. The invite silhouette is a tall hit area anchored to the
     stage floor, and that floor line sits BEHIND the deck — so with the chair
     at z-index 6 and the deck at 3, the chair's lower third covered CO-OP PLAY
     and swallowed every tap on it. The controls always win: a decorative hit
     area must never be able to eat a button. */
  pointer-events: none; z-index: 9;
}

.prop-btn {
  display: flex; flex-direction: column; align-items: center; gap: 5px;   /* the label had 2px and sat ON the art */
  background: none; border: 0; padding: 2px; margin: 0;
  cursor: pointer; pointer-events: auto;
  min-width: 44px; min-height: 44px;                 /* the touch floor holds */
  transition: none;                                   /* a press is discrete */
}
.prop-art {
  display: block; width: var(--prop-art); aspect-ratio: 1 / 1;
  background: center/contain no-repeat;
  filter: drop-shadow(0 2px 0 rgba(16,14,12,.5));
}
.prop-art[data-prop="sound"]    { background-image: url('assets/vintage/prop-sound.png'); }
.prop-art[data-prop="route"]    { background-image: url('assets/vintage/prop-route.png'); }
.prop-art[data-prop="missions"] { background-image: url('assets/vintage/prop-missions.png'); }
.prop-art[data-prop="pilots"]   { background-image: url('assets/vintage/prop-pilots.png'); }
/* the label: SMALL caps. It names the prop, it never competes with it — so its
   plate must stay INSIDE the prop's own width. At 2.3vw/.08em, TOP PILOTS and
   ROUTE 210 printed WIDER than the art above them, which is exactly why the
   right-hand props read as the smaller pair. */
.prop-label {
  font-family: var(--disp); font-size: clamp(8px, 2.15vw, 9.5px);
  letter-spacing: .05em; line-height: 1.05; white-space: nowrap;
  color: var(--paper, #f2eee4);
  /* a plate, not a shadow: over the lit drape a glow still merged into white */
  padding: 3px 6px 4px; border-radius: 5px;
  background: rgba(18,16,14,.82);
}
.prop-btn:active .prop-art { transform: translateY(2px); }

/* PLAY: the one loud thing on the stage. Its own material, twice the presence. */
.play-plaque {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: clamp(128px, 40vw, 210px); min-height: 62px; padding: 10px 16px;
  cursor: pointer; pointer-events: auto; transition: none;
  background-color: var(--chip); background-image: var(--paper-grain);
  border: 3px solid var(--ink);
  border-radius: 12px 9px 11px 9px / 9px 12px 9px 11px;
  box-shadow: 0 5px 0 var(--ink), 0 5px 0 4px rgba(16,14,12,.35);
}
.play-plaque::before {                                /* the inner letterpress rule */
  content: ""; position: absolute; inset: 4px;
  border: 1.5px solid var(--ink); border-radius: 8px 6px 7px 6px / 6px 8px 6px 7px;
  pointer-events: none;
}
.play-word {
  font-family: var(--disp); font-size: clamp(22px, 7vw, 34px);
  letter-spacing: .1em; color: var(--ink); line-height: 1;
}
.play-plaque:active { transform: translateY(4px); box-shadow: 0 1px 0 var(--ink); }

/* ── THE TWO PLAQUES ───────────────────────────────────────────────────────
   With #coopBtn hidden (display:none) the stack has exactly one child and the
   deck lays out precisely as it did before co-op existed. That is the whole
   point of the wrapper: the review build must not move by a pixel. */
/* THE TWO WAYS TO FLY, side by side. CO-OP is the larger and comes first:
   it is the game we want played, so it is the button that looks like it. The
   old stack had them vertical with co-op as the SMALL one, which said the
   opposite. */
.play-stack {
  display: flex; flex-direction: row; align-items: stretch; justify-content: center;
  gap: 10px;
  pointer-events: none;                    /* the plaques re-assert it themselves */
}
/* A PAIR OF PLAQUES CANNOT BOTH SHOUT. The co-op plaque is the same material
   and the same shape at a quieter weight, so PLAY stays the loudest thing on
   the apron and the second option reads as an option rather than a rival.
   Both words are longer than "PLAY", so the display face steps down — set here
   rather than left to fitLabel, because a size chosen by a measuring loop
   varies with the word and these two must look like a matched pair. */
/* SAME SIZE, DIFFERENT VOICE. Hierarchy by size made the row read as one slab
   with a big half and a small half. Both plaques are now identical in box and
   in type; CO-OP carries the weight instead — printed solid, the way a poster
   sets the headline act against the supporting one. */
.play-stack .play-plaque.pair { flex: 1 1 0; min-height: 54px; padding: 9px 12px; }
/* On the solo apron CO-OP sits on the LEFT, so it wears the second voice: solid
   ink. See THE TWO MATERIALS below — the moment it moves right, it takes the
   paper and #startBtn takes the ink. */
.play-stack .play-plaque.pair {
  background-color: var(--ink); background-image: none;
  box-shadow: 0 5px 0 rgba(16,14,12,.55), 0 5px 0 4px rgba(16,14,12,.3);
}
.play-stack .play-plaque.pair::before { border-color: var(--card); opacity: .5; }
.play-stack .play-plaque.pair .play-word { color: var(--card); }
/* THE CEILING IS SET BY THE PLAQUE, NOT BY THE VIEWPORT.

   The plaque is clamp(128px, 40vw, 210px), so past a ~525px viewport it STOPS
   growing while a vw-based type size keeps going — the two clamps top out at
   different widths and the label overflows at exactly the sizes nobody tests on.
   21px is the largest size at which the longest word here ("CO-OP PLAY", with
   its .1em tracking) fits the widest plaque, so the pair never depends on
   fitLabel rescuing it. fitLabel still runs, as the safety net it is meant to
   be rather than the mechanism the layout leans on. */
/* ONE TYPE SIZE FOR BOTH — they are the same offer, differently pressed. */
/* Sized so the LONGEST word in the pair — CO-OP PLAY — fits its half of the
   row without fitLabel having to shrink it, which is what was ellipsising it
   to "CO-OP PL...". Both plaques are the same box, so the smaller word simply
   sits in more air. */
.play-stack .play-plaque .play-word { font-size: clamp(14px, 4.1vw, 19px); }
/* SECOND CLASS, and only when there is something to be second to. With co-op
   hidden (the review build) the single plaque keeps its full size and reads
   PLAY, exactly as it always has. */
.play-stack.paired #startBtn { flex: 1 1 0; min-height: 54px; padding: 9px 12px; }
/* ── THE TWO MATERIALS ─────────────────────────────────────────────────────
   Founder's rule, arrived at over three passes and worth stating once, plainly:

       THE RIGHT-HAND PLAQUE IS THE ACCENT AND IS PRINTED ON PAPER.
       THE LEFT-HAND PLAQUE IS THE SECOND OPTION AND IS PRINTED IN SOLID INK.

   Both keep the same box and the same type — hierarchy by voice, never by size.

   The wrong turns, so nobody takes them again. Printing the ACTION in solid ink
   made it recede: on a dark stage solid ink sinks into the floor while paper
   with a hard shadow leaps off it, so the way out was the loudest thing on
   screen. And giving the second option a flat ghost fill read as DISABLED —
   a button that looks broken is worse than one that looks loud. Solid ink is
   the right second voice: full presence, obviously pressable, plainly not the
   one you came for.

   Which ELEMENT is on the right changes with the state, so the materials are
   keyed to the side and not to the button. Default — the solo apron — is
   CO-OP left, SOLO right. .joined flips the row for waiting and joined, where
   WAITING and I'M READY take the accent slot, and the materials flip with it. */
.play-stack.joined .play-plaque.pair {                  /* co-op moved right: it takes the paper */
  background-color: var(--chip); background-image: var(--paper-grain);
  box-shadow: 0 5px 0 var(--ink), 0 5px 0 4px rgba(16,14,12,.35);
}
.play-stack.joined .play-plaque.pair::before { border-color: var(--ink); opacity: 1; }
.play-stack.joined .play-plaque.pair .play-word { color: var(--ink); }
.play-stack.joined #startBtn {                          /* ...and the way out takes the ink */
  background-color: var(--ink); background-image: none;
  box-shadow: 0 5px 0 rgba(16,14,12,.55), 0 5px 0 4px rgba(16,14,12,.3);
}
.play-stack.joined #startBtn::before { border-color: var(--card); opacity: .5; }
.play-stack.joined #startBtn .play-word { color: var(--card); }
.play-stack .play-plaque { min-width: 0; width: auto; }
/* Not edge to edge: they filled 98% of the row with a 10px gap, which is why
   they read as one bar rather than two choices. */
/* WIDTH, NOT AUTO MARGINS. `margin: 0 auto` on a stretched flex item stops it
   stretching and makes it shrink-to-fit — so the row never took its 95%, the
   plaques sized to their own text, and CO-OP PLAY had 142px of room for a word
   that needs 143. (That word measures 124px of glyphs plus 19px of
   letter-spacing; measuring the glyphs alone is how it looked like it fitted.) */
.play-stack { width: 95%; align-self: center; }

/* ── THE CHIP ROW ────────────────────────────────────────────────────────
   Four equal slots, always four. MISSIONS holds its slot while locked using
   visibility rather than display, so the three chips a new player learns do not
   slide sideways the day the fourth appears. */
.chip-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 4px; pointer-events: none;
}
.chip-row > .prop-btn { flex: 1 1 0; min-width: 0; }
.chip-row > .prop-btn.hidden { display: flex !important; visibility: hidden; }
.chip-row .prop-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }

/* ── THE INVITE CARD ───────────────────────────────────────────────────────
   The link is the point of the card, so it is set as a readable object rather
   than a caption: a full-width slip in the mono face, broken where it must be
   so a long room link never pushes the card wider than the phone. */
.invite-link {
  width: 100%; box-sizing: border-box; margin: 2px 0 10px;
  padding: 9px 11px; background: var(--chip); background-image: var(--paper-grain);
  border: 2px solid var(--ink); border-radius: 9px 7px 8px 7px / 7px 9px 7px 8px;
  /* The house has --disp and --body and no mono token; inventing one for a
     single slip would put a face in the design system that nothing else uses.
     The room-code alphabet already excludes i/l/o/0/1, so the ambiguity a mono
     face would be solving does not exist here. */
  font-family: var(--body, system-ui, sans-serif); font-weight: 600;
  font-size: 12.5px; line-height: 1.4; letter-spacing: .01em;
  color: var(--ink); overflow-wrap: anywhere; text-align: center;
}
.invite-state { margin: 9px 0 4px; opacity: .8; min-height: 1.1em; }
.invite-state.joined { opacity: 1; font-weight: 700; }

/* THE BUBBLE must never cover him: it is pinned ABOVE his head, which the canvas
   reports as --cat-top (feet minus height), so it tracks any scale change. */
.home-status {
  position: absolute; left: 0; right: 0; top: 4.5%; margin: 0;
  text-align: center; pointer-events: none; z-index: 3;
  font-family: var(--body); font-size: 12px; letter-spacing: .06em;
  color: var(--paper, #f2eee4); text-shadow: 0 1px 0 rgba(16,14,12,.9);
}
.start-hint { display: none; }
.start-hint-unused {   /* retired: the bubble speaks and M7 teaches in-run */
  position: absolute; left: 50%; top: auto; bottom: calc(4.5% + 70px); transform: translateX(-50%);
  margin: 0; white-space: nowrap; letter-spacing: .12em;
  /* it sits on painted deck boards: type alone was illegible, so it prints on
     its own slip of paper like every other label in the house */
  padding: 5px 12px; color: var(--ink);
  background-color: var(--chip); background-image: var(--paper-grain);
  border: 2px solid var(--ink); border-radius: 7px 6px 7px 6px / 6px 7px 6px 7px;
  box-shadow: 0 2px 0 var(--shadow-drop);
}

/* the HUD steps offstage while the home scene / reel owns the frame; its
   infinite spin pauses there (S6 — nothing animates while occluded) */
.hud.offstage { opacity: 0; }
.hud.offstage .ring-icon::after { animation-play-state: paused; }

/* ---- THE STAT ROW (reference-shaped) + THE BACK ARROW -----------------------
   The reference reads its top bar as icon+value pairs. Ours does the same with
   real numbers: rank, best metres, lifetime coins, keepers beaten. */
.pilot-stats { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.pilot-stats .stat { display: inline-flex; align-items: center; gap: 4px; }
.pilot-stats b {
  font-family: var(--disp); font-size: 12.5px; letter-spacing: .02em;
  color: var(--paper, #f2eee4); font-weight: 400;
}
.stat-ic { width: 15px; height: 15px; display: block; background: center/contain no-repeat; opacity: .95; }
.ic-rank { background-image: url('assets/vintage/prop-pilots.png'); }
.ic-best { background-image: url('assets/vintage/prop-route.png'); }
.ic-coin { background-image: url('assets/vintage/ston-coin.webp'); }  /* the real filename */
.ic-keep { background-image: url('assets/vintage/prop-missions.png'); }
.pilot-unlink {
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer; pointer-events: auto;
  font-family: var(--body); font-size: 10.5px; letter-spacing: .06em;
  color: #b9b0a0; text-decoration: underline; text-underline-offset: 2px;
}
/* the drawn back arrow — one prop, every panel, top-left of the card. The arrow
   owns a BAND of its own: at 6px of air a long title (YOU CROSSED THE BRIDGE)
   printed straight through the arrowhead. 22px clears the art on every card. */
/* THE ARROW GETS A WHOLE ROW, and the number is arithmetic rather than taste.
   The arrow is absolutely positioned at top:6px and is 44px tall, so it ends
   50px down the card's padding box. 22px only pushed the title to 44px — six
   pixels INSIDE the arrowhead, which is exactly what the founder photographed
   twice. 40px puts the title at 62px: clear of the arrow with a real gap, and
   the same rule still serves every panel that already used it. */
.overlay-card > .back-arrow + h2 { margin-top: 40px; }
.back-arrow {
  position: absolute; left: 8px; top: 6px; z-index: 5;
  width: 44px; height: 44px; padding: 0; margin: 0;      /* the touch floor holds (was 40) */
  background: center/58% no-repeat url('assets/vintage/prop-back.png');
  border: 0; cursor: pointer; pointer-events: auto; transition: none;
}
.back-arrow:active { transform: translateX(-2px); }
.overlay-card { position: relative; }        /* so the arrow can anchor to it */
.album-key { color: var(--muted); letter-spacing: .04em; margin-top: 2px; }
/* SOUND OFF is its own drawing, not a restyle of the horn */
.prop-art[data-prop="sound"].off { background-image: url('assets/vintage/prop-soundoff.png'); }


/* ── THE TAKEOFF (curtain call) ─────────────────────────────────────────────
   While the sequence plays the frame belongs to the stage. Everything the
   lobby put on top steps off — chips, plaque, bubble, pilot strip, hint —
   and ONLY the STONIC marquee stays, hanging over the scene like a theatre
   sign. One class, so every prop leaves and returns together. */
body.reel-up #startScreen > *:not(.marquee) { display: none !important; }
body.reel-up #catBubble,
body.reel-up .pilot-strip,
body.reel-up .deck { display: none !important; }


/* ── THE PILOT'S FACE (Mini App) ────────────────────────────────────────────
   A round letterpress disc carrying the pilot's initial, struck in our own ink
   so a leaderboard of strangers still looks like one printed page. When a
   Telegram photo exists it is laid OVER this as a background — so an expired
   CDN url degrades to the letter rather than to a broken image, which is why
   the letter is always painted underneath rather than instead. */
.lb-face {
  flex: 0 0 auto; width: 26px; height: 26px; margin-right: 9px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--disp); font-size: 13px; line-height: 1; color: var(--ink);
  background-color: var(--chip); background-size: cover; background-position: center;
  border: 2px solid var(--ink); border-radius: 50%;
  box-shadow: 0 2px 0 var(--shadow-drop);
  overflow: hidden; user-select: none;
}
.lb-row.you .lb-face { box-shadow: 0 2px 0 var(--ink); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE SECOND CHAIR (co-op builds only)

   The chair is drawn on the canvas; this button is its hit target, positioned
   from the --chair-* vars drawHomeScene publishes. Doing it this way rather
   than hit-testing the canvas means focus, hit slop, keyboard access and
   VoiceOver all come for free, and it matches the deck's own "the prop is the
   button" language.

   It stays .hidden on the review build — the markup ships, the chair does not.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ── THE LAST MINUTE ───────────────────────────────────────────────────────
   Three transparent buttons over the drawn props, positioned from the render.
   Same family as the empty chair's hit area, so they inherit the overlay
   hit-testing law: the screen above them is pointer-events:none, and every
   interactive descendant must re-assert auto or it renders perfectly and does
   nothing.

   Each rule names its own tokens rather than routing through a generic
   --cx/--top/--w/--h. Three more lines, and worth it twice over: an undeclared
   generic name is invisible to the token checker, and a reader can see which
   box a rule is about without following an indirection.

   The fallbacks are not decoration. The render publishes these, so before the
   first frame they resolve to NOTHING and a button would sit at the origin with
   no size at all. */
/* THE AFTERMATH LETS THE WRECK SHOW.

   The founder's second plate is the whole reason LOSE IT lands somewhere, and
   the card was covering all but its corners. Narrowed and lifted HERE ONLY —
   scoped to #deathScreen rather than to .overlay-card, because that class is
   the house material every card in the game is printed on and a change to it
   would quietly re-set the home screen, the win card, the furnace and the
   leaderboard along with this one.

   The plate is behind glass, so the card also stops being opaque about it: a
   touch of the drawing reads through the margins, which is what makes the two
   look like one screen rather than a dialog over a picture. */
#deathScreen .overlay-card { max-width: min(21rem, 86vw); margin-top: max(3vh, 12px); }
@media (min-height: 720px) { #deathScreen .overlay-card { margin-top: 8vh; } }

/* THE LAST MINUTE'S CARD. House material throughout — it is the same plate,
   the same rules and the same link family every other card in the game uses,
   because a player who has learned to read one of our cards has learned to
   read this one. */
.cp-body { font-family: var(--body); font-size: 15px; line-height: 1.45; margin: 6px 0 14px; color: var(--ink); }
.cp-steps { font-family: var(--body); font-size: 14px; line-height: 1.6; text-align: left;
            margin: 0 0 14px; padding-left: 20px; color: var(--ink); }
.cp-steps li { margin: 2px 0; }
/* The link is shown, not hidden behind a verb: a player who cannot make the
   share sheet work can still read six characters out to a friend. */
/* THE ROOM CODE HAS TO BE LEGIBLE — it is the fallback for a player whose
   share sheet fails, and it inherited the card's colour rather than declaring
   one, which on the paper card rendered near-white on near-white. */
/* The crash card's link field is the lobby's field — see the note in
   index.html. overflow-wrap:anywhere rather than word-break:break-all, so a URL
   wraps at a sensible point instead of mid-token. */
.cp-link { width: 100%; box-sizing: border-box; margin: 2px 0 10px;
           padding: 9px 11px; background: var(--chip); background-image: var(--paper-grain);
           border: 2px solid var(--ink); border-radius: 9px 7px 8px 7px / 7px 9px 7px 8px;
           font-family: var(--body, system-ui, sans-serif); font-weight: 600;
           font-size: 12.5px; line-height: 1.4; letter-spacing: .01em;
           color: var(--ink); overflow-wrap: anywhere; text-align: center; }
#cpLinkText { color: var(--ink); font-weight: 600; }
/* THE FLARE, and never digits — a countdown in numerals beside a paid button
   is pressure-selling, and this game does not do that. */
.cp-flare { height: 4px; background: rgba(20,19,16,.14); margin: 0 0 14px; overflow: hidden; }
.cp-flare i { display: block; height: 100%; width: 100%; background: var(--ink);
              transform-origin: left center; animation: cpburn 90s linear forwards; }
@keyframes cpburn { from { transform: scaleX(1); } to { transform: scaleX(0); } }
.crash-hit {
  position: absolute;
  transform: translateX(-50%);
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer; z-index: 12;
  pointer-events: none;                 /* armed by body.crash-armed, below */
  min-width: 48px; min-height: 48px;    /* never smaller than a thumb */
}
#crashCall {
  left: var(--crash-call-cx, 50%); top: var(--crash-call-top, 30%);
  width: var(--crash-call-w, 60%); height: var(--crash-call-h, 22%);
}
#crashBurn {
  left: var(--crash-burn-cx, 50%); top: var(--crash-burn-top, 74%);
  width: var(--crash-burn-w, 60%); height: var(--crash-burn-h, 18%);
}
#crashLose {
  left: var(--crash-lose-cx, 50%); top: var(--crash-lose-top, 60%);
  width: var(--crash-lose-w, 44%); height: var(--crash-lose-h, 8%);
}
body.crash-armed .crash-hit { pointer-events: auto; }
/* The HUD and every other overlay step aside; the cockpit owns the frame the
   same way the takeoff reel does. */
body.crashing #hud, body.crashing #bossUI, body.crashing #titleCard, body.crashing #verdict, body.crashing .deck,
body.crashing #chairBtn, body.crashing #chairNote,
body.crashing .chev, body.crashing #tut, body.crashing #banner,
body.crashing .cat-bubble, body.crashing .peer-bubble { display: none !important; }

.chair-hit {
  position: absolute;
  left: var(--chair-cx, 30%);
  top: var(--chair-top, 40%);
  height: var(--chair-h, 30%);
  /* WIDTH IS A WIDTH. This was calc(var(--chair-h) * 0.62) — a HEIGHT
     percentage resolved against the container's WIDTH, which on a 390px phone
     turned a 208px-tall chair into a 60px-wide button. game.js publishes the
     real box now, in the axis it belongs to. */
  width: var(--chair-w, 20%);
  /* game.js overwrites this transform every frame the chair breathes
     (chairRig) so the + rides the drawing instead of sitting still on it. The
     origin is the FEET — the same anchor the canvas draws from — because any
     other origin walks the disc sideways as the figure widens. Both are
     restated here so the button is still placed correctly before the first
     frame lands, and on the review build where game.js never calls it. */
  transform-origin: 50% 100%;
  transform: translateX(-50%);
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer; z-index: 6;
  /* THE OVERLAY HIT-TESTING LAW (see .overlay.closing above). #startScreen is
     pointer-events:none so canvas taps reach the game; every interactive
     descendant must RE-ASSERT auto. Without this the chair rendered perfectly,
     sat at the right coordinates, reported itself visible — and swallowed every
     tap, because the computed value was inherited `none`. It looked finished
     and did nothing. */
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
}
.chair-hit:disabled { cursor: default; }
/* The + is the invite gesture. It rides the chair's top-left like a tack on the
   playbill, and only appears when tapping would actually do something. */
.chair-plus {
  /* ON THE PART YOU CAN SEE. The silhouette is full size and the star stands in
     front of most of it, so a + centred on the chair box lands behind him. This
     sits on the shoulder that is actually in clear air. */
  position: absolute; left: 26%; top: 30%;
  transform: translate(-50%, -50%);
  display: grid; place-items: center;
  /* A REAL INVITATION, not a footnote. 34px on a chair a fifth of the screen
     tall read as a speck; this is the one thing on the lobby that asks the
     player to bring somebody. */
  width: clamp(56px, 15vmin, 92px); height: clamp(56px, 15vmin, 92px);
  /* WHITE DISC, INKED CROSS, HARD SHADOW. A 1930s cel has no blur in it: the
     shadow is a second flat shape offset from the first, zero blur and zero
     spread, exactly as the plaques and the chips already do it. */
  background: var(--card, #f2eee4);
  border: 3px solid var(--ink, #1a1a18);
  border-radius: 50%;
  box-shadow: 0 5px 0 var(--ink, #1a1a18);
  transition: transform .12s ease-out;
}
.chair-plus svg { width: 62%; height: 62%; display: block; overflow: visible; }
.chair-plus svg path {
  fill: none; stroke: var(--ink, #1a1a18);
  stroke-width: 13; stroke-linecap: round; vector-effect: non-scaling-stroke;
}
/* The press is discrete, and the shadow collapses with it — the disc lands on
   the boards rather than sliding under itself. */
.chair-hit:active .chair-plus {
  transform: translate(-50%, -50%) translateY(3px);
  box-shadow: 0 2px 0 var(--ink, #1a1a18);
}
.chair-hit.busy .chair-plus { display: none; }

/* The room code / status line, set under the chair on the apron. */
.chair-note {
  position: absolute;
  /* IT BELONGS TO THE PILOT, AND IT SITS ON TOP OF WHAT HE IS ALREADY SAYING.

     It used to hang off the empty CHAIR at a fixed height — so it did not move
     when the cat breathed, and it landed across the bubbles instead of above
     them. It rides the star's own crown now, offset by the measured height of
     the bubble AND the tag beneath it, which is why those two publish theirs. */
  left: var(--cat-cx, 50%);
  /* OVER THE SEAT, NOT UNDER IT. This hung below the chair's feet — and the
     feet stand on the stage floor, which is behind the deck, so the line ran
     under the buttons and clipped off the left edge of the screen. Above the
     head it is always in clear air, and it reads as a label on the empty chair
     rather than a caption floating in the furniture. */
  top: calc(var(--cat-top, 40%) - 14px);
  transform: translate(-50%, calc(-100% - var(--bub-h, 0px) - var(--tag-h, 0px)
                                 + var(--bub-lap) * 2));
  transition: left .18s linear;
  margin: 0; z-index: 2;                       /* under the tag: the comic stack */
  max-width: 46vw; overflow: hidden; text-overflow: ellipsis;
  text-align: center; white-space: nowrap;
  letter-spacing: .06em; text-transform: uppercase;
  font-weight: 800;
  text-shadow: 0 1px 0 var(--card, #f2eee4);
}
/* A PLATE WHEN IT IS NEWS, not just a caption.

   "That chair is taken" is the ONLY thing a player who followed their friend's
   link is ever told, and as a 12.5px line of ink on a wooden floor it is easy
   to walk straight past — leaving them to conclude the link, or the game, is
   broken. The house rule for exactly this is plate-over-stroke: give the words
   their own ground rather than fighting the art behind them. Same type, same
   place, same voice; it just stops competing with the stage. */
.chair-note.news {
  /* IT MAY WRAP. The base caption is nowrap + ellipsis, which is right for a
     one-line label on a stage and wrong for a sentence — "that link has gone
     cold" arrived as "THAT LINK HAS GONE CO...". A bubble is allowed two
     lines; a label is not. */
  white-space: normal; overflow: visible; text-overflow: clip;
  max-width: min(58vw, 264px); line-height: 1.25;
  padding: 5px 10px;
  background: var(--chip); background-image: var(--paper-grain);
  border: 2px solid var(--ink);
  border-radius: 8px 6px 7px 6px / 6px 8px 6px 7px;
  box-shadow: 0 3px 0 var(--ink);
  color: var(--ink); text-shadow: none;
}

/* ═══ LABELS MUST NOT BREAK THE BOX ═══════════════════════════════════════
   fitLabel() shrinks type to fit at runtime, but CSS has to hold the line for
   the frame before it runs and for any label written by a path that forgets to
   call it. Ten labels were measured overflowing on a 390px phone — including
   every co-op word on the plaque, which is sized for the four characters of
   "PLAY". Clipping is the honest failure here: a button that silently grows
   shoves the whole card sideways. */
.play-btn, .play-plaque, .play-word {
  min-width: 0;                       /* a flex/grid child defaults to min-content */
  overflow: hidden;
  text-overflow: ellipsis;
}
.play-word { display: block; white-space: nowrap; }

/* ── WAITING · · · ────────────────────────────────────────────────────────
   Three dots that arrive one at a time and clear together, the printed
   shorthand for a held beat. Real elements on staggered keyframes rather than
   animated `content`, which is not reliable across engines. */
.wait-dots { display: inline-flex; gap: .28em; margin-left: .4em; align-items: center; }
.wait-dots i {
  width: .26em; height: .26em; border-radius: 50%;
  /* INK, NOT PAPER. These were cream because the plaque under them was solid
     ink; now that the accent IS the paper plaque, cream dots on cream paper are
     three invisible dots and the word reads "WAITING" with a shrug after it. */
  background: var(--ink, #1a1a18); opacity: 1;
  /* VISIBLE AT REST, blinking off — not invisible at rest, blinking on. The
     dots ARE the "still waiting" affordance, and an animation that fails to
     start (a paused tab, a reduced-motion setting, a repaint that lands between
     the unhide and the first keyframe) left the plaque reading WAITING with a
     shrug after it. At worst now it reads three steady dots. */
  animation: waitdot 1.5s steps(1, end) infinite;
}
.wait-dots i:nth-child(2) { animation-delay: .375s; }
.wait-dots i:nth-child(3) { animation-delay: .75s; }
@keyframes waitdot { 0%, 74% { opacity: 1; } 75%, 100% { opacity: 0; } }
/* The plaque is the same box in every state; only the word and the weight move.
   COUNTDOWN is the one moment the choice is over, so the row becomes one word. */
.play-stack.counting #startBtn { display: none; }
.play-stack.counting .play-plaque.pair { flex: 1 1 100%; }

/* STAND BY is a state, not an offer: the plaque keeps its box but stops
   advertising itself, so the eye goes to the way out beside it. */
.play-plaque.holding { opacity: .72; }
.play-plaque.holding:active { transform: none; }
/* A STATE, NOT A CONTROL. WAITING occupies the loud slot because it is the
   thing you came to do, but there is nothing behind it while the room is open —
   so it does not take the tap, does not show a pointer, and does not depress.
   The way back into the invite card is the silhouette beside it. */
.play-plaque.inert { pointer-events: none; cursor: default; }

/* THE FRIEND'S BUBBLE — same material, hung off the second chair's mark rather
   than the star's. Left of centre, so its tail points down at the pilot it
   belongs to. */
.peer-bubble { left: var(--chair-cx, 30%); top: calc(var(--chair-top, 40%) - 14px); }

/* BOTH READY: the lobby steps aside exactly as the takeoff does, and the canvas
   says STARTING on its own. Same selector family as .reel-up so the two
   mechanisms stay independent but behave identically. */
body.counting #startScreen > *:not(.marquee) { display: none !important; }
body.counting #catBubble,
body.counting #peerBubble,
body.counting .pilot-strip,
body.counting .deck { display: none !important; }

/* MORE AIR BETWEEN THE ROWS. The plaques and the chips were 10px apart and read
   as one block of furniture. */
.deck { gap: 22px; }
/* AND THE ACCENT SITS ON THE RIGHT ONCE A FRIEND IS ABOARD: I'M READY is the
   thing to tap, LEAVE is the thing beside it. Reversing the row moves the
   accented plaque without moving the element that carries its behaviour. */
/* ALWAYS REVERSED, not just once a friend is aboard. The accent belongs on the
   right in every state — CO-OP PLAY, WAITING and I'M READY are the same slot
   doing the same job — and the second option belongs on the left. Reversing the
   ROW moves the accent without moving the element that carries the behaviour,
   which is what lets the exit interceptor keep keying on #startBtn by identity.
   (.joined is left in place: paint() still sets it, nothing reads it now, and
   removing it is churn in a third file for no behaviour.) */
.play-stack.joined { flex-direction: row-reverse; }

/* ── THE IDENTITY BUBBLE ──────────────────────────────────────────────────
   Smaller than the one below it, and stacked on the same anchor, so the two
   read as a comic pair: who is speaking, then what they are saying. It rides
   above the main bubble by that bubble's own measured height, which the render
   publishes as --bub-h — so it sits correctly whether the bubble below is one
   line, two, or hidden entirely (0px, and the tag simply takes its place).

   It is the ONLY wallet control in the lobby now that the strip is gone:
   unsigned it asks, signed it names you and offers to let go. */
/* ── THE BUBBLE STACK ─────────────────────────────────────────────────────
   Three layers can hang over a pilot: the main bubble at the bottom, the
   identity tag above it, and a news note above that. Each rides the SAME
   anchor — the character's own crown — and each is offset by the measured
   height of everything beneath it, so the whole column follows the cat as he
   breathes instead of hanging in the air.

   --bub-lap is the one number that sets how close they sit. Negative is a gap;
   positive would tuck them into one another. */
/* THEY TOUCH BY 4px, AND THE TAIL IS WHAT MAKES THAT SAFE.

   A gap was the wrong instinct. Each bubble's tail hangs ~8px below its own
   border box, so with a GAP the tail flies through open air and lands on the
   type below — which is what the founder photographed at 3px. Widening the gap
   to 12px only bought air; it also broke the comic, because two signs with
   daylight between them read as two signs.

   Overlapping fixes both at once. Every layer is painted UNDER the one beneath
   it (note 2, tag 3, bubble 4) and every bubble is opaque, so a tail that
   crosses into the box below is HIDDEN BY it. The pointer cannot reach the
   text, because the text's own background is in front of it.

   THE NUMBER, WALKED IN: 4px overlap crowded, 2px overlap was close, and the
   founder asked for four more pixels of air on top of that — so the stack now
   opens to a 2px GAP. Positive laps overlap, negative ones separate; this is
   the only line that decides it for all three layers. */
:root { --bub-lap: -2px; }

.cat-tag {
  position: absolute; left: var(--cat-cx, 50%);
  top: calc(var(--cat-top, 40%) - 14px);
  /* A COMIC STACK, NOT TWO SEPARATE SIGNS. The tag sits slightly INTO the
     bubble below it and passes BEHIND it, so the pair reads as one object
     with depth rather than as two labels that happen to be near each other. */
  transform: translate(-50%, calc(-100% - var(--bub-h, 0px) + var(--bub-lap)));
  transition: left .18s linear;
  z-index: 3;
  max-width: min(46vw, 210px);
  padding: 4px 10px 5px; box-sizing: border-box;
  /* WHITE, not the paper cream the big bubble uses. Two bubbles in the same
     material read as one torn sheet; a lighter, cleaner card reads as a second
     voice — which is what it is. */
  background-color: #fff; color: var(--ink);
  border: 3px solid var(--ink);
  /* A touch rounder and a touch wobblier than the big one — a smaller pen. */
  border-radius: 26px 22px 24px 20px / 20px 26px 20px 24px;
  font-family: var(--disp); font-size: 12px; letter-spacing: .07em;
  line-height: 1.15; text-align: center; white-space: nowrap;
  /* NO overflow:hidden HERE. It clipped the tail clean off — the pseudo-element
     hangs 8px below the box, so hiding overflow deletes the one thing that
     makes this a speech bubble rather than a chip. The label is short by
     construction (a name, or the wallet's own EQab…wxyz), so there is nothing
     to clip anyway. */
  cursor: pointer; pointer-events: auto;
}
.cat-tag::after {          /* the tail, pointing down at the bubble it belongs to */
  content: ""; position: absolute; left: 50%; bottom: -7px;
  z-index: -1;                                    /* under the type — see .cat-bubble::after */
  width: 13px; height: 13px;
  transform: translateX(-50%) scaleY(var(--tail-h, 1.5)) rotate(45deg);
  background-color: #fff;
  border-right: 3px solid var(--ink); border-bottom: 3px solid var(--ink);
  /* A NIB, NOT A SET SQUARE. A bare rotated square gives a needle point that
     reads as a UI caret; the cartoonists rounded the tip because a pen does.
     One radius on the outer corner is the whole difference. */
  border-radius: 0 5px 6px 5px;
}
/* UNSIGNED IS THE LOUD ONE. It is the only thing on the stage asking for
   anything, so it earns the accent; signed, it goes quiet and becomes a label
   that happens to be tappable. */
/* Weight, not colour. The pink shouted — on a black-and-white stage the only
   colour in the frame reads as an advertisement, and this is the cat's own
   voice. Bold on white is enough to make it the loud one. */
.cat-tag.offered { font-weight: 700; letter-spacing: .1em; }
/* A FRIEND'S TAG IS INFORMATION, NOT A CONTROL — nobody may disconnect anybody
   else's wallet, so it must not look pressable. */
.peer-tag { left: var(--chair-cx, 30%); top: calc(var(--chair-top, 40%) - 14px);
  transform: translate(-50%, calc(-100% - var(--peer-bub-h, 0px) + var(--bub-lap)));
  cursor: default; pointer-events: none; }
body.crashing .cat-tag, body.counting .cat-tag { display: none !important; }


/* ── THE CHAIR'S NEWS, AS A BUBBLE ────────────────────────────────────────
   "your friend left" was a bare paragraph in the middle of a painted stage,
   in a game where everything anybody says is in a bubble. It speaks now, in
   the same material as the cat's, and say() clears it after seven seconds. */
.chair-note.news {
  background-color: var(--card); color: var(--ink);
  border: 3px solid var(--ink);
  border-radius: 26px 22px 24px 20px / 20px 26px 20px 24px;
  padding: 7px 12px 8px; max-width: min(52vw, 240px);
  font-family: var(--disp); font-size: 12px; letter-spacing: .05em;
  line-height: 1.2; text-align: center;
  /* NO `position` HERE. This block is a second .chair-note.news rule appended
     after the first, and declaring position:relative overrode the base
     absolute — so the note fell out of the bubble stack and back into normal
     flow, landing beside the cat instead of above him. A later duplicate that
     re-declares layout is how a rule wins an argument it was never having. */
}
.chair-note.news::after {
  content: ""; position: absolute; left: 50%; bottom: -7px;
  z-index: -1;                                    /* under the type — see .cat-bubble::after */
  width: 13px; height: 13px;
  transform: translateX(-50%) scaleY(var(--tail-h, 1.5)) rotate(45deg);
  background-color: var(--card);
  border-right: 3px solid var(--ink); border-bottom: 3px solid var(--ink);
  border-radius: 0 5px 6px 5px;                    /* the nib, see .cat-tag::after */
}

/* ── THE PAIR'S AGREEMENT ON AN END CARD ───────────────────────────────────
   Founder, after a co-op win and a co-op loss where every button looked dead:
   the button carries YOUR switch, this cap carries your friend's. It is a
   printed line, not a widget — the house voice for "here is the situation". */
.coop-cap {
  margin: 10px auto 0; max-width: 88%;
  font-family: var(--body, serif); font-size: 13px; line-height: 1.45;
  letter-spacing: .14em; text-transform: uppercase; text-align: center;
  /* INK ON PAPER, NOT GOLD ON CREAM. This was var(--gold) — #e7e1d2 — printed
     on the card's cream stock at .92 opacity, so the one line telling a pilot
     what their FRIEND has done was very nearly invisible: photographed on
     device it read as a grey smudge under the button. The founder asked that
     the modal "clearly tells both players about the state"; it cannot do that
     in a colour chosen for a dark surface. Secondary weight now comes from
     size and opacity, which survive being on paper. */
  color: var(--ink); opacity: .74;
}
.coop-cap::before, .coop-cap::after { content: "\00a0\2014\00a0"; opacity: .5; }

/* ── CAMPAIGN LOBBY (founder, 2026-08-17): one plaque, full width — PLAY
   spans exactly the chip-row of four props below it. Scoped to .campaign so
   the two-plaque co-op layout returns untouched after the window. */
.campaign .play-stack { width: 100%; }
.campaign .play-stack .play-plaque { flex: 1 1 auto; width: 100%; max-width: none; }

/* CAMPAIGN END CARDS (founder): with the COINS cell hidden the stats grid
   still reserved its third column, so the two live cells sat left-of-centre
   and the numbers hugged the divider. Two real columns, and air on both
   sides of the rule. Win card shares .death-stats, so both screens agree. */
.campaign .death-stats { grid-template-columns: repeat(2, 1fr); }
.campaign .death-stats div { padding: 0 16px; }
