/* Tidy Datacenter — drafting table.
 *
 * Warm paper, graphite ink, blueprint-blue rules. Calm and unhurried: nothing
 * glows, nothing saturates, nothing moves faster than it needs to. The board is
 * the drawing; the chrome is the desk it sits on.
 *
 * Type is three faces with three jobs: Oxanium carries titles and anything that
 * has to sound like the game, Barlow Semi Condensed carries reading text and
 * labels, IBM Plex Mono carries every figure. Numbers stay monospaced and
 * tabular on purpose — this is a game about ports and port counts, and figures
 * that line up in columns are easier to compare.
 *
 * All three are self-hosted, which does not weaken the CSP: `style-src 'self'`
 * and the missing `font-src` mean an external font silently fails, so hosting
 * them here is the only way to have them at all.
 */

/* ---------- faces ---------- */
/* Subset to Latin-1 + Latin Extended + punctuation by tools/fonts.py. A player
   name outside that range falls through to the system stack, which is the right
   failure — a missing glyph box would be worse than a different face. */
@font-face { font-family: Oxanium; src: url(/assets/fonts/oxanium-500.woff2) format('woff2');
             font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: Oxanium; src: url(/assets/fonts/oxanium-700.woff2) format('woff2');
             font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: "IBM Plex Mono"; src: url(/assets/fonts/plexmono-400.woff2) format('woff2');
             font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "IBM Plex Mono"; src: url(/assets/fonts/plexmono-600.woff2) format('woff2');
             font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: "Barlow Semi Condensed"; src: url(/assets/fonts/barlow-400.woff2) format('woff2');
             font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Barlow Semi Condensed"; src: url(/assets/fonts/barlow-500.woff2) format('woff2');
             font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Barlow Semi Condensed"; src: url(/assets/fonts/barlow-600.woff2) format('woff2');
             font-weight: 600; font-style: normal; font-display: swap; }

:root {
  /* paper */
  --bg: #efe7d8;
  --bg-2: #f5efe3;
  --panel: #f8f3e9;
  --panel-2: #efe7d8;
  --line: #ddd0b9;
  --rule: #b9c6d0;          /* blueprint blue, for grids and hairlines */

  /* graphite */
  --ink: #2f2a22;
  --ink-2: #6d6355;
  --ink-3: #9a8f7d;

  /* muted pigments — pencil box, not neon */
  --accent: #5d8f85;        /* spine sage */
  --accent-2: #bd7a4f;      /* leaf clay */
  --gold: #b8873a;
  --good: #5a8a5e;
  --bad: #b5523f;

  /* ---------- the room ----------
     The play screen's own palette. Dark, because the board is the lit thing and
     everything else is the room it sits in — not because dark is a theme. */
  --hull: #080B0D;
  --plate: #121A1F;
  --plate-2: #1A242A;
  --edge: #2E3D45;
  --sig: #FFB23F;          /* signal amber: the one colour that means "act" */
  --live: #3FD98A;         /* link up */
  --dead: #FF4D4D;
  --cut: 14px;             /* the corner every panel and key is cut by */

  /* ---------- the board's own palette ----------
     Read by the canvas renderer through syncPalette(). These are the ONE
     definition of the board's colours; render.js used to carry a second
     hardcoded copy that could not drift into agreement with this one.

     The sheet is lit in both schemes on purpose. The room is dark and the board
     is the thing the light falls on — that is the whole composition, and a
     "dark mode board" would be a dark thing in a dark room. */
  --board-paper: #ECE7DC;
  --board-paper-dark: #ECE7DC;
  --board-grid: #D2CBBC;
  --board-grid-dark: #D2CBBC;
  --board-chassis: #162026;
  --board-spine: #3FBFB0;
  --board-spine-dark: #2A9C90;
  --board-leaf: #F08A5D;
  --board-leaf-dark: #D96F44;
  --board-notch: #162026;
  --board-tick: #162026;
  --board-stub: #FFFFFF;
  --board-fault: #FF4D4D;
  --board-ok: #3FD98A;

  /* Cable colour carries information rather than decoration: hue is the spine
     the link lands on, shade is the leaf it came from. autoColor() indexes
     (leaf * 2 + spine), so odd slots are one spine and even slots the other —
     reorder these and the fabric stops being readable. */
  --cable-1: #31C8D6;   /* spine 1 — aqua, OM3 */
  --cable-2: #FFB23F;   /* spine 2 — amber */
  --cable-3: #2AA7B8;
  --cable-4: #E08A21;
  --cable-5: #6FDCE4;
  --cable-6: #FFCC7A;
  --cable-7: #1E8A9B;
  --cable-8: #C06F12;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow: 0 2px 10px rgba(74, 62, 44, 0.10);
  --shadow-lift: 0 8px 26px rgba(74, 62, 44, 0.16);

  /* Three roles, three faces. One face doing all three — which is what a lone
     system-monospace stack was doing here — is the loudest thing on the page
     saying nobody chose it. Self-hosted: the CSP has no font-src, so a Google
     Fonts link is not slow, it is silently dead. */
  --display: Oxanium, "Barlow Semi Condensed", system-ui, sans-serif;
  --ui: "Barlow Semi Condensed", system-ui, -apple-system, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Late-night drafting: warm charcoal, never blue-black. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #1d1b17;
    --bg-2: #232019;
    --panel: #26231d;
    --panel-2: #2d2922;
    --line: #3c3730;
    --rule: #3f4a52;

    --ink: #e7e0d0;
    --ink-2: #a89d8a;
    --ink-3: #7d735f;

    --accent: #79aaa0;
    --accent-2: #cf8e60;
    --gold: #d0a04a;
    --good: #7bab7f;
    --bad: #d4715c;

    --shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    --shadow-lift: 0 10px 28px rgba(0, 0, 0, 0.45);
  }
}

* { box-sizing: border-box; }
html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  /* faint engineering rule, so even the chrome sits on graph paper */
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--rule) 22%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--rule) 22%, transparent) 1px, transparent 1px);
  background-size: 22px 22px;
  color: var(--ink);
  /* Barlow Semi Condensed is narrow, so it needs a point more than a monospace
     at the same apparent size. */
  font: 400 15px/1.55 var(--ui);
  font-variant-ligatures: none;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ---------- top bar ---------- */

.topbar {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 22px;
  padding: 9px 18px;
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  border-bottom: 1px solid var(--line);
  z-index: 5;
}

.brand {
  display: flex; align-items: center; gap: 9px;
  color: inherit; text-decoration: none;
  font-weight: 600; letter-spacing: 0.04em;
}
.brand-mark { color: var(--accent); font-size: 17px; }
.brand-name { font-size: 14px; }

.topnav { display: flex; gap: 2px; }
.topnav a {
  color: var(--ink-2); text-decoration: none;
  padding: 5px 11px; border-radius: var(--radius-sm);
  font-size: 13px; letter-spacing: 0.02em;
}
.topnav a:hover { color: var(--ink); background: var(--panel-2); }

.who { margin-left: auto; color: var(--ink-3); font-size: 12.5px; }

/* ---------- stage ---------- */

.stage {
  position: relative; flex: 1 1 auto; min-height: 0;
  /* The board area's side insets live here by name so the tutorial can widen
     one of them without restating the whole layout. */
  /* The board is centred in a channel between the two panel columns. It is
     never inset by a panel's box, so a wider board can never slide underneath
     one — which is what an overlay-only HUD risks the moment a level grows. */
  --board-left: 16px;
  --board-right: 364px;
  --rail: 366px;
}

/* A canvas is a replaced element, so insets would leave it at its intrinsic
   300x150. It fills a positioned wrapper instead, and the wrapper is what the
   renderer measures — these insets are the playable area. */
.board-wrap {
  position: absolute;
  /* --hud-top is measured from the real top bar, not assumed. It used to be a
     hardcoded 76px, which held right up until the blurb wrapped to two lines on
     a tablet — then the stats panel sat on top of the Run failure test button
     and there was no way to press it. */
  inset: var(--hud-top, 76px) var(--board-right) 42px var(--board-left);
  display: none;
  overflow: hidden;
}
/* On the play screen the wrapper is a transparent channel, not a panel. The
   canvas clears itself and the room shows through — a bordered, filled box
   around the board reads as a document figure rather than a world. */
