/* Bomberman Arena — the whole client stylesheet.
   Extracted from the inline <style> in packages/client/public/index.html; the
   server reads it at boot and serves it at /assets/styles.css. */

/* ---- box-sizing, globally, and it is load-bearing ----

   Padding and borders count INSIDE a declared width. Without this the pattern
   `width: 100%` + `padding` — which this sheet uses on its most common
   containers — resolves to "the parent's width, plus the padding", and the
   surplus hangs off the right-hand edge.

   That was not hypothetical. `.card` is `width: 100%; padding: 14px` (see the
   compact block), so on a phone it measured 731px inside a 703px viewport: 28px
   of horizontal overflow on EVERY page, because every page is built out of
   cards. The visible symptom is the one that gets reported as "the padding is
   wrong on the right" — and the padding was never wrong. It was 14px on both
   sides; the right-hand 14 was simply off-screen, along with 14px of content.
   `.steps` had the identical bug from its own 12px compact padding.

   `#nav` already carried this declaration with a comment explaining the same
   failure ("would put the whole document into horizontal scroll"). It belongs
   here, once, rather than being rediscovered per component. Measured before and
   after on all five pages: scrollWidth 731 -> 703, matching clientWidth exactly,
   with no element changing except the cards' own descendants. */
*, *::before, *::after { box-sizing: border-box; }

/* ---- Predicto design tokens (cyan-forward command deck) ---- */
:root {
  color-scheme: dark;
  --bg: hsl(200 20% 6%);           /* void background */
  --card: hsl(200 25% 8%);         /* card surface */
  --fg: hsl(180 100% 95%);         /* icy mint-white */
  --primary: hsl(174 100% 41%);    /* signal cyan */
  --primary-bright: hsl(174 100% 48%);
  --on-primary: hsl(200 20% 6%);
  --secondary: hsl(200 20% 12%);   /* secondary fill / input fill */
  --secondary-fg: hsl(180 100% 90%);
  --muted: hsl(200 15% 15%);       /* muted fill */
  --muted-fg: hsl(200 10% 55%);    /* metadata text */
  --border: hsl(200 20% 18%);
  --success: hsl(142 76% 36%);
  --danger: hsl(0 84% 60%);
  --warning: hsl(38 92% 50%);
  --radius: 0.5rem;
  /* Share of the screen height the game canvas gets. 100% everywhere
     except a split phone layout, where GameView writes 50% or 60%.
     Lives on :root because fixed overlays (#banner) are siblings of
     #game and cannot inherit a value set there. */
  --stage-pct: 100%;

  /* Viewport geometry (plan §6.10). game/telegram.ts overwrites --app-height
     with Telegram's reported stable height — inside the Telegram webview
     100vh is NOT the visible area — and overwrites the insets when the client
     reports its own. These are the browser fallbacks. */
  --app-height: 100dvh;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  /* Height of the fixed bottom tab bar; content reserves this much. */
  --tabbar-h: 56px;
  /* Minimum comfortable touch target. */
  --touch: 44px;

  /* ---- One page width for the whole entry app ----
     Every top-level page root — Live, Play, Squad, Hall, Event, Settings,
     About — measures against these two and nothing else. Switching tabs must
     not resize the content column under the nav.

     lg  ≥1025px : capped column, centred
     md  721–1024: fluid — the viewport is already under the cap, so the
                   gutter is what defines the page
     sm  ≤720px  : full bleed, gutter 0 (the compact shell's cards carry
                   their own padding and run edge to edge)

     Pages must occupy this column even when their content is narrower
     (Play's stepper). #root is `display: contents` so these roots are
     flex items of body; without that, #root shrink-wraps to the visible
     page and Play comes out ~300px narrower than Live/Squad. */
  --page-w: 920px;
  --page-pad: 16px;
}
@media (max-width: 1024px) { :root { --page-w: 100%; --page-pad: 20px; } }
/* The hidden attribute must ALWAYS win: our id-selector display rules (e.g.
   #loading { display:flex }) otherwise override the UA's [hidden] rule and
   overlays get stuck visible from page load. */
[hidden] { display: none !important; }

/* ---- Boot asset splash (pre-React: index.html #boot-loading) ----
   Full-screen while main.tsx probes optional drop-ins + jerseys. The tile
   drifts toward top-left (negative background-position) one repeat unit so
   the loop is seamless. --boot-progress is 0..1, written by boot.ts. */
#boot-loading {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
  /* Single motif tile is 176²; 40px keeps the old 8×8-at-320px density. */
  --boot-tile: 40px;
  --boot-progress: 0;
}
#boot-loading .boot-bg {
  position: absolute; inset: 0;
  background-image: url(/assets/bg1.jpg);
  background-repeat: repeat;
  background-size: var(--boot-tile) var(--boot-tile);
  animation: boot-bg-drift 24s linear infinite;
  opacity: 0.9;
}
@keyframes boot-bg-drift {
  from { background-position: 0 0; }
  to   { background-position: calc(-1 * var(--boot-tile)) calc(-1 * var(--boot-tile)); }
}
#boot-loading .boot-ui {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  width: min(280px, 70vw);
}
#boot-loading .boot-label {
  font-family: Orbitron, "Space Grotesk", sans-serif;
  font-size: 14px; font-weight: 700; letter-spacing: .28em;
  color: var(--primary);
  text-shadow: 0 0 14px hsl(174 100% 41% / 0.45);
}
#boot-loading .boot-bar {
  width: 100%; height: 6px;
  background: hsl(200 20% 12%);
  border: 1px solid hsl(174 100% 41% / 0.35);
  border-radius: 999px;
  overflow: hidden;
}
#boot-loading .boot-fill {
  height: 100%; width: 100%;
  transform-origin: left center;
  transform: scaleX(var(--boot-progress));
  background: linear-gradient(90deg, hsl(174 100% 34%), var(--primary-bright));
  box-shadow: 0 0 12px hsl(174 100% 41% / 0.5);
  transition: transform 120ms linear;
}

body {
  margin: 0; min-height: 100vh; background: var(--bg); color: var(--fg);
  font: 14px/1.5 "Space Grotesk", system-ui, sans-serif;
  font-feature-settings: "rlig" 1, "calt" 1;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 28px var(--page-pad) 40px;
  background-image:
    radial-gradient(ellipse 70% 45% at 50% -5%, hsl(174 100% 41% / 0.10), transparent),
    linear-gradient(hsl(174 100% 41% / 0.05) 1px, transparent 1px),
    linear-gradient(90deg, hsl(174 100% 41% / 0.05) 1px, transparent 1px);
  background-size: 100% 100%, 40px 40px, 40px 40px;
  background-attachment: fixed;
}
/* React mounts here. `contents` so nav/pages are body's flex items —
   the width rules below were written for that. A real box shrink-wraps to
   the visible page (Play's stepper ~618px vs Live/Squad at 920px). */
#root { display: contents; }
::selection { background: hsl(174 100% 41% / 0.3); }

/* ---- Wordmark hero ---- */
h1 {
  font-family: Orbitron, "Space Grotesk", sans-serif;
  font-size: clamp(26px, 5vw, 38px); font-weight: 800; letter-spacing: .18em;
  margin: 8px 0 0; color: var(--fg);
  text-shadow: 0 0 18px hsl(174 100% 41% / 0.5), 0 0 42px hsl(174 100% 41% / 0.3);
}
h1::after {
  content: "ARENA"; display: block; text-align: center;
  font-size: .32em; font-weight: 700; letter-spacing: .9em; padding-left: .9em;
  color: var(--primary); margin-top: 2px;
}
/* On a wide screen the status line rides at the right end of the nav bar
   (.navstatus, see Nav.tsx), so this copy is only the in-game overlay and the
   compact-screen line — both re-enabled in their own blocks below. */
#status { color: var(--muted-fg); min-height: 1.5em; font-size: 13px; }
#status:empty { display: none; }
body:not(.ingame) #status { display: none; }

/* ---- Cards, sections ---- */
#event { display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start;
         gap: 20px; }
.card {
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 20px 22px; display: flex; flex-direction: column; gap: 10px;
  /* min() not a bare 300px: on a 320 px phone the body's own padding leaves
     288 px of content box, and a hard 300 px floor pushed every page into
     horizontal scroll. This was the single widest-reaching mobile bug. */
  min-width: min(300px, 100%); flex: 1 1 340px; max-width: 420px;
  box-shadow: 0 0 24px hsl(174 100% 41% / 0.06), inset 0 1px 0 hsl(174 100% 41% / 0.05);
}
.sect { color: var(--muted-fg); font-size: 11px; font-weight: 600; letter-spacing: .3em;
        margin: 2px 0 4px; }
.sect::before { content: "▸ "; color: var(--primary); }
.row { display: flex; gap: 8px; align-items: center; }

/* ---- Inputs & selects ---- */
input, select {
  background: var(--secondary); color: var(--fg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 9px 12px; font-size: 14px;
  font-family: inherit;
}
input::placeholder { color: var(--muted-fg); }
select:disabled { opacity: .5; cursor: not-allowed; }
#itemrow { flex-wrap: wrap; gap: 6px; }
.itemlbl { color: var(--muted-fg); font-size: 11px; letter-spacing: .15em; }
.itbox { display: inline-flex; align-items: center; gap: 3px; font-size: 13px;
         background: var(--secondary); border: 1px solid var(--border);
         border-radius: var(--radius); padding: 4px 8px; cursor: pointer; }
.itbox:hover { border-color: hsl(174 60% 30%); }
.itbox input { accent-color: var(--primary); margin: 0; }
input:focus-visible, select:focus-visible, button:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 1px;
}
#joincode { font-family: "JetBrains Mono", monospace; letter-spacing: .2em; }

/* ---- Buttons ---- */
button {
  background: var(--secondary); color: var(--secondary-fg);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 9px 14px; font-size: 14px; font-weight: 500; font-family: inherit;
  cursor: pointer; transition: border-color .15s, box-shadow .15s, background .15s;
}
button:hover { border-color: hsl(174 60% 30%); box-shadow: 0 0 12px hsl(174 100% 41% / 0.15); }
button:disabled { opacity: .5; cursor: default; box-shadow: none; }
.primary, #create, #start {
  background: var(--primary); color: var(--on-primary); border-color: transparent;
  font-weight: 600;
}
.primary:hover, #create:hover, #start:hover {
  background: var(--primary-bright);
  box-shadow: 0 0 18px hsl(174 100% 41% / 0.45);
}
#start { padding: 11px 30px; font-size: 15px; letter-spacing: .05em; align-self: center;
         box-shadow: 0 0 22px hsl(174 100% 41% / 0.35); }
.ghost, #clearhead, #removebot { background: transparent; border-color: transparent;
         color: var(--muted-fg); }
.ghost:hover, #clearhead:hover, #removebot:hover { color: var(--danger);
         border-color: hsl(0 84% 60% / 0.4); box-shadow: none; }

/* The invite / share button (§4.5). X's own black-and-white rather than the
   deck's cyan, for one reason: it is never the primary action on any surface
   it appears on — Find a match, Start match and Back to Live all outrank it —
   and a second cyan button beside those competes for a click it should not
   win. The ✕ is drawn from text, not an asset: no network fetch, no logo file
   to go stale, and it still reads at 12 px. */
.xbtn { background: #000; color: #fff; border-color: hsl(0 0% 100% / 0.25);
        font-weight: 600; display: inline-flex; align-items: center; gap: 7px; }
.xbtn::before { content: "𝕏"; font-size: 15px; line-height: 1; }
.xbtn:hover { border-color: hsl(0 0% 100% / 0.6); background: hsl(0 0% 10%);
              box-shadow: 0 0 14px hsl(0 0% 100% / 0.15); }
.invrow { align-items: center; gap: 10px; }
/* The arriving half of an invite. Tinted and ruled on the left so it reads as
   something that HAPPENED to the page rather than another control on it — the
   reader needs to see at a glance that the link worked and a seat is waiting. */
.invited { align-items: center; gap: 12px; flex-wrap: wrap;
           background: hsl(174 100% 41% / 0.09); border-left: 3px solid var(--primary);
           border-radius: 4px; padding: 9px 12px; margin-bottom: 4px; }
.invited .hint { margin: 0; }
/* The count stays left, the two buttons stay together on the right. */
#results .racts { justify-content: flex-end; }
#results .rcount { margin-right: auto; }

/* ---- Room state ---- */
#code {
  font-family: "JetBrains Mono", monospace; font-size: 26px; letter-spacing: .45em;
  font-weight: 700; color: var(--primary); text-align: center; padding-left: .45em;
  text-shadow: 0 0 14px hsl(174 100% 41% / 0.5), 0 0 34px hsl(174 100% 41% / 0.3);
}
#roster { background: var(--muted); border: 1px solid var(--border);
          border-radius: var(--radius); padding: 10px 14px; font-size: 13px; }
#roster div { padding: 3px 0; }
.keys { color: var(--muted-fg); font-size: 12px; font-family: "JetBrains Mono", monospace; }

canvas { border-radius: var(--radius); background: var(--bg); image-rendering: pixelated; }
#preview { border: 1px solid var(--border); }
.swatch { width: 26px; height: 26px; border-radius: 6px; border: 2px solid transparent;
          cursor: pointer; padding: 0; }
.swatch:hover { box-shadow: none; }

/* ---- Face rows: skin / hair / eyes / style ----
   The label is a fixed gutter and everything after it takes the REST of the
   width, so a row of eight tones spans the card instead of huddling at the left
   in 14px dots. `auto-fit` + `1fr` is what makes the swatches grow to fill the
   space rather than leaving a ragged tail — more tones make them narrower, never
   the row shorter. */
.facerow { align-items: center; gap: 8px; }
.facelbl { flex: 0 0 34px; font-size: 10px; opacity: .7; }
.facerow select { flex: 1 1 auto; min-width: 0; }
.chips { flex: 1 1 auto; min-width: 0; display: grid; gap: 6px;
         grid-template-columns: repeat(auto-fit, minmax(28px, 1fr)); }
.chips .swatch { width: auto; height: 28px; min-width: 0; }
.swatch.sel { border-color: var(--primary); box-shadow: 0 0 8px hsl(174 100% 41% / 0.5); }
#palettes { flex-wrap: wrap; max-width: 180px; gap: 5px; }
.filebtn { background: var(--secondary); color: var(--secondary-fg);
           border: 1px solid var(--border); border-radius: var(--radius);
           padding: 9px 12px; cursor: pointer; font-size: 14px; }
.filebtn:hover { border-color: hsl(174 60% 30%); }

/* ---- Game page: full-bleed viewport, HUD overlaid on the canvas ---- */
body.ingame { padding: 0; overflow: hidden; background-image: none;
             -webkit-tap-highlight-color: transparent;
             -webkit-touch-callout: none;
             -webkit-user-select: none; user-select: none; }
body.ingame ::selection { background: transparent; }
body.ingame h1 { display: none; }
body.ingame #status { position: fixed; top: 52px; left: 50%; transform: translateX(-50%);
                      z-index: 2; background: hsl(200 25% 8% / 0.8);
                      border: 1px solid var(--border); padding: 4px 14px;
                      border-radius: 999px; }
#game { display: flex; flex-direction: column; align-items: center; gap: 8px; }
body.ingame #game { position: fixed; inset: 0; }
#game3d { width: 100%; height: 100%; position: relative; }
/* DIRECT CHILD, not any descendant. The renderer appends its canvas straight to
   this element and owns nothing else in it — but the tutorial band lives in here
   too, and its portrait is a canvas. As a descendant rule this stretched a
   128×128 face across the whole band with !important and squeezed the coach's
   words to a one-character column at the right edge. */
