/* =============================================================================
 * client/styles.css
 * -----------------------------------------------------------------------------
 * Layout, letterboxing, safe areas and the arcade-cabinet look.
 *
 * Three layout modes, in priority order:
 *   1. PORTRAIT PHONE (the primary target) — canvas on top, touch controls in
 *      the space that remains below it.
 *   2. LANDSCAPE PHONE — canvas takes the full height; the controls move to the
 *      left and right edges and stay inside the letterbox, never over the
 *      dungeon.
 *   3. DESKTOP — canvas centred, touch controls hidden until a touch is seen.
 *
 * Two rules this file exists to keep:
 *
 *   **The canvas is not ours.** `client/render/renderer.js` owns its on-screen
 *   size outright; `main.js#layout()` measures the result and publishes it as
 *   `--canvas-w`, `--canvas-h` and `--cscale` (CSS pixels per logical pixel).
 *   Everything here is expressed against those, so the chrome tracks a 0.8x
 *   canvas on a folding phone as faithfully as a 3x one on a monitor.
 *
 *   **Nothing covers the playfield.** Fixed-pixel chrome over a canvas whose
 *   scale runs 0.8x to 3x is chrome that eats a third of a HUD panel on a
 *   phone. The menu button, the ping readout and copy-the-code live in `#shell`,
 *   which `layout()` parks in the thumb pad or a letterbox. Only the spectator
 *   strip sits over the canvas, and it is sized in `--cscale`.
 *
 * ZOOM: `user-scalable=no` is gone from index.html. iOS has ignored it since
 * iOS 10, so it only ever punished Android; WCAG 1.4.4 requires 200%; and the
 * play surfaces already opt out of browser gestures with `touch-action`, so
 * pinch costs the game nothing. The small type was raised to an 11 px floor as
 * well — zoom is a fallback, not a substitute for legible defaults.
 * ========================================================================== */

/* ==== TOKENS ============================================================= */

:root {
  --bg:        #07070c;
  --bg2:       #0b0b12;
  --panel:     #171722;
  --panel2:    #1f1f2e;
  --line:      #2e2e42;
  --bevel-hi:  #3b3b56;
  --bevel-lo:  #05050a;
  --ink:       #dfe3f0;
  --dim:       #8f95ad;
  --gold:      #f2c13d;
  --gold-dk:   #8a6208;
  --red:       #e2453c;
  /* 4.5:1 red, for text. `--red` is a 4.36:1 fill and stays one. */
  --red-lite:  #ff6b61;
  --blue:      #3d7fe0;
  --green:     #4bb85f;

  --font: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas,
          "DejaVu Sans Mono", monospace;

  /* Written by main.js#layout() every resize, from a *measurement* of the
     canvas the renderer produced — never from a size this file guessed. */
  --canvas-w: 352px;
  --canvas-h: 308px;
  --cscale: 1;          /* CSS px per logical px: 0.8 on a Fold, 3 on a desk */
  --hud-h: 52px;        /* height of the drawn HUD strip, already scaled */
  --pad-h: 0px;
  --app-max: 1056px;
  --chrome-top: 8px;    /* where the shell cluster may sit without covering */
  --chrome-right: 8px;
  --chrome-max: 320px;

  /* Touch geometry, also written by layout() so the bottom row of the pad is
     provably free of overlaps from 280 CSS px upward. */
  --stick-w: 52%;
  --coin-w: 120px;
  --coin-h: 56px;
  --btn-fire: 88px;
  --btn-magic: 70px;
  --pad-inset: 10px;
  --pad-gap: 8px;

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
}

/* ==== RESET ============================================================== */

*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.4;
  -webkit-text-size-adjust: 100%;
  /* Dragging on the play surface must never scroll the page or trigger the
     browser's pull-to-refresh. */
  overscroll-behavior: none;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
}

/* ...but a text field is for text. Without this, iOS suppresses the long-press
   Paste callout inside the code boxes, and the paste handler below has no
   gesture that can reach it. */
input, textarea {
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}

h1, h2, h3, p, ul { margin: 0; }
button { font: inherit; color: inherit; }
[hidden] { display: none !important; }

:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

.offscreen {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==== FRAME ============================================================== */

#app {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--safe-t) var(--safe-r) var(--safe-b) var(--safe-l);
  overflow: hidden;
}

#stage {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

#game {
  display: block;
  background: #000;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  /* Safety net, not a layout rule. The renderer sizes this element and is
     expected to fit the box it was handed; if a future build ever hands back
     something wider than the window, these three lines letterbox the frame
     instead of letting `#stage` clip columns off the left and right — losing
     4.5 of 22 tile columns at 280 px was how monsters used to walk in from
     off-screen. In the normal case the element's aspect already matches the
     bitmap's and `object-fit` does nothing at all. */
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* box-shadow, not border: it must not change the element's box, or the
     bitmap would be resampled off the pixel grid. */
  box-shadow:
    0 0 0 2px var(--line),
    0 0 0 4px var(--bevel-lo),
    0 0 48px rgba(0, 0, 0, 0.85);
  touch-action: none;
}

/* Thin chrome layer pinned exactly over the *painted* canvas. */
#hud {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--canvas-w);
  height: var(--canvas-h);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 6;
}
#hud > * { pointer-events: auto; }

/* ==== HUD CHROME (over the canvas — sized in --cscale) =================== */