body[data-screen="play"] .board-wrap {
  inset: 18px var(--rail) 18px var(--rail);
  border: 0; background: none; box-shadow: none;
}
#board { width: 100%; height: 100%; display: block; touch-action: none; cursor: crosshair; }
body[data-screen="play"] .board-wrap { display: block; }
body[data-screen="run"] .board-wrap { display: block; inset: var(--hud-top, 76px) 16px 16px 16px; }

#app { position: absolute; inset: 0; overflow: auto; }
body[data-screen="play"] #app,
body[data-screen="run"] #app { overflow: hidden; pointer-events: none; }
body[data-screen="play"] #app > *,
body[data-screen="run"] #app > * { pointer-events: auto; }

/* ---------- buttons ---------- */

.btn {
  font: inherit; font-size: 12.5px; letter-spacing: 0.02em;
  padding: 7px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.btn:hover:not(:disabled) { border-color: var(--accent); background: var(--panel-2); }
.btn:disabled { opacity: 0.5; cursor: default; }

.btn-primary {
  background: var(--accent); border-color: var(--accent);
  color: var(--panel); font-weight: 600;
}
.btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 86%, var(--ink));
  border-color: color-mix(in srgb, var(--accent) 86%, var(--ink));
  color: var(--panel);
}
.btn-ghost { background: transparent; }
.btn-quiet { background: transparent; border-color: transparent; color: var(--ink-3); }
.btn-quiet:hover { color: var(--ink); background: var(--panel-2); }
.btn-lg { font-size: 14px; padding: 10px 24px; }
.btn-sm { font-size: 11.5px; padding: 4px 9px; }

/* ---------- centred screens ---------- */

.centered {
  min-height: 100%;
  display: flex; flex-direction: column; align-items: center;
  gap: 20px; padding: 52px 20px 60px;
}

.hero { text-align: center; max-width: 560px; }
.hero-title {
  font-family: var(--display);
  margin: 0 0 12px;
  font-size: clamp(26px, 4.4vw, 38px);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
}
.hero-title::after {
  content: '';
  display: block;
  width: 64px; height: 1px;
  margin: 14px auto 0;
  background: var(--accent);
}
.hero-sub { margin: 0 0 28px; color: var(--ink-2); font-size: 13.5px; line-height: 1.7; }
.hero-actions { display: flex; flex-direction: column; align-items: center; gap: 12px; }

/* The name is always an editable field — there is no separate sign-out. */
.name-form { display: flex; gap: 8px; align-items: flex-end; }
.name-field { display: flex; flex-direction: column; gap: 4px; text-align: left; }
.name-label {
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3);
}
.name-input {
  font: inherit; font-size: 15px;
  padding: 9px 13px; min-width: 240px;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.name-input:focus { outline: none; border-color: var(--accent); }
.form-error { color: var(--bad); font-size: 12.5px; margin: 0; }
.hint { color: var(--ink-3); font-size: 11.5px; max-width: 400px; margin: 2px 0 0; line-height: 1.6; }

/* ---------- cards ---------- */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 15px;
  box-shadow: var(--shadow);
}
.card h2 {
  font-family: var(--display);
  margin: 0 0 11px;
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--ink-3); font-weight: 600;
}
.card h3 {
  margin: 15px 0 6px;
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-3); font-weight: 600;
}
.card.wide, .home-board { width: min(720px, 100%); }
.card-head { margin-bottom: 12px; }
.card-head h2 { margin-bottom: 4px; }
.card-head p { margin: 0; font-size: 12px; }

/* ---------- the room ----------
   The play screen is a screen, not a page: the viewport is the frame, nothing
   scrolls, and the board is the lit thing in a dark room. */
body[data-screen="play"] {
  background: var(--hull);
  background-image: none;
  /* Ink inverts for the room. Everything in the HUD reads against plate, not
     against paper, so the three ink steps are redefined rather than overridden
     one rule at a time. */
  --ink: #E8F2F6;
  --ink-2: #92A8B2;
  --ink-3: #5A6D77;
  --line: var(--edge);
  --panel: var(--plate);
  --panel-2: var(--plate-2);
  --accent: var(--sig);
  --good: var(--live);
  --bad: var(--dead);
}
body[data-screen="play"] .stage::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image:
    linear-gradient(rgba(140,200,220,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(140,200,220,.05) 1px, transparent 1px);
  background-size: 48px 48px;
  -webkit-mask-image: radial-gradient(120% 90% at 50% 40%, #000 15%, transparent 78%);
  mask-image: radial-gradient(120% 90% at 50% 40%, #000 15%, transparent 78%);
}
body[data-screen="play"] .stage::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: radial-gradient(72% 58% at 50% 46%, transparent 32%, rgba(0,0,0,.78) 100%);
}
/* A game does not carry a nav bar over its own world. */
body[data-screen="play"] .topbar { display: none; }

/* ---------- HUD ---------- */

.hud { position: absolute; }

.hud-top {
  /* Above the side panel. If the two ever overlap again, the controls win —
     a button you can see and cannot press is the worst of both. */
  z-index: 3;
  top: 14px; left: 16px; right: 16px;
  display: flex; align-items: center; gap: 18px;
  padding: 9px 13px;
  background: color-mix(in srgb, var(--panel) 95%, transparent);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.hud-title { display: flex; flex-direction: column; min-width: 0; }
.hud-title strong { font-weight: 600; letter-spacing: 0.02em; font-size: 13px; }
.hud-title .muted { font-size: 11.5px; }
.hud-actions { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }

/* ---------- HUD panels ----------
   Anchored to the corners of the screen with cut corners and a header tab.
   Not cards in a column: a column of rounded cards is a document, and that is
   the single loudest thing that made this read as a webpage. */
.hudp {
  position: absolute; z-index: 3; width: 340px;
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid var(--edge);
  clip-path: polygon(0 var(--cut), var(--cut) 0, 100% 0,
                     100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%);
  box-shadow: 0 20px 48px rgba(0,0,0,.62);
  opacity: 0;
  animation: hud-rise 520ms cubic-bezier(.2,.8,.25,1) forwards;
}
@keyframes hud-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

.hudp-head {
  display: flex; align-items: center; gap: 9px; padding: 10px 15px 9px;
  font: 700 10px/1 var(--display); letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink-3); border-bottom: 1px solid var(--edge); background: rgba(0,0,0,.28);
}
.hudp-head .tab { width: 5px; height: 12px; background: var(--sig); flex: none; }
.hudp-body { padding: 13px 15px; }

.hudp.mission { top: 22px; left: 22px; animation-delay: 60ms; }
.hudp.telem   { top: 22px; right: 22px; width: 300px; animation-delay: 140ms; }
.hudp.ladder  { left: 22px; bottom: 22px; animation-delay: 220ms; }
.hudp.report  { right: 22px; top: 210px; width: 300px; bottom: 22px; overflow: auto; animation-delay: 300ms; }

.mission-name { margin: 0; font: 700 28px/.98 var(--display); text-transform: uppercase; color: var(--ink); }
.mission-blurb { margin: 9px 0 0; font: 400 13px/1.5 var(--ui); color: var(--ink-2); }
.mission-goal {
  margin: 12px 0 0; padding-top: 11px; border-top: 1px solid var(--edge);
  display: flex; gap: 9px; font: 600 11px/1.3 var(--ui);
  letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3);
}
.mission-goal b { color: var(--live); font-weight: 600; }

/* ---------- readout ----------
   One line above the belt. Not a bar pinned across the bottom of a document. */