#game3d > canvas { width: 100% !important; height: 100% !important; display: block;
                 border-radius: 0; }

/* ---- CRT scanline filter (plan §6.14) ----
   Alpha-composited black bands, not mix-blend-mode: over a black band the two
   produce the same pixels, and a blend mode stacked on a WebGL canvas can cost
   the compositor the canvas's own layer — which would break the claim that this
   tier is free. The period arrives from the component as a fixed CSS-pixel
   value (3px, or 4px on displays where 3 would not land on whole device
   pixels); nothing here derives it from the stage size. Static by design: a
   rolling flicker is the memorable half of the effect and also flashing
   content, so it belongs to a later tier behind its own switch.
   z-index 1 puts it above the canvas and below everything that carries
   meaning — #status (2), #hud (5), #pad (10), the spectate and side-bomber UI
   (12), the options chip (13), the end-of-round banner (20), the spectacle
   beats (21), the options sheet (30). A "FINISH" banner read through scanlines
   is a bug, not a look, and the same goes for a KO card.
   #finaltwoedge is the exception that proves the ladder: it sits at 3, BELOW
   the HUD, because it is a backdrop-filter and only desaturates what is painted
   behind it. Above the HUD it would drain the colour out of the seat rail and
   the clock, which are the two things a watcher reads to follow the fight. */
#crt { position: absolute; inset: 0; z-index: 1; pointer-events: none;
       background:
         repeating-linear-gradient(to bottom,
           transparent 0,
           transparent calc(var(--crt-pitch) - 1px),
           rgb(0 0 0 / var(--crt-a)) calc(var(--crt-pitch) - 1px),
           rgb(0 0 0 / var(--crt-a)) var(--crt-pitch)),
         radial-gradient(125% 105% at 50% 50%,
           transparent 55%, rgb(0 0 0 / calc(var(--crt-a) * 0.9)) 100%); }
/* Cinematic bars over the cold open (plan §6.17.1). A sibling of the canvas,
   like #crt and for the same reason: this renderer has no EffectComposer on the
   game canvas, so the compositor does the work and the frame loop does none.
   Height is a percentage of the stage box, so the bars scale with it instead of
   eating a phone's whole screen. Cold open only — a warm join gets no bars. */
#letterbox { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
#letterbox .lb-bar { position: absolute; left: 0; right: 0; height: 7.5%;
       background: #000; transform: scaleY(0); will-change: transform; }
#letterbox .lb-top { top: 0; transform-origin: top; }
#letterbox .lb-bottom { bottom: 0; transform-origin: bottom; }
#letterbox[data-phase="in"] .lb-bar { transform: scaleY(1);
       transition: transform var(--lb-in) cubic-bezier(.22,.61,.36,1); }
#letterbox[data-phase="out"] .lb-bar { transform: scaleY(0);
       transition: transform var(--lb-out) cubic-bezier(.4,0,.2,1); }
/* Anyone who asked for less motion gets no bars at all: the component returns
   null, and this is the belt-and-braces for a stale DOM node. */
@media (prefers-reduced-motion: reduce) {
  #letterbox { display: none; }
}

/* ---- Tutorial dialogue band (docs/TUTORIALS.md §5) ----
   ANCHORED TO THE BOTTOM OF THE STAGE, NOT THE VIEWPORT. On desktop the stage
   IS the screen (body.ingame is full-bleed, --stage-pct falls back to 100%), so
   this is the bottom ~150px of the window. On a phone the pad owns everything
   below --stage-pct (#pad, below) and a viewport-pinned band would land on it —
   the same pile-up that got the side-bomber strip deleted.

   AN OVERLAY, NEVER A LAYOUT BOX: #game3d is watched by a ResizeObserver, so a
   band that took part in layout would re-fit the arena under the player's feet
   every time it opened and closed. It slides rather than fades for the reason
   docs/transition.md gives about the wallpaper cut — an opacity tween reads as
   a modern UI, a positional move reads as a machine doing something. */
#tutdlg {
  --dlg-h: clamp(112px, 18vh, 168px);   /* 150px on a normal desktop window */
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  height: calc(var(--dlg-h) + var(--safe-bottom));
  transform: translateY(101%);
  transition: transform 220ms cubic-bezier(.2,.8,.2,1);
  pointer-events: none;
}
#tutdlg[data-open] { transform: none; pointer-events: auto; }
/* An ambient line runs over live play and takes no press, so it must not eat a
   tap meant for the pad or the board underneath it. */
#tutdlg[data-ambient] { pointer-events: none; }
/* It is a <button> so it is reachable by keyboard and switch control, which
   means the global button rule (radius, padding, secondary fill) is landing on
   it and every one of those has to be undone: a full-bleed bar with rounded
   corners and a 9px inset reads as a dialog someone dropped on the stage. */
.tutband {
  box-sizing: border-box; appearance: none; border-radius: 0;
  width: 100%; height: 100%; display: flex; align-items: center; gap: 16px;
  padding: 14px 20px calc(14px + var(--safe-bottom));
  text-align: left; cursor: pointer; font: inherit; color: var(--fg);
  border: 0; border-top: 2px solid var(--primary);
  /* OPAQUE, not a blur: a translucent box over a bright theme (goldmine,
     soccer) is where legibility goes to die. */
  background: linear-gradient(180deg, hsl(200 26% 9%), hsl(200 28% 6%));
  box-shadow: 0 -16px 40px hsl(0 0% 0% / 0.55);
}
/* The global button hover lights a border and throws a glow; this bar is not a
   thing you hover, it is a thing you read. */
.tutband:hover { box-shadow: 0 -16px 40px hsl(0 0% 0% / 0.55); border-top-color: var(--primary); }
.tutface { flex: 0 0 auto; width: 108px; height: 108px; image-rendering: pixelated;
           border-radius: var(--radius); background: hsl(200 25% 12%);
           border: 1px solid var(--border); object-fit: contain; }
.tuttext { flex: 1 1 auto; min-width: 0; }
.tutwho { display: block; color: var(--primary); font-size: 12px; font-weight: 700;
          letter-spacing: .22em; text-transform: uppercase; margin-bottom: 6px; }
.tutsay { margin: 0; font-size: 18px; line-height: 1.45; max-width: 68ch; }
.tutcaret { display: inline-block; width: .5em; height: 1.05em; margin-left: 2px;
            vertical-align: -.15em; background: var(--primary); opacity: .8;
            animation: tutblink 1s steps(2, start) infinite; }
@keyframes tutblink { 50% { opacity: 0; } }
.tutnext { flex: 0 0 auto; align-self: flex-end; color: var(--muted-fg);
           font-size: 20px; opacity: 0; transition: opacity 160ms; }
.tutnext[data-ready] { opacity: 1; animation: tutbob 1.1s ease-in-out infinite; }
@keyframes tutbob { 50% { transform: translateY(4px); } }
/* Phone: the band shares the stage with a d-pad below it, so it gives back
   what it can — the portrait first, since the words are the point. */
@media (max-width: 720px) {
  #tutdlg { --dlg-h: clamp(96px, 22vh, 132px); }
  .tutface { width: 72px; height: 72px; }
  .tutsay { font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) {
  .tutcaret, .tutnext[data-ready] { animation: none; }
}

/* ---- Tutorial chrome: the pill, the pips, the doors out ---- */
#tuthud { position: fixed; top: 0; left: 0; right: 0; z-index: 11;
          display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
          padding: calc(8px + var(--safe-top)) 14px 8px;
          pointer-events: none; }
#tuthud > * { pointer-events: auto; }
.tuttop { display: flex; align-items: center; gap: 10px; }
.tutcount { font-family: "JetBrains Mono", monospace; font-size: 12px; color: var(--muted-fg); }
.tuttitle { font-size: 14px; letter-spacing: .04em; }
.tutpips { display: flex; gap: 4px; }
.tutpips i { width: 14px; height: 4px; border-radius: 2px; background: var(--border); }
.tutpips i.on { background: var(--primary); }
.tutgoal { margin-left: auto; padding: 6px 14px; border-radius: 999px;
           background: hsl(200 25% 8% / 0.92); border: 1px solid var(--border);
           font-size: 13px; }
.tutgoal b { color: var(--primary); }
/* Answered, and the board is still finishing. The pill lights rather than
   changes shape — it must not move the row it sits in. */
.tutgoal[data-done] { border-color: hsl(174 60% 30%); color: var(--fg);
                      box-shadow: 0 0 14px hsl(174 100% 41% / 0.2); }
.tutbtns { display: flex; gap: 6px; }
.tutbtns button { background: hsl(200 25% 8% / 0.92); color: var(--muted-fg);
                  border: 1px solid var(--border); border-radius: 999px;
                  padding: 5px 12px; font-size: 12px; cursor: pointer; }
.tutbtns button:hover { color: var(--fg); border-color: hsl(174 60% 30%); }

/* What is in effect on the player, in the top-right corner (components/
   TutorialHud.tsx). Its OWN row — `flex-basis: 100%` inside #tuthud's wrapping
   flex — so a player carrying eight things can never push the objective pill
   off a phone. Right-aligned, because the corner is where it was asked for and
   because the row above it already ends in the buttons. */
.tutloot { flex-basis: 100%; display: flex; justify-content: flex-end;
           flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.tutlootone { position: relative; display: block; width: 30px; height: 30px;
              border-radius: 8px; background: hsl(200 25% 8% / 0.92);
              border: 1px solid var(--border); padding: 2px; }
/* `display: block` is load-bearing, not tidiness: .looticons is drawn on an <i>,
   and an inline box ignores width/height — so the cell sized itself to zero and
   the tile drew empty. Everywhere else the icon happens to be a flex item, which
   blockifies it and hid this. */
.tutlootone .looticons { display: block; width: 100%; height: 100%; }
/* The badge is a count (×3 bombs) or a countdown (a disease), never both — see
   TutorialItems. Anchored to the corner so a two-character value and a
   three-character one sit in the same place. */
.tutlootone b { position: absolute; right: -3px; bottom: -4px;
                font-family: "JetBrains Mono", monospace; font-size: 10px;
                line-height: 1; padding: 1px 3px; border-radius: 5px;
                background: var(--primary); color: var(--on-primary); }

/* The render-failure card (components/Boundary.tsx). Deliberately plain: it is
   the one surface that has to work when the rest of the app did not. */
#crashed { max-width: 640px; margin: 12vh auto; display: grid; gap: 12px;
           text-align: left; padding: 0 20px; }
#crashed pre { background: var(--muted); border: 1px solid var(--border);
               border-radius: var(--radius); padding: 12px; overflow-x: auto;
               font-family: "JetBrains Mono", monospace; font-size: 12px;
               white-space: pre-wrap; user-select: text; }
#crashed button { justify-self: start; }

/* ---- The school's lesson list ---- */
.learnhead p { color: var(--muted-fg); font-size: 14px; max-width: 62ch; }
.learnlist { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.learncard { display: flex; align-items: center; gap: 14px;
             background: var(--muted); border: 1px solid var(--border);
             border-radius: var(--radius); padding: 12px 14px; }
.learncard[data-locked] { opacity: .45; }
.learnnum { flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%;
            display: grid; place-items: center; font-size: 13px; font-weight: 700;
            background: hsl(200 25% 12%); border: 1px solid var(--border); }
.learnwho { flex: 1 1 auto; min-width: 0; display: grid; }
.learnwho span { color: var(--muted-fg); font-size: 13px; }

/* One tab per floor of the school (components/TutorialPage.tsx). Reuses .chip
   rather than growing a second pill: the roster's star filter is the same
   control doing the same job three pages away, and two of them that drift apart
   is the thing worth avoiding. */
.learntabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 6px; }
.learntab { display: inline-flex; align-items: center; gap: 7px; }
.learntabname { font-size: 13px; }
.learntabcount { font-family: "JetBrains Mono", monospace; font-size: 11px;
                 color: var(--muted-fg); }
.learntab.on .learntabcount { color: var(--on-primary); opacity: .8; }
.learntab.on .fstars { color: var(--on-primary); }
/* A floor that has not been unlocked yet. Dimmed but still clickable — the tab
   opens, the cards read, nothing on it starts (TutorialPage.tsx). */
.learntab[data-locked] { opacity: .55; }
.learntab[data-locked].on { opacity: .8; }
.learntierblurb { color: var(--muted-fg); font-size: 13px; max-width: 62ch;
                  margin: 0 0 10px; }
/* The items a lesson is about, off the powerup atlas. Sits between the blurb and
   the Start button and stands down on a phone, where the row has no width to
   spare for a cross-reference. */
.learnitems { display: flex; gap: 4px; flex-shrink: 0; }
.learnitems .looticons { width: 22px; height: 22px; }
@media (max-width: 520px) { .learnitems { display: none; } }

/* Announced, never drawn: the band's live region carries the WHOLE page, while
   the box draws the typewriter (a reader re-reading a growing string is
   unusable). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
           overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
/* Phone in portrait: the stage takes the full width across the top and the
   virtual controller owns the section below it. Percent of the fixed #game
   box, not vh — mobile browsers measure vh against the toolbar-hidden
   viewport, which would push the stage taller than the half you can actually
   see. The camera fits the arena to whatever box it is handed, so this
   scales, never crops. Tablets keep the stage full-bleed and float the pad. */
body.ingame #game.mobile #game3d { width: 100%; height: var(--stage-pct); flex: none; }
body.ingame #game.mobile.tablet #game3d { height: 100%; }
/* Dead: no pad to make room for, so the stage takes the whole screen and the
   spectate table floats over it. */
body.ingame #game.mobile.dead #game3d { height: 100%; }
body.ingame #game.mobile .keys { display: none; } /* no keyboard to hint at */

/* Options chip: straddles the seam between stage and pad. Lives outside #game
   (see #gameopts) so z-index 13 actually sits above the spectate picker (12)
   instead of being trapped under it. Fixed, not absolute: there is no #game
   box to position against anymore, and --stage-pct already lives on :root. */
#stagegear { position: fixed; left: 50%; top: var(--stage-pct);
             transform: translate(-50%, -50%); z-index: 13;
             display: flex; align-items: center; gap: 6px;
             padding: 5px 14px; border-radius: 999px;
             background: hsl(200 25% 8% / 0.92); border: 1px solid var(--border);
             color: var(--muted-fg); font-size: 12px; letter-spacing: .04em;
             cursor: pointer; }
/* Tablet: bottom centre belongs to the floating pad, so the chip steps aside. */
#gameopts.mobile.tablet #stagegear { top: auto; left: auto; right: 14px; bottom: 16px;
                                 transform: none; }
/* Three more cases with no seam to straddle, all taking the corner the tablet
   takes: desktop (the key hint owns the bottom centre), and a dead or watching
   phone (full-bleed stage, and the spectate table is centred over it). The
   desktop one is also the whole desktop settings surface for now — §6.6
   promises one and the game page never grew it, and a display toggle is not the
   reason to build a panel. */
#gameopts:not(.mobile) #stagegear,
#gameopts.mobile.dead #stagegear,
#gameopts.mobile.watching #stagegear { top: auto; left: auto; right: 14px; bottom: 14px;
                                   transform: none; }

/* Bottom sheet: backdrop dismisses, panel slides up from the bottom edge. */
#sheet { position: fixed; inset: 0; z-index: 30; display: flex; flex-direction: column;
         justify-content: flex-end; background: hsl(200 20% 3% / 0.6); }
#sheet .panel { background: var(--card); border-top: 1px solid var(--border);
                border-radius: 18px 18px 0 0; padding: 10px 18px 24px;
                animation: sheetup .18s ease-out;
                /* Keyboard diagram (§6.18.4) plus eight option rows would
                   push Done off the viewport; the sheet had no overflow. */
                max-height: min(88dvh, 720px); overflow-y: auto; }