/* The only element left over the playfield. It sits *below* the drawn HUD
   strip, so it can never land on a score or a health bar, and it scales with
   the canvas so it covers the same fraction of the dungeon at every size. */
.hud-spectate {
  position: absolute;
  left: 50%;
  top: calc(var(--hud-h) + 4px);
  transform: translateX(-50%);
  padding: calc(2px + 2px * var(--cscale)) calc(6px + 4px * var(--cscale));
  font-size: clamp(11px, calc(6px * var(--cscale) + 5px), 20px);
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--bg2);
  background: var(--gold);
  pointer-events: none;
  max-width: 96%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ==== SHELL CHROME (never over the canvas) =============================== */

#shell {
  position: absolute;
  inset: 0;
  z-index: 15;
  pointer-events: none;
}

.shell-cluster {
  position: absolute;
  top: var(--chrome-top);
  right: var(--chrome-right);
  max-width: var(--chrome-max);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  pointer-events: none;
}
.shell-cluster > * { pointer-events: auto; }

.shell-btn {
  min-width: 44px;
  min-height: 44px;
  padding: 0 12px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--ink);
  background: rgba(11, 11, 18, 0.92);
  border: 2px solid var(--line);
  box-shadow: inset 1px 1px 0 var(--bevel-hi), inset -1px -1px 0 var(--bevel-lo);
  cursor: pointer;
}
.shell-btn:disabled { opacity: 0.4; cursor: default; }
.shell-menu {
  width: 44px;
  padding: 0;
  font-size: 22px;
  line-height: 1;
}
.shell-copy.is-copied { border-color: var(--green); color: var(--green); }

/* Was 1.73:1 over a torch-lit floor, and 2.36:1 in its red state. It is now
   off the playfield entirely, and carries its own plate wherever it lands:
   15.3:1 in the letterbox, 13.5:1 in the worst-case fallback position. */
.shell-ping {
  min-height: 28px;
  display: flex;
  align-items: center;
  padding: 0 8px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: rgba(11, 11, 18, 0.92);
  border: 1px solid var(--line);
  pointer-events: none;
}
.shell-ping:empty { display: none; }
.shell-ping.is-bad { color: #ff8f86; border-color: #7a2a25; }

/* Last-resort placement: the window has no letterbox and no pad, so the
   cluster has to sit over the bottom-right corner of the dungeon. Shed
   everything but the menu button and give it an opaque plate. */
body.chrome-over .shell-copy,
body.chrome-over .shell-ping { display: none; }
body.chrome-over .shell-menu { background: var(--bg2); opacity: 0.9; }

/* The continue prompt for players with no thumb pad; on a phone the pad's own
   INSERT COIN button does this job, within reach of a thumb. */
.shell-continue {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 52px;
  padding: 8px 20px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: #1a1405;
  background: linear-gradient(180deg, #ffdd7a, var(--gold));
  border: 2px solid var(--gold-dk);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.7);
  cursor: pointer;
  animation: coin-pulse 900ms ease-in-out infinite alternate;
}
.shell-continue-hint { font-size: 11px; letter-spacing: 0.1em; opacity: 0.75; }
.shell-continue-secs { font-variant-numeric: tabular-nums; }
body.has-touch .shell-continue { display: none; }

@keyframes coin-pulse {
  from { filter: brightness(1); }
  to   { filter: brightness(1.35); }
}

/* ==== TOUCH CONTROLS ===================================================== */
/*
 * Bottom row, left to right: INSERT COIN, then MAGIC and FIRE. `layout()`
 * sizes all three from the window width, so they cannot overlap at 280 CSS px
 * and none of them is ever below the 44 px minimum. The stick zone stops
 * above the coin button rather than sharing space with it.
 */

#touch {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  max-width: var(--app-max);
  height: var(--pad-h);
  z-index: 10;
}
body:not(.has-touch) #touch { display: none; }

.tc { position: absolute; inset: 0; }

.tc-stickzone {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--stick-w);
  bottom: calc(var(--coin-h) + var(--pad-inset) * 2);
  touch-action: none;
}
.tc-stickzone::after {
  content: "MOVE";
  position: absolute;
  left: 14px;
  bottom: 8px;
  font-size: 11px;
  letter-spacing: 0.24em;
  /* Was rgba(143,149,173,0.45) — 2.18:1. Solid --dim on the app ground is
     6.77:1. */
  color: var(--dim);
}
.tc-stickzone.is-active::after { opacity: 0; }

.tc-stick { position: absolute; width: 0; height: 0; }
.tc-ring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 116px;
  height: 116px;
  margin: -58px 0 0 -58px;
  border-radius: 50%;
  border: 2px solid rgba(242, 193, 61, 0.35);
  background: radial-gradient(circle, rgba(242, 193, 61, 0.12), rgba(0, 0, 0, 0.3));
}
.tc-knob {
  position: absolute;
  left: 0;
  top: 0;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: linear-gradient(180deg, var(--bevel-hi), var(--panel));
  border: 2px solid var(--gold);
  box-shadow: 0 0 14px rgba(242, 193, 61, 0.35);
}

.tc-actions {
  position: absolute;
  right: var(--pad-inset);
  bottom: var(--pad-inset);
  display: flex;
  align-items: flex-end;
  gap: var(--pad-gap);
  touch-action: none;
}