.status {
  position: absolute; z-index: 4;
  left: 50%; transform: translateX(-50%); bottom: 108px;
  max-width: min(720px, 60vw); text-align: center;
  padding: 8px 16px;
  background: rgba(8,11,13,.88);
  border: 1px solid var(--edge);
  clip-path: polygon(0 8px, 8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
  font: 400 12px/1.4 var(--mono); color: var(--ink-2);
  pointer-events: none;
}
.status::before { content: '› '; color: var(--sig); }
.status.error { color: var(--dead); }

/* ---------- tool belt ----------
   Weighted keys carrying an icon and the key that fires them. A row of text
   buttons is a toolbar; this is a belt, and the difference is most of what
   separates a game's screen from an app's. */
.belt {
  position: absolute; z-index: 4;
  left: 50%; bottom: 26px; transform: translateX(-50%);
  display: flex; gap: 10px; align-items: center;
  opacity: 0; animation: belt-rise 520ms cubic-bezier(.2,.8,.25,1) 300ms forwards;
}
/* Its own keyframes, because hud-rise finishes on `transform: none` and
   fill-mode forwards makes that stick — which silently cancels the
   translateX(-50%) doing the centring. */
@keyframes belt-rise {
  from { opacity: 0; transform: translateX(-50%) translateY(14px); }
  to   { opacity: 1; transform: translateX(-50%); }
}
.key {
  width: 60px; height: 60px; position: relative; padding: 0;
  color: var(--ink-2);
  background: linear-gradient(180deg, #1F2A31, #131B20);
  border: 1px solid var(--edge);
  clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%);
  display: grid; place-items: center; cursor: pointer;
  box-shadow: 0 12px 26px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.08);
  transition: transform 110ms ease, border-color 140ms ease, color 140ms ease;
}
.key:hover:not(:disabled) { transform: translateY(-2px); border-color: var(--sig); color: var(--sig); }
.key:focus-visible { outline: 2px solid var(--sig); outline-offset: 2px; }
.key:disabled { opacity: .4; cursor: default; }
.key.is-on { border-color: var(--sig); color: var(--sig); background: linear-gradient(180deg, #2A3238, #171F24); }
.key.is-busy { opacity: .6; }
.key .cap {
  position: absolute; bottom: 4px; right: 6px;
  font: 400 9px/1 var(--mono); color: var(--ink-3);
}
.key.go {
  width: auto; padding: 0 24px; gap: 12px; grid-auto-flow: column;
  color: #2A1A02;
  background: linear-gradient(180deg, #FFC45C, #E89218);
  border-color: transparent;
  box-shadow: 0 0 40px rgba(255,178,63,.32), 0 12px 26px rgba(0,0,0,.5),
              inset 0 1px 0 rgba(255,255,255,.45);
}
.key.go:hover { transform: translateY(-2px); color: #2A1A02; border-color: transparent; }
.key.go .lbl {
  font: 700 15px/1 var(--display); letter-spacing: .08em; text-transform: uppercase;
}
.key.go .cap { position: static; color: rgba(42,26,2,.6); }

/* Contextual actions join the belt only while they apply. */
.belt .hud-contextual { display: flex; gap: 10px; }
.belt .hud-color { display: flex; align-items: center; gap: 6px; }

@media (prefers-reduced-motion: reduce) {
  .hudp, .belt { animation: none; opacity: 1; }
}

/* ---------- stats, ladder, report ---------- */

.progress { display: grid; grid-template-columns: repeat(4, 1fr); gap: 7px; }
.stat {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 8px 9px;
  display: flex; flex-direction: column; gap: 1px;
}
.stat-value {
  font-family: var(--mono); font-size: 17px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.stat-of { font-size: 11px; font-weight: 400; color: var(--ink-3); }
.stat-label {
  font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);
}
.stat-warn { border-color: var(--gold); }
.stat-warn .stat-value { color: var(--gold); }

.star-row {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 7px 0; border-top: 1px dotted var(--line);
}
.star-row:first-child { border-top: 0; }
.star-mark { font-size: 15px; line-height: 1.3; color: var(--ink-3); }
.star-row.earned .star-mark { color: var(--gold); }
.star-row.locked { opacity: 0.42; }
.star-body { display: flex; flex-direction: column; }
.star-name { font-weight: 600; font-size: 12.5px; letter-spacing: 0.02em; }
.star-hint { font-size: 11px; color: var(--ink-3); line-height: 1.5; }

.verdict {
  display: flex; flex-direction: column; gap: 6px; align-items: center;
  padding: 15px; margin-bottom: 13px;
  border-radius: var(--radius-sm);
  background: var(--panel-2); border: 1px dashed var(--line);
}
.verdict-stars { font-size: 24px; letter-spacing: 5px; color: var(--gold); }
.verdict-text { font-size: 12.5px; color: var(--ink-2); margin: 0; text-align: center; line-height: 1.6; }
.verdict-5 { border-color: var(--good); border-style: solid; }

.tier-table, .div-table, .board-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.tier-table td { padding: 5px 3px; border-top: 1px dotted var(--line); }
.tier-table tr:first-child td { border-top: 0; }
.tier-name { font-weight: 500; }
.tier-count { text-align: right; width: 56px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.tier-state { text-align: right; width: 94px; }
.tier-table tr.good .tier-state { color: var(--good); }
.tier-table tr.bad .tier-state { color: var(--bad); }

.fail-list { list-style: none; margin: 0; padding: 0; }
.fail-list li {
  display: flex; flex-direction: column;
  padding: 5px 0; border-top: 1px dotted var(--line); font-size: 11.5px;
}
.fail-what { font-weight: 500; }
.fail-why { color: var(--bad); font-size: 11px; }

.table-scroll { overflow-x: auto; }
.div-table th, .board-table th {
  text-align: left; padding: 5px 3px;
  font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-3); font-weight: 600;
}
.div-table td, .board-table td {
  padding: 7px 3px; border-top: 1px dotted var(--line); font-variant-numeric: tabular-nums;
}
.div-table td.weak { color: var(--bad); font-weight: 600; }

.board-table { font-size: 13px; }
.board-table .rank { color: var(--ink-3); width: 30px; }
.board-table .stars { color: var(--gold); letter-spacing: 1px; }
.board-table .num { text-align: right; width: 76px; }
.board-table tr.is-me {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow: inset 2px 0 0 var(--accent);
}
.board-table tr.is-me .who-cell, .board-table tr.is-me td:nth-child(2) { color: var(--accent); }

.help ul { margin: 0; padding-left: 16px; font-size: 11.5px; color: var(--ink-2); line-height: 1.65; }
.help li { margin-bottom: 6px; }
.help b { color: var(--ink); font-weight: 600; }

.muted { color: var(--ink-3); font-size: 12px; }
.error { color: var(--bad); }

/* ---------- results ---------- */

#modal {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  padding: 20px;
  width: min(440px, 92vw);
  box-shadow: var(--shadow-lift);
}
#modal::backdrop { background: color-mix(in srgb, var(--ink) 38%, transparent); }

.result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0 0 16px; }
.result-stat {
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 8px 10px;
}
.result-stat dt {
  font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);
}
.result-stat dd {
  margin: 2px 0 0; font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.results-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.results-actions .muted { text-align: center; line-height: 1.6; }

/* ---------- misc ---------- */

.tooltip {
  position: fixed; z-index: 20; pointer-events: none;
  background: var(--ink); color: var(--bg);
  padding: 4px 8px; border-radius: 4px;
  font-size: 11px; white-space: nowrap;
}

/* Tablets. An iPad Pro is 1024 CSS px, and the sidebar is the RIGHT layout
   there — the board is 46x67, so it wants height, and a 332px panel beside it
   still leaves room for a tile size of about 14. Stacking it dropped that to 8
   and wasted 300px of width on nothing. What was actually broken was the top
   bar growing when the blurb wrapped, which is fixed above by measuring it.
   The blurb goes rather than the layout. */
@media (max-width: 1200px) {
  .hud-title .muted { display: none; }
  .hud-top { gap: 10px; padding: 8px 10px; }
  .hud-actions { gap: 5px; }
  .hud-actions .btn { padding: 7px 10px; }
}

/* Genuinely narrow: a phone, or a tablet in split view. Below this a sidebar
   leaves the board nothing, so stacking really is better. */
@media (max-width: 820px) {
  .stage { --board-left: 12px; --board-right: 12px; }
  .board-wrap { inset: var(--hud-top, 76px) var(--board-right) 46% var(--board-left); }
  .hud-side {
    position: absolute; top: 55%; left: 12px; right: 12px; bottom: 10px;
    width: auto;
  }
  .status { left: 12px; right: 12px; bottom: auto; top: 55%; transform: translateY(-34px); }
  .progress { grid-template-columns: repeat(2, 1fr); }
}

/* Phone-ish. Four stat tiles across is unreadable below this. */
@media (max-width: 640px) {
  .progress { grid-template-columns: repeat(2, 1fr); }
}

/* name field + reroll sit together */
.name-row { display: flex; align-items: stretch; gap: 6px; }
.name-row .btn-quiet { white-space: nowrap; }

/* A pinned reference layer — the button stays lit while the guide is on board. */
.btn.is-on {
  border-color: var(--accent);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* ---------- the three ranked boards ---------- */

.boards {
  display: grid;
  gap: 14px;
  width: min(1120px, 100%);
  grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
}
.board-card { display: flex; flex-direction: column; gap: 9px; }
.board-card h2 { margin: 0; }
.board-blurb { margin: 0; color: var(--ink-3); font-size: 11.5px; line-height: 1.5; min-height: 2.3em; }

.period-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); padding-bottom: 6px; }
.period-tab {
  font: inherit; font-size: 11px; letter-spacing: 0.02em;
  padding: 3px 8px; border-radius: var(--radius-sm);
  border: 1px solid transparent; background: transparent;
  color: var(--ink-3); cursor: pointer;
}
.period-tab:hover { color: var(--ink); background: var(--panel-2); }
.period-tab.is-on {
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* One row saying which level everything below is about. Sits between the level
   picker and the boards, so the question is answered before the tables are
   read rather than after. */
.board-scope {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  /* Matches .boards, the widest thing it introduces. */
  width: min(1120px, 100%);
  margin: 4px auto -4px;
  padding: 0 2px;
}
.board-scope-label {
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-3);
}
/* The level names are longer than Day/Week/Month, so this row gets no rule
   under it — two stacked borders a few pixels apart read as a mistake. */
.board-scope .level-tabs { border-bottom: 0; padding-bottom: 0; }
.level-boards { display: contents; }

.board-body { min-height: 96px; }
.board-card .board-table { width: 100%; }
.board-table.compact { font-size: 12px; }
.board-table.compact td, .board-table.compact th { padding: 3px 6px; }
.board-table .strong { color: var(--ink); font-weight: 600; }
/* A long generated name must not push the number off the card. */
.who-cell { max-width: 13ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The breakdown under a result stat — why the number is the number. */
.stat-detail { margin: 2px 0 0; font-size: 10px; color: var(--ink-3); letter-spacing: 0.01em; }

/* An unclaimed rank. Present, numbered, and visibly waiting. */
.board-table tr.slot-open td { color: var(--ink-3); opacity: 0.45; }
.board-table tr.slot-open .rank { opacity: 0.6; }

/* ---------- level card: Play, and the name confirmation ---------- */

/* The card selects; this starts. Sized and weighted like a key so it reads as
   the action on the card rather than a caption on it. */
.slot-play {
  display: inline-flex; align-items: center; gap: 7px;
  margin-left: auto; padding: 7px 13px; cursor: pointer;
  font: 700 11px/1 var(--display); letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-2);
  background: color-mix(in srgb, var(--sig) 8%, transparent);
  border: 1px solid var(--edge);
  clip-path: polygon(0 6px, 6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%);
  transition: transform 110ms ease, background 110ms ease, color 110ms ease, border-color 110ms ease;
}
.slot-play svg { width: 15px; height: 15px; stroke: currentColor; fill: currentColor; }
.slot-play:hover {
  transform: translateX(2px);
  color: #2A1A02; border-color: transparent;
  background: linear-gradient(180deg, #FFC45C, #E89218);
}
.slot-play:focus-visible { outline: 2px solid var(--sig); outline-offset: 2px; }
/* The chosen card's key is already lit: on the selected fabric it is the thing
   the hero key would do, so it should not look like the quiet option. */
.slot.is-chosen .slot-play { color: var(--sig); border-color: color-mix(in srgb, var(--sig) 55%, var(--edge)); }
.slot.is-chosen .slot-play:hover { color: #2A1A02; }

.name-confirm {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  background: rgba(4, 7, 9, .78);
  backdrop-filter: blur(5px);
  animation: name-confirm-in 140ms ease-out;
}
@keyframes name-confirm-in { from { opacity: 0 } to { opacity: 1 } }
.name-confirm-card {
  width: min(400px, 88vw); padding: 26px 28px 22px; text-align: center;
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid var(--edge);
  clip-path: polygon(0 16px, 16px 0, 100% 0, 100% calc(100% - 16px), calc(100% - 16px) 100%, 0 100%);
  box-shadow: 0 24px 54px rgba(0,0,0,.6);
}
.name-confirm-kicker {
  margin: 0 0 6px;
  font: 400 9.5px/1 var(--mono); letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-3);
}
/* The name is the subject of the question, so it is the biggest thing in the
   box. overflow-wrap because a 20-character name with no spaces must not push
   the card wider than the phone it is on. */
.name-confirm-name {
  margin: 0 0 14px; overflow-wrap: anywhere;
  font: 700 26px/1.15 var(--display); letter-spacing: .02em;
  color: var(--sig);
}
.name-confirm-ask {
  margin: 0 0 20px;
  font: 400 12.5px/1.6 var(--ui); color: var(--ink-2);
}
.name-confirm-acts { display: flex; flex-direction: column; gap: 9px; }
.name-yes, .name-no {
  padding: 11px 18px; cursor: pointer;
  font: 700 12px/1 var(--display); letter-spacing: .12em; text-transform: uppercase;
  clip-path: polygon(0 8px, 8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
  transition: transform 110ms ease;
}
.name-yes {
  border: 0; color: #2A1A02;
  background: linear-gradient(180deg, #FFC45C, #E89218);
  box-shadow: 0 0 30px rgba(255,178,63,.28), inset 0 1px 0 rgba(255,255,255,.45);
}
.name-no { background: transparent; border: 1px solid var(--edge); color: var(--ink-2); }
.name-yes:hover, .name-no:hover { transform: translateY(-2px); }
.name-no:hover { border-color: var(--sig); color: var(--sig); }
.name-yes:focus-visible, .name-no:focus-visible { outline: 2px solid var(--sig); outline-offset: 3px; }
.name-confirm-skip {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 16px; cursor: pointer;
  font: 400 11px/1 var(--ui); color: var(--ink-3);
}
.name-confirm-skip input { accent-color: var(--sig); cursor: pointer; }
.name-confirm-skip:hover { color: var(--ink-2); }

@media (prefers-reduced-motion: reduce) {
  .name-confirm { animation: none; }
  .slot-play, .name-yes, .name-no, .support-btn { transition: none; }
}

/* ---------- support ---------- */

.support { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 8px; }
/* A quiet sibling of .play-key: same cut corners, same type, no glow. The
   primary key is the only thing on this screen allowed to be filled amber. */
.support-btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 10px 20px; cursor: pointer; text-decoration: none;
  font: 700 12px/1 var(--display); letter-spacing: .12em; text-transform: uppercase;
  color: var(--gold);
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid color-mix(in srgb, var(--gold) 45%, var(--edge));
  clip-path: polygon(0 8px, 8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
  transition: transform 110ms ease, border-color 110ms ease, color 110ms ease;
}
.support-btn:hover {
  transform: translateY(-2px);
  border-color: var(--gold);
  color: var(--sig);
}
.support-btn:focus-visible { outline: 2px solid var(--sig); outline-offset: 3px; }
.support-note {
  margin: 0; max-width: 46ch; text-align: center;
  font-size: 11px; line-height: 1.6; color: var(--ink-3);
}

/* Sound toggle, sitting quietly at the end of the top bar. */
.sound-toggle { margin-left: 10px; font-size: 14px; line-height: 1; padding: 4px 8px; }
.sound-toggle.is-off { color: var(--ink-3); opacity: 0.65; }
.who { margin-left: auto; }

/* Stars land one at a time. Earned ones start dim and are lit on a timer, so a
   two-star board visibly and audibly stops short. */
.verdict-stars .star {
  display: inline-block;
  color: var(--ink-3);
  opacity: 0.35;
  transition: opacity 180ms ease, transform 180ms ease, color 180ms ease;
}
.verdict-stars .star.earned.lit {
  color: var(--gold);
  opacity: 1;
  transform: scale(1.18);
}
@media (prefers-reduced-motion: reduce) {
  .verdict-stars .star { transition: none; }
  .verdict-stars .star.earned.lit { transform: none; }
}

/* Contextual board controls. Hidden until there is something to act on, so the
   bar does not carry two dead buttons the whole time. */
.hud-contextual { display: flex; gap: 6px; margin-right: 10px; }
.hud-contextual[hidden] { display: none; }
.btn-danger { border-color: var(--bad); color: var(--bad); }
.btn-danger:hover:not(:disabled) {
  border-color: var(--bad);
  background: color-mix(in srgb, var(--bad) 16%, transparent);
  color: var(--ink);
}

/* The overall board sits above the three metric boards and matches their width.
   Specificity has to beat `.card.wide`, which is two classes and would
   otherwise pin this at 720px and leave it visibly narrower than the row below. */
.card.overall-board { width: min(1120px, 100%); }
.overall-board .board-blurb { margin: -4px 0 10px; }

/* Cable colour. The native input is the platform's colour wheel. */
.hud-color { display: flex; align-items: center; gap: 4px; margin-right: 8px; }
.swatch {
  width: 30px; height: 28px; padding: 2px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--panel); cursor: pointer;
}
.swatch::-webkit-color-swatch-wrapper { padding: 0; }
.swatch::-webkit-color-swatch { border: none; border-radius: 3px; }
.hud-color .btn-quiet[hidden] { display: none; }

/* The walkthrough, as a card ON the board rather than beside it.
   In the side panel it sat below the fold on a tablet and read as one more
   readout among five — several players never saw it at all. */
.teach-pop {
  position: absolute;
  z-index: 6;
  width: min(330px, calc(100% - 32px));
  padding: 14px 16px 12px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 12%, var(--panel));
  box-shadow: var(--shadow-lift);
  animation: teach-in 220ms ease-out;
}
.teach-pop[hidden] { display: none; }
.teach-pop.is-centred { left: 50%; top: 24px; transform: translateX(-50%); }

@keyframes teach-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .teach-pop { animation: none; }
}

.teach-step {
  margin: 0 0 4px;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent); font-weight: 600;
}
.teach-title { margin: 0 0 6px; font-size: 14px; font-weight: 600; color: var(--ink); }
.teach-body { margin: 0 0 11px; font-size: 12.5px; line-height: 1.65; color: var(--ink-2); }
.teach-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.teach-wait { font-size: 11px; color: var(--ink-3); font-style: italic; }