@keyframes sheetup { from { transform: translateY(100%); } to { transform: none; } }
#sheet .grab { width: 42px; height: 4px; border-radius: 999px;
               background: var(--border); margin: 0 auto 12px; }
#sheet h3 { margin: 0 0 6px; font-size: 15px; letter-spacing: .06em; color: var(--fg); }
#sheet .row, #settings .optlist .row { display: flex; align-items: center; justify-content: space-between;
              gap: 12px; padding: 14px 0; border-top: 1px solid var(--border);
              font-size: 14px; }
#sheet .row.col, #settings .optlist .row.col { flex-direction: column; align-items: stretch; }
#sheet .row .muted, #settings .optlist .row .muted { color: var(--muted-fg); font-size: 12px; text-align: right; }
#sheet .seg, #settings .optlist .seg { display: flex; gap: 8px; }
#sheet .seg.wrap, #settings .optlist .seg.wrap { flex-wrap: wrap; }
#sheet .seg button, #settings .optlist .seg button { flex: 1 1 auto; min-width: 72px; padding: 10px 14px;
                     background: var(--secondary); color: var(--secondary-fg);
                     border: 1px solid var(--border); border-radius: 10px;
                     font-size: 13px; cursor: pointer; }
#sheet .seg button.sel, #settings .optlist .seg button.sel { background: var(--primary); color: var(--on-primary);
                         border-color: var(--primary); font-weight: 700; }
/* A bottom sheet full-width across a desktop monitor reads as a page, not a
   panel, so the same markup gets a column there. */
#gameopts:not(.mobile) #sheet .panel { width: min(440px, 100%); margin: 0 auto;
                                   border-radius: 18px 18px 0 0; }
#sheet .hint, #settings .optlist .hint { color: var(--muted-fg); font-size: 12px; margin-top: 8px; }
#sheet .close { width: 100%; margin-top: 18px; padding: 13px;
                background: var(--secondary); color: var(--fg);
                border: 1px solid var(--border); border-radius: 12px;
                font-size: 14px; cursor: pointer;
                position: sticky; bottom: 0; z-index: 1;
                box-shadow: 0 -14px 18px var(--card); }

#sheet .ctrlhead, #settings .optlist .ctrlhead {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex: 1; min-width: 0;
}

/* Control-layout diagram. Caps, not a photo: the highlight is the binding
   table, and the asset allowlist cannot serve the reference shots. DIR / A / B
   colours are the same three the pad and touch faces will reuse. */
.ctrlmap {
  --slot-dir: hsl(174 80% 42%);
  --slot-a: hsl(142 60% 38%);
  --slot-b: hsl(32 85% 48%);
  --key-bg: var(--secondary);
  --key-edge: var(--border);
  --key-ink: var(--muted-fg);
  container-type: inline-size;
  display: flex; flex-direction: column; gap: 3px;
  margin-top: 4px; user-select: none; cursor: pointer;
  outline: none;
}
.ctrlmap:focus {
  outline: 1px solid var(--primary);
  outline-offset: 3px;
}
.ctrlmap .kbrow { display: flex; gap: 3px; }
.ctrlmap .unit, .ctrlmap .stack { flex: var(--u, 1) 1 0; min-width: 0; display: flex; height: 22px; }
.ctrlmap .stack { flex-direction: column; gap: 2px; }
.ctrlmap .cap {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0 1px;
  background: var(--key-bg); color: var(--key-ink);
  border: 1px solid var(--key-edge); border-radius: 4px;
  font-size: clamp(7px, 2.2cqi, 11px); font-weight: 600;
  letter-spacing: .02em; line-height: 1; white-space: nowrap;
  overflow: hidden;
}
.ctrlmap.full .fn .unit { height: 14px; }
.ctrlmap.full .fn .cap { font-size: clamp(6px, 1.8cqi, 9px); font-weight: 500; }
.ctrlmap.mini .unit, .ctrlmap.mini .stack { height: 36px; }
.ctrlmap.mini .cap { font-size: 13px; }
.ctrlmap.mini .stack .cap { font-size: 11px; }
.ctrlmap .cap.slot-dir { color: var(--slot-dir); border-color: color-mix(in srgb, var(--slot-dir) 45%, var(--border)); }
.ctrlmap .cap.slot-a { color: var(--slot-a); border-color: color-mix(in srgb, var(--slot-a) 45%, var(--border)); }
.ctrlmap .cap.slot-b { color: var(--slot-b); border-color: color-mix(in srgb, var(--slot-b) 45%, var(--border)); }
.ctrlmap .cap.lit {
  color: var(--fg); background: var(--muted);
  box-shadow: 0 0 0 1px var(--fg);
}
.ctrlmap .cap.slot-dir.lit {
  color: var(--fg);
  background: color-mix(in srgb, var(--slot-dir) 40%, var(--secondary));
  box-shadow: 0 0 10px color-mix(in srgb, var(--slot-dir) 55%, transparent);
}
.ctrlmap .cap.slot-a.lit {
  color: var(--fg);
  background: color-mix(in srgb, var(--slot-a) 40%, var(--secondary));
  box-shadow: 0 0 10px color-mix(in srgb, var(--slot-a) 55%, transparent);
}
.ctrlmap .cap.slot-b.lit {
  color: var(--fg);
  background: color-mix(in srgb, var(--slot-b) 40%, var(--secondary));
  box-shadow: 0 0 10px color-mix(in srgb, var(--slot-b) 55%, transparent);
}
.ctrlmap .cap.faint { opacity: 0.45; }
.ctrlmap .cap.faint.lit { opacity: 1; }
.ctrlmap .legend {
  display: flex; gap: 14px; margin-top: 8px;
  font-size: 11px; color: var(--muted-fg);
}
.ctrlmap .legend .leg { display: flex; align-items: center; gap: 6px; }
.ctrlmap .legend .leg::before {
  content: ""; width: 8px; height: 8px; border-radius: 2px;
  background: var(--key-ink);
}
.ctrlmap .legend .leg.slot-dir::before { background: var(--slot-dir); }
.ctrlmap .legend .leg.slot-a::before { background: var(--slot-a); }
.ctrlmap .legend .leg.slot-b::before { background: var(--slot-b); }
.ctrlmaps .legend { display: flex; gap: 14px; margin-top: 8px;
  font-size: 11px; color: var(--muted-fg); }
.ctrlmaps .legend .leg { display: flex; align-items: center; gap: 6px; }
.ctrlmaps .legend .leg::before {
  content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--muted-fg);
}
.ctrlmaps .legend .leg.slot-dir::before { background: var(--slot-dir, hsl(174 80% 42%)); }
.ctrlmaps .legend .leg.slot-a::before { background: var(--slot-a, hsl(142 60% 38%)); }
.ctrlmaps .legend .leg.slot-b::before { background: var(--slot-b, hsl(32 85% 48%)); }

.ctrlmaps {
  --slot-dir: hsl(174 80% 42%);
  --slot-a: hsl(142 60% 38%);
  --slot-b: hsl(32 85% 48%);
  display: flex; flex-direction: column; gap: 16px;
  margin-top: 4px; outline: none; cursor: pointer;
}
.ctrlmaps:focus { outline: 1px solid var(--primary); outline-offset: 3px; }
.ctrlmap .face-name {
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted-fg); margin-bottom: 6px;
}
.ctrlmap .chassis-seg { display: flex; gap: 6px; margin-bottom: 8px; }
.ctrlmap .chassis-seg button {
  flex: 1; min-width: 0; padding: 6px 8px; font-size: 11px; cursor: pointer;
  background: var(--secondary); color: var(--secondary-fg);
  border: 1px solid var(--border); border-radius: 8px;
}
.ctrlmap .chassis-seg button.sel {
  background: var(--primary); color: var(--on-primary); border-color: var(--primary); font-weight: 700;
}
.ctrlmap .board { display: flex; gap: 6px; align-items: flex-end; }
.ctrlmap .block { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
.ctrlmap .block.nav { flex: 0 0 22%; }
.ctrlmap .block.num { flex: 0 0 24%; }
.ctrlmap .unit.spacer { visibility: hidden; }
.ctrlmap .touch-body, .ctrlmap .pad-body {
  display: flex; align-items: center; justify-content: space-around; gap: 20px;
  padding: 8px 4px 12px;
}
.ctrlmap .touch-dpad, .ctrlmap .pad-diamond {
  display: grid; grid-template-columns: repeat(3, 28px); grid-template-rows: repeat(3, 28px); gap: 4px;
}
.ctrlmap .touch-dpad .cap, .ctrlmap .pad-diamond .cap, .ctrlmap .pad-left .stick {
  width: 28px; height: 28px; flex: none;
}
.ctrlmap .touch-acts { display: flex; gap: 14px; align-items: center; }
.ctrlmap .touch-acts .cap {
  width: 40px; height: 40px; flex: none; border-radius: 50%; font-size: 14px;
}
.ctrlmap .touch-acts .slot-a { transform: translateY(10px); }
.ctrlmap .touch-acts .slot-b { transform: translateY(-10px); }
.ctrlmap .pad-left { display: flex; gap: 12px; align-items: center; }
.ctrlmap .pad-left .stick { border-radius: 50%; font-size: 9px; }
.ctrlmap .pad-dpad { grid-template-columns: repeat(3, 22px); grid-template-rows: repeat(3, 22px); }
.ctrlmap .pad-dpad .cap { width: 22px; height: 22px; font-size: 8px; }
.ctrlmap .pad-diamond .cap { border-radius: 50%; font-size: 11px; }

/* ================= virtual pad =================
   Two independent axes. LAYOUT (`pad-*`) owns geometry: which controls exist
   and how big they are. SKIN (`skin-*`) owns finish, and is ONLY a block of
   custom properties — no skin may set a size, a position or a descendant
   colour, or 4 layouts x 6 skins stops being a matrix and becomes 24 rules.
   Defaults below are the Default skin, so an unskinned pad still renders. */
#pad {
  --pad-shell-a: hsl(200 20% 8% / 0.42);
  --pad-shell-b: hsl(200 20% 3% / 0.42);
  --pad-shell-edge: hsl(200 25% 40% / 0.25);
  --pad-bubble: transparent;
  --pad-well: transparent;
  --pad-key: hsl(200 15% 45% / 0.55);
  --pad-key-edge: hsl(200 20% 80% / 0.28);
  --pad-key-ink: #fff;
  --pad-a: linear-gradient(135deg, #5be07a, #0a0);
  --pad-b: linear-gradient(135deg, #ff5b5b, #c00);
  --pad-btn-ink: #fff;
  --pad-glow: transparent;
  --pad-press: hsl(174 100% 41% / 0.55);
  --pad-key-radius: 10px;

  position: fixed; left: 0; right: 0; bottom: 0; top: var(--stage-pct); z-index: 10;
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; padding: 0 22px; touch-action: none;
  background:
    radial-gradient(120% 90% at 18% 8%, hsl(0 0% 100% / 0.10), transparent 55%),
    radial-gradient(90% 70% at 84% 96%, hsl(0 0% 0% / 0.16), transparent 60%),
    linear-gradient(168deg, var(--pad-shell-a), var(--pad-shell-b));
  box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.16),
              inset 0 -26px 44px hsl(0 0% 0% / 0.20);
}
/* Tablet: no reserved section, so the pad floats over the stage instead. */
#pad.overlay { top: auto; left: 50%; right: auto; bottom: 18px;
               transform: translateX(-50%); gap: 44px; padding: 12px 20px;
               border-radius: 22px; border: 1px solid var(--pad-shell-edge); }

/* ---- shell mouldings: decorative, never touchable ---- */
#pad .shell { position: absolute; inset: 0; pointer-events: none;
              overflow: hidden; border-radius: inherit; }
/* Default is the pre-faceplate look, so it takes no mouldings at all. */
#pad.skin-default .shell { display: none; }
/* Speaker grille: a dot field clipped to a disc, in the lower-right dead space. */
#pad .shell .grille {
  position: absolute; right: 5%; bottom: 8%; width: 92px; height: 92px;
  border-radius: 50%; opacity: .5;
  background-image: radial-gradient(circle, hsl(0 0% 0% / 0.42) 1.4px, transparent 1.6px);
  background-size: 11px 11px;
  box-shadow: inset 0 1px 2px hsl(0 0% 100% / 0.18);
}
/* Blank embossed plate. Deliberately carries NO text or mark of any kind. */
#pad .shell .plate {
  position: absolute; left: 50%; top: 12%; transform: translateX(-50%);
  width: 118px; height: 16px; border-radius: 4px;
  box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.28),
              inset 0 -1px 0 hsl(0 0% 0% / 0.22);
  opacity: .55;
}
/* Bubbles suspended in translucent shells; `transparent` on opaque skins. */
#pad .shell::after {
  content: ""; position: absolute; inset: 0; opacity: .8;
  background-image:
    radial-gradient(circle at 12% 62%, var(--pad-bubble) 0 7px, transparent 8px),
    radial-gradient(circle at 31% 24%, var(--pad-bubble) 0 4px, transparent 5px),
    radial-gradient(circle at 58% 78%, var(--pad-bubble) 0 9px, transparent 10px),
    radial-gradient(circle at 73% 33%, var(--pad-bubble) 0 5px, transparent 6px),
    radial-gradient(circle at 88% 60%, var(--pad-bubble) 0 6px, transparent 7px),
    radial-gradient(circle at 45% 12%, var(--pad-bubble) 0 3px, transparent 4px);
}

#pad button { color: var(--pad-key-ink); font-size: 20px; cursor: pointer;
              user-select: none; position: relative; z-index: 1;
              background: var(--pad-key);
              border: 1px solid var(--pad-key-edge);
              border-radius: var(--pad-key-radius); }
/* Press feedback is a filter, not a transform: the A/B diagonal offsets and
   the joystick knob both already own `transform`. */
#pad button:active { background: var(--pad-press); filter: brightness(1.15); }

/* ---- D-pad: four hit targets that read as one moulded cross ---- */
#pad .dpad { position: relative; display: grid;
             grid-template-columns: repeat(3, 46px);
             grid-template-rows: repeat(3, 46px); gap: 5px; }
#pad .d-up { grid-area: 1 / 2; } #pad .d-left { grid-area: 2 / 1; }
#pad .d-right { grid-area: 2 / 3; } #pad .d-down { grid-area: 3 / 2; }
#pad .acts { display: flex; gap: 18px; align-items: center;
             position: relative; z-index: 1; }
#pad .acts button { width: 64px; height: 64px; border-radius: 50%; font-size: 22px;
                    font-weight: 700; color: var(--pad-btn-ink);
                    border-color: transparent; }
#pad .acts .b { background: var(--pad-b); }
#pad .acts .a { background: var(--pad-a); }

/* Joystick: a base the thumb drags inside; the knob tracks the touch. */
#pad .joy { position: relative; z-index: 1; width: 132px; height: 132px;
            border-radius: 50%;
            background: var(--pad-well, hsl(200 15% 30% / 0.4));
            border: 1px solid var(--pad-key-edge);
            box-shadow: inset 0 3px 10px hsl(0 0% 0% / 0.35);
            display: flex; align-items: center; justify-content: center; }
#pad .joy .knob { width: 58px; height: 58px; border-radius: 50%;
                  background: var(--pad-key);
                  border: 1px solid var(--pad-key-edge);
                  box-shadow: inset 0 2px 3px hsl(0 0% 100% / 0.30),
                              inset 0 -4px 7px hsl(0 0% 0% / 0.35),
                              0 3px 7px hsl(0 0% 0% / 0.40);
                  transition: transform .06s linear; }