.tc-btn {
  width: var(--btn-fire);
  height: var(--btn-fire);
  min-width: 44px;
  min-height: 44px;
  border-radius: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: 3px solid var(--bevel-lo);
  box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.22), 0 3px 0 var(--bevel-lo);
  cursor: pointer;
  touch-action: none;
  transition: transform 60ms linear, filter 60ms linear;
}
.tc-btn .tc-glyph { font-size: 24px; line-height: 1; }
.tc-btn .tc-label { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; }
.tc-fire { background: linear-gradient(180deg, #ff7a70, var(--red)); color: #2a0805; }
.tc-magic {
  width: var(--btn-magic);
  height: var(--btn-magic);
  background: linear-gradient(180deg, #8fbaff, var(--blue));
  color: #04122b;
}
.tc-btn.is-down { transform: translateY(3px) scale(0.95); filter: brightness(1.35); box-shadow: none; }

/* INSERT COIN: bottom-left, on the same row as FIRE, inside the same thumb
   arc. It used to live at the top of the pad — 327 px above the bottom of an
   iPhone SE — with a 15-second continue window to reach it in. */
.tc-coin {
  position: absolute;
  left: var(--pad-inset);
  bottom: var(--pad-inset);
  width: var(--coin-w);
  height: var(--coin-h);
  min-width: 44px;
  min-height: 44px;
  padding: 4px 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  color: var(--gold);
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 2px solid var(--gold-dk);
  box-shadow: inset 1px 1px 0 var(--bevel-hi), inset -1px -1px 0 var(--bevel-lo);
  cursor: pointer;
}
.tc-coin .tc-coin-head { font-size: 15px; font-weight: 700; letter-spacing: 0.08em; }
.tc-coin .tc-label { font-size: 11px; font-weight: 700; letter-spacing: 0.12em; }
.tc-coin .tc-coin-slot {
  width: 18px;
  height: 4px;
  background: var(--bevel-lo);
  box-shadow: inset 0 0 0 1px var(--gold-dk);
}
.tc-coin .tc-coin-secs { display: none; }
.tc-coin.is-down { filter: brightness(1.4); transform: translateY(2px); }

/* One tap armed it; the next one spends the quarter. */
.tc-coin.is-armed {
  color: #1a1405;
  background: linear-gradient(180deg, #ffdd7a, var(--gold));
  border-color: var(--gold);
}

/* Low health: no confirmation step, and hard to miss. */
.tc-coin.is-urgent {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(242, 193, 61, 0.5), inset 1px 1px 0 var(--bevel-hi);
  animation: coin-pulse 900ms ease-in-out infinite alternate;
}

/* Dead, inside the continue window: the hero cannot move, so the button takes
   the whole bottom row and says what it is for. */
.tc-coin.is-continue {
  left: var(--pad-inset);
  right: var(--pad-inset);
  width: auto;
  height: 64px;
  flex-direction: row;
  gap: 12px;
  z-index: 3;
  color: #1a1405;
  background: linear-gradient(180deg, #ffdd7a, var(--gold));
  border-color: var(--gold);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.7);
  animation: coin-pulse 900ms ease-in-out infinite alternate;
}
.tc-coin.is-continue .tc-coin-head { font-size: 17px; letter-spacing: 0.16em; }
.tc-coin.is-continue .tc-label { font-size: 12px; }
.tc-coin.is-continue .tc-coin-secs {
  display: block;
  font-size: 17px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ==== SCREENS ============================================================ */

#screens {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: calc(var(--safe-t) + 8px) calc(var(--safe-r) + 8px)
           calc(var(--safe-b) + 8px) calc(var(--safe-l) + 8px);
}
#screens.is-modal {
  background: rgba(4, 4, 9, 0.9);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* Passthrough screens (in game, spectating) leave the playfield visible and
   clickable; only their own children take pointer events. */
#screens.is-pass { pointer-events: none; background: none; padding: 0; }
#screens.is-pass .screen > * { pointer-events: auto; }

.screen {
  /* Positioned so it stacks over the attract backdrop, which is a fixed canvas
     at z-index 0 inside this same container. */
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--app-max);
  /* These three are never set on a screen by anything except `.is-secret`
     below, so this transition only ever runs when the takeover goes up. */
  transition: filter 320ms ease-out, opacity 320ms ease-out, transform 320ms ease-out;
}
.screen[hidden] { display: none !important; }

.sheet {
  width: 100%;
  max-width: 620px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 2px 0 12px;
}
.sheet-narrow { max-width: 460px; }
.sheet-wide { max-width: 900px; }

/* ==== THE THREE-ZONE FRAME =============================================== */
/*
 * The title and character-select screens own the viewport instead of floating
 * in it: a fixed head, a middle that scrolls, and a fixed foot.
 *
 *   ┌─ zone-head ──┐  marquee + best score   │  title + code + ready rail
 *   ├─ zone-scroll ┤  rooms, attract card    │  the four heroes
 *   └─ zone-foot ──┘  NEW GAME, JOIN         │  countdown, READY, LEAVE
 *
 * The head and the foot are laid out first and the middle takes what is left,
 * so the primary action of each screen cannot be pushed off the bottom at any
 * height — which is the whole reason this exists. A mobile audit found READY
 * below the fold at 375x537 and the old title screen deleting its own
 * best-score line by media query to fit 320x454; both were the same bug, an
 * unbounded column in a scrolling page.
 *
 * `align-items: stretch` is what hands the screen the container's full height,
 * and `overflow: hidden` stops the page scroller from fighting the inner one.
 */

body[data-screen="title"] #screens.is-modal,
body[data-screen="character"] #screens.is-modal {
  align-items: stretch;
  overflow: hidden;
}
.screen-title,
.screen-character {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.sheet-zones {
  flex: 1 1 auto;
  min-height: 0;
  gap: 8px;
  padding: 0;
}
.zone-head,
.zone-foot {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 8px;
  background: rgba(7, 7, 12, 0.86);
}
.zone-head { border-bottom: 2px solid var(--line); }
.zone-foot { border-top: 2px solid var(--line); }
.zone-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* `overflow-y: auto` computes `overflow-x` to `auto` as well, which clips.
     Four pixels of gutter keep a focus outline on an edge card from being
     sliced off; the selection ring is inset for the same reason. */
  padding: 2px 4px;
}

/* ==== ATTRACT BACKDROP =================================================== */
/*
 * A fixed, full-bleed canvas behind the title screen and anything opened from
 * it. `object-fit: cover` stretches a 256x160 (or 512x320) backing store over
 * whatever the viewport is; `ui/attract.js` picks which, so one dungeon pixel
 * never grows past about five CSS pixels even on a 2560 px monitor.
 * `render()` removes it outright the moment the base screen stops being the
 * title, so it costs nothing during a game.
 */

.attract-canvas {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
/* Under a panel the player opened themselves, the art steps back so the type
   on top of it keeps its contrast. */
body:not([data-screen="title"]) .attract-canvas { opacity: 0.42; }

/* ==== THE HIDDEN THING =================================================== */
/*
 * A full-screen takeover, opened by the combination in `ui/secret.js` and by
 * nothing else. It is a child of `#screens`, which is where it belongs in the
 * stack: over every screen and over the attract backdrop, under the error
 * toast, and under the connection notice at z-index 40 on `#app` — a socket
 * that drops mid-show still gets to say so.
 *
 * The canvas is sized in JavaScript, in CSS pixels, from `secret.js#stageBox`.
 * Nothing here guesses at it.
 */

#screens.is-secret { overflow: hidden; }
#screens.is-secret .screen {
  filter: blur(3px);
  opacity: 0.18;
  transform: scale(0.98);
  pointer-events: none;
}
#screens.is-secret .attract-canvas { opacity: 0.1; }

.secret {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--safe-t) + 12px) calc(var(--safe-r) + 12px)
           calc(var(--safe-b) + 12px) calc(var(--safe-l) + 12px);
  /* The same ink `ui/mushroom.js` letterboxes in, so the edge of the stage is
     not a visible rectangle on a monitor wider than the show. */
  background: #04050a;
  /* The whole surface is the skip target; nothing here scrolls or zooms. */
  touch-action: none;
  overscroll-behavior: contain;
}
.secret[hidden] { display: none !important; }