.teach-close {
  position: absolute; top: 6px; right: 8px;
  font: inherit; font-size: 16px; line-height: 1;
  padding: 2px 6px;
  border: none; background: transparent; color: var(--ink-3); cursor: pointer;
  border-radius: var(--radius-sm);
}
.teach-close:hover { color: var(--ink); background: var(--panel-2); }

/* ---------- level picker ---------- */

.card.level-picker { width: min(1120px, 100%); }
.level-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.level-card {
  display: flex; flex-direction: column; gap: 5px;
  text-align: left; font: inherit; cursor: pointer;
  padding: 12px 14px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--panel-2); color: var(--ink);
  transition: border-color 140ms ease, background 140ms ease;
}
.level-card:hover { border-color: var(--accent); background: var(--panel); }
.level-name { font-size: 13.5px; font-weight: 600; letter-spacing: 0.02em; }
.level-blurb { font-size: 11.5px; line-height: 1.55; color: var(--ink-2); }
.level-facts {
  display: flex; gap: 10px; flex-wrap: wrap; margin-top: 3px;
  font-size: 10.5px; letter-spacing: 0.04em; color: var(--ink-3);
}
.level-stars { color: var(--gold); letter-spacing: 0; }

/* The leader line from the card to what it is describing.
   The card lives in the margin now, so the association has to be drawn — this
   is the pointer half of the coachmark; the board dimming is the spotlight
   half. An L: out from the card, then down or up to the target. */