/* Diamond: the arrows split into spaced round keys, angled like a rocker. */
#pad.pad-diamond .dpad { grid-template-columns: repeat(3, 50px);
                         grid-template-rows: repeat(3, 50px); gap: 8px; }
#pad.pad-diamond .dpad button { border-radius: 50%; }

/* Compact: smaller and dimmer, for players who want the stage back — so it
   takes a skin's colours but never its shell. */
#pad.pad-compact { background: none; box-shadow: none; }
#pad.pad-compact .shell { display: none; }
#pad.pad-compact .dpad { grid-template-columns: repeat(3, 38px);
                         grid-template-rows: repeat(3, 38px); gap: 4px; }
#pad.pad-compact button { font-size: 15px; }
#pad.pad-compact .acts { gap: 12px; }
#pad.pad-compact .acts button { width: 50px; height: 50px; font-size: 17px; }
#pad.pad-compact .dpad button::before { content: ""; position: absolute; inset: -4px; }
/* Compact takes a skin's colours but none of its furniture — a recessed
   well with no faceplate behind it reads as a blob floating on nothing. */
#pad.pad-compact .dpad::before { display: none; }

/* ================= skinned faceplate =================
   Everything below applies only when a skin other than Default is chosen:
   the moulded one-piece cross, the recessed well, and the A/B diagonal. */
#pad:not(.skin-default):not(.pad-compact) .dpad { gap: 0; }
/* The recessed circular well the cross sits in. */
#pad:not(.skin-default) .dpad::before {
  content: ""; position: absolute; inset: -15px; border-radius: 50%;
  background: var(--pad-well);
  box-shadow: inset 0 3px 8px hsl(0 0% 0% / 0.28),
              0 1px 0 hsl(0 0% 100% / 0.22);
  z-index: 0; pointer-events: none;
}
/* The hub filling the centre cell, so four arms read as one piece of plastic. */
#pad:not(.skin-default):not(.pad-compact):not(.pad-diamond) .dpad::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 46px; height: 46px; transform: translate(-50%, -50%);
  background: radial-gradient(circle, hsl(0 0% 0% / 0.20) 0 6px, transparent 7px),
              var(--pad-key);
  z-index: 1; pointer-events: none;
}
/* Arms: only the OUTER corners round off, so the joins stay square. */
#pad:not(.skin-default):not(.pad-compact):not(.pad-diamond) .dpad button {
  z-index: 2; border: none;
  box-shadow: inset 0 2px 2px hsl(0 0% 100% / 0.22),
              inset 0 -3px 5px hsl(0 0% 0% / 0.28);
}
#pad:not(.skin-default):not(.pad-compact):not(.pad-diamond) .d-up
  { border-radius: 9px 9px 0 0; }
#pad:not(.skin-default):not(.pad-compact):not(.pad-diamond) .d-down
  { border-radius: 0 0 9px 9px; }
#pad:not(.skin-default):not(.pad-compact):not(.pad-diamond) .d-left
  { border-radius: 9px 0 0 9px; }
#pad:not(.skin-default):not(.pad-compact):not(.pad-diamond) .d-right
  { border-radius: 0 9px 9px 0; }
/* A lower-left (bomb), B upper-right (action), each seated in its own recess.
   The offsets swapped with the DOM order so the thumb-resting circle still
   places bombs and is now called A (§6.18.1). */
#pad:not(.skin-default) .acts .a { transform: translateY(15px); }
#pad:not(.skin-default) .acts .b { transform: translateY(-15px); }
#pad:not(.skin-default) .acts button {
  box-shadow: inset 0 3px 4px hsl(0 0% 100% / 0.30),
              inset 0 -5px 8px hsl(0 0% 0% / 0.35),
              0 3px 6px hsl(0 0% 0% / 0.35),
              0 0 0 7px var(--pad-well),
              0 0 18px var(--pad-glow);
}
#pad:not(.skin-default) button:active { filter: brightness(1.22) saturate(1.1); }
/* The floating tablet panel has no dead space for mouldings. */
#pad.overlay .shell .grille, #pad.overlay .shell .plate { display: none; }

/* ---- skins: custom properties only ---- */
#pad.skin-crystal {
  --pad-shell-a: hsl(196 55% 88% / 0.62); --pad-shell-b: hsl(197 42% 72% / 0.62);
  --pad-shell-edge: hsl(0 0% 100% / 0.55); --pad-bubble: hsl(0 0% 100% / 0.28);
  --pad-well: hsl(195 40% 74% / 0.45);
  --pad-key: linear-gradient(160deg, #7d848a, #565c62);
  --pad-key-edge: hsl(0 0% 100% / 0.30); --pad-key-ink: #eef4f7;
  --pad-a: linear-gradient(160deg, #7e858b, #5d6369);
  --pad-b: linear-gradient(160deg, #7e858b, #5d6369);
  --pad-btn-ink: #e6edf1; --pad-press: #98a0a6;
}
#pad.skin-glow {
  --pad-shell-a: hsl(260 20% 95% / 0.80); --pad-shell-b: hsl(258 22% 84% / 0.80);
  --pad-shell-edge: hsl(0 0% 100% / 0.70); --pad-bubble: hsl(260 60% 92% / 0.40);
  --pad-well: hsl(258 55% 84% / 0.50);
  --pad-key: linear-gradient(160deg, #7059ff, #4a34cf);
  --pad-key-edge: hsl(255 90% 80% / 0.55); --pad-key-ink: #ffffff;
  --pad-a: linear-gradient(160deg, #8360ff, #5a3ce0);
  --pad-b: linear-gradient(160deg, #5e93ff, #3a63d8);
  --pad-btn-ink: #ffffff; --pad-glow: hsl(258 100% 70% / 0.75);
  --pad-press: #9b83ff;
}
#pad.skin-neon {
  --pad-shell-a: hsl(24 100% 58% / 0.66); --pad-shell-b: hsl(12 82% 48% / 0.66);
  --pad-shell-edge: hsl(30 100% 82% / 0.50); --pad-bubble: hsl(30 100% 88% / 0.26);
  --pad-well: hsl(22 90% 76% / 0.40);
  --pad-key: linear-gradient(160deg, #3d51e8, #1f2fae);
  --pad-key-edge: hsl(228 90% 80% / 0.45); --pad-key-ink: #e3e9ff;
  --pad-a: linear-gradient(160deg, #f5537f, #cf1f56);
  --pad-b: linear-gradient(160deg, #24c9ba, #0f9a8e);
  --pad-btn-ink: #ffffff; --pad-press: #ffd166;
}
#pad.skin-light {
  --pad-shell-a: hsl(205 30% 96%); --pad-shell-b: hsl(203 26% 87%);
  --pad-shell-edge: hsl(0 0% 100% / 0.85); --pad-bubble: hsl(0 0% 100% / 0.55);
  --pad-well: hsl(205 25% 92% / 0.85);
  --pad-key: linear-gradient(160deg, #c8d1d9, #a9b4bf);
  --pad-key-edge: hsl(0 0% 100% / 0.70);
  /* Dark ink on purpose — pale-on-pale is the one skin that can wash out. */
  --pad-key-ink: #3f4a54;
  --pad-a: linear-gradient(160deg, #ffdde8, #f6b9cd);
  --pad-b: linear-gradient(160deg, #d8f0e2, #b2ddc6);
  --pad-btn-ink: #35424c; --pad-press: #8fa3b3;
}
#pad.skin-solid {
  --pad-shell-a: #dedacf; --pad-shell-b: #bfbbb1;
  --pad-shell-edge: hsl(40 8% 55% / 0.60); --pad-bubble: transparent;
  --pad-well: hsl(40 8% 62% / 0.45);
  --pad-key: linear-gradient(160deg, #55585c, #3a3c3f);
  --pad-key-edge: hsl(0 0% 100% / 0.14); --pad-key-ink: #d6d6d6;
  --pad-a: linear-gradient(160deg, #b03a63, #8a2247);
  --pad-b: linear-gradient(160deg, #b03a63, #8a2247);
  --pad-btn-ink: #f5e2e9; --pad-press: #c9557e;
}
body.ingame .keys { position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
                    z-index: 2; background: hsl(200 25% 8% / 0.7);
                    border: 1px solid var(--border); padding: 4px 12px;
                    border-radius: 999px; }

/* In-game header: round info left, countdown center, players/watchers right.
   A GRID, not three absolutely-positioned boxes. Absolute siblings cannot see
   each other, so a long room line on the left and a ten-player roster on the
   right used to slide straight under the countdown on any narrow screen. The
   1fr/auto/1fr track list keeps the clock exactly centred (both flex tracks
   resolve equal) while giving the side columns a real width to be clipped
   against — overlap stops being possible rather than being tuned away. */
#hud { position: fixed; top: 0; left: 0; right: 0; z-index: 5; pointer-events: none;
       padding: 8px 14px;
       display: grid; grid-template-columns: 1fr auto 1fr; align-items: start;
       column-gap: 12px; }
/* The scrim is a fixed strip on a pseudo-element, not #hud's own background:
   the grid is as tall as its tallest column, and a ten-player roster would
   otherwise drag a dark gradient a third of the way down the arena. */
#hud::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 68px;
               background: linear-gradient(hsl(200 20% 4% / 0.85), transparent);
               pointer-events: none; }
#hudleft { grid-column: 1; justify-self: start; min-width: 0; max-width: 100%;
           color: var(--muted-fg); font-size: 13px; letter-spacing: .08em;
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#hudleft .trighint { color: var(--warning); font-weight: 700; animation: trigpulse 1s 6; }
@keyframes trigpulse { 50% { opacity: .35; } }
#hudtime { grid-column: 2; justify-self: center;
           font-family: "JetBrains Mono", monospace; font-variant-numeric: tabular-nums;
           font-size: 25px; line-height: 1; font-weight: 700; color: var(--primary);
           text-shadow: 0 0 12px hsl(174 100% 41% / 0.5), 0 2px 10px rgba(0,0,0,.7); }
#hudtime.hurry { color: var(--danger); animation: hurrypulse .5s infinite;
                 text-shadow: 0 0 12px hsl(0 84% 60% / 0.6); }
@keyframes hurrypulse { 50% { opacity: .45; } }
/* Ten seats is the cap, so the roster is bounded — but bound it against the
   VIEWPORT too: on a short screen ten rows would otherwise run down over the
   arena the HUD is supposed to sit above. */
#hudright { grid-column: 3; justify-self: end; min-width: 0; max-width: 100%;
            text-align: right; font-size: 13px; line-height: 1.5;
            max-height: 38vh; overflow: hidden; }
#hudright > div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#hudright .watch { color: var(--muted-fg); font-size: 12px; }
#hudright .dead { color: hsl(200 10% 40%); text-decoration: line-through; }
.pdot { display: inline-block; width: 10px; height: 10px; border-radius: 50%;
        margin-right: 6px; vertical-align: baseline; }

/* End-of-round banner: scales up into the center of the screen. */
/* Height rather than inset: on a split phone screen the banner has to centre
   over the arena, not the whole screen, or it lands on the D-pad. --stage-pct
   is 100% in every other layout, so this is a no-op there. */
#banner { position: fixed; top: 0; left: 0; right: 0; height: var(--stage-pct);
          display: flex; align-items: center; justify-content: center;
          z-index: 20; pointer-events: none; }
#banner span { font-family: Orbitron, "Space Grotesk", sans-serif;
               font-size: min(15vw, 150px); font-weight: 900; letter-spacing: .06em;
               white-space: nowrap; text-align: center;
               color: var(--warning);
               text-shadow: 0 0 24px hsl(38 92% 50% / 0.55), 0 8px 40px rgba(0,0,0,.85);
               transform: scale(.1); opacity: 0; }
#banner.show span { animation: bannerpop .65s cubic-bezier(.2, 1.7, .4, 1) forwards; }
@keyframes bannerpop {
  from { transform: scale(.1); opacity: 0; }
  to   { transform: scale(1);  opacity: 1; }
}

/* The spectacle beats (plan §6.20): the KO card and FINAL TWO.
   Sized to the stage box the same way #banner is, and for the same reason —
   on a split phone screen it has to centre over the arena, not over the pad. */
#spectacle { position: fixed; top: 0; left: 0; right: 0; height: var(--stage-pct);
             width: 100%; z-index: 21; pointer-events: none;
             /* THE GLOBAL `canvas` RULE PAINTS `--bg`, AND THIS IS AN OVERLAY.
                Without the override the element is an opaque sheet the moment it
                stops being `hidden`, and at z-index 21 that buries the board, the
                HUD and the spectate panel while leaving the seat rail (30) on top
                — which reads as "the KO banner blacked the screen out" and points
                at the card rather than at the cascade. #matchwipe and #demowipe
                carry the same override for the same reason.
                Radius and pixelation come from that rule too, and neither belongs
                here: this is a full-bleed layer, and the glyph atlas is smooth
                antialiased art being scaled — `pixelated` would crunch every
                edge the baker spent a distance field getting clean. */
             background: transparent; border-radius: 0; image-rendering: auto; }

/* The cold border. A backdrop-filter rather than paint, because desaturating
   the arena means reading the pixels underneath and a 2D canvas cannot see
   another canvas's backdrop. The mask is what makes it a BORDER: fully clear
   through the middle, so the fight itself keeps its colour and only the frame
   around it goes cold.
   The background tint is not decoration — it is the fallback. A browser without
   backdrop-filter still gets a darkened edge, which is the half of the beat
   that carries at thumbnail size. */
#finaltwoedge { position: fixed; top: 0; left: 0; right: 0; height: var(--stage-pct);
                z-index: 3; pointer-events: none; opacity: var(--ft, 0);
                background: radial-gradient(ellipse 74% 74% at 50% 50%,
                            transparent 46%, rgb(6 9 14 / .55) 100%);
                /* saturate() is 1 - DESATURATE (game/finalTwo.ts). A stylesheet
                   cannot import a constant, so the two are kept in step by the
                   test that pins DESATURATE rather than by the build. */
                backdrop-filter: saturate(.45) brightness(.82) contrast(1.06);
                -webkit-backdrop-filter: saturate(.45) brightness(.82) contrast(1.06);
                -webkit-mask-image: radial-gradient(ellipse 74% 74% at 50% 50%,
                                    transparent 46%, #000 100%);
                mask-image: radial-gradient(ellipse 74% 74% at 50% 50%,
                            transparent 46%, #000 100%); }

/* Reduced motion keeps the CARDS — they say who won, and that is information,
   not movement — and drops the framing change, which is the part that moves. */
@media (prefers-reduced-motion: reduce) {
  #finaltwoedge { backdrop-filter: saturate(.6); -webkit-backdrop-filter: saturate(.6); }
}

/* Room-start loading transition: fullscreen while the round spins up.
   Same tiled bg1 + boot-bg-drift as the boot splash (keyframes read
   --boot-tile). A random loadimage sits bottom-right. */
#loading {
  position: fixed; inset: 0; z-index: 15; background: var(--bg);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 20px;
  --boot-tile: 40px;
  overflow: hidden;
}
#loading .load-bg {
  position: absolute; inset: 0;
  background-image: url(/assets/bg1.jpg);
  background-repeat: repeat;
  background-size: var(--boot-tile) var(--boot-tile);
  animation: boot-bg-drift 24s linear infinite;
  opacity: 0.9;
}
#loading .txt {
  position: absolute; left: 50%; bottom: max(28px, calc(12px + var(--safe-bottom, 0px)));
  transform: translateX(-50%);
  z-index: 2;
  font-family: Orbitron, "Space Grotesk", sans-serif; font-size: 20px;
  letter-spacing: .35em; color: var(--primary); font-weight: 700;
  text-shadow: 0 0 16px hsl(174 100% 41% / 0.5);
  white-space: nowrap;
}
#loading .dots span { display: inline-block; animation: dotbounce 1s infinite; }
#loading .dots span:nth-child(2) { animation-delay: .15s; }
#loading .dots span:nth-child(3) { animation-delay: .3s; }
@keyframes dotbounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-8px); } }
#loading .load-art {
  position: absolute; right: 0; bottom: 0; z-index: 1;
  width: min(42vw, 380px); height: auto;
  pointer-events: none;
  user-select: none;
}