.secret-stage {
  display: block;
  max-width: 100%;
  max-height: 100%;
}

.secret-skip {
  position: absolute;
  top: calc(var(--safe-t) + 10px);
  right: calc(var(--safe-r) + 10px);
  min-width: 88px;
  min-height: 44px;
  padding: 0 18px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--dim);
  background: rgba(7, 7, 12, 0.72);
  border: 2px solid var(--line);
  cursor: pointer;
}
.secret-skip:hover,
.secret-skip:focus-visible {
  color: var(--ink);
  border-color: var(--gold);
}

/* The primary action of a long screen — READY, BACK, SUBMIT — never falls
   below the fold: it rides the bottom of the scroll container instead. A
   375x537 character select computes to more than one screen however hard the
   cards are squeezed, so the button stops depending on the arithmetic. */
.sheet-actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  padding: 8px 0 2px;
  margin-top: -2px;
  background: linear-gradient(180deg, rgba(7, 7, 12, 0), rgba(7, 7, 12, 0.92) 22%);
}

.panel {
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 2px solid var(--line);
  box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo);
  padding: 10px;
}
.panel-title {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--gold);
  margin-bottom: 8px;
}
.panel-title-lg { font-size: 24px; text-align: center; }
.panel-body { display: flex; flex-direction: column; gap: 8px; }

.lede { font-size: 12px; color: var(--dim); }
.hint { font-size: 12px; color: var(--dim); }
.form-error {
  font-size: 12px;
  font-weight: 700;
  color: var(--red-lite);
  border-left: 3px solid var(--red);
  padding-left: 8px;
}

.menu { display: flex; flex-wrap: wrap; gap: 8px; }
.menu > .btn { flex: 1 1 auto; }