.teach-leader {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  border-color: var(--accent);
  border-style: solid;
  border-width: 0;
}
.teach-leader[hidden] { display: none; }

/* The L leaves the card horizontally and turns toward the target, so the
   horizontal leg sits at the CARD's height and the vertical leg on the edge
   nearest the TARGET. Card on the right means the target is to its left, so
   the vertical leg is the box's left border — and vice versa. */
.teach-leader[data-side='right'][data-drop='down'] { border-top-width: 2px;    border-left-width: 2px; }
.teach-leader[data-side='right'][data-drop='up']   { border-bottom-width: 2px; border-left-width: 2px; }
.teach-leader[data-side='left'][data-drop='down']  { border-top-width: 2px;    border-right-width: 2px; }
.teach-leader[data-side='left'][data-drop='up']    { border-bottom-width: 2px; border-right-width: 2px; }

/* A dot where the line lands on the switch — the far end of the vertical leg. */
.teach-leader::after {
  content: '';
  position: absolute;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
.teach-leader[data-side='right']::after { left: -4px; }
.teach-leader[data-side='left']::after  { right: -4px; }
.teach-leader[data-drop='down']::after  { bottom: -4px; }
.teach-leader[data-drop='up']::after    { top: -4px; }

/* Nowhere beside the board fits — a phone, or a short window. The card becomes
   a strip along the bottom and the board gives up that space, rather than the
   two fighting over it. Clamping the card into the leftover gap was the bug:
   when the card is taller than the gap, clamping puts it back on the board. */
.teach-pop.is-docked {
  left: 12px;
  /* Stop at the board column's edge. Spanning the whole stage laid the card
     across the bottom of the side panel — the ladder and the failure report
     are the two things a player checks while the tutorial is talking. */
  right: calc(var(--board-right) + 12px);
  top: auto;
  bottom: 12px;
  width: auto;
  max-height: 38vh;
  overflow-y: auto;
}
.stage.teach-docked .board-wrap { bottom: calc(38vh + 24px); }
/* A card in a side gutter takes its strip out of the board area — the same
   trade docking makes, turned on its side. Without this the board fills the
   width, and a card that will not fit beside it has nowhere to go but down. */
.stage.teach-side-right .board-wrap { right: calc(var(--board-right) + var(--teach-gutter, 0px)); }
.stage.teach-side-left .board-wrap { left: calc(var(--board-left) + var(--teach-gutter, 0px)); }
.stage.teach-docked .status { display: none; }


/* ============================================================
   Play screen overrides: the HUD's own version of shared widgets.
   The same markup the paper screens use, dressed for the room.
   ============================================================ */

/* Telemetry: two columns, because four across a 300px panel truncates the
   figure — which is the one thing on a gauge that must never be cut. */
body[data-screen="play"] .progress { grid-template-columns: repeat(2, 1fr); gap: 10px; }
body[data-screen="play"] .stat {
  background: none; border: 0; border-radius: 0; padding: 0;
  gap: 3px;
}
body[data-screen="play"] .stat + .stat { }
body[data-screen="play"] .stat-value { font-size: 25px; letter-spacing: -.02em; }
body[data-screen="play"] .stat-of { font-size: 12px; }
body[data-screen="play"] .stat-label {
  font: 600 9.5px/1.25 var(--ui); letter-spacing: .13em; color: var(--ink-3);
}
body[data-screen="play"] .stat-warn { border: 0; }
body[data-screen="play"] .stat-warn .stat-value { color: var(--sig); }

/* The ladder reads as a track of segments that light, not a list of ticks. */
body[data-screen="play"] .star-row {
  align-items: center; gap: 11px; padding: 8px 0;
  border-top: 1px solid rgba(255,255,255,.06);
}
body[data-screen="play"] .star-mark {
  width: 24px; height: 24px; flex: none;
  display: grid; place-items: center;
  font-size: 0; color: transparent;
  background: #0C1216; border: 1px solid var(--edge);
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}
body[data-screen="play"] .star-row.earned .star-mark {
  background: var(--live); border-color: transparent;
  box-shadow: 0 0 14px rgba(63,217,138,.5);
}
body[data-screen="play"] .star-name {
  font: 700 12.5px/1.2 var(--display); letter-spacing: .04em; text-transform: uppercase;
}
body[data-screen="play"] .star-row:not(.earned) .star-name { color: var(--ink-3); }
body[data-screen="play"] .star-hint { font: 400 11px/1.4 var(--ui); color: var(--ink-3); }

/* The colour swatch is a key like everything else on the belt. */
.belt .swatch {
  width: 34px; height: 34px; padding: 0; cursor: pointer;
  background: none; border: 1px solid var(--edge); border-radius: 0;
}
.belt .hud-color .btn { font: 600 10px/1 var(--ui); letter-spacing: .1em; text-transform: uppercase; }

/* Report tables, dressed down. They carry numbers, so they keep the mono face
   and lose every border a document would have given them. */
body[data-screen="play"] .tier-table td { border-color: rgba(255,255,255,.06); }
body[data-screen="play"] .report h3 {
  font: 700 10px/1 var(--display); letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink-3); margin: 14px 0 6px;
}
body[data-screen="play"] .verdict { background: rgba(255,255,255,.03); border-color: var(--edge); }