/* Spectator modal (dead / watch-only): no close button by design — it lives
   until the next round. Tabs split in-field survivors from rail riders. */
#spectate { position: fixed; left: 50%; bottom: 48px; transform: translateX(-50%);
            z-index: 12; background: hsl(200 25% 8% / 0.94);
            border: 1px solid hsl(174 100% 41% / 0.3); border-radius: var(--radius);
            padding: 10px 18px 14px; min-width: 340px;
            box-shadow: 0 0 30px hsl(174 100% 41% / 0.12); }
#spectate .spectabs { display: flex; gap: 0; margin: 0 0 10px;
                      border-bottom: 1px solid hsl(174 100% 41% / 0.2); }
#spectate .spectabs button {
  flex: 1 1 0; margin: 0; padding: 6px 8px 8px; border: none; border-radius: 0;
  background: transparent; color: var(--muted-fg); box-shadow: none;
  font-family: Orbitron, "Space Grotesk", sans-serif;
  font-size: 11px; font-weight: 500; letter-spacing: .14em; cursor: pointer;
}
#spectate .spectabs button:hover {
  border: none; color: var(--fg); background: transparent;
  box-shadow: none;
}
#spectate .spectabs button.on {
  color: var(--primary);
  box-shadow: inset 0 -2px 0 var(--primary);
  text-shadow: 0 0 10px hsl(174 100% 41% / 0.4);
}
#spectate .spectabs button.on:hover { color: var(--primary); }
#spectate table { border-collapse: collapse; width: 100%; font-size: 13px; }
#spectate th { color: var(--muted-fg); font-weight: 500; text-align: left;
               padding: 2px 10px 6px 6px; }
#spectate td { padding: 4px 10px 4px 6px; font-family: "JetBrains Mono", monospace;
               font-size: 12px; }
#spectate td:first-child { font-family: "Space Grotesk", sans-serif; font-size: 13px; }
#spectate tbody tr { cursor: pointer; }
#spectate tbody tr:hover { background: hsl(174 100% 41% / 0.07); }
#spectate tbody tr.sel { background: hsl(174 100% 41% / 0.14);
                         outline: 1px solid var(--primary); }
/* Side bomber (plan §3.12): NO PANEL. The rider's readout used to be a fixed
   strip at the bottom of the stage, which is the one band a phone cannot spare —
   the touch pad's buttons and the ⚙ options chip are already there, and all
   three overlapped. Removed rather than nudged upward, because moving it only
   picks a different thing for it to cover. See app.tsx for where the cooldown
   should live if it returns. */

/* ---- the final table (plan §6.19) ---- */
/* Centred over the WHOLE viewport, not over --stage-pct like the banner: the
   match is over, so there is no longer a stage half to stay out of, and on a
   split phone the pad's half is gone with it. The backdrop is part of the
   component rather than a separate scrim — one element to fade means the panel
   and the dimming can never come apart mid-transition. */
#results { position: fixed; inset: 0; z-index: 24; display: grid;
           place-items: center; padding: 16px;
           background: hsl(200 20% 4% / 0.72);
           -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
           opacity: 0; animation: resultsin .45s ease forwards; }
/* The exit. `.closing` stops the countdown from re-firing dismiss; the iris
   (`#matchwipe`) covers the still-opaque table. Pin opacity: cancelling
   `resultsin` (which is `forwards` to 1) would otherwise drop back to the
   `#results { opacity: 0 }` rest state and the tiles would eat an empty frame.
   Do not tween — that reads as a DS fade, not a Game Boy tile paint. */
#results.closing { animation: none; opacity: 1; }
@keyframes resultsin { from { opacity: 0; } to { opacity: 1; } }

#results .rpanel { width: min(680px, 100%); max-height: min(80vh, 620px);
                   display: flex; flex-direction: column;
                   background: hsl(200 25% 8% / 0.97);
                   border: 1px solid hsl(174 100% 41% / 0.35);
                   border-radius: var(--radius); padding: 18px 20px 16px;
                   box-shadow: 0 0 60px hsl(174 100% 41% / 0.16); }
#results .rhead { text-align: center; margin-bottom: 14px; }
#results h2 { margin: 0; font-family: Orbitron, "Space Grotesk", sans-serif;
              font-size: 15px; letter-spacing: .34em; color: var(--primary);
              text-shadow: 0 0 14px hsl(174 100% 41% / 0.5); }
#results .rchamp { margin: 6px 0 0; font-size: 15px; color: var(--fg); }
/* The table scrolls, the header and the countdown do not: ten seats at 13px is
   taller than a phone, and the one thing that must never scroll out of reach is
   the button that says how you leave. */
#results .rscroll { overflow: auto; flex: 1 1 auto; margin: 0 -4px; }
#results table { border-collapse: collapse; width: 100%; font-size: 13px; }
#results th { position: sticky; top: 0; z-index: 1;
              background: hsl(200 25% 8%); color: var(--muted-fg);
              font-weight: 500; text-align: right; white-space: nowrap;
              padding: 2px 8px 6px; }
#results th:nth-child(2) { text-align: left; }
#results td { padding: 5px 8px; text-align: right; white-space: nowrap;
              font-family: "JetBrains Mono", monospace; font-size: 12px; }
#results .rrank { width: 2.2em; text-align: center; color: var(--muted-fg); }
#results .rname { text-align: left; font-family: "Space Grotesk", sans-serif;
                  font-size: 13px; }
#results .rkit { font-size: 11px; letter-spacing: .06em; }
#results .rbot { margin-left: 6px; padding: 1px 5px; border-radius: 999px;
                 font-family: "JetBrains Mono", monospace; font-size: 9px;
                 letter-spacing: .1em; color: var(--muted-fg);
                 background: hsl(200 20% 14%); }
#results tbody tr + tr { border-top: 1px solid hsl(200 20% 18% / 0.6); }
#results tbody tr.champ { background: hsl(174 100% 41% / 0.12); }
#results tbody tr.champ .rname { color: var(--primary); }
#results .racts { display: flex; align-items: center; justify-content: space-between;
                  gap: 12px; margin-top: 14px; padding-top: 12px;
                  border-top: 1px solid hsl(200 20% 18%); }
#results .rcount { font-family: "JetBrains Mono", monospace; font-size: 12px;
                   color: var(--muted-fg); }
#results .rcount b { color: var(--fg); }
#results .racts button { margin: 0; }

/* Phone (compact ≤720 px): the ten-column table cannot be seen in full, and
   86vh is the toolbar-hidden viewport — the same trap #game already left
   (`--app-height`, --safe-*). So this is a different surface, not a tighter
   table: the overlay fills the reported height, each seat is a card, and the
   leave button stays pinned under the scroll. Desktop is untouched. */
#results.phone {
  top: 0; left: 0; right: 0; bottom: auto;
  height: var(--app-height);
  place-items: stretch;
  padding: max(8px, var(--safe-top)) max(8px, var(--safe-right))
           max(8px, var(--safe-bottom)) max(8px, var(--safe-left));
}
#results.phone .rpanel {
  width: 100%; max-width: none; min-width: 0; min-height: 0;
  height: 100%; max-height: none; overflow: hidden;
  padding: 12px 12px 10px;
  border-radius: 12px;
}
#results.phone .rhead { flex: 0 0 auto; margin-bottom: 10px; }
#results.phone h2 { font-size: 13px; letter-spacing: .22em; }
#results.phone .rchamp { font-size: 14px; }
#results.phone .rscroll { flex: 1 1 auto; min-height: 0; min-width: 0;
                          margin: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
#results.phone .racts { flex: 0 0 auto; flex-direction: column; align-items: stretch;
                        gap: 8px; margin-top: 10px; padding-top: 10px; }
#results.phone .racts button { width: 100%; min-height: var(--touch); }
#results.phone .rcount { text-align: center; }

#results .rcards { list-style: none; margin: 0; padding: 0;
                   display: flex; flex-direction: column; gap: 8px; }
#results .rcard {
  display: grid; grid-template-columns: 2em 1fr; gap: 2px 8px;
  padding: 10px 10px 8px;
  background: hsl(200 20% 10% / 0.7);
  border: 1px solid hsl(200 20% 18%);
  border-radius: 10px;
}
#results .rcard.champ { background: hsl(174 100% 41% / 0.12);
                        border-color: hsl(174 100% 41% / 0.4); }
#results .rcard .rrank { grid-row: 1 / span 3; align-self: center;
                         font-size: 16px; width: auto; }
#results .rcard .rwho { display: flex; align-items: center; gap: 6px; min-width: 0; }
#results .rcard .rname { flex: 1 1 auto; min-width: 0; font-size: 14px;
                         overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#results .rcard.champ .rname { color: var(--primary); }
#results .rcard .rbot { margin-left: 0; flex: 0 0 auto; }
#results .rcard .rstats { grid-column: 2; display: grid;
                          grid-template-columns: repeat(4, minmax(0, 1fr));
                          gap: 4px; margin: 4px 0 2px; }
#results .rcard .rstat { text-align: center; }
#results .rcard .rstat b { display: block; font-family: "JetBrains Mono", monospace;
                           font-size: 16px; font-weight: 600; line-height: 1.15;
                           color: var(--fg); }
#results .rcard .rstat span { font-size: 9px; letter-spacing: .1em; text-transform: uppercase;
                              color: var(--muted-fg); }
#results .rcard .rload { grid-column: 2; display: flex; flex-wrap: wrap; gap: 6px 10px;
                         font-family: "JetBrains Mono", monospace; font-size: 11px;
                         color: var(--muted-fg); padding-top: 4px;
                         border-top: 1px solid hsl(200 20% 18% / 0.6); }
#results .rcard .rkit { margin-left: auto; letter-spacing: .08em; color: var(--fg); }

/* The phone's dismiss. Absolute inside the panel rather than a row of its own,
   so it costs no height on a surface that is already covering the board. */
#spectate .spectclose { position: absolute; top: 6px; right: 8px; z-index: 1;
                        width: 30px; height: 30px; padding: 0; line-height: 1;
                        display: grid; place-items: center; border-radius: 999px;
                        font-size: 13px; color: var(--muted-fg);
                        background: hsl(200 25% 12% / 0.9); }
#spectate.sheet { position: fixed; }

/* Phone/tablet: the picker becomes the primary control, so it gets the width
   and the row height a thumb needs. */
@media (max-width: 1024px) and (orientation: portrait) {
  #spectate { left: 12px; right: 12px; transform: none; min-width: 0;
              bottom: 18px; padding: 12px 10px; }
  #spectate table { table-layout: fixed; }
  #spectate td, #spectate th { padding: 9px 4px; white-space: nowrap;
                               overflow: hidden; text-overflow: ellipsis; }
  #spectate tbody tr:hover { background: none; } /* no hover on touch */
  /* "GAME OVER" is the longest title; at 15vw it overflows a portrait screen. */
  #banner span { font-size: min(11vw, 110px); }

  /* Touch HUD: the three columns share ~380 px, so the clock keeps its size
     (it is the one thing read at a glance) and the side columns give way.
     The watcher sub-line goes entirely — on a phone it is the row that pushes
     the roster over the arena, and the count is already in the seat list. */
  #hud { padding: 6px 10px; column-gap: 8px; }
  #hudleft { font-size: 11px; letter-spacing: .04em; }
  #hudright { font-size: 11px; line-height: 1.45; max-height: 30vh; }
  #hudright .watch { display: none; }
  #hudright .pdot { width: 8px; height: 8px; margin-right: 4px; }
}

/* ---- Bomb Fire Upgrade Settings ---- */
#bombfire button {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--secondary-fg);
  padding: 10px 12px;
  text-align: left;
}
#bombfire button:hover {
  border-color: hsl(174 60% 30%);
  box-shadow: 0 0 8px hsl(174 100% 41% / 0.15);
}
#bombfire button[style*="ffd75e"] {
  border-color: #ffd75e !important;
  background: rgba(255, 215, 94, 0.1) !important;
}

/* ==== Gamification: Live / Play / Hall (docs/GAMIFICATIONS.md §1) ==== */

/* ---- Top nav ---- */
/* The bar is page chrome, not page content: it runs edge to edge while the
   pages below stay in the --page-w column. Stretch to the body's content box,
   then pull back out over the body's own horizontal padding — negative
   margins, not 100vw, because 100vw counts the scrollbar and would put the
   whole document into horizontal scroll. box-sizing so the padding and border
   stay inside that width. */
#nav { display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
       box-sizing: border-box;
       align-self: stretch; width: auto; max-width: none;
       margin: 0 calc(-1 * var(--page-pad));
       padding: 6px max(var(--page-pad), 14px);
   
      
      }
#nav button { background: transparent; border-color: transparent; position: relative;
              color: var(--muted-fg); letter-spacing: .04em; }
#nav button.on { background: var(--secondary); border-color: hsl(174 60% 30%);
                 color: var(--fg); box-shadow: 0 0 14px hsl(174 100% 41% / 0.2); }
#nav .ico { margin-right: 6px; }
#nav .dot { font-style: normal; font-size: 10px; font-weight: 700; margin-left: 6px;
            background: var(--danger); color: #fff; border-radius: 999px; padding: 1px 6px; }
/* School's "never started" mark is an INVITATION, not an alert: the live-match
   count owns red, so this one takes the signal cyan. It is the whole of what
   the deleted first-run card used to say, and it retires after one lesson. */
#nav .dot.new { background: var(--primary); color: var(--on-primary); }
/* Right end of the bar = top-right corner of the page. One line, ellipsised:
   a long message (queue text, an error) must never wrap the tabs onto a
   second row — #nav is flex-wrap: wrap. */
#nav .navstatus { margin-left: auto; padding-right: 8px; color: var(--muted-fg);
                  font-size: 12px; letter-spacing: .04em;
                  max-width: 42%; white-space: nowrap; overflow: hidden;
                  text-overflow: ellipsis; }
#nav .navstatus:empty { display: none; }

/* Pages are body's flex items (#root is display:contents). width:100% now
   resolves against the body, which is definite, then max-width caps at
   --page-w — so Play stays 920 next to Live/Squad instead of shrink-wrapping
   to the stepper. min-width:0 lets the compact stepper scroll instead of
   forcing the column wider than the phone. */
#live, #wizard, #hall, #about, #settings, #squad, #event, #learn, #achieve {
  box-sizing: border-box;
  width: 100%;
  max-width: var(--page-w);
  min-width: 0;
}
#live, #wizard, #hall, #about, #settings, #squad, #learn, #achieve { display: flex; flex-direction: column; align-items: stretch;
                        gap: 16px; }
#live .card, #wizard .card, #hall .card, #squad .card, #achieve .card { max-width: none; width: 100%; }
/* Eight cards stacked down one column, so they HUG their content instead of
   sharing the viewport out between them: `.card`'s `flex: 1 1 340px` is right
   for a page with one card and leaves 300px of empty card under a two-line
   bounty note on a page with eight. */