.btn {
  min-height: 44px;
  min-width: 44px;
  padding: 10px 12px;
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--ink);
  background: linear-gradient(180deg, #2a2a3d, var(--panel));
  border: 2px solid var(--line);
  box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo);
  cursor: pointer;
}
.btn:hover:not(:disabled) { filter: brightness(1.2); }
.btn:active:not(:disabled) {
  box-shadow: inset -2px -2px 0 var(--bevel-hi), inset 2px 2px 0 var(--bevel-lo);
  transform: translateY(1px);
}
.btn:disabled { opacity: 0.38; cursor: default; }
.btn-primary {
  color: #1a1405;
  background: linear-gradient(180deg, #ffdd7a, var(--gold));
  border-color: var(--gold-dk);
}
.btn-danger {
  color: #fff;
  background: linear-gradient(180deg, var(--red), #7a1a15);
  border-color: #7a1a15;
}
.btn-sm { min-height: 44px; padding: 6px 12px; font-size: 11px; }

/* ==== TITLE ============================================================== */
/*
 * The marquee is a canvas, not type: `ui/logo.js` cuts DEEPHOLD out of the
 * HUD's own 5x7 font at three times the resolution, bevels it, keylines it and
 * bakes a shine sweep into a filmstrip. The element has a fixed backing store
 * (310 x 56) and an aspect of 155:28, so `width: 100%` sizes it against the
 * sheet with no measuring and no re-rasterising on resize.
 */

/*
 * The wordmark is also the touch door to the hidden thing in `ui/secret.js`:
 * eight taps, alternating between its left and right halves. It stays a
 * drawing and not a control — no cursor, no hover state, no focus ring — but
 * the two halves are targets, and a target is 44 px. The width cap moved off
 * the canvas and onto the hotspot so both agree, and the hotspot carries a
 * 48 px floor because the landscape media query squeezes the mark itself to
 * 43 px tall.
 */
.marquee { display: flex; justify-content: center; }
.marquee-tap {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 460px;
  min-height: 48px;
  padding: 2px 0;
  touch-action: manipulation;
}
.marquee-mark {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  filter: drop-shadow(0 0 18px rgba(242, 193, 61, 0.28));
  transition: filter 220ms ease-out, transform 220ms ease-out;
}
/* Half a combination in, and not one tap before, the mark answers back. Two
   stray taps must not advertise a secret to somebody who has not found one. */
.marquee-tap.is-near .marquee-mark {
  filter: drop-shadow(0 0 26px rgba(242, 193, 61, 0.66));
}
.marquee-tap.is-hit .marquee-mark {
  filter: drop-shadow(0 0 36px rgba(255, 244, 221, 0.92));
  transform: scale(1.05);
}
.tagline {
  text-align: center;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--dim);
}
/* 37 characters at 11px need 272 px of tracking-free advance; a 320 px
   viewport leaves 288. The spacing goes, not the words. */
@media (max-width: 399px) {
  .tagline { letter-spacing: 0; }
}

/* The all-time best, with its quarter count, on screen at every size — and the
   door to the full board, which is why it is a button and says so. */
.best {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
  min-height: 44px;
  padding: 7px 10px;
  text-align: left;
  color: var(--ink);
  background: linear-gradient(180deg, #1a1a28, #101019);
  border: 2px solid var(--line);
  box-shadow: inset 1px 1px 0 var(--bevel-hi), inset -1px -1px 0 var(--bevel-lo);
  cursor: pointer;
}
.best:hover { border-color: var(--gold-dk); }
.best-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.best-key { font-size: 11px; font-weight: 700; letter-spacing: 0.14em; color: var(--dim); }
.best-more { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; color: var(--gold); }
.best-bottom { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.best-val {
  flex: 0 0 auto;
  font-size: 20px;
  line-height: 1.1;
  font-weight: 700;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}
.best-who {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 11px;
  color: var(--green);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ==== ATTRACT CARD ======================================================= */
/* Three faces on a 6.5 s cycle. Only one is in the flow at a time, and the
   card has a floor height, so a swap never moves the rooms panel below it.
   The fade is an animation that restarts by itself when a face stops being
   `hidden` — no timer here touches opacity. */

.ac {
  min-height: 116px;
  padding: 10px;
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 2px solid var(--line);
  box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo);
}
.ac-face {
  display: flex;
  flex-direction: column;
  gap: 6px;
  animation: ac-in 420ms ease-out;
}
.ac-title { font-size: 11px; font-weight: 700; letter-spacing: 0.2em; color: var(--gold); }
.ac-lines { display: flex; flex-direction: column; gap: 3px; }
.ac-line { display: flex; gap: 6px; font-size: 11px; line-height: 1.4; }
.ac-key { flex: 0 0 52px; font-weight: 700; letter-spacing: 0.06em; color: #9ecbff; }
.ac-text { flex: 1 1 auto; min-width: 0; color: var(--ink); }

@keyframes ac-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* ==== TOGGLES ============================================================ */

.toggles { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.toggles-col { flex-direction: column; align-items: stretch; }

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 4px 8px;
  cursor: pointer;
  border: 2px solid var(--line);
  background: var(--panel);
}
.toggle-input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-box {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  background: var(--bg2);
  border: 2px solid var(--line);
}
.toggle-input:checked + .toggle-box { background: var(--gold); border-color: var(--gold-dk); }
.toggle-input:focus-visible + .toggle-box { outline: 3px solid var(--gold); outline-offset: 2px; }
.toggle-label { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; }

/* ==== ROOM LIST ========================================================== */
/*
 * The whole row is the button. It used to be a row *containing* a 44 px JOIN
 * button, which cost twelve pixels of height and a hundred of width per room
 * for a second target that did exactly what tapping the row would.
 */

.roomlist { display: flex; flex-direction: column; gap: 4px; }
.rm-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  padding: 6px 8px;
  text-align: left;
  color: var(--ink);
  background: var(--bg2);
  border: 1px solid var(--line);
  cursor: pointer;
}
.rm-row:hover:not(:disabled) { border-color: var(--gold-dk); background: #14141f; }
.rm-row:active:not(:disabled) { transform: translateY(1px); }
.rm-row:disabled { opacity: 0.45; cursor: default; }
.rm-main { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
.rm-name {
  font-size: 11px;
  line-height: 1.2;
  letter-spacing: 0.08em;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rm-code {
  font-size: 15px;
  line-height: 1.15;
  font-weight: 700;
  color: var(--gold);
  letter-spacing: 0.28em;
}
.rm-meta { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; }
.rm-count { font-size: 13px; line-height: 1.2; font-weight: 700; }
.rm-status { font-size: 11px; line-height: 1.2; color: var(--dim); letter-spacing: 0.06em; }
.rm-go { flex: 0 0 auto; font-size: 16px; line-height: 1; color: var(--dim); }
.rm-row:disabled .rm-go { visibility: hidden; }

/* ==== CODE ENTRY ========================================================= */

.code-entry { display: flex; gap: 8px; justify-content: center; }
.code-box {
  width: 56px;
  height: 68px;
  min-width: 44px;
  text-align: center;
  font-family: var(--font);
  font-size: 30px;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--gold);
  background: var(--bg2);
  border: 2px solid var(--line);
  box-shadow: inset 2px 2px 0 var(--bevel-lo);
}
/* A recoloured 2 px border is not a focus indicator you can find on a phone in
   daylight. Keep the recolour and add a real ring. */
.code-box:focus,
.name-input:focus {
  border-color: var(--gold);
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

.bigcode {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Wraps rather than overflows: at a 320 px viewport the caption, a
     0.3em-tracked four-character code and the COPY hint add up to 304 px of
     content in a 288 px zone. The rule below buys that back by tightening the
     code itself; `flex-wrap` is the guarantee for a font wider than the 0.6em
     the arithmetic assumes. */
  flex-wrap: wrap;
  gap: 12px;
  width: 100%;
  min-height: 52px;
  padding: 6px 14px;
  background: linear-gradient(180deg, var(--panel2), var(--bg2));
  border: 2px solid var(--gold-dk);
  box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo);
  cursor: pointer;
}
.bigcode-key { font-size: 11px; letter-spacing: 0.18em; color: var(--dim); }
.bigcode-val {
  font-size: clamp(24px, 7vw, 36px);
  font-weight: 700;
  letter-spacing: 0.3em;
  color: var(--gold);
}
.bigcode-copy { font-size: 11px; letter-spacing: 0.14em; color: var(--dim); }
@media (max-width: 399px) {
  .bigcode { gap: 8px; }
  .bigcode-val { font-size: 22px; letter-spacing: 0.22em; }
}
.bigcode.is-copied { border-color: var(--green); }
.bigcode.is-copied .bigcode-copy { color: var(--green); }

/* ==== CHARACTER SELECT =================================================== */
/*
 * Two columns of heroes on a phone, four from 640 px up — the same breakpoint
 * `screens.js#heroColumns()` reads back through `matchMedia`, so the arrow keys
 * move the way the grid looks.
 *
 * The portrait is a 20 x 20 canvas scaled by whole pixels: 100 CSS px (5x) in
 * the two-column layout, 120 (6x) in the four-column one, 80 (4x) on a short
 * landscape screen. Those all fit the narrowest card this layout ever makes —
 * 129 px of usable width, at a 320 px viewport.
 */

.char-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
}
.char-top .panel-title { margin-bottom: 0; }
.char-top .bigcode { flex: 0 1 auto; }

/* Everyone's state, in one line, above the cards that carry the detail. */
.rail { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; }
.rail-chips { display: flex; flex: 1 1 auto; gap: 4px; min-width: 0; }
.rail-chip {
  flex: 1 1 0;
  min-width: 0;
  padding: 2px 4px 2px 5px;
  font-size: 11px;
  line-height: 1.2;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--dim);
  background: rgba(255, 255, 255, 0.04);
  border-left: 3px solid var(--line);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-chip.is-seated {
  color: var(--hero-lite);
  background: rgba(255, 255, 255, 0.08);
  border-left-color: var(--hero);
}
.rail-chip.is-ready {
  color: #08060c;
  background: var(--hero);
  border-left-color: var(--hero-lite);
}
.rail-chip.is-ready::after { content: ' \2713'; }
.rail-text {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--gold);
}

.heroes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  align-items: stretch;
}
@media (min-width: 640px) {
  .heroes { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.hero-card {
  --portrait: 100px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  padding: 8px;
  text-align: left;
  color: var(--ink);
  background: linear-gradient(180deg, #191926, #101018);
  border: 2px solid var(--line);
  box-shadow: inset 1px 1px 0 var(--bevel-hi), inset -1px -1px 0 var(--bevel-lo);
  cursor: pointer;
}
@media (min-width: 640px) { .hero-card { --portrait: 120px; } }
@media (max-height: 520px) { .hero-card { --portrait: 80px; } }

.hero-card.is-mine {
  border-color: var(--hero);
  box-shadow: inset 0 0 0 3px var(--hero);
}
.hero-card.is-ready { background: linear-gradient(180deg, #14261a, #0e1512); }
.hero-card[aria-disabled="true"] { cursor: not-allowed; }
.hero-card[aria-disabled="true"] .hero-short,
.hero-card[aria-disabled="true"] .hero-blurb,
.hero-card[aria-disabled="true"] .hero-stats { opacity: 0.55; }

.hero-stage {
  display: flex;
  justify-content: center;
  padding: 0;
  background:
    radial-gradient(ellipse at 50% 86%, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0) 62%),
    linear-gradient(180deg, #0b0b13, #15151f);
  border: 1px solid var(--bevel-lo);
}
/* Taken heroes get a hatched plinth as well as a dimmed sprite: colour alone
   is not a state, and the sprite has to stay recognisable. */
.hero-card[aria-disabled="true"] .hero-stage {
  background: repeating-linear-gradient(135deg, #0b0b13 0 6px, #131320 6px 12px);
}
.hero-portrait {
  display: block;
  width: min(var(--portrait), 100%);
  height: auto;
  aspect-ratio: 1 / 1;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

.hero-short {
  font-size: 14px;
  line-height: 1.15;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--hero-lite);
}
.hero-name { font-size: 11px; line-height: 1.15; color: var(--dim); }
.hero-blurb { font-size: 11px; line-height: 1.35; }

/* Six stats in two columns of three. The label is a fixed 5 characters wide so
   every bar in the card starts at the same x, and the bar is segmented by a
   repeating gradient so a value stays countable at any width. */
.hero-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px 6px; }
.stat {
  display: grid;
  grid-template-columns: 5ch minmax(0, 1fr);
  align-items: center;
  gap: 4px;
  font-size: 11px;
  line-height: 1.2;
}
.stat-label { color: var(--dim); }
.sl-short { display: none; }
/* Below 400 px the card is 129 px wide and a 5-character label leaves the bar
   24 px. Three characters leave it 41. The screen reader is unaffected: the
   name it reads is on the track, and it is the full one either way. */
@media (max-width: 399px) {
  .stat { grid-template-columns: 3ch minmax(0, 1fr); }
  .sl-long { display: none; }
  .sl-short { display: inline; }
}
.stat-track {
  position: relative;
  height: 8px;
  background: #23233a;
  border: 1px solid #2e2e42;
  overflow: hidden;
}
.stat-fill { display: block; height: 100%; background: var(--hero, var(--gold)); }
.stat-track::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    90deg,
    rgba(0, 0, 0, 0) 0 calc(20% - 1px),
    rgba(7, 7, 12, 0.85) calc(20% - 1px) 20%
  );
}

.hero-taken {
  margin-top: auto;
  padding: 2px 5px;
  font-size: 11px;
  line-height: 1.2;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-align: center;
  color: var(--dim);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--line);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hero-card.is-mine .hero-taken {
  color: #08060c;
  background: var(--hero);
  border-color: var(--hero-lite);
}
.hero-card[aria-disabled="true"] .hero-taken {
  color: var(--hero-lite);
  background: rgba(0, 0, 0, 0.4);
  border-color: var(--hero-dark);
}

/* ==== COUNTDOWN ========================================================== */
/* In the foot zone, which cannot be scrolled away from, at the largest type
   size on the screen, with a bar that drains over LOBBY_COUNTDOWN_MS. */

.countdown {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-height: 32px;
  text-align: center;
}
.count-num {
  font-size: clamp(30px, 10vw, 56px);
  line-height: 1.1;
  font-weight: 700;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 22px rgba(242, 193, 61, 0.55);
}
.count-num:empty { display: none; }
.count-msg { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; color: var(--dim); }
.count-bar {
  display: none;
  width: 100%;
  height: 8px;
  background: var(--bg2);
  border: 2px solid var(--line);
  overflow: hidden;
}
.count-bar-fill { display: block; height: 100%; background: linear-gradient(90deg, var(--gold), #ffdd7a); }

.screen-character.is-counting .count-bar { display: block; }
.screen-character.is-counting .count-msg { color: var(--gold); letter-spacing: 0.2em; }
.screen-character.is-counting .zone-foot {
  border-top-color: var(--gold-dk);
  box-shadow: 0 -2px 26px rgba(242, 193, 61, 0.22);
  animation: count-pulse 1000ms steps(2, end) infinite;
}
@keyframes count-pulse {
  0%, 100% { border-top-color: var(--gold-dk); }
  50% { border-top-color: var(--gold); }
}

/* READY is the point of the screen, so it takes two thirds of the bar. */
.btn-ready { flex: 2 1 auto; font-size: 14px; }
.btn-ready.is-on { background: linear-gradient(180deg, #8ef0a2, var(--green)); border-color: #12471f; }

/* ==== SPECTATING ========================================================= */

/* The spectator chrome hugs the bottom of the viewport so it never covers the
   fight it is describing. */
#screens.is-pass .screen-spectate {
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  padding: 0 8px calc(var(--safe-b) + 8px);
}
.spectate-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  max-width: 460px;
  padding: 8px;
  background: rgba(7, 7, 12, 0.9);
  border: 2px solid var(--line);
}
.spectate-bar .bigcode { flex: 1 1 auto; min-height: 48px; }

.modal {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(4, 4, 9, 0.88);
}
.modal-card {
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 2px solid var(--gold-dk);
  box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo);
}
.offer-bar {
  height: 14px;
  background: var(--bg2);
  border: 2px solid var(--line);
  overflow: hidden;
}
.offer-bar-fill {
  height: 100%;
  width: 100%;
  background: linear-gradient(90deg, var(--gold), #ffdd7a);
}
.offer-time { font-size: 12px; color: var(--dim); text-align: center; }
.offer-secs { color: var(--gold); font-weight: 700; }

/* ==== GAME OVER + SCORES ================================================= */

.go-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.go-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 4px;
  background: var(--bg2);
  border: 1px solid var(--line);
}
.go-key { font-size: 11px; letter-spacing: 0.12em; color: var(--dim); }
.go-value { font-size: clamp(18px, 6vw, 26px); font-weight: 700; color: var(--gold); }
.go-result { font-size: 13px; font-weight: 700; color: var(--green); text-align: center; }

.name-entry { display: flex; flex-direction: column; gap: 8px; }
.field-label { font-size: 11px; letter-spacing: 0.14em; color: var(--dim); }
.name-input {
  min-height: 52px;
  padding: 8px 12px;
  font-family: var(--font);
  font-size: 24px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
  background: var(--bg2);
  border: 2px solid var(--line);
}

/* The header and the rows share one column track list. It used to be written
   in `em` on two elements with different font sizes (10 px and 12 px), so the
   same declaration resolved to different widths and SCORE sat 12 px right of
   its own numbers. `rem` is root-relative: one value, one result. */
.scorescroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.scoretable { display: flex; flex-direction: column; }
.score-head, .score-row, .score-empty { min-width: 19rem; }
/* Narrow phones get narrower columns instead of a sideways scroll. */
@media (max-width: 360px) {
  .score-head, .score-row, .score-empty { min-width: 0; }
  .score-head, .score-row {
    grid-template-columns: 1.6rem minmax(0, 1fr) 4.6rem 2rem 2rem;
    gap: 4px;
    font-size: 11px;
  }
}
.score-head, .score-row {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr) 5.4rem 2.6rem 2.6rem;
  gap: 6px;
  padding: 5px 4px;
  font-size: 12px;
  align-items: center;
}
.score-head {
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--dim);
  border-bottom: 2px solid var(--line);
}
.score-row:nth-child(odd) { background: rgba(255, 255, 255, 0.03); }
.score-row.is-you { background: rgba(242, 193, 61, 0.16); color: var(--gold); font-weight: 700; }
.sc-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-score, .sc-q, .sc-lvl { text-align: right; font-variant-numeric: tabular-nums; }
.score-empty { padding: 14px 4px; font-size: 12px; color: var(--dim); text-align: center; }

/* ==== CONNECTION NOTICE ================================================== */

.conn {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(4, 4, 9, 0.9);
}
.conn-card {
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px;
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 2px solid var(--red);
  box-shadow: inset 2px 2px 0 var(--bevel-hi), inset -2px -2px 0 var(--bevel-lo);
}

.noscript {
  padding: 40px 20px;
  text-align: center;
  color: var(--ink);
}

/* ==== LANDSCAPE PHONE ==================================================== */
/* The controls leave the flow and hug the left and right edges. They stay
   inside the safe-area insets — a fixed `inset: 0` put the stick zone under a
   59 px display cutout — and inside the letterbox, so the opaque knob never
   lands on the dungeon. `--stick-w` is the measured letterbox width. */

body.is-landscape.has-touch #touch {
  position: fixed;
  top: var(--safe-t);
  right: var(--safe-r);
  bottom: var(--safe-b);
  left: var(--safe-l);
  height: auto;
  max-width: none;
  pointer-events: none;
}
body.is-landscape.has-touch .tc > * { pointer-events: auto; }
body.is-landscape .tc-coin { max-width: calc(var(--stick-w) - var(--pad-inset) * 2); }
body.is-landscape .tc-coin.is-continue { max-width: none; }

/* Short viewports, the front-of-house half: the marquee gives up width, the
   tagline (decoration, not an affordance) goes, and in landscape the marquee
   and the best score share one row so the hero grid keeps its height. Nothing
   the player can act on is ever hidden — only scrolled to. */
@media (max-height: 520px) {
  .marquee-tap { max-width: 300px; }
  .ac { min-height: 0; }
}
@media (max-height: 430px) {
  .tagline { display: none; }
  .marquee-tap { max-width: 240px; }
  .count-num { font-size: clamp(26px, 7vh, 40px); }
}
@media (max-height: 430px) and (min-width: 640px) {
  .screen-title .zone-head { flex-direction: row; align-items: center; }
  .screen-title .marquee { flex: 0 0 240px; }
  .screen-title .best { flex: 1 1 auto; }
  .hero-blurb { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* Short viewports: shrink the ring rather than losing the playfield. The
   button sizes come from layout(), which already knows the letterbox width. */
@media (max-height: 420px) {
  :root { --coin-h: 50px; }
  .tc-btn .tc-glyph { font-size: 18px; }
  .tc-ring { width: 92px; height: 92px; margin: -46px 0 0 -46px; }
  .tc-knob { width: 44px; height: 44px; }
}

/* ==== DESKTOP ============================================================ */

@media (hover: hover) and (pointer: fine) {
  .shell-menu { opacity: 0.75; }
  .shell-menu:hover { opacity: 1; }
}

/* ==== REDUCED MOTION ===================================================== */
/* main.js also mirrors this onto the renderer (screen shake, heavy particles);
   this block only covers the DOM chrome. */

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

/* Errors, above every overlay — the announcer banner is only visible in game. */
.toast {
  position: fixed;
  left: 50%;
  top: calc(var(--safe-t) + 10px);
  transform: translateX(-50%);
  z-index: 60;
  max-width: min(92vw, 520px);
  padding: 10px 14px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--ink);
  background: linear-gradient(180deg, #3a1614, #1c0b0a);
  border: 2px solid var(--red);
  box-shadow: inset 1px 1px 0 var(--bevel-hi), 0 6px 24px rgba(0, 0, 0, 0.7);
  pointer-events: none;
}