/* The walkthrough card, dressed for the room. Same geometry logic as before —
   coachmark.js still decides where it goes — but it is a panel now, not a
   pale note pinned over a dark screen. */
body[data-screen="play"] .teach-pop {
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid var(--sig);
  border-radius: 0;
  clip-path: polygon(0 var(--cut), var(--cut) 0, 100% 0,
                     100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%);
  box-shadow: 0 0 40px rgba(255,178,63,.14), 0 20px 48px rgba(0,0,0,.66);
}
body[data-screen="play"] .teach-step {
  font: 600 9.5px/1 var(--mono); letter-spacing: .2em; color: var(--sig);
}
body[data-screen="play"] .teach-title {
  font: 700 15px/1.2 var(--display); text-transform: uppercase; letter-spacing: .03em;
  color: var(--ink);
}
body[data-screen="play"] .teach-body { font: 400 12.5px/1.6 var(--ui); color: var(--ink-2); }
body[data-screen="play"] .teach-wait { font: 400 11px/1 var(--ui); color: var(--ink-3); }
body[data-screen="play"] .teach-leader { background: none; }


/* ============================================================
   The verdict.
   Not a dialog with a table in it — the screen stops and tells you what
   happened to the fabric. <dialog> is kept for the focus trap and Escape;
   everything else about it is overridden.
   ============================================================ */
body[data-screen="play"] #modal {
  border: 0; border-radius: 0; background: none; box-shadow: none;
  padding: 0; width: auto; max-width: none; max-height: none;
  inset: 0; overflow: visible;
}
body[data-screen="play"] #modal::backdrop {
  background: rgba(4,7,9,.8);
  backdrop-filter: blur(3px);
}
body[data-screen="play"] .results {
  position: fixed; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0; text-align: center; padding: 24px;
}

.v-banner {
  margin: 0; font: 700 12px/1 var(--display); letter-spacing: .44em;
  text-transform: uppercase; color: var(--live);
  opacity: 0; animation: v-rise 400ms ease forwards;
}
.v-headline {
  margin: 14px 0 0; font: 700 clamp(38px, 6vw, 68px)/1 var(--display);
  text-transform: uppercase; letter-spacing: .01em; color: var(--ink);
  opacity: 0; transform: scale(1.14);
  animation: v-slam 460ms cubic-bezier(.15,.9,.2,1) 120ms forwards;
}
.v-headline.is-good { text-shadow: 0 0 56px rgba(63,217,138,.38); }
.v-headline.is-bad  { text-shadow: 0 0 56px rgba(255,77,77,.30); }
.v-headline.is-bad + .v-stars .pip.lit { box-shadow: 0 0 26px rgba(255,178,63,.45); }
@keyframes v-slam { to { opacity: 1; transform: scale(1); } }
@keyframes v-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

.v-stars { display: flex; justify-content: center; gap: 12px; margin: 26px 0 0; }
.v-stars .pip {
  width: 40px; height: 40px; background: #162026;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  transform: scale(.4); opacity: .32;
}
.v-stars .pip.lit {
  background: var(--sig); opacity: 1; transform: scale(1);
  box-shadow: 0 0 30px rgba(255,178,63,.6);
  transition: transform 260ms cubic-bezier(.2,1.6,.4,1), opacity 180ms ease, box-shadow 260ms ease;
}

.v-sub {
  margin: 20px 0 0; max-width: 46ch;
  font: 400 14px/1.55 var(--ui); color: var(--ink-2);
  opacity: 0; animation: v-rise 400ms ease 900ms forwards;
}

.v-score {
  margin: 30px 0 0; display: grid; grid-template-columns: repeat(4, minmax(120px, 1fr));
  gap: 1px; background: var(--edge); border: 1px solid var(--edge);
  opacity: 0; animation: v-rise 420ms ease 1000ms forwards;
}
.v-cell { background: var(--plate); padding: 15px 10px; }
.v-n {
  font: 600 25px/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--ink);
}
.v-l {
  margin-top: 7px; font: 600 9.5px/1 var(--ui); letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3);
}
.v-d { margin-top: 5px; font: 400 10px/1.3 var(--ui); color: var(--ink-3); }

.v-acts {
  margin-top: 28px; display: flex; gap: 12px; align-items: center; justify-content: center;
  opacity: 0; animation: v-rise 420ms ease 1150ms forwards;
}
.v-acts .results-actions { display: flex; align-items: center; gap: 12px; flex-direction: row; margin: 0; }
.key.wide { width: auto; padding: 0 20px; gap: 10px; grid-auto-flow: column; }
.key.wide .lbl {
  font: 700 13px/1 var(--display); letter-spacing: .08em; text-transform: uppercase;
}
.v-note { margin: 0; max-width: 34ch; font: 400 12px/1.5 var(--ui); color: var(--ink-3); text-align: left; }
.v-note.error { color: var(--dead); }

@media (prefers-reduced-motion: reduce) {
  .v-banner, .v-headline, .v-sub, .v-score, .v-acts { animation: none; opacity: 1; transform: none; }
  .v-stars .pip { transition: none; }
}


/* ============================================================
   The home screen: a menu, not a landing page.
   Same room as the play screen. The level rack is the hero, because the
   thing a player is here to choose is a fabric.
   ============================================================ */