#achieve .card { flex: 0 0 auto; }

/* About / Settings. Same --page-w column as the other tabs. Settings reuses
   the title/lede/sect rhythm; its option rows share #sheet's .row/.seg. */
#about, #settings { text-align: left; gap: 6px; }
/* The page title is an h2, not an h1: the global h1 above is the WORDMARK and
   carries an ::after that stamps "ARENA" under whatever it is applied to. */
#about .pagetitle, #settings .pagetitle { font-family: Orbitron, "Space Grotesk", sans-serif; font-size: 20px;
            font-weight: 800; letter-spacing: .18em; color: var(--primary);
            margin: 4px 0 2px; text-shadow: 0 0 18px hsl(174 100% 41% / 0.35); }
#about .sect, #settings .sect { margin: 22px 0 4px; }
#about .lede, #settings .lede { margin: 0 0 4px; line-height: 1.6; }
#about .note, #settings .note { color: var(--muted-fg); font-size: 12px; margin: 2px 0 8px; line-height: 1.5; }
#about .by a { font-size: 16px; }
#about .build { font-family: "JetBrains Mono", monospace; font-size: 14px;
                color: var(--primary); margin: 0;
                /* Selectable and obvious: its whole job is to be pasted into a
                   bug report. */
                user-select: all; }
#about a, #settings a { color: var(--primary); text-decoration: none; border-bottom: 1px solid transparent; }
#about a:hover, #settings a:hover { border-bottom-color: currentColor; }
#about .refs { list-style: none; margin: 0; padding: 0; display: flex;
               flex-direction: column; gap: 12px; }
#about .refs li { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; }
#about .refs .title { font-weight: 600; }
#about .refs .meta { color: var(--muted-fg); font-size: 12px;
                     font-family: "JetBrains Mono", monospace; text-align: right; }
#about .refs .what { grid-column: 1 / -1; color: var(--muted-fg); font-size: 13px;
                     line-height: 1.5; }
#about .refs.compact li { grid-template-columns: 1fr auto; }
#about .refs.compact .what { grid-column: 1; font-size: 12px; }

.hint { color: var(--muted-fg); font-size: 12px; margin: 0; }
.count { margin-left: 10px; color: var(--primary); font-family: "JetBrains Mono", monospace; }
.empty { text-align: center; color: var(--muted-fg); padding: 40px 20px; }
/* `.card` is a column flex, so a chip in the empty state stretches the full
   width and stops reading as a chip. */
.empty .chip { align-self: center; margin-top: 12px; }

.chip { padding: 6px 12px; font-size: 13px; }
.chip.on { background: var(--primary); color: var(--on-primary); border-color: transparent;
           font-weight: 600; }

/* ---- Live cards ---- */
.queuecard {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column;
  min-height: 220px;
  background: hsl(200 20% 5%);
}
.queuecard .qdemo {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
}
.queuecard .qdemo #game {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; gap: 0;
}
.queuecard .qdemo #game3d::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: linear-gradient(
    90deg,
    hsl(200 20% 6% / 0.72) 0%,
    hsl(200 20% 6% / 0.28) 55%,
    hsl(200 20% 6% / 0.18) 100%
  );
}
#demowipe {
  position: absolute; inset: 0; z-index: 4; pointer-events: none;
  width: 100% !important; height: 100% !important; display: block;
  image-rendering: pixelated; image-rendering: crisp-edges;
  /* The global `canvas` rule paints `--bg`. These overlays are a tile mask,
     not a surface — an opaque fill would hide the wallpaper (and, on
     #matchwipe, the whole Live page) even when the bitmap is clear. */
  background: transparent;
}
#matchwipe {
  position: fixed; inset: 0; z-index: 25; pointer-events: none;
  width: 100% !important; height: 100% !important; display: block;
  image-rendering: pixelated; image-rendering: crisp-edges;
  background: transparent;
}
.queuecard .qbody {
  position: relative; z-index: 1;
  flex: 1; display: flex; flex-direction: column; gap: 10px;
}
.queuecard .qhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.queuecard .qhead .sect { margin: 0; }
.queuecard .cctv {
  width: 40px;
  height: 27px;
  flex-shrink: 0;
  color: var(--danger);
  background: none;
  filter: drop-shadow(0 1px 8px hsl(200 20% 6% / 0.9));
}
.queuecard .qact { margin-top: auto; align-self: flex-start; }
.queuecard .sect, .queuecard .hint, .queuecard .qstat {
  text-shadow: 0 1px 10px hsl(200 20% 6% / 0.9);
}
.queuecard .row { flex-wrap: wrap; }
.qstat { color: var(--muted-fg); font-size: 12px;
         font-family: "JetBrains Mono", monospace; }
.livehead { width: 100%; align-self: flex-start;
            display: flex; align-items: center; gap: 8px; }
.livehead .count { margin-left: 0; }
/* Reads as a dismissal, not a fifth filter: it lives beside the count it is
   explaining and never takes the accent the "on" chips wear. */
.clearfilters { margin-left: auto; letter-spacing: normal; font-size: 11px;
                padding: 3px 10px; }

/* ---- Live filters (gamification §1.1) ---- */
.livefilters { width: 100%; align-self: flex-start;
               display: flex; flex-direction: column; gap: 6px;
               margin: 0 0 12px; }
.facet { display: flex; align-items: center; gap: 10px; }
.facetlabel { flex: 0 0 54px; color: var(--muted-fg); font-size: 10px;
              font-weight: 600; letter-spacing: .18em; }
/* The chips scroll sideways rather than wrapping to a second line: the bar sits
   above the fold on the landing page, and a THEME row that grows a row of its
   own pushes the first card off the screen it is advertising. */
.facetchips { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0;
              overflow-x: auto; scrollbar-width: none;
              -webkit-overflow-scrolling: touch; }
.facetchips::-webkit-scrollbar { display: none; }
.facetchips .chip { flex: 0 0 auto; padding: 4px 10px; font-size: 12px; }
/* Scroll anchoring: when a card above the viewport grows/shrinks on a
   directory tick, keep the anchored card in place instead of shifting the
   whole list under the thumb. */