body[data-screen="home"], body[data-screen="leaderboard"], body[data-screen="run"] {
  background: var(--hull);
  background-image: none;
  --ink: #E8F2F6;
  --ink-2: #92A8B2;
  --ink-3: #5A6D77;
  --line: var(--edge);
  --panel: var(--plate);
  --panel-2: var(--plate-2);
  --accent: var(--sig);
  --good: var(--live);
  --bad: var(--dead);
}
body[data-screen="home"] .stage::before,
body[data-screen="leaderboard"] .stage::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image:
    linear-gradient(rgba(140,200,220,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(140,200,220,.05) 1px, transparent 1px);
  background-size: 48px 48px;
  -webkit-mask-image: radial-gradient(120% 90% at 50% 25%, #000 12%, transparent 72%);
  mask-image: radial-gradient(120% 90% at 50% 25%, #000 12%, transparent 72%);
}

/* the top bar becomes a thin instrument strip rather than a site nav */
body[data-screen="home"] .topbar,
body[data-screen="leaderboard"] .topbar {
  background: rgba(8,11,13,.86);
  border-bottom: 1px solid var(--edge);
}
body[data-screen="home"] .brand-name,
body[data-screen="leaderboard"] .brand-name {
  font: 700 14px/1 var(--display); letter-spacing: .05em; text-transform: uppercase;
}

body[data-screen="home"] .hero-title,
body[data-screen="leaderboard"] .card-head h2 {
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: .03em;
}
body[data-screen="home"] .hero-title { font-size: clamp(30px, 5vw, 48px); font-weight: 700; }
body[data-screen="home"] .hero-sub { font: 400 14px/1.55 var(--ui); color: var(--ink-2); }

/* ---------- the level rack ---------- */
.rack-wrap { width: min(1100px, 100%); }
.rack-lead {
  margin: 0 0 14px; text-align: center;
  font: 600 10px/1 var(--ui); letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3);
}
.rack { display: flex; gap: 18px; justify-content: center; flex-wrap: wrap; }
.slot {
  width: 312px; padding: 0; cursor: pointer; text-align: left;
  /* Was a <button>, which gave these for free. It contains a button now, so it
     cannot be one — nesting buttons is invalid and browsers drop the inner. */
  box-sizing: border-box; font: inherit; -webkit-appearance: none; appearance: none;
  display: flex; flex-direction: column; overflow: hidden;
  color: var(--ink);
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid var(--edge);
  clip-path: polygon(0 18px, 18px 0, 100% 0, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%);
  box-shadow: 0 24px 54px rgba(0,0,0,.6);
  opacity: 0; animation: hud-rise 520ms cubic-bezier(.2,.8,.25,1) forwards;
  transition: transform 160ms ease, border-color 160ms ease;
}
.slot:hover { transform: translateY(-4px); border-color: var(--sig); }
.slot:focus-visible { outline: 2px solid var(--sig); outline-offset: 3px; }
.slot-shelf {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px 10px; border-bottom: 1px solid var(--edge);
}
.slot-no { font: 700 28px/1 var(--display); color: #2A3A44; }
.slot:hover .slot-no { color: var(--sig); }
.slot-tag {
  margin-left: auto; padding: 5px 8px;
  font: 400 9.5px/1 var(--mono); letter-spacing: .1em; color: var(--ink-3);
  border: 1px solid var(--edge);
}
/* Fixed height, because the three boards have three different aspect ratios —
   left to size themselves the bays ended up different depths and the rack
   stopped lining up along the bottom. */
.slot-bay { background: #0A0E11; display: grid; place-items: center; height: 228px; }
.slot-board { display: block; box-shadow: 0 0 0 1px rgba(255,255,255,.08), 0 10px 26px rgba(0,0,0,.6); }
.slot-meta { padding: 14px 16px 16px; display: flex; flex-direction: column; }
.slot-name { font: 700 19px/1 var(--display); text-transform: uppercase; letter-spacing: .02em; }
.slot-blurb { margin-top: 8px; font: 400 12px/1.45 var(--ui); color: var(--ink-2); min-height: 52px; }
.slot-pips { display: flex; gap: 5px; margin-top: 12px; }
.slot-pips .pip {
  width: 12px; height: 12px; background: var(--sig);
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}

/* ---------- boards, dressed for the room ---------- */
body[data-screen="home"] .card,
body[data-screen="leaderboard"] .card {
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid var(--edge); border-radius: 0;
  clip-path: polygon(0 var(--cut), var(--cut) 0, 100% 0,
                     100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%);
  box-shadow: 0 18px 44px rgba(0,0,0,.5);
}
body[data-screen="home"] .card h2,
body[data-screen="leaderboard"] .card h2 {
  font: 700 10px/1 var(--display); letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink-3);
}
body[data-screen="home"] .board-table td,
body[data-screen="leaderboard"] .board-table td { border-color: rgba(255,255,255,.06); }
body[data-screen="home"] .board-table .stars,
body[data-screen="leaderboard"] .board-table .stars { color: var(--sig); }
body[data-screen="home"] tr.is-me td,
body[data-screen="leaderboard"] tr.is-me td {
  background: color-mix(in srgb, var(--sig) 12%, transparent);
}
body[data-screen="home"] .name-input {
  background: #0C1216; border: 1px solid var(--edge); color: var(--ink);
  font: 400 14px/1 var(--mono); border-radius: 0;
}
body[data-screen="home"] .btn-primary.btn-lg {
  font: 700 15px/1 var(--display); letter-spacing: .1em; text-transform: uppercase;
  background: linear-gradient(180deg, #FFC45C, #E89218); color: #2A1A02;
  border: 0; border-radius: 0;
  clip-path: polygon(0 8px, 8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%);
  box-shadow: 0 0 34px rgba(255,178,63,.3), inset 0 1px 0 rgba(255,255,255,.4);
}


/* ============================================================
   Motion. Events only — the one non-event animation in the game is the HUD's
   entrance, and it plays once.
   ============================================================ */

/* While the failure test runs, the room changes state: the board loses colour
   so the surviving cables stop competing with the ones going down, and the
   belt steps back because there is nothing useful to press. */
.stage.is-testing #board { filter: saturate(.55) brightness(.84); }
#board { transition: filter 380ms ease; }
.stage.is-testing .belt { opacity: .35; pointer-events: none; }
.stage .belt { transition: opacity 260ms ease; }

/* Alarm rides the failures rather than the whole sweep. */
.stage::after { transition: background 200ms ease; }
.stage.is-alarm::after {
  background:
    radial-gradient(72% 58% at 50% 46%, transparent 32%, rgba(0,0,0,.78) 100%),
    radial-gradient(78% 62% at 50% 50%, transparent 38%, rgba(255,77,77,.22) 100%);
}

/* A refused move: the room flinches. Two frames of shake with a red edge, then
   gone. It puts the refusal where the player's eyes already are — on the board,
   not on the status line they are not reading. */
.stage.is-deny .board-wrap { animation: deny-shake 320ms cubic-bezier(.36,.07,.19,.97); }
.stage.is-deny::after {
  background:
    radial-gradient(72% 58% at 50% 46%, transparent 32%, rgba(0,0,0,.78) 100%),
    radial-gradient(90% 70% at 50% 50%, transparent 52%, rgba(255,77,77,.30) 100%);
}
@keyframes deny-shake {
  10%, 90% { transform: translateX(-3px); }
  30%, 70% { transform: translateX(4px); }
  50%      { transform: translateX(-2px); }
}

@media (prefers-reduced-motion: reduce) {
  .stage.is-deny .board-wrap { animation: none; }
  #board, .stage .belt, .stage::after { transition: none; }
}


/* ============================================================
   Narrow. The HUD is four fixed panels flanking a board channel, which is a
   desktop arrangement — below about 1200px the rails eat the world.
   ============================================================ */
@media (max-width: 1200px) {
  /* On .stage, not on body: .stage declares --rail itself, so a value set on an
     ancestor is shadowed by its own declaration and the rails never narrow.
     Costs a board 700px of width and is invisible until someone measures it. */
  body[data-screen="play"] .stage { --rail: 18px; }
  /* Panels float over the board instead of flanking it, and go translucent so
     the cabling underneath stays readable. */
  .hudp {
    width: 296px;
    background: linear-gradient(162deg,
      color-mix(in srgb, var(--plate-2) 94%, transparent),
      color-mix(in srgb, var(--plate) 94%, transparent));
    backdrop-filter: blur(6px);
  }
  /* The report is the one panel that duplicates something else — every number
     in it is in the verdict, which is on screen the moment the test ends. */
  .hudp.report { display: none; }

  /* The belt is centred, so it runs off BOTH edges once it is wider than the
     window — and the key it loses first is the one that runs the test. */
  .belt { gap: 7px; }
  .key { width: 50px; height: 50px; }
  .key svg { width: 20px; height: 20px; }
  .key.go { padding: 0 16px; gap: 9px; }
  .key.go .lbl { font-size: 13px; }
}

@media (max-width: 900px) {
  /* Panels stop flanking and start stacking: two strips along the top, the
     belt along the bottom, and the board gets everything in between. Floating
     them over a board this size buries the cabling under the HUD. */
  .hudp.mission { left: 8px; right: 8px; top: 8px; width: auto; }
  .mission-name { font-size: 18px; }
  .mission-blurb { display: none; }
  .mission-goal { margin-top: 8px; padding-top: 8px; }

  .hudp.telem { left: 8px; right: 8px; top: 104px; bottom: auto; width: auto; }
  .hudp.telem .hudp-head { display: none; }   /* the labels already say it */
  body[data-screen="play"] .progress { grid-template-columns: repeat(4, 1fr); gap: 8px; }
  body[data-screen="play"] .stat-value { font-size: 19px; }

  /* The ladder is the one panel with nowhere to go. Every rung of it is in the
     verdict, which arrives the moment the test ends. */
  .hudp.ladder { display: none; }

  body[data-screen="play"] .board-wrap { inset: 186px 8px 92px 8px; }

  /* One line, clipped rather than wrapped. Wrapped, it became a slab across
     the middle of the board. */
  .status {
    /* top and right are neutralised deliberately: the pre-existing 820px rule
       sets top:55% and right:12px, and an absolutely positioned box with both
       top and bottom stretches — which turned the readout into a slab across
       the middle of the board. */
    top: auto; right: auto; left: 50%; transform: translateX(-50%);
    bottom: 76px; max-width: 86vw; padding: 6px 12px;
    font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .belt { bottom: 12px; gap: 6px; }
  .key { width: 46px; height: 46px; }
  .key.go { padding: 0 14px; }
  .key.go .lbl { display: none; }   /* the icon and SPACE carry it */
}

@media (max-width: 640px) {
  .rack { gap: 12px; }
  .slot { width: min(340px, 100%); }
  .v-score { grid-template-columns: repeat(2, 1fr); }
}


/* ============================================================
   The pause menu. The play screen has no nav bar, so this is the only way
   off the board — Escape, or the key in the corner where back always lives.
   ============================================================ */
.mission-menu {
  width: 24px; height: 24px; padding: 0; margin-right: 2px; flex: none;
  display: grid; place-items: center; cursor: pointer;
  color: var(--ink-3); background: none; border: 1px solid var(--edge);
  transition: color 140ms ease, border-color 140ms ease;
}
.mission-menu svg { width: 14px; height: 14px; }
.mission-menu:hover { color: var(--sig); border-color: var(--sig); }
.mission-menu:focus-visible { outline: 2px solid var(--sig); outline-offset: 2px; }

.pause {
  position: absolute; inset: 0; z-index: 20;
  display: grid; place-items: center;
  background: rgba(4,7,9,.8);
  backdrop-filter: blur(3px);
}
.pause[hidden] { display: none; }
.pause-card { width: min(380px, 88vw); text-align: center; }
.pause-kicker {
  margin: 0; font: 700 11px/1 var(--display); letter-spacing: .44em;
  text-transform: uppercase; color: var(--sig);
}
.pause-title {
  margin: 12px 0 26px; font: 700 34px/1 var(--display);
  text-transform: uppercase; letter-spacing: .02em; color: var(--ink);
}
.pause-acts { display: flex; flex-direction: column; gap: 8px; }
.pause-item {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 18px; cursor: pointer; text-align: left;
  color: var(--ink);
  background: linear-gradient(180deg, #1F2A31, #131B20);
  border: 1px solid var(--edge);
  clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%);
  transition: transform 110ms ease, border-color 140ms ease, color 140ms ease;
}
.pause-item:hover { transform: translateX(3px); border-color: var(--sig); color: var(--sig); }
.pause-item:focus-visible { outline: 2px solid var(--sig); outline-offset: 2px; }
.pause-item svg { width: 20px; height: 20px; flex: none; }
.pause-label {
  font: 700 14px/1 var(--display); letter-spacing: .06em; text-transform: uppercase;
}
.pause-hint { margin-left: auto; font: 400 10px/1 var(--mono); color: var(--ink-3); }

@media (prefers-reduced-motion: reduce) {
  .pause-item:hover { transform: none; }
}


/* ============================================================
   Home: one primary action, and cards that tell the truth.
   ============================================================ */

/* The play key names its target. It is the only high-contrast thing in the
   hero, so whatever it says is what a first-time visitor believes will happen. */
.play-key {
  display: inline-flex; align-items: center; gap: 14px;
  padding: 14px 26px; cursor: pointer; border: 0;
  color: #2A1A02;
  background: linear-gradient(180deg, #FFC45C, #E89218);
  clip-path: polygon(0 10px, 10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%);
  box-shadow: 0 0 44px rgba(255,178,63,.34), 0 12px 26px rgba(0,0,0,.45),
              inset 0 1px 0 rgba(255,255,255,.45);
  transition: transform 110ms ease;
}
.play-key:hover { transform: translateY(-2px); }
.play-key:focus-visible { outline: 2px solid var(--sig); outline-offset: 3px; }
.play-key svg { width: 26px; height: 26px; stroke: #2A1A02; fill: #2A1A02; }
.play-text { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.play-label {
  font: 600 9.5px/1 var(--ui); letter-spacing: .2em; text-transform: uppercase;
  color: rgba(42,26,2,.65);
}
.play-level {
  font: 700 18px/1 var(--display); letter-spacing: .04em; text-transform: uppercase;
}

/* The name is a label on a leaderboard row, not a sign-up. It sits under the
   play key and stays quiet. */
body[data-screen="home"] .name-form {
  display: flex; flex-direction: column; align-items: center; gap: 18px;
}
body[data-screen="home"] .name-field { display: flex; align-items: center; gap: 8px; }
body[data-screen="home"] .name-label {
  font: 600 9px/1 var(--ui); letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3);
}
body[data-screen="home"] .name-row { display: flex; align-items: center; gap: 6px; }
body[data-screen="home"] .name-input {
  width: 168px; padding: 6px 10px; font-size: 12px; text-align: center;
}
body[data-screen="home"] .hint { font-size: 11px; color: var(--ink-3); }

/* The card the play key will start. */
.slot.is-chosen { border-color: var(--sig); box-shadow: 0 0 44px rgba(255,178,63,.2), 0 24px 54px rgba(0,0,0,.6); }
.slot.is-chosen .slot-no { color: var(--sig); }

.slot-foot { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.slot-pips { display: flex; gap: 5px; margin: 0; }
/* Unearned pips are hollow. Filled ones used to be drawn for the level's whole
   ladder, which read as "already finished" on a board nobody had touched. */
.slot-pips .pip { background: #22303A; }
.slot-pips .pip.lit { background: var(--sig); }


/* ---------- patch lane picker ----------
   A menu, not a widget: patching is a pick from a list, and a list shows every
   lane, its colour and what is already in it — which four one-tile squares
   beside a chassis never could. */
.lane-picker {
  position: absolute; z-index: 8; width: 228px;
  background: linear-gradient(162deg, var(--plate-2), var(--plate));
  border: 1px solid var(--edge); border-radius: var(--radius-sm);
  box-shadow: 0 18px 40px rgba(0,0,0,.6);
  padding: 9px; display: flex; flex-direction: column; gap: 7px;
}
.lane-picker-head {
  margin: 0; font: 600 9.5px/1.3 var(--ui); letter-spacing: .13em;
  text-transform: uppercase; color: var(--ink-3);
}
.lane-list { display: flex; flex-direction: column; gap: 3px; }
.lane-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 6px 8px; color: var(--ink); cursor: pointer; text-align: left;
  font: 500 11.5px/1.2 var(--ui);
}
.lane-row:hover:not([disabled]), .lane-row:focus-visible { border-color: var(--gold); }
.lane-row[disabled] { opacity: .45; cursor: default; }
.lane-dot { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.lane-name { flex: 1; }
.lane-state { font-size: 10px; color: var(--ink-3); }
.lane-cancel {
  background: none; border: 0; color: var(--ink-3); cursor: pointer;
  font: 500 10.5px/1 var(--ui); padding: 2px; align-self: flex-end;
}
.lane-cancel:hover { color: var(--ink); }