#live, .livegrid { overflow-anchor: auto; }
.livegrid { display: grid; width: 100%; gap: 16px;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
.gamecard { background: var(--card); border: 1px solid var(--border);
            border-radius: var(--radius); padding: 16px; display: flex;
            flex-direction: column; gap: 10px;
            box-shadow: 0 0 24px hsl(174 100% 41% / 0.06); }
.gamecard:hover { border-color: hsl(174 60% 30%); }
.gchead { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.gcstage { font-weight: 600; letter-spacing: .04em; }
.gcround { color: var(--muted-fg); font-size: 11px; }
.gcbadges { display: flex; flex-wrap: wrap; gap: 6px; }
.badge { font-size: 11px; padding: 2px 8px; border-radius: 999px;
         background: hsl(174 100% 41% / 0.12); color: var(--primary);
         border: 1px solid hsl(174 100% 41% / 0.3); }
.gcfighters { display: flex; flex-direction: column; gap: 4px; }
.fighter { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.fighter.dead { opacity: .35; text-decoration: line-through; }
.fname { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fstars { color: var(--warning); font-size: 10px; letter-spacing: -1px; }
.fpips { display: flex; gap: 3px; }
.fpips i { width: 7px; height: 7px; border-radius: 50%; background: var(--muted);
           border: 1px solid var(--border); }
.fpips i.on { background: var(--primary); border-color: transparent; }
.gcfoot { display: flex; gap: 8px; align-items: center; margin-top: 2px; }
.gcfoot .watchers { color: var(--muted-fg); font-size: 12px; margin-right: auto; }
.gcfoot button { padding: 7px 12px; font-size: 13px; }

/* ---- Entry wizard ---- */
.steps { display: flex; gap: 6px; list-style: none; padding: 0; margin: 0;
         width: 100%; flex-wrap: wrap; }
.steps li { flex: 1 1 120px; display: flex; align-items: center; gap: 8px;
            padding: 10px 12px; border: 1px solid var(--border);
            border-radius: var(--radius); color: var(--muted-fg); font-size: 13px; }
.steps li b { width: 20px; height: 20px; border-radius: 50%; background: var(--muted);
              display: grid; place-items: center; font-size: 11px; }
.steps li.on { color: var(--fg); border-color: hsl(174 60% 30%);
               box-shadow: 0 0 14px hsl(174 100% 41% / 0.2); }
.steps li.on b { background: var(--primary); color: var(--on-primary); }
.steps li.done { cursor: pointer; color: var(--secondary-fg); }
.steps li.done b { background: hsl(174 100% 41% / 0.3); color: var(--fg); }

.pickgrid { display: grid; gap: 10px;
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.pickgrid.small { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
.pick { display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
        padding: 12px; text-align: left; }
.pick .big { font-size: 20px; font-family: "JetBrains Mono", monospace;
             color: var(--primary); }
.pick .sub { color: var(--muted-fg); font-size: 11px; }
/* The seat cap, drawn only where the map actually restricts it (PlayWizard
   mapCap). A chip rather than another line of prose, because it is the one fact
   on this card that can stop a lobby starting. */
.pick .cap { margin-top: 2px; align-self: flex-start; padding: 1px 6px;
             border-radius: 999px; font-size: 10px; letter-spacing: .04em;
             color: var(--on-primary); background: hsl(174 100% 41% / 0.75); }
.pick.on { border-color: var(--primary);
           background: hsl(174 100% 41% / 0.08); }

/* Play/Lobby loot table — icons + count steppers (protocol v21). */
.loot { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.loothead { display: flex; align-items: center; justify-content: space-between;
            gap: 10px; flex-wrap: wrap; }
.lootcap { color: var(--muted-fg); font-size: 12px;
           font-family: "JetBrains Mono", monospace; }
.lootcap.warn { color: var(--warning); }
.lootgroup { display: flex; flex-direction: column; gap: 6px; }
.lootgroup-lbl { color: var(--muted-fg); font-size: 10px; font-weight: 600;
                 letter-spacing: .28em; text-transform: uppercase; }
.lootgrid { display: grid; gap: 6px;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.lootrow { display: flex; align-items: center; gap: 8px;
           padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--radius);
           background: var(--secondary); min-width: 0; }
.lootrow.off { opacity: .45; }
.lootrow:hover { border-color: hsl(174 60% 30%); }
.looticons { width: 32px; height: 32px; flex-shrink: 0;
             image-rendering: pixelated; image-rendering: crisp-edges;
             /* One 8x8 atlas, the same sheet render3d draws every pickup from
                (game/loot.ts). 800% scales one 64px cell to the box; the
                positions are the classic n/(cols-1) sprite-sheet formula. */
             background-image: url("/assets/powerups.png");
             background-size: 800% 800%;
             background-position: calc(var(--pu-col) * 100% / 7)
                                  calc(var(--pu-row) * 100% / 7);
             background-repeat: no-repeat; }
.lootname { flex: 1; min-width: 0; font-size: 13px; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
.lootstep { display: inline-flex; align-items: center; gap: 2px; margin-left: auto;
            flex-shrink: 0; }
.lootbtn { width: 28px; height: 28px; padding: 0; display: grid; place-items: center;
           font-size: 16px; line-height: 1; border-radius: 4px; }
.lootbtn:disabled { opacity: .35; }
.lootnum { min-width: 1.6em; text-align: center; font-size: 13px;
           font-family: "JetBrains Mono", monospace; color: var(--primary); }
.loot.compact .lootgrid { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
.loot.compact .lootrow { padding: 4px 6px; gap: 6px; }
.loot.compact .looticons { width: 28px; height: 28px; }
.lootblock { width: 100%; margin: 4px 0 8px; }

/* Loot presets — a shelf per family, one chip per table (game/lootPresets.ts).
   The shelves wrap on their own rather than sharing the steppers' grid: a
   family is a short list with a name over it, not a column of equal cells. */
.lootpresets { display: flex; flex-direction: column; gap: 8px;
               padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.lootfam { display: flex; flex-direction: column; gap: 5px; }
.lootchips { display: flex; flex-wrap: wrap; gap: 6px; }
/* The chip and its forget button read as one control, so they share a hover. */
.lootpick { display: inline-flex; align-items: stretch; }
.lootpick .chip { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.lootpick:has(.lootdrop) .chip { border-right: 0; }
.lootdrop { padding: 0 8px; font-size: 13px; line-height: 1; color: var(--muted-fg);
            border-top-left-radius: 0; border-bottom-left-radius: 0; }
.lootdrop:hover { color: var(--warning); }
.lootsave { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.lootsave .fsearch { flex: 1 1 160px; max-width: 260px; }
.lootsavebtn { align-self: flex-start; }
.loot.compact .lootpresets { gap: 6px; }
.loot.compact .lootchips { gap: 4px; }

.fsearch { flex: 1; min-width: 0; padding: 7px 10px; font-size: 13px; }
.fightergrid { display: grid; gap: 8px; max-height: 46vh; overflow-y: auto;
               grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); padding: 2px; }
.fcard { display: flex; flex-direction: column; gap: 1px; align-items: flex-start;
         padding: 9px 11px; text-align: left; font-size: 13px; }
.fcard b { font-size: 13px; overflow: hidden; text-overflow: ellipsis;
           white-space: nowrap; max-width: 100%; }
.fcard .fcflag { font-size: 16px; }
.fcard .fcmeta { color: var(--muted-fg); font-size: 11px;
                 font-family: "JetBrains Mono", monospace; }
.fcard .fch2h { color: var(--warning); font-size: 11px; }
.fcard .fcbusy { color: var(--danger); font-size: 11px; }
.fcard.on { border-color: var(--primary); background: hsl(174 100% 41% / 0.1); }
.fcard.busy { opacity: .45; }

.summary { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; margin: 0; }
.summary dt { color: var(--muted-fg); font-size: 11px; letter-spacing: .2em; }
.summary dd { margin: 0; }
button.big { padding: 14px 34px; font-size: 16px; letter-spacing: .1em; align-self: center;
             margin-top: 8px; }
.wizfoot { display: flex; gap: 10px; justify-content: space-between; width: 100%; }

/* ---- Play wizard step coverflow (off Telegram) ----
   The wizard's steps are the slides. Neighbours recede in Z with no Y-tilt, the
   same coverflow the Salty Pelican pen uses (rotate: 0, depth via translateZ).
   --drag is written from pointer code so the live card follows the finger
   without a React render per pixel. */
#wizard.swipe { flex: 1 1 auto; min-width: 0; width: 100%; }
.wizdeck {
  --drag: 0px;
  position: relative;
  flex: 1 1 auto;
  width: 100%;
  min-height: 28rem;
  height: 62vh;
  /* Perspective only. Overflow sits on .wizstage. Neighbour offset is
     parent-relative left/top from JS (the wireframe peeks). */
  perspective: 900px;
  cursor: grab;
  outline: none;
}
.wizstage {
  position: absolute; inset: 0;
  overflow: hidden;
}
.wizdeck:focus-visible { box-shadow: 0 0 0 2px var(--primary); }
.wizdeck.dragging { cursor: grabbing; }
.wizdeck.dragging .wizslide { transition: none; }
.wizslide {
  position: absolute;
  inset: auto;
  overflow: hidden;
  pointer-events: auto;
  transform-origin: center center;
  transition: transform .35s ease, opacity .35s ease, left .35s ease, top .35s ease;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
}
.wizdeck[data-axis="x"] { touch-action: pan-y; }
.wizdeck[data-axis="y"] { touch-action: pan-y; }
.wizslide.on {
  overflow: auto;
  border-color: hsl(174 60% 30%);
}
.wizslide:not(.on) { cursor: pointer; }
.wizslide:not(.on) > * { pointer-events: none; }
.wizslide.far { visibility: hidden; pointer-events: none; }
#wizard.swipe .wizslide .card {
  flex: 1 1 auto; max-width: none; width: 100%; min-height: 100%;
  min-width: 0; border: none; box-shadow: none; background: transparent;
}
#wizard.swipe .fightergrid { max-height: none; }

/* ---- Play wizard "You": trading cards (max four) ----
   Pokémon-card bones on the Predicto deck: foil frame, HP, illustration
   window, type chip, two attack rows. Accent comes from the fighter's kit. */
.pkgrid { display: grid; gap: 18px; width: 100%;
          grid-template-columns: repeat(auto-fit, minmax(196px, 228px));
          justify-content: center; perspective: 900px; }
.pkcard {
  --accent: var(--primary);
  --fire: #ff9950;
  appearance: none; -webkit-appearance: none;
  width: 100%; aspect-ratio: 2.5 / 3.5;
  padding: 7px; border: 0; border-radius: 16px; cursor: pointer;
  color: inherit; text-align: left; font: inherit;
  background:
    linear-gradient(155deg, #fff6d0 0%, #e0c36a 18%, #8a6a14 48%, #f3d98a 78%, #b8860b 100%);
  box-shadow:
    inset 0 1px 0 hsl(48 80% 90% / 0.7),
    0 10px 24px hsl(200 40% 4% / 0.55),
    0 0 0 1px hsl(38 50% 22% / 0.55);
  transform: translateZ(0);
  transition: transform .2s ease, box-shadow .2s ease, background .2s ease;
}
.pkcard:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
.pkcard:hover {
  transform: translateY(-8px) rotateX(5deg);
  box-shadow:
    inset 0 1px 0 hsl(48 80% 90% / 0.8),
    0 18px 36px hsl(200 40% 4% / 0.6),
    0 0 22px color-mix(in srgb, var(--accent) 35%, transparent);
}
.pkcard.on {
  background:
    linear-gradient(155deg, #fffce6 0%, #ffe566 16%, #d4af37 46%, #fff1a8 78%, #e6c35c 100%);
  box-shadow:
    inset 0 1px 0 #fff,
    0 0 0 2px #ffe566,
    0 0 32px hsl(48 100% 50% / 0.4),
    0 16px 32px hsl(200 40% 4% / 0.55);
}
.pkinner {
  position: relative; height: 100%; min-height: 0;
  display: flex; flex-direction: column; gap: 5px;
  padding: 8px 8px 7px; border-radius: 10px; overflow: hidden;
  background:
    radial-gradient(120% 80% at 50% -10%, color-mix(in srgb, var(--accent) 28%, transparent), transparent 55%),
    linear-gradient(180deg, hsl(200 28% 13%), hsl(200 25% 7%));
  box-shadow: inset 0 0 0 1px hsl(200 20% 22% / 0.8);
}
.pkhead { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.pkname {
  flex: 1; min-width: 0; margin: 0;
  font-family: Orbitron, "Space Grotesk", sans-serif;
  font-size: 13px; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 45%, transparent);
}
.pkhp {
  flex: 0 0 auto; color: #ff6b6b;
  font-family: Orbitron, "Space Grotesk", sans-serif;
  font-size: 15px; font-weight: 800; letter-spacing: .02em;
  line-height: 1;
}
.pkhp small {
  display: inline-block; margin-right: 3px;
  font-size: 8px; letter-spacing: .16em; color: #ff9a9a; vertical-align: 2px;
}
.pkart {
  position: relative; flex: 1 1 auto; min-height: 0;
  display: grid; place-items: center;
  border-radius: 6px; overflow: hidden;
  background:
    radial-gradient(ellipse at 50% 78%, color-mix(in srgb, var(--accent) 55%, #000) 0%, transparent 62%),
    repeating-linear-gradient(90deg, hsl(200 20% 10%) 0 8px, hsl(200 22% 12%) 8px 16px),
    hsl(200 25% 8%);
  box-shadow:
    inset 0 0 0 2px color-mix(in srgb, var(--accent) 55%, #000),
    inset 0 0 18px hsl(200 40% 4% / 0.7);
}
.pkart canvas {
  width: 78%; height: auto; max-height: 100%;
  background: transparent; border-radius: 0;
  image-rendering: pixelated; image-rendering: crisp-edges;
  filter: drop-shadow(0 8px 10px hsl(200 40% 4% / 0.7));
}
.pknum {
  position: absolute; top: 6px; right: 7px;
  font-family: "JetBrains Mono", monospace; font-size: 11px; font-weight: 700;
  color: #fff; background: hsl(200 25% 6% / 0.72);
  border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
  border-radius: 999px; padding: 1px 7px; letter-spacing: .04em;
}
.pktype {
  display: flex; align-items: center; gap: 6px; align-self: flex-start;
  padding: 2px 9px 2px 5px; border-radius: 999px;
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  color: var(--fg);
  background: color-mix(in srgb, var(--accent) 22%, hsl(200 25% 10%));
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
}
.pkflag { font-size: 14px; line-height: 1; }
.pkstats { list-style: none; margin: 0; padding: 2px 0 0; display: flex;
           flex-direction: column; gap: 3px; }
.pkstats li {
  display: grid; grid-template-columns: 10px 1fr auto; gap: 7px; align-items: center;
  padding: 4px 6px; border-radius: 5px;
  background: hsl(200 20% 100% / 0.04);
  border-bottom: 1px solid hsl(48 40% 40% / 0.18);
}
.pkstats i {
  display: block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px color-mix(in srgb, var(--accent) 70%, transparent);
}
.pkstats b { font-size: 11px; font-weight: 600; letter-spacing: .02em; }
.pkstats span { font-family: "JetBrains Mono", monospace; font-size: 11px;
                color: var(--muted-fg); }
.pkfoot {
  margin-top: auto; padding-top: 3px;
  font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted-fg);
  text-align: right;
}
.pkcard.on .pkinner::after {
  content: ""; pointer-events: none;
  position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(
    115deg,
    transparent 18%,
    hsl(0 0% 100% / 0.18) 36%,
    transparent 48%,
    hsl(48 100% 70% / 0.22) 64%,
    transparent 78%
  );
  background-size: 220% 220%;
  animation: pkfoil 3.6s linear infinite;
  mix-blend-mode: overlay;
}
@keyframes pkfoil {
  0% { background-position: 120% 0; }
  100% { background-position: -40% 100%; }
}

/* ---- Hall of fame ---- */
.halltable { overflow-x: auto; max-height: 62vh; overflow-y: auto; }
.halltable table { border-collapse: collapse; width: 100%; font-size: 13px; }
.halltable th { position: sticky; top: 0; background: var(--card); z-index: 1;
                text-align: left; color: var(--muted-fg); font-size: 11px;
                letter-spacing: .15em; padding: 8px 10px; cursor: pointer;
                border-bottom: 1px solid var(--border); white-space: nowrap; }
.halltable th:hover, .halltable th.sorted { color: var(--primary); }
.halltable td { padding: 7px 10px; border-bottom: 1px solid hsl(200 20% 12%); }
.halltable td.num, .halltable th.num { text-align: right;
                                       font-family: "JetBrains Mono", monospace; }
.halltable td.rank, .halltable th.rank { color: var(--muted-fg); text-align: right;
                                         font-family: "JetBrains Mono", monospace; }
.halltable td.strong { color: var(--primary); font-weight: 600; }
.halltable tbody tr:hover { background: hsl(174 100% 41% / 0.05); }
.halltable tr.me { background: hsl(174 100% 41% / 0.1); outline: 1px solid hsl(174 60% 30%); }

.squadgrid { display: grid; gap: 10px;
             grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.scard { position: relative; display: flex; flex-direction: column; gap: 2px;
         align-items: flex-start; padding: 12px 14px 12px 12px; text-align: left;
         background: var(--secondary); border: 1px solid var(--border);
         border-radius: var(--radius); cursor: pointer; }
.scard:hover { border-color: hsl(174 60% 30%); }
.scard.on { border-color: var(--primary); background: hsl(174 100% 41% / 0.08); }
.scard .scflag { font-size: 20px; }
.scard b { font-size: 15px; }
.scard .scmeta { color: var(--muted-fg); font-size: 12px;
                 font-family: "JetBrains Mono", monospace; }
.scard .scdel { position: absolute; top: 6px; right: 6px; min-width: 0;
                padding: 2px 8px; font-size: 12px; background: transparent;
                border-color: transparent; color: var(--muted-fg); }
.scard .scdel:hover { color: var(--danger); }
.scard .scedit { margin-top: 8px; padding: 4px 10px; font-size: 11px;
                 letter-spacing: .12em; text-transform: uppercase; }
.scach { margin-top: 8px; padding: 4px 10px; font-size: 11px; letter-spacing: .06em;
         background: hsl(38 92% 50% / 0.12); border-color: hsl(38 92% 50% / 0.4);
         color: var(--warning); }
.scach:hover { background: hsl(38 92% 50% / 0.22); border-color: var(--warning); }
.scadd { align-items: center; justify-content: center; min-height: 96px;
         border-style: dashed; color: var(--muted-fg); letter-spacing: .12em;
         text-transform: uppercase; font-size: 12px; }
.scadd:hover { color: var(--fg); border-color: var(--primary); }

/* ---- Achievements, level and bounty (gamification §12) ----

   Three shapes, and the order on the page is the order of the questions a
   player actually asks: how far am I (the level box), on what (the stat tiles),
   and what is next (the achievement grid). Every bar in here is the same
   two-element construction — a track with one span sized in percent — because
   they mean the same thing and should not read as two different widgets. */
.lvlbox { width: 100%; margin: 12px 0 4px; padding: 12px 14px;
          background: var(--secondary); border: 1px solid var(--border);
          border-radius: var(--radius); }
.lvlhead { display: flex; align-items: center; gap: 14px; }
.lvlnum { display: flex; flex-direction: column; align-items: center; line-height: 1;
          padding: 6px 12px; border-radius: var(--radius);
          background: hsl(174 100% 41% / 0.12); border: 1px solid hsl(174 60% 30%); }
.lvlnum b { font-size: 26px; color: var(--primary); }
.lvlnum i { font-style: normal; font-size: 9px; letter-spacing: .18em;
            color: var(--muted-fg); }
.lvlname { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
.lvlname b { font-size: 16px; letter-spacing: .04em; }
.lvlname .hint { margin: 0; font-size: 12px; }
.lvlxp { font-family: "JetBrains Mono", monospace; font-size: 15px; color: var(--fg);
         white-space: nowrap; }
.lvlxp i { font-style: normal; font-size: 10px; color: var(--muted-fg); }
.lvlbar, .achbar { position: relative; width: 100%; height: 8px; margin-top: 10px;
                   background: var(--muted); border-radius: 999px; overflow: hidden; }
.lvlbar span, .achbar span { display: block; height: 100%; border-radius: 999px;
                             background: linear-gradient(90deg, var(--primary), var(--primary-bright));
                             transition: width .4s ease; }
.lvlbox .hint { margin: 8px 0 0; }

.statgrid { display: grid; gap: 8px; width: 100%; margin-top: 12px;
            grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); }
.stile { display: flex; flex-direction: column; align-items: center; gap: 2px;
         padding: 8px 6px; background: var(--secondary);
         border: 1px solid var(--border); border-radius: var(--radius); }
.stile b { font-family: "JetBrains Mono", monospace; font-size: 17px; color: var(--fg); }
.stile span { font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
              color: var(--muted-fg); text-align: center; }

.achgrid { display: grid; gap: 10px; width: 100%;
           grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
/* A LOCKED ROW IS DIMMED, NEVER HIDDEN. The list of what you have not done yet
   is the part of this page that gives somebody a reason to press Play, so it
   is drawn at full detail and simply desaturated. */
.ach { display: flex; flex-direction: column; gap: 3px; padding: 10px 12px;
       background: var(--secondary); border: 1px solid var(--border);
       border-radius: var(--radius); opacity: .62; }
.ach.on { opacity: 1; border-color: hsl(38 92% 50% / 0.55);
          background: hsl(38 92% 50% / 0.07); }
.achico { font-size: 20px; filter: grayscale(1); }
.ach.on .achico { filter: none; }
.ach b { font-size: 14px; }
.achblurb { font-size: 12px; color: var(--muted-fg); }
/* margin-top:auto pins the bar and its numbers to the bottom of the cell, so a
   two-line blurb lengthens the card without pushing its bar out of line with
   the row's. */
.achbar { margin-top: auto; }
.achmeta { font-family: "JetBrains Mono", monospace; font-size: 11px;
           color: var(--muted-fg); margin-top: 2px; }
.ach.on .achbar span { background: linear-gradient(90deg, var(--warning), hsl(45 95% 60%)); }

.bountylog { list-style: none; margin: 8px 0 0; padding: 0; width: 100%;
             display: flex; flex-direction: column; }
.bountylog li { display: grid; grid-template-columns: 1fr auto auto; gap: 10px;
                align-items: center; padding: 7px 2px;
                border-top: 1px solid var(--border); }
.bountylog .bname { font-size: 13px; }
.bountylog .bsp { font-family: "JetBrains Mono", monospace; color: var(--warning); }
.bountylog .bat { font-size: 11px; color: var(--muted-fg); }

/* The toast. Fixed, top-right on desktop and full-width above the tab bar on a
   phone — it fires while a match is settling, so it must not land on the one
   button the results table is counting down to. */
#unlocks { position: fixed; top: calc(12px + var(--safe-top)); right: 12px; z-index: 60;
           display: flex; flex-direction: column; gap: 8px; max-width: 320px;
           pointer-events: none; }
.unlock { pointer-events: auto; display: flex; align-items: center; gap: 10px;
          text-align: left; padding: 10px 14px; border-radius: var(--radius);
          background: hsl(200 25% 8% / 0.96); border: 1px solid var(--warning);
          box-shadow: 0 0 24px hsl(38 92% 50% / 0.25);
          animation: unlockpop .45s cubic-bezier(.2, 1.4, .4, 1) both; }
.unlockico { font-size: 26px; }
.unlocktext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.unlocktext i { font-style: normal; font-size: 9px; letter-spacing: .18em;
                color: var(--warning); }
.unlocktext b { font-size: 15px; }
.unlocktext span { font-size: 11px; color: var(--muted-fg); }
@keyframes unlockpop {
  from { opacity: 0; transform: translateX(28px) scale(.94); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .unlock { animation: none; }
  .lvlbar span, .achbar span { transition: none; }
}

/* Create/edit wizard: sticky preview column from Look through Ready. */
.createflow { display: grid; grid-template-columns: 160px 1fr; gap: 16px;
              align-items: start; width: 100%; }
.createflow.noprev { grid-template-columns: 1fr; }
.createbody { min-width: 0; display: flex; flex-direction: column; gap: 16px; }
.createprev { position: sticky; top: 12px; display: flex; flex-direction: column;
              align-items: center; gap: 8px; }
.createprev #preview { width: 128px; height: 128px; }
.createwho { display: flex; flex-direction: column; align-items: center;
             text-align: center; gap: 2px; }
.createwho .scflag { font-size: 20px; }
.createwho b { font-size: 14px; }
.createwho .scmeta { color: var(--muted-fg); font-size: 11px;
                     font-family: "JetBrains Mono", monospace; }
.hname { margin-right: 6px; }

/* ---- Spectator rail (in-arena) ----
   Owns the top-right corner outright: Hud.tsx drops #hudright while watching
   precisely so this box has the corner to itself. Capped and scrollable so a
   ten-seat room cannot grow past the viewport on a short screen. */
#specrail { position: fixed; top: 12px; right: 12px; z-index: 30;
            background: hsl(200 25% 8% / 0.92); border: 1px solid var(--border);
            border-radius: var(--radius); padding: 10px 12px;
            min-width: 210px; max-width: min(280px, calc(100vw - 24px));
            max-height: calc(100vh - 24px);
            display: flex; flex-direction: column; gap: 5px;
            overflow: auto; overscroll-behavior: contain;
            backdrop-filter: blur(6px); }
/* The two exits must stay reachable when the seat list scrolls. */
.sracts { position: sticky; bottom: 0; background: inherit; padding-top: 6px; }
.srhead { color: var(--muted-fg); font-size: 10px; letter-spacing: .3em; }
.srrow { display: flex; align-items: center; gap: 7px; font-size: 12px; }
.srrow.dead { opacity: .35; text-decoration: line-through; }
.srname { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srwins { font-family: "JetBrains Mono", monospace; color: var(--primary); }
.sracts { display: flex; gap: 6px; margin-top: 6px; flex: 0 0 auto; }
#specrail button { padding: 7px 10px; font-size: 12px; }
/* "Take a seat" is the point of the rail; Back stays out of its way. */
.sracts button { flex: 1 1 auto; min-width: 0; }
.sracts button:first-child { flex: 0 0 auto; }

@media (max-width: 640px) {
  #nav { justify-content: center; }
  #nav .navstatus { display: none; }
  .livegrid { grid-template-columns: 1fr; }
  /* Phone: the rail takes the bottom edge, where the pad would be — a watcher
     has no pad (GameView renders none without a seat), so the bottom is free.
     Half the viewport at most, or a full room buries the arena it is about. */
  #specrail { top: auto; bottom: 12px; right: 12px; left: 12px;
              min-width: 0; max-width: none; max-height: 50vh; }
}

/* ================================================================
   Compact entry shell (plan §6.10) — phones and Telegram Mini Apps.

   Driven by body.compact, which app.tsx toggles from one media query, so
   every rule below turns on together. The shape is: content scrolls
   edge-to-edge between a minimal header and a fixed bottom tab bar, and each
   page parks its own primary action just above that bar. Nothing here is a
   scaled-down desktop layout — the desktop shell puts navigation at the top
   and actions inline, which on a phone means the two things you touch most
   are the two furthest from your thumb.
   ================================================================ */
body.compact {
  /* sm tier of the shared page tokens: full bleed, no gutter. Set here rather
     than in a fourth media query so the shell has exactly one switch. */
  --page-w: 100%;
  --page-pad: 0px;
  /* Bottom padding clears the fixed tab bar AND the page action bar that
     sits above it, or the last row of every list is unreachable. */
  padding: calc(var(--safe-top) + 8px) var(--page-pad)
           calc(var(--tabbar-h) + var(--safe-bottom) + 8px);
  gap: 10px;
  /* A compact screen has no room to waste on parallax; the fixed grid also
     repaints on every scroll frame in the Telegram webview. */
  background-attachment: scroll;
  overflow-x: hidden;
}

/* ---- Header: the wordmark is a landing-page flourish, not a fixture ---- */
body.compact h1 { font-size: 20px; letter-spacing: .12em; margin: 0; }
body.compact h1::after { font-size: .38em; letter-spacing: .5em; padding-left: .5em; }
/* Too little width for a corner badge: back into the flow, centred, and it
   collapses to nothing when there is no message (see the hidden attr too). */
/* The tab bar is a row of icons down here — no room for a status line in it,
   so on compact the header copy comes back, centred under the wordmark. */
body.compact:not(.ingame) #status {
  display: block; min-height: 0; font-size: 12px; padding: 0 12px; text-align: center;
}

/* ---- Pages fill the width; cards become edge-to-edge surfaces.
   The width itself comes from --page-w above — only the rhythm changes. ---- */
body.compact #live, body.compact #wizard, body.compact #hall,
body.compact #event, body.compact #squad, body.compact #settings, body.compact #about,
body.compact #achieve { gap: 10px; padding: 0; }
body.compact #about, body.compact #settings { padding: 0 14px; }
body.compact #event { flex-direction: column; flex-wrap: nowrap; }
body.compact .card {
  min-width: 0; max-width: none; width: 100%; flex: 0 0 auto;
  padding: 14px 14px; border-radius: 0; border-left: none; border-right: none;
  box-shadow: none;
}
body.compact .sect { font-size: 10px; letter-spacing: .22em; }
body.compact .hint { font-size: 11px; }

/* ---- Touch targets: 44 px is the floor, and every control shared one of
   three different paddings before this ---- */
body.compact button, body.compact .filebtn, body.compact input, body.compact select {
  min-height: var(--touch); font-size: 15px;
}
body.compact .chip { min-height: 36px; padding: 7px 14px; font-size: 13px; }
body.compact .swatch { width: 34px; height: 34px; min-height: 0; }
/* Touch: taller targets, and one fewer per row so they stay thumb-sized. */
body.compact .chips { grid-template-columns: repeat(auto-fit, minmax(38px, 1fr)); gap: 8px; }
body.compact .chips .swatch { width: auto; height: 38px; }
body.compact #palettes { max-width: none; }
/* Rows of controls wrap instead of overflowing — several were fixed-width
   flex rows that simply ran off the right edge. */
body.compact .row { flex-wrap: wrap; }
body.compact input, body.compact select { min-width: 0; max-width: 100%; }
body.compact #name { flex: 1 1 140px; }

/* ---- Bottom tab bar ---- */
body.compact #nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  /* fixed + the desktop pull-out margins would shift the bar off both edges */
  margin: 0; max-width: none; width: auto; flex-wrap: nowrap; gap: 0;
  padding: 0 var(--safe-right) var(--safe-bottom) var(--safe-left);
  border: none; border-top: 1px solid var(--border);
  background: hsl(200 25% 8% / 0.97); backdrop-filter: blur(12px);
}
body.compact #nav button {
  flex: 1 1 0; min-width: 0; height: var(--tabbar-h); border-radius: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 6px 2px; font-size: 10px; letter-spacing: .04em;
}
body.compact #nav button.on { background: transparent; border-color: transparent;
                              color: var(--primary); box-shadow: none; }
/* The active tab reads as a lit segment rather than a filled pill — a filled
   pill at this size swallows its own label. */
body.compact #nav button.on::before {
  content: ""; position: absolute; top: 0; left: 22%; right: 22%; height: 2px;
  background: var(--primary); box-shadow: 0 0 10px hsl(174 100% 41% / 0.7);
}
body.compact #nav .ico { margin: 0; font-size: 18px; line-height: 1; }
body.compact #nav .navlabel { display: none; }
body.compact #nav .dot { position: absolute; top: 5px; left: 55%; margin: 0;
                         font-size: 9px; padding: 0 5px; }
body.compact #nav .navstatus { display: none; }
#nav .navshort { display: none; } /* desktop uses the long label */
body.compact #nav .navshort { display: block; }

/* ---- Page action bar: one page, one primary intent, always in the same
   place. Sticky rather than fixed so it settles at the end of short pages
   instead of floating over them. ---- */
body.compact .pageact {
  position: sticky; bottom: 0; z-index: 35;
  display: flex; gap: 8px; align-items: center; width: 100%;
  margin-top: auto; padding: 10px 12px calc(10px + var(--safe-bottom));
  background: linear-gradient(hsl(200 20% 6% / 0), hsl(200 20% 6% / 0.96) 40%);
  backdrop-filter: blur(8px);
}
body.compact .pageact > button { flex: 1 1 auto; min-height: 48px; font-size: 15px; }
/* Back is a step sideways, not the intent — it takes only what it needs. */
body.compact .wizfoot > button:first-child { flex: 0 0 auto; min-width: 96px; }
/* Hall's bar is a filter set, so its buttons share the row evenly. */
body.compact .hallact { justify-content: stretch; }
body.compact .hallact > button { flex: 1 1 0; }

/* ---- Live: one card per row, actions full-width ---- */
body.compact .livefilters { gap: 4px; margin-bottom: 8px; }
/* Labels stay INLINE on a phone, narrower rather than stacked: four rows of
   label-over-chips is 230px of bar above the first card, and the front page's
   job is to show a match, not its own controls. */
body.compact .facet { gap: 6px; }
body.compact .facetlabel { flex: 0 0 40px; font-size: 9px; letter-spacing: .1em; }
body.compact .facetchips .chip { padding: 4px 9px; font-size: 11px; }
body.compact .livegrid { grid-template-columns: 1fr; gap: 10px; }
body.compact .gamecard { border-radius: 0; border-left: none; border-right: none;
                         box-shadow: none; padding: 14px; }
body.compact .gcfoot { flex-wrap: wrap; }
body.compact .gcfoot button { flex: 1 1 40%; min-height: 40px; }
body.compact .gcfoot .watchers { flex: 1 1 100%; margin-right: 0; }

/* ---- Achievements on a phone ----
   The trophy grid goes 2-up like every other card grid, the stat tiles go 3-up
   (they are one number each), and the toast crosses the screen at the TOP: the
   bottom of a phone is the tab bar, the touch pad and the results table's own
   button, and a card landing on any of them is a card in the way. */
body.compact .achgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
body.compact .ach { padding: 8px 10px; }
body.compact .statgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
body.compact .lvlhead { gap: 10px; }
body.compact .lvlxp { font-size: 13px; }
body.compact #unlocks { top: calc(8px + var(--safe-top)); right: 8px; left: 8px;
                        max-width: none; }

/* ---- Wizard: the stepper is a scroll strip, the pickers are 2-up ---- */
body.compact .steps { flex-wrap: nowrap; overflow-x: auto; padding: 0 12px 2px;
                      gap: 6px; scrollbar-width: none; }
body.compact .steps::-webkit-scrollbar { display: none; }
body.compact .steps li { flex: 0 0 auto; padding: 8px 12px; font-size: 12px; }
body.compact .pickgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
body.compact .pickgrid.small { grid-template-columns: repeat(2, minmax(0, 1fr)); }
body.compact .pick { padding: 10px; }
body.compact .pick .big { font-size: 17px; }
body.compact .lootgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
body.compact .loot.compact .lootgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
body.compact .lootbtn { width: 36px; height: 36px; }
body.compact .lootrow { padding: 8px; }
/* 170 px minimum columns collapsed to ONE on a phone, turning 92 fighters
   into a single-file scroll. Two real columns fit; let them. */
body.compact .fightergrid { grid-template-columns: repeat(2, minmax(0, 1fr));
                            gap: 6px; max-height: none; }
body.compact .fcard { padding: 8px 9px; font-size: 12px; }
body.compact .summary { grid-template-columns: 1fr; gap: 2px 0; }
body.compact .summary dt { margin-top: 8px; }
body.compact .pkgrid { grid-template-columns: repeat(2, minmax(0, 1fr));
                       gap: 10px; padding: 0 4px; perspective: none; }
body.compact .pkcard { max-width: none; padding: 5px; border-radius: 12px;
                       min-height: 0; font-size: inherit; }
body.compact .pkcard:hover { transform: none; }
body.compact .pkinner { padding: 6px 6px 5px; gap: 4px; }
body.compact .pkname { font-size: 11px; }
body.compact .pkhp { font-size: 13px; }
body.compact .pkart canvas { width: 88%; }

/* Compact swipe: the deck fills the column between the stepper and the
   action bar, and the live card is a real card again (radius, border) —
   edge-to-edge surfaces are the classic Telegram layout, not the coverflow. */
body.compact #wizard.swipe {
  height: calc(var(--app-height) - var(--tabbar-h) - var(--safe-top) - 7.5rem);
  min-height: 0;
}
body.compact #wizard.swipe .wizdeck {
  height: auto; min-height: 0; flex: 1 1 auto;
}
body.compact #wizard.swipe .wizslide .card {
  border-radius: 0; border: none; box-shadow: none;
}

/* ---- Hall: list rows instead of an eight-column table ---- */
.halllist { display: flex; flex-direction: column; }
.hlrow { display: grid; grid-template-columns: 2.2em 1fr auto; align-items: center;
         gap: 10px; padding: 9px 2px; border-bottom: 1px solid hsl(200 20% 12%); }
.hlrow.me { background: hsl(174 100% 41% / 0.1);
            outline: 1px solid hsl(174 60% 30%); outline-offset: -1px; }
.hlrank { color: var(--muted-fg); font-family: "JetBrains Mono", monospace;
          font-size: 12px; text-align: right; }
.hlname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
.hlstats { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.25; }
.hlstats b { color: var(--primary); font-family: "JetBrains Mono", monospace; font-size: 14px; }
.hlstats span { color: var(--muted-fg); font-size: 11px;
                font-family: "JetBrains Mono", monospace; }

/* ---- Hall on a phone: a frame, not a long page.
   The status line and the HALL OF FAME header hold the top, the filter set
   holds the bottom just above the tab bar, and the ladder is the only thing
   that moves — a leaderboard you scroll by the page keeps taking the header
   (and the rank you were comparing against) off screen with it.
   The FRAME is the body, not a computed height on #hall: #status collapses to
   nothing when there is no message and the tab bar is out of flow, so leaving
   the arithmetic to flexbox is what makes the bottom edge land exactly on the
   bar instead of tracking three magic numbers that drift apart. ---- */
body.compact:has(#hall:not([hidden])) { height: var(--app-height); overflow: hidden; }
body.compact #hall { flex: 1 1 auto; min-height: 0; }
body.compact #hall .card {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
/* The one scroller. `contain` so a flick that runs past the end of the ladder
   scrolls the ladder and stops, instead of rubber-banding the frame behind it. */
body.compact #hall .halllist {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
/* The frame took away the page's ability to grow, so every OTHER flex item in
   it has to be told to hold its natural height: a shrinkable one gets shaved
   instead, and the status line lost the bottom half of its text to exactly
   that before these two lines existed. */
body.compact:has(#hall:not([hidden])) #status { flex: 0 0 auto; }
body.compact #hall .pageact { flex: 0 0 auto; }

/* ---- Create wizard: preview sticks under the step bar ---- */
body.compact #builder .row { align-items: flex-start; }
body.compact .createflow { display: flex; flex-direction: column; gap: 10px; }
body.compact .createprev { position: sticky; top: 0; z-index: 5; flex-direction: row;
                           justify-content: center; gap: 12px; padding: 8px 0;
                           background: var(--bg); }
body.compact .createprev #preview { width: 96px; height: 96px; }
body.compact .createwho { align-items: flex-start; text-align: left; }

/* ---- In-game: the arena measures itself against the REPORTED viewport.
   Telegram's webview is taller than what you can see, so inset:0 put the
   bottom of the pad under the client's own chrome. ---- */
body.ingame #game { top: 0; bottom: auto; height: var(--app-height); }
body.compact.ingame #hud { padding-top: calc(8px + var(--safe-top)); }
body.compact.ingame #hud::before { height: calc(68px + var(--safe-top)); }
body.compact.ingame .keys,
body.compact.ingame #specrail { bottom: calc(12px + var(--safe-bottom)); }

@media (prefers-reduced-motion: reduce) {
  #loading .dots span, #hudtime.hurry, #hudleft .trighint,
  #boot-loading .boot-bg, #loading .load-bg, .pkcard.on .pkinner::after {
    animation: none;
  }
  /* The final table still APPEARS and still LEAVES — only the fade goes. Its
     opacity is animated from 0, so cancelling the animation without pinning
     opacity would leave the whole scoreboard invisible. */
  #results { animation: none; opacity: 1; }
  #boot-loading .boot-fill {
    transition: none;
  }
}
