/* Self-hosted VT323 (Open Font License, see fonts/OFL.txt) — no external CDN at runtime. */
@font-face {
  font-family: 'VT323';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/vt323-latin-400-normal.woff2') format('woff2');
}

/* Self-hosted Monomaniac One (Open Font License, see fonts/MonomaniacOne-OFL.txt) — used ONLY for the
   enemy "Matrix katakana" rendering (subset to the katakana block U+30A0-30FF). No external CDN. */
@font-face {
  font-family: 'Monomaniac One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/monomaniac-one-katakana-400-normal.woff2') format('woff2');
}

/* Self-hosted Iosevka Charon Mono (Open Font License, see fonts/IosevkaCharonMono-OFL.txt) — a true
   monospace with full box-drawing, block-element AND Braille-pattern coverage, used ONLY for the attrezzo
   (background scenery) glyphs on the canvas, so box-art and Braille figures render cleanly. Subset to
   exactly the attrezzo figure charset (printable ASCII U+0021-007E, Box Drawing U+2500-257F, Block
   Elements U+2580-259F, Braille Patterns U+2800-28FF — matching the server's FIGURE_GLYPH_RANGES). No
   external CDN. */
@font-face {
  font-family: 'Iosevka Charon Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/iosevka-charon-mono-figures-400-normal.woff2') format('woff2');
}

/* Theme tokens. The colour palette lives in client/src/config/palette.ts and is mirrored onto :root
   as --color-<family>-<shade> custom properties at startup (see client/src/theme.ts), so these
   semantic aliases — and every colour in this file — resolve to that single source of truth instead
   of duplicating hex. --bg carries a baked oklch fallback (identical to slate-950) so the first
   paint, before applyPalette() runs, already shows the right background (no flash). The resource
   tokens (energy/memory/bytes) intentionally match the bonus colours the board paints. */
:root {
  --bg: var(--color-slate-950, oklch(12.9% 0.042 264.695));
  --fg: var(--color-slate-300);
  --dim: var(--color-slate-600);
  --locked: var(--color-slate-700); /* darker, muted tone for locked shop rows */
  --accent: var(--color-cyan-400);
  --energy: var(--color-red-400); /* matches the energy bonus on the board */
  --memory: var(--color-emerald-400); /* matches the memory bonus on the board */
  --bytes: var(--color-amber-400); /* matches the bytes bonus on the board */
  --danger: var(--color-red-500); /* death modal & damage-flash red */
  --rail-w: 216px; /* shared width of the right-hand column boxes: RANK (top) and the minimap (bottom) */
  /* Level-up rarity ramp (COMMON -> LEGENDARY): tints the upgrade options and the MAX marker. COMMON is
     the muted stat-NAME tone (slate-400, the same as .hud-name — not the dim 0x/bracket chrome, nor the
     brighter --fg value tone) so a common option reads as an understated stat. */
  --r-common: var(--color-slate-400);
  --r-uncommon: var(--memory);
  --r-rare: var(--accent);
  --r-epic: var(--color-purple-400);
  --r-legendary: var(--bytes);
}

* {
  box-sizing: border-box;
}

/* Keyboard-focus ring in the theme accent, so tabbing through buttons and links shows a themed
   outline instead of the browser default (which would flash white). Inputs keep their own focus
   border + glow — they set outline: none, which wins by source order. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

body {
  margin: 0;
  height: 100vh;
  overflow: hidden;
  background: var(--bg);
  color: var(--fg);
  font-family: 'VT323', monospace;
}

/* The game canvas fills the whole viewport; the camera keeps the player centred. The CSS background
   only shows for the instant before the first render; the renderer then paints the playfield. */
#board {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  background: var(--bg);
  z-index: 0;
}

/* Info panels float above the canvas as translucent boxes — a slight transparency lets the board
   bleed through without getting in the way (a small backdrop blur keeps the text crisp). Corners
   are always square. */
.overlay {
  position: fixed;
  z-index: 10;
  background: color-mix(in oklch, var(--color-slate-900) 90%, transparent);
  border: 1px solid var(--dim);
  padding: 8px 14px;
  box-shadow: 0 0 24px color-mix(in oklch, var(--color-black) 60%, transparent);
  backdrop-filter: blur(2px);
}

/* PLATFORM box wordmark: the TYPEOVER title matches the access modal's title (.modal-title) —
   same colour and glow — so the game name reads identically on the start screen and in-game. */
.panel-title {
  font-size: 20px;
  letter-spacing: 0.16em;
  color: var(--memory);
  text-shadow: 0 0 14px color-mix(in oklch, var(--memory) 50%, transparent);
}

/* PLATFORM meta line under the wordmark (SERVICES + PING): small and dim, the labels in the muted
   tone with their status value coloured + bold (stat-*). */
.platform-meta {
  display: flex;
  gap: 12px;
  align-items: baseline;
  font-size: 13px;
  color: var(--dim);
  margin-top: 4px;
}

/* SERVICES and PING each take half the row, so neither crowds the other. */
.pf-cell {
  flex: 1;
}

/* Status values (SERVICES OK/OFF, PING ms): always bold, coloured by state — green optimal/up, amber
   high, red too-high/down. */
.stat-ok {
  color: var(--memory);
  font-weight: 700;
}
.stat-warn {
  color: var(--bytes);
  font-weight: 700;
}
.stat-down {
  color: var(--danger);
  font-weight: 700;
}

/* Brief red flash hugging the viewport edge when the local player takes damage: a tight, concentrated
   inner vignette in the danger red (red-500). */
body.hit::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  box-shadow: inset 0 0 60px color-mix(in oklch, var(--danger) 70%, transparent);
}

/* ---- the HUD PLATES (DESIGN_SYSTEM.md §3) ----
   The always-on HUD is three corner-bracketed terminal plates over the board: OPERATOR (top-left),
   SECTOR (top-right) and the COMMAND DECK (bottom-centre). One chrome, everywhere: a translucent
   near-black face, a faint hairline, glowing CORNER BRACKETS in the plate accent, and a ▌-tagged
   section label. The accent retints per plate state (--plate-accent). */
.plate {
  position: fixed;
  z-index: 10;
  --plate-accent: color-mix(in oklch, var(--accent) 60%, transparent);
  background: color-mix(in oklch, var(--color-slate-950) 82%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent) 12%, transparent);
  padding: 10px 14px 12px;
  backdrop-filter: blur(3px);
}

/* The corner brackets: four L-shaped ticks painted as 8 no-repeat gradient strokes, sitting on the
   hairline (inset -1px) so the bracket caps the border. Shared by the MENU panel (the same chrome,
   console-sized). */
.plate::before,
.menu-box::before {
  content: '';
  position: absolute;
  inset: -1px;
  pointer-events: none;
  background-image:
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent)),
    linear-gradient(var(--plate-accent), var(--plate-accent));
  background-position:
    left top,
    left top,
    right top,
    right top,
    left bottom,
    left bottom,
    right bottom,
    right bottom;
  background-size:
    14px 2px,
    2px 14px,
    14px 2px,
    2px 14px,
    14px 2px,
    2px 14px,
    14px 2px,
    2px 14px;
  background-repeat: no-repeat;
}

/* The plate's section tag: `▌LABEL`, small and letterspaced, the ▌ tick in the live accent. */
.plate-tag {
  font-size: 12px;
  letter-spacing: 0.3em;
  color: var(--color-slate-500);
  margin-bottom: 7px;
}
.plate-tag::before {
  content: '▌';
  margin-right: 7px;
  color: var(--accent);
}
.plate-tag-sub {
  margin-top: 10px;
}

/* The segmented gauge (built by format.ts → segBar): a dim segment track under a themed, glowing
   segment fill whose width is the value's fraction. Variants: .wide (deck modules), .xp (OPERATOR). */
.seg-bar {
  position: relative;
  height: 10px;
  width: 150px;
  background: repeating-linear-gradient(
    90deg,
    color-mix(in oklch, var(--fg) 13%, transparent) 0 5px,
    transparent 5px 7px
  );
}
.seg-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: repeating-linear-gradient(90deg, var(--theme, var(--fg)) 0 5px, transparent 5px 7px);
  box-shadow: 0 0 9px color-mix(in oklch, var(--theme, var(--fg)) 40%, transparent);
}
.seg-bar.wide {
  width: 190px;
  height: 13px;
}
.seg-bar.xp {
  height: 7px;
  flex: 1;
  width: auto;
}

/* HUD ALARM (the deck's ENERGY module below the low threshold; SECTOR's BOSS line): the block flips
   to the danger tone and pulses — survival information must move, not just recolour. Reduced-motion
   holds it statically lit (the colour stays; only the throb goes). .oc-live is the same urgency beat,
   faster, for the live Overclock window. */
.hud-alarm {
  animation: hud-alarm 0.9s ease-in-out infinite;
}
.hud-alarm .mod-label,
.hud-alarm .mod-value,
.hud-alarm span,
.hud-alarm b {
  color: var(--danger) !important;
}
.oc-live {
  animation: hud-alarm 0.7s ease-in-out infinite;
}
@keyframes hud-alarm {
  50% {
    filter: brightness(1.7);
  }
}
/* The Overclock READY tell: the meter is fully charged and HELD (it never auto-fires) — a slow, flashy
   breathing glow that begs to be pressed. The whole module is the IGNITE button (data-ignite), so it
   takes the pointer; the keycap chip names its hotkey. */
.oc-ready {
  animation: oc-ready 1.1s ease-in-out infinite;
  cursor: pointer;
}
.oc-ready .mod-label {
  color: var(--theme);
  text-shadow: 0 0 10px currentColor;
}
@keyframes oc-ready {
  50% {
    filter: brightness(1.8);
    box-shadow: 0 0 18px color-mix(in oklch, var(--theme) 70%, transparent);
  }
}
.oc-key {
  display: inline-block;
  padding: 0 5px;
  margin-right: 1ch;
  border: 1px solid currentColor;
  border-radius: 3px;
  font: inherit;
}

/* ---- OPERATOR (top-left): who you are and what you have ---- */
#plate-id {
  top: 14px;
  left: 14px;
  min-width: 232px;
}

/* The nick, in YOUR ink colour (--theme set inline from the colour directory) with a soft glow — the
   one place the HUD says "this colour on the board is you". */
.id-name {
  font-size: 24px;
  letter-spacing: 0.05em;
  color: var(--theme);
  text-shadow: 0 0 12px color-mix(in oklch, var(--theme) 45%, transparent);
}

/* LV + the level numeral + the XP progress gauge, one line: the bar fills toward the next level. */
.id-level {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 5px;
}
.id-lv {
  font-size: 12px;
  letter-spacing: 0.2em;
  color: var(--dim);
}
.id-lv-num {
  font-size: 24px;
  line-height: 1;
  min-width: 2ch;
  color: var(--memory);
  text-shadow: 0 0 10px color-mix(in oklch, var(--memory) 45%, transparent);
}

.id-bytes {
  margin-top: 4px;
  font-size: 14px;
  letter-spacing: 0.12em;
  color: var(--dim);
}
.id-bytes b {
  font-size: 18px;
  font-weight: 400;
  color: var(--bytes);
}

/* PURSE pulse (§12 typing/painting juice): the BYTES counter flashes briefly when the purse rises —
   the landing beat of the bytes float drifting to it. A one-shot brightness kick (the class is
   timer-removed by ui/hud/panel), not a standing loop. */
.purse-pulse .id-bytes b {
  text-shadow: 0 0 12px currentColor;
  filter: brightness(1.5);
}
/* The XP-CHUNK flash (a kill's payout / a multi-cell claim / a level-up): the LV gauge mirrors the
   purse pulse so combat XP is FELT, not just banked — per-keystroke paint never triggers it. */
.xp-pulse .id-level {
  text-shadow: 0 0 12px var(--memory);
  filter: brightness(1.5);
}

/* The unspent-points chip: hidden at zero; while points wait it pulses in the memory colour and opens
   the MENU (landing on UPGRADES) on click. In flow RIGHT UNDER the XP gauge, spanning the plate's
   full width (the bytes line follows it) — no layout reads. */
.pts-chip {
  display: none;
  appearance: none;
  cursor: pointer;
  width: 100%;
  margin-top: 8px;
  padding: 3px 12px;
  background: color-mix(in oklch, var(--memory) 10%, transparent);
  border: 1px solid var(--memory);
  color: var(--memory);
  font:
    700 15px 'VT323',
    monospace;
  letter-spacing: 0.14em;
  animation: pts-pulse 1.2s ease-in-out infinite;
}
.pts-chip.show {
  display: block;
}
.pts-chip:hover {
  color: var(--fg);
  border-color: var(--fg);
}
@keyframes pts-pulse {
  0%,
  100% {
    box-shadow: 0 0 4px color-mix(in oklch, var(--memory) 20%, transparent);
  }
  50% {
    box-shadow: 0 0 16px color-mix(in oklch, var(--memory) 70%, transparent);
  }
}

/* OPERATOR's system footer: NET/PING meta on the left, the MENU launcher on the right. */
.plate-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid color-mix(in oklch, var(--dim) 35%, transparent);
  font-size: 13px;
  color: var(--dim);
}
.meta-cell {
  margin-right: 10px;
}

/* Status values (NET OK/OFF, PING ms, wave PHASE): bold, coloured by state — green good, amber
   strained, red down/danger. */
.stat-ok {
  color: var(--memory);
  font-weight: 700;
}
.stat-warn {
  color: var(--bytes);
  font-weight: 700;
}
.stat-down {
  color: var(--danger);
  font-weight: 700;
}

/* ---- SECTOR (top-right): where you are and what is coming ---- */
#plate-world {
  top: 14px;
  right: 14px;
  width: 232px;
}

/* The minimap canvas scales to the plate, crisp (nearest-neighbour upscale of its fixed backing). */
#minimap {
  display: block;
  width: 100%;
  image-rendering: pixelated;
  border: 1px solid color-mix(in oklch, var(--dim) 35%, transparent);
}

#sector-info {
  margin-top: 7px;
}
.sec-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 15px;
  letter-spacing: 0.06em;
  margin: 2px 0;
}
.sec-row span {
  color: var(--dim);
}
.sec-row b {
  font-weight: 400;
  color: var(--fg);
}

/* RECORDS rows: rank · nick (in that player's ink colour) · score; the viewer's own line leads,
   highlighted. The players-online count closes the block as a quiet footer. */
.rec-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 15px;
  margin: 2px 0;
  padding: 0 4px;
}
.rec-pos {
  width: 2ch;
  text-align: right;
  color: var(--dim);
}
.rec-nick {
  flex: 1;
  overflow: hidden;
  color: var(--theme);
}
.rec-score {
  color: var(--fg);
}
.rec-row.you {
  background: color-mix(in oklch, var(--theme) 14%, transparent);
  outline: 1px solid color-mix(in oklch, var(--theme) 35%, transparent);
}
.rec-meta {
  margin-top: 7px;
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--dim);
}
.rec-meta b {
  font-weight: 400;
  color: var(--fg);
}

/* ---- COMMAND DECK (bottom-centre): what keeps you alive ---- */
#deck {
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: stretch;
  gap: 16px;
  padding: 10px 18px 12px;
}

/* A deck module (ENERGY / FLOW): the small label over the gauge with its reading beside it. */
.mod {
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  gap: 3px 10px;
}
.mod-label {
  grid-column: 1 / -1;
  font-size: 12px;
  letter-spacing: 0.26em;
  color: var(--color-slate-500);
}
.mod-value {
  font-size: 27px;
  line-height: 1;
  min-width: 2.2ch;
  color: var(--theme);
  text-shadow: 0 0 10px color-mix(in oklch, var(--theme) 45%, transparent);
}

/* The FLOW module's readings: the streak ×multiplier (lit cyan while a chain is alive) over the
   WPM/accuracy flex. */
.flow-meta {
  display: flex;
  flex-direction: column;
  line-height: 1.1;
}
.flow-streak {
  font-size: 17px;
  color: var(--dim);
}
.flow-streak.live {
  color: var(--theme);
  text-shadow: 0 0 8px color-mix(in oklch, var(--theme) 45%, transparent);
}
.flow-wpm {
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--dim);
}

/* The deck's CENTRE column: the FLOW (overclock) gauge over the ENERGY gauge, fenced from the two
   hotbar halves by hairline dividers (the fences the old dock carried). The modules STRETCH so both
   share one left edge — their labels and (equal-width) gauges read as one aligned column. */
.deck-center {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  padding: 0 16px;
  border-left: 1px solid color-mix(in oklch, var(--dim) 35%, transparent);
  border-right: 1px solid color-mix(in oklch, var(--dim) 35%, transparent);
}

/* The HACK HOTBAR: eight numbered slots in unlock order, split 1-4 | 5-8 around the centre gauges. */
.deck-dock {
  display: flex;
  align-items: stretch;
  gap: 8px;
}

/* A hotbar slot: its NUMBER key above the chip. A filled slot is a pointer target (a click casts);
   a locked one keeps an EMPTY frame, so the row never reflows and a number always means one hack. */
.slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}
.slot-key {
  font-size: 11px;
  line-height: 1;
  color: var(--dim);
}
.slot[data-hack] {
  cursor: pointer;
}
.slot[data-hack]:hover .chip {
  background: color-mix(in oklch, var(--theme) 16%, transparent);
}

/* A hack chip: the literal cast command (/NAME) over its bytes cost, framed in the hack's colour;
   the remaining cooldown is a dark wipe that drains downward as the hack recharges. States:
   READY (full colour + glow) · COOLING (greyed, the wipe is the live part) · BROKE (lit, the COST in
   the danger tone — the bytes are the blocker). */
.chip {
  position: relative;
  overflow: hidden;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 76px;
  padding: 5px 9px 4px;
  text-align: center;
  border: 1px solid color-mix(in oklch, var(--theme) 55%, transparent);
  background: color-mix(in oklch, var(--theme) 7%, transparent);
}
/* A locked slot's empty frame: the chip's footprint with no content, in the muted tone. */
.chip-empty {
  min-height: 42px;
  border-color: color-mix(in oklch, var(--dim) 30%, transparent);
  background: color-mix(in oklch, var(--dim) 5%, transparent);
}
.chip-cd {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: color-mix(in oklch, var(--color-black) 62%, transparent);
}
.chip-name {
  position: relative;
  font-size: 16px;
  letter-spacing: 0.04em;
  color: var(--theme);
}
.chip-cost {
  position: relative;
  font-size: 13px;
  color: var(--bytes);
}
.chip.ready {
  box-shadow: 0 0 12px color-mix(in oklch, var(--theme) 30%, transparent);
}
.chip.ready .chip-name {
  text-shadow: 0 0 9px color-mix(in oklch, var(--theme) 65%, transparent);
}
.chip.cooling {
  border-color: color-mix(in oklch, var(--locked) 60%, transparent);
}
.chip.cooling .chip-name {
  color: var(--locked);
}
.chip.cooling .chip-cost {
  color: var(--locked);
}
.chip.broke .chip-cost {
  color: var(--danger);
  font-weight: 700;
}

/* The MENU accordion section headers (PLAYER stat groups / HACKS list) keep the muted header tone. */
.hack-head,
.stat-head {
  color: var(--color-slate-500);
}

.modal {
  background: color-mix(in oklch, var(--color-slate-950) 90%, transparent);
  border: 2px solid var(--memory);
  box-shadow: 0 0 40px color-mix(in oklch, var(--memory) 35%, transparent);
  padding: 24px 28px;
  text-align: center;
  backdrop-filter: blur(2px);
}

.modal-title {
  color: var(--memory);
  font-size: 30px;
  letter-spacing: 0.12em;
  margin-bottom: 16px;
  text-shadow: 0 0 12px color-mix(in oklch, var(--memory) 50%, transparent);
}

/* The UPGRADES tab: the level-up offer as real trading CARDS (3 across) and a row of real action BUTTONS.
   A card is a bordered panel with a top accent strip in its colour (--theme, set inline per card): a
   rarity/kind TAG, the NAME, the resulting VALUE as the hero (large, glowing by rarity) and a one-line
   gloss along the bottom. No 0x… cells or [---] brackets here — this is a roomy menu page, not the
   compact CORE panel. The grid's gap owns the spacing (no per-card margin). */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-top: 10px;
}
.card {
  position: relative; /* anchors the UNLOCK card's rainbow border/glow pseudo-elements + the key badge */
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 128px;
  padding: 10px 12px 11px;
  cursor: pointer;
  color: var(--fg);
  border: 1px solid color-mix(in oklch, var(--theme) 45%, transparent);
  border-top: 3px solid var(--theme);
  border-radius: 3px;
  background:
    repeating-linear-gradient(
      0deg,
      color-mix(in oklch, var(--color-black) 16%, transparent) 0 1px,
      transparent 1px 3px
    ),
    linear-gradient(
      180deg,
      color-mix(in oklch, var(--theme) 14%, transparent),
      color-mix(in oklch, var(--theme) 4%, transparent)
    );
  transition:
    transform 80ms ease,
    border-color 80ms ease,
    box-shadow 80ms ease;
}
.card:hover {
  transform: translateY(-3px);
  border-color: color-mix(in oklch, var(--theme) 80%, transparent);
  box-shadow: 0 8px 20px -6px color-mix(in oklch, var(--theme) 60%, transparent);
}
.card:active {
  transform: translateY(0);
}
/* The KEY BADGE: the digit that picks this card from the keyboard, pinned to the top-right corner —
   shown exactly where the eye checks "how do I take this one fast". Lights up on hover. */
.card-key {
  position: absolute;
  top: 6px;
  right: 7px;
  min-width: 16px;
  text-align: center;
  /* The top padding optically centres the digit: VT323 rides high inside its em box (see .up-key). */
  padding: 2px 4px 0;
  font:
    700 13px/1 'VT323',
    monospace;
  color: var(--dim);
  border: 1px solid color-mix(in oklch, var(--dim) 60%, transparent);
  background: color-mix(in oklch, var(--color-slate-950) 70%, transparent);
}
.card:hover .card-key {
  color: var(--theme);
  border-color: var(--theme);
}
/* TAG: the rarity tier / UNLOCK label, small and wide-tracked in the card colour. */
.card-tag {
  color: var(--theme);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
/* NAME: the stat / attribute / hack name. */
.card-name {
  color: var(--fg);
  font-size: 19px;
  line-height: 1.05;
  letter-spacing: 0.04em;
}
/* VALUE: the hero — what the card would compute to, anchored in the card's lower third. */
.card-val {
  margin-top: auto;
  color: var(--theme);
  font-size: 30px;
  line-height: 1;
}
/* DESC: a one-line gloss along the bottom edge. */
.card-desc {
  color: var(--dim);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Higher rarities radiate: an ambient outer glow that ramps from RARE up to LEGENDARY, so a hot pull
   reads across the room before the tag is even read. */
.card.r3 {
  box-shadow: 0 0 10px -2px color-mix(in oklch, var(--theme) 30%, transparent);
}
.card.r4 {
  box-shadow: 0 0 14px -2px color-mix(in oklch, var(--theme) 45%, transparent);
}
.card.r5 {
  box-shadow: 0 0 20px -2px color-mix(in oklch, var(--theme) 60%, transparent);
}
/* An UNLOCK card is set apart from every rarity tier by a FAST-ROTATING RAINBOW border + glow + tag (no
   single tier colour). The animated angle drives a conic gradient: `::before` shows it as a crisp 2px ring
   (masked out of the content box, so it never bleeds onto the face), `::after` is a blurred masked copy
   behind for the glow halo, and the UNLOCK tag fills its own text with the same rotating rainbow. The hack
   word and the card's gradient face, by contrast, both ride the hack's own assigned colour (--theme): the
   hack NAME sits bottom-aligned in the value slot (.card-val), beside the gloss. */
@property --rainbow-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
@keyframes rainbow-spin {
  to {
    --rainbow-angle: 360deg;
  }
}
.card.unlock {
  border: 2px solid transparent; /* the visible ring is the ::before gradient, not this border */
  background: linear-gradient(
    180deg,
    color-mix(in oklch, var(--theme) 18%, transparent),
    color-mix(in oklch, var(--theme) 7%, transparent)
  );
  background-clip: padding-box; /* the hack-coloured face stays inside the rainbow ring */
}
.card.unlock::before,
.card.unlock::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  padding: 2px; /* the gradient lives in this ring; the content box is masked out */
  background: conic-gradient(
    from var(--rainbow-angle),
    oklch(0.72 0.2 20),
    oklch(0.8 0.2 90),
    oklch(0.85 0.2 150),
    oklch(0.8 0.2 220),
    oklch(0.7 0.24 290),
    oklch(0.72 0.2 360),
    oklch(0.72 0.2 20)
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
  animation: rainbow-spin 1.2s linear infinite;
}
.card.unlock::after {
  z-index: -1; /* the glow sits behind the crisp ring and the content */
  filter: blur(7px);
  opacity: 0.65;
}
/* The UNLOCK tag word filled with the same fast-rotating rainbow (its own text, not the hack colour). The
   conic EPICENTRE is pushed well below the tag (~350% of its height) so it lands near the CARD's centre,
   not in the tag itself — otherwise the swirl pivot sits visibly behind the word "HACK". The tag then
   shows a clean sweeping arc that shares the card's centre with the border/glow rings. */
.card.unlock .card-tag {
  background: conic-gradient(
    from var(--rainbow-angle) at 50% 350%,
    oklch(0.72 0.2 20),
    oklch(0.8 0.2 90),
    oklch(0.85 0.2 150),
    oklch(0.8 0.2 220),
    oklch(0.7 0.24 290),
    oklch(0.72 0.2 360),
    oklch(0.72 0.2 20)
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: rainbow-spin 1.2s linear infinite;
}
/* Rarity glow on the hero value, growing toward LEGENDARY (COMMON/r1 stays flat). */
.card.r2 .card-val {
  text-shadow: 0 0 8px color-mix(in oklch, var(--theme) 45%, transparent);
}
.card.r3 .card-val {
  text-shadow: 0 0 11px color-mix(in oklch, var(--theme) 55%, transparent);
}
.card.r4 .card-val {
  text-shadow: 0 0 14px color-mix(in oklch, var(--theme) 65%, transparent);
}
.card.r5 .card-val {
  text-shadow: 0 0 18px color-mix(in oklch, var(--theme) 80%, transparent);
}

/* The "OR" divider between row 1 (player picks) and row 2 (a hack's upgrades): a centred label flanked by
   dashed rules — the two rows are the mutually-exclusive halves of the one point being spent. */
.up-or {
  display: flex;
  align-items: center;
  gap: 1ch;
  margin: 14px 0 2px;
  color: var(--dim);
  font-size: 13px;
  letter-spacing: 0.24em;
}
.up-or::before,
.up-or::after {
  content: '';
  flex: 1;
  border-top: 1px dashed var(--dim);
}
/* The row-3 header naming the hack whose upgrades fill it, in that hack's colour (set inline via --theme). */
.up-row-head {
  margin: 8px 0 0;
  color: var(--theme, var(--fg));
  font-size: 14px;
  letter-spacing: 0.16em;
  text-shadow: 0 0 8px color-mix(in oklch, var(--theme) 45%, transparent);
}
/* The dedicated UNLOCK row's header (row 1, present only while hack milestones are due): the rainbow
   sweep of the unlock cards themselves, so the whole row reads as the milestone event it is. */
.up-unlock-head {
  background: linear-gradient(90deg, #f66, #fc6, #6f6, #6cf, #c6f);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  text-shadow: none;
}

/* The menu's fixed action FOOTER: sits below the scrolling body (and the roller editor), inside the
   content column, so the active tab's action buttons — RANDOM / RANDOM ALL / REROLL on UPGRADES, the
   roller's SET / CLEAR — are always in reach, never scrolled away with the content. Hidden whole (by
   menu.ts) on tabs with no actions. Each row stretches its buttons edge to edge. */
.menu-foot {
  flex: none;
  padding: 10px 14px;
  border-top: 1px solid var(--dim);
}
.menu-foot-row {
  display: flex;
  gap: 8px;
}
/* The KEY HINT riding inside an action button (`A` RANDOM · `S` RANDOM ALL · `R` REROLL · the roller's
   chords): a small keycap chip before the label. A fixed-height flex box centres the glyphs optically
   (VT323 rides high inside its em box, so plain inline padding leaves the letters looking off-centre). */
.up-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 17px;
  padding: 2px 5px 0;
  font:
    700 13px/1 'VT323',
    monospace;
  color: var(--dim);
  border: 1px solid color-mix(in oklch, var(--dim) 60%, transparent);
  background: color-mix(in oklch, var(--color-slate-950) 70%, transparent);
}
.up-btn:hover .up-key {
  color: var(--fg);
  border-color: color-mix(in oklch, var(--fg) 60%, transparent);
}

.up-btn {
  flex: 1;
  /* A centred flex ROW: the keycap and the label sit side by side on one baseline (never stacked, however
     narrow the button). The label is its own span (.up-lbl) so any `[000]` number inside it flows as plain
     inline text — the flex gap only separates keycap from label, never the brackets from the number. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 9px 10px;
  font: inherit;
  font-size: 16px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg);
  cursor: pointer;
  border: 1px solid color-mix(in oklch, var(--fg) 30%, transparent);
  border-radius: 2px;
  background: color-mix(in oklch, var(--fg) 9%, transparent);
  box-shadow: 0 2px 0 color-mix(in oklch, var(--fg) 20%, transparent);
  transition:
    transform 60ms ease,
    background 80ms ease,
    box-shadow 60ms ease;
}
.up-btn:hover {
  background: color-mix(in oklch, var(--fg) 16%, transparent);
}
.up-btn:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 color-mix(in oklch, var(--fg) 20%, transparent);
}
/* The inline count / cost numbers inside a button's `{N}` / `[N]` brackets: the count in the memory/level
   green, the bytes cost in the bytes amber. The braces/brackets themselves keep the neutral button text. */
.up-meta-mem {
  color: var(--memory);
}
.up-meta-bytes {
  color: var(--bytes);
}
.up-btn.cant {
  opacity: 0.42;
  cursor: not-allowed;
  box-shadow: none;
}
.up-btn.cant:active {
  transform: none;
}

/* The empty state when there is nothing to spend. */
.up-empty {
  color: var(--dim);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* A capped stat value (STATS panel) or a maxed hack level (HACKS panel): kept legible in the gold
   legendary colour with a soft glow, so a maxed entry stands out without a "MAX" word replacing the value. */
.stat-max {
  color: var(--r-legendary);
  text-shadow: 0 0 12px color-mix(in oklch, var(--r-legendary) 55%, transparent);
}

/* Death screen: a red-tinted modal shown over the game while the player is dead, until they revive.
   Sits above the HUD but below the start screen / connection notices. */
#gameover {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 55;
  background: color-mix(in oklch, var(--color-slate-950) 84%, transparent);
  align-items: center;
  justify-content: center;
}

/* Reuse the .modal box with the danger palette (the level-up modal is green; death is the danger red,
   red-500 — matching the damage flash). */
.modal-over {
  border-color: var(--danger);
  box-shadow: 0 0 40px color-mix(in oklch, var(--danger) 35%, transparent);
}

.over-title {
  color: var(--danger);
  margin-bottom: 4px;
  text-shadow: 0 0 12px color-mix(in oklch, var(--danger) 50%, transparent);
}

/* The SHARE CARD block (DESIGN_SYSTEM.md §5): the screenshot-clean artifact inside the death modal —
   brand, verdict, operator name, the run score as the glowing hero, the typing flex and the kept
   record. Bordered so a cropped capture carries its own frame; the CTA lives outside it. */
.over-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 14px 16px 12px;
  border: 1px solid color-mix(in oklch, var(--danger) 45%, transparent);
}

.over-brand {
  color: var(--dim);
  font-size: 14px;
  letter-spacing: 0.3em;
}

.over-name {
  color: var(--fg);
  font-size: 22px;
  letter-spacing: 0.08em;
}

.over-score-label {
  margin-top: 6px;
  color: var(--dim);
  font-size: 14px;
  letter-spacing: 0.25em;
}

.over-score-hero {
  color: var(--accent);
  font-size: 46px;
  line-height: 1.05;
  text-shadow: 0 0 18px color-mix(in oklch, var(--accent) 45%, transparent);
}

/* Typing-FLOW flex line — the run's peak streak / WPM / accuracy brag stats, cyan-tinted. */
.over-flow {
  color: var(--dim);
  font-size: 16px;
  margin: 4px 0 2px;
}

.over-flow b {
  color: var(--color-cyan-400);
  font-weight: 400;
}

/* All-time record line — dimmer than the run score, with a note that it is preserved. */
.over-record {
  color: var(--dim);
  font-size: 18px;
}

.over-record b {
  color: var(--fg);
  font-weight: 400;
}

/* Two classes so this red beats the base .start-btn (cyan), which is defined later in the file. The
   button stretches full-width like every other modal CTA (inherits .start-modal's align-items). */
.start-btn.over-btn {
  color: var(--bg);
  background: var(--danger);
  box-shadow: 0 0 18px color-mix(in oklch, var(--danger) 40%, transparent);
}

/* Start screen: the identity handshake shown until the world is joined (above the level-up modal). */
#start {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 60;
  background: color-mix(in oklch, var(--color-slate-950) 92%, transparent);
  align-items: center;
  justify-content: center;
}

/* CRT chrome on the full-screen hub/death overlays (DESIGN_SYSTEM.md §5): a static scanline raster +
   corner vignette painted over the backdrop AND the panel — the hub IS a CRT powering on. Scoped to
   these screens only (never over live gameplay, so board legibility and compositing cost are
   untouched); pointer-events off so the forms keep receiving every click beneath it. */
#start::before,
#gameover::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      0deg,
      color-mix(in oklch, var(--color-black) 22%, transparent) 0 1px,
      transparent 1px 3px
    ),
    radial-gradient(
      ellipse at center,
      transparent 58%,
      color-mix(in oklch, var(--color-black) 38%, transparent)
    );
}

/* The hub's POWER-ON boot (plays once per page load — screens.ts adds .boot on the first showStart):
   the panel snaps open from a bright horizontal line (the classic CRT turn-on), then its lines cascade
   in like terminal output. A returning player's fast path (.boot-fast, saved key) collapses the
   timings so a regular is reading the hub in under ~0.4 s. Finite, Event-tier (§2.3). */
#start.boot {
  --boot-ms: 620ms;
  --boot-stagger: 70ms;
  --boot-line-ms: 240ms;
}
#start.boot.boot-fast {
  --boot-ms: 200ms;
  --boot-stagger: 12ms;
  --boot-line-ms: 90ms;
}
#start.boot .start-modal {
  animation: crt-power-on var(--boot-ms) cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
#start.boot .start-modal > * {
  animation: boot-line var(--boot-line-ms) ease-out backwards;
}
/* Terminal output lands line by line: each child waits for the power-on, then its slot in the cascade. */
#start.boot .start-modal > :nth-child(1) {
  animation-delay: var(--boot-ms);
}
#start.boot .start-modal > :nth-child(2) {
  animation-delay: calc(var(--boot-ms) + 1 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(3) {
  animation-delay: calc(var(--boot-ms) + 2 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(4) {
  animation-delay: calc(var(--boot-ms) + 3 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(5) {
  animation-delay: calc(var(--boot-ms) + 4 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(6) {
  animation-delay: calc(var(--boot-ms) + 5 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(7) {
  animation-delay: calc(var(--boot-ms) + 6 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(8) {
  animation-delay: calc(var(--boot-ms) + 7 * var(--boot-stagger));
}
#start.boot .start-modal > :nth-child(n + 9) {
  animation-delay: calc(var(--boot-ms) + 8 * var(--boot-stagger));
}

@keyframes crt-power-on {
  0% {
    transform: scaleY(0.012) scaleX(0.7);
    filter: brightness(5);
  }
  60% {
    transform: scaleY(1.03) scaleX(1);
    filter: brightness(1.5);
  }
  100% {
    transform: none;
    filter: none;
  }
}

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

.start-modal {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  width: min(440px, calc(100vw - 32px));
  text-align: center;
}

.start-tag {
  color: var(--dim);
  font-size: 17px;
  margin-bottom: 6px;
}

/* The hub tagline ends in a blinking block cursor — the terminal awaiting input. Decorative (the
   inputs carry the real affordance), so reduced-motion holds it solid below. Zero-width inline-block:
   the glyph paints past the box, so the cursor never changes how the tagline wraps. */
.start-prompt::after {
  content: '█';
  display: inline-block;
  width: 0;
  margin-left: 6px;
  color: var(--memory);
  animation: prompt-blink 1.1s steps(1, end) infinite;
}

@keyframes prompt-blink {
  50% {
    opacity: 0;
  }
}

/* Connection notices (session moved, disconnected, …) reuse the start container. */
.start-notice-msg {
  color: var(--fg);
  font-size: 18px;
  line-height: 1.5;
  margin: 4px 0 6px;
}

/* Vintage-terminal "busy" indicator: a spinning ASCII cursor (| / - \) instead of a graphical
   spinner, animated purely through the ::after content so there is no JS timer to manage. */
.tw-loader {
  font-family: 'VT323', monospace;
  font-size: 40px;
  height: 46px;
  line-height: 46px;
  color: var(--accent);
  text-shadow: 0 0 12px color-mix(in oklch, var(--accent) 50%, transparent);
  margin: 8px auto 4px;
}

.tw-loader::after {
  content: '|';
  animation: tw-spin-ascii 0.4s steps(1, end) infinite;
}

@keyframes tw-spin-ascii {
  0% {
    content: '|';
  }
  25% {
    content: '/';
  }
  50% {
    content: '-';
  }
  75% {
    content: '\\';
  }
}

.start-input {
  font-family: 'VT323', monospace;
  font-size: 22px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--dim);
  padding: 8px 12px;
  outline: none;
  text-align: center;
}

.start-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 12px color-mix(in oklch, var(--accent) 30%, transparent);
}

/* The one-time secret key: smaller so the full token fits, tinted as a value to copy. */
.start-key-show {
  font-size: 15px;
  color: var(--accent);
  letter-spacing: 0.02em;
}

.start-btn {
  font-family: 'VT323', monospace;
  font-size: 22px;
  letter-spacing: 0.08em;
  color: var(--bg);
  background: var(--accent);
  border: none;
  padding: 9px 12px;
  cursor: pointer;
}

.start-go {
  background: var(--memory);
  box-shadow: 0 0 18px color-mix(in oklch, var(--memory) 40%, transparent);
}

.start-btn:hover {
  filter: brightness(1.12);
}

.start-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.start-link {
  font-family: 'VT323', monospace;
  font-size: 16px;
  color: var(--dim);
  background: none;
  border: none;
  cursor: pointer;
  text-decoration: underline;
}

.start-link:hover {
  color: var(--fg);
}

.start-or {
  color: var(--dim);
  font-size: 15px;
}

.start-error {
  color: var(--energy);
  font-size: 16px;
  min-height: 18px;
}

/* The controls primer on the start modal: a two-column KEY · ACTION grid (one verb per row), the keycaps
   right-aligned in a fixed track so the actions line up — the same table shape the menu's HELP tab uses. */
.start-help {
  display: grid;
  grid-template-columns: max-content 1fr;
  align-items: center;
  gap: 5px 12px;
  margin-top: 10px;
  padding-top: 12px;
  border-top: 1px solid color-mix(in oklch, var(--dim) 40%, transparent);
  color: var(--color-slate-400);
  font-size: 15px;
  text-align: left;
}
.start-help kbd {
  justify-self: end;
  padding: 2px 6px 0;
  font-size: 13px;
  line-height: 1;
  letter-spacing: 0.08em;
  color: var(--color-slate-300);
}

.start-note {
  margin-top: 8px;
  font-size: 13px;
  color: var(--dim);
  opacity: 0.8;
}

kbd {
  background: var(--color-slate-900);
  border: 1px solid var(--dim);
  padding: 1px 6px;
  color: var(--fg);
}

/* The MENU launcher in OPERATOR's footer: a compact ghost button with its keyboard hint riding along —
   visible but understated. Square corners; opens the tabbed MENU. */
.menu-btn {
  appearance: none;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--dim);
  color: var(--color-slate-400);
  padding: 2px 8px;
  font:
    14px 'VT323',
    monospace;
  letter-spacing: 0.1em;
}
.menu-btn:hover {
  color: var(--fg);
  border-color: color-mix(in oklch, var(--fg) 35%, var(--dim));
}
/* The keyboard hint riding inside a button/label: dim and a touch smaller than its label. */
.key-hint {
  color: var(--dim);
  font-size: 11px;
  margin-left: 6px;
}

/* The MENU overlay: a full-screen, keyboard-capturing modal (the cursor is exposed while it is open).
   Sits above the rail/banners but below the start screen and death modal. Toggled to flex by menu.ts. */
.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: 58;
  display: none;
  align-items: center;
  justify-content: center;
  background: color-mix(in oklch, var(--color-black) 45%, transparent);
}

/* The menu panel: terminal-box chrome, capped to the viewport. Laid as a ROW — a fixed-width sidebar
   (tabs + EXIT) on the left, the scrolling content on the right. */
.menu-box {
  position: relative; /* the corner brackets (shared .plate::before rule) anchor to the panel */
  --plate-accent: color-mix(in oklch, var(--accent) 60%, transparent);
  display: flex;
  flex-direction: row;
  width: min(720px, 94vw);
  /* A FIXED height (not just a cap) so the window never resizes between tabs — a tall tab (the STATS table
     is ~700px) scrolls within it, short tabs leave empty space below. */
  height: min(560px, 86vh);
  background: color-mix(in oklch, var(--color-slate-900) 94%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent) 16%, var(--dim));
  box-shadow: 0 0 36px color-mix(in oklch, var(--color-black) 70%, transparent);
  backdrop-filter: blur(2px);
  font:
    18px 'VT323',
    monospace;
}

/* Left sidebar (fixed width): the tab list at the top, the EXIT button pinned to the bottom. */
.menu-sidebar {
  flex: none;
  width: 168px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--dim);
  padding: 10px;
  gap: 10px;
}

/* The tab list: a vertical stack of full-width buttons, the active one inverted. */
.menu-tabs {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.menu-tab {
  appearance: none;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--dim);
  color: var(--color-slate-400);
  padding: 4px 10px;
  font:
    700 16px 'VT323',
    monospace;
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  gap: 14px;
  text-align: left;
}
.menu-tab:hover {
  color: var(--fg);
  border-color: color-mix(in oklch, var(--fg) 35%, var(--dim));
}
.menu-tab.active {
  background: var(--fg);
  border-color: var(--fg);
  color: var(--color-slate-900);
}

/* The EXIT button: styled identically to a tab (same border/padding/font, the accelerator on the LEFT,
   labels aligned via the fixed-width .menu-tab-key), but pinned to the bottom of the sidebar. */
.menu-exit {
  margin-top: auto;
  appearance: none;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--dim);
  color: var(--color-slate-400);
  padding: 4px 10px;
  font:
    700 16px 'VT323',
    monospace;
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  gap: 14px;
  text-align: left;
}
.menu-exit:hover {
  color: var(--fg);
  border-color: color-mix(in oklch, var(--fg) 35%, var(--dim));
}

/* The content area to the right of the sidebar: a column that caps its children so each tab body scrolls
   within the box rather than stretching it. */
.menu-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
/* The accelerator shown on each tab / the EXIT button (Alt+N, or Esc). Dim so it reads as a hint, not a
   label; inherits the inverted colour on the active tab. A FIXED width so the labels after it line up in
   one column across every button regardless of the key text ("Alt+1"…"Alt+6" / "Esc"). */
.menu-tab-key {
  flex: none;
  width: 5ch;
  color: var(--color-slate-500);
  font-size: 13px;
}
.menu-tab.active .menu-tab-key {
  color: color-mix(in oklch, var(--color-slate-900) 70%, transparent);
}

/* The UPGRADES tab wears the level tab's standout glow while points are unspent — a memory-coloured
   border + inset glow that marks the place to spend. Placed after .menu-tab.active so it keeps the glow
   even on the active tab. */
.menu-tab.has-points {
  border-color: color-mix(in oklch, var(--memory) 45%, var(--dim));
  box-shadow: inset 0 0 14px color-mix(in oklch, var(--memory) 24%, transparent);
}

/* The section-name header: the active tab's name, fixed above the scrolling body. */
.menu-head {
  flex: none;
  padding: 10px 14px;
  border-bottom: 1px solid var(--dim);
  color: var(--fg);
  font-size: 20px;
  letter-spacing: 0.12em;
}
/* The `SYS://` scheme prefix on the section header: the console speaks in paths, the prefix stays
   quiet so the section name carries the line. */
.menu-head-sys {
  color: var(--dim);
}
/* The unspent-points count shown beside the UPGRADES section title — green (the memory/XP tone), in the
   bracketed [000] form (the peeking level tab shows the bare count). */
.menu-head-points {
  color: var(--memory);
}

/* The dynamic tab body (STATS / HACKS / UPGRADES / HELP): fills the content column and scrolls within
   the capped box. The stat tables drop the padding (`.flush`) so they meet the panel edges. */
.menu-body {
  flex: 1;
  min-height: 0;
  padding: 12px 14px;
  overflow-y: auto;
}
.menu-body.flush {
  padding: 0;
  /* Keep the symmetric both-edges gutter (inherited from .menu-body) even on the flush tables, so the
     reserved scrollbar space mirrors on the left and the content stays centred whether or not it scrolls. */
  scrollbar-gutter: stable both-edges;
}

/* Themed scrollbars for the menu's scroll areas (body, roller grid) — the standard, no-JS approach
   (three native properties, no ::-webkit hacks, no simulated thumb):
     - scrollbar-width: thin   — ask the browser for its slim native bar (Chromium-Linux/Windows and
                                 Firefox obey; macOS bars are already thin overlay bars);
     - scrollbar-color: <thumb> transparent — paint the thumb in the on-theme neutral (--dim ==
                                 slate-600, our outline tone) over a transparent track;
     - scrollbar-gutter: stable both-edges — reserve the bar's width on BOTH inline edges, so the
                                 content shifts left never against the bar (the left inset matches the
                                 space the scrollbar leaves on the right), and switching tabs never
                                 shifts the content horizontally as the bar appears/disappears.
   The browser handles drag, keyboard (arrows, Page/Home/End, Space), wheel, touch and a11y. (The
   dev-only debug panel mirrors these three properties in its own injected stylesheet, so the static
   sheet stays free of any dev-only selector in production.) */
.menu-body,
#menu-tab-roller {
  scrollbar-width: thin;
  scrollbar-color: var(--dim) transparent;
  scrollbar-gutter: stable both-edges;
}
/* The STATS / SYSTEM tables: a three-column terminal table — STAT · VALUE · DESCRIPTION — instead of the
   compact 0x/bracket HUD format. Rows are `display: contents` so their three spans land directly in the
   grid tracks. The tracks are FIXED widths (not content-sized): every tab that uses this table shares the
   exact same column rhythm, so switching tabs never shifts the columns to that tab's content. The body
   goes FLUSH on these tabs (no padding) so the table meets the panel edges, and each cell carries a
   subtle bottom border (no column gaps) so the rows read as one integrated, lightly-ruled table. */
.stats-table {
  display: grid;
  grid-template-columns: 176px 124px 1fr;
  align-items: stretch;
}
.str-row {
  display: contents;
}
.str-name,
.str-val,
.str-desc {
  padding: 3px 14px;
  border-bottom: 1px solid color-mix(in oklch, var(--dim) 30%, transparent);
  overflow-wrap: anywhere; /* fixed tracks: a long value (e.g. a nickname) wraps instead of overflowing */
}
.str-name {
  color: var(--color-slate-400);
}
.str-val {
  color: var(--fg);
  text-align: right;
}
.str-desc {
  color: var(--color-slate-500);
}
/* A capped stat keeps its value but turns the gold MAX colour (two-class selector beats .str-val). */
.str-val.stat-max {
  color: var(--r-legendary);
  text-shadow: 0 0 12px color-mix(in oklch, var(--r-legendary) 55%, transparent);
}
/* A full-width sub-header (a stat category, a hack name, or SYSTEM) spanning all columns. Its colour rides
   --theme — the category's tone (OFFENSE red, DEFENSE sky, …) or a hack's own colour — falling back to the
   muted section-header tone when no theme is set. */
/* A section sub-header: the title in its category colour over a LEFT ACCENT BAR — the console's
   consistent "new section" tell, shared by every tab body (PLAYER groups, SYSTEM groups, HELP groups,
   DATABASE blocks, the HACKS arsenal). */
.str-sub {
  grid-column: 1 / -1;
  margin-top: 6px;
  padding: 7px 14px 4px 11px;
  color: var(--theme, var(--color-slate-500));
  letter-spacing: 0.16em;
  font-size: 15px;
  border-left: 3px solid var(--theme, var(--dim));
  border-bottom: 1px solid color-mix(in oklch, var(--theme, var(--dim)) 22%, transparent);
}
/* The HACKS accordion headers: clickable sub-headers (one expands at a time, see menu.ts). A NEUTRAL
   wash on hover / while open marks it as interactive — a light tint of the foreground tone that
   contrasts with the dark panel, deliberately NOT the row's own --theme colour (the title keeps that). */
.str-sub.hack-head {
  cursor: pointer;
}
.str-sub.hack-head:hover {
  background: color-mix(in oklch, var(--fg) 9%, transparent);
}
.str-sub.hack-head.open {
  background: color-mix(in oklch, var(--fg) 15%, transparent);
}

/* The SYSTEM tab's AUDIO controls (audio-section.ts): each row spans the stats-table and lays its own
   tracks — the name keeps the table's 176px rhythm, the slider takes the room a 124px value cell can't
   give, and the right cell holds the live % (or the Alt+A hint). The sliders are native ranges tinted
   to the terminal accent; their drag state is theirs alone (the menu never re-renders mid-drag). */
.audio-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 176px 1fr 88px;
  align-items: center;
  border-bottom: 1px solid color-mix(in oklch, var(--dim) 30%, transparent);
}
.audio-row .str-name {
  border-bottom: none; /* the row carries the rule; the cell rule would double up */
}
.audio-slider {
  width: 100%;
  margin: 0;
  accent-color: var(--accent);
  background: transparent;
  cursor: pointer;
}
.audio-val {
  padding: 3px 14px;
  color: var(--fg);
  text-align: right;
}
.audio-mute {
  margin: 4px 0;
  padding: 2px 10px;
  font: inherit;
  letter-spacing: 0.08em;
  color: var(--fg);
  background: color-mix(in oklch, var(--fg) 8%, transparent);
  border: 1px solid color-mix(in oklch, var(--dim) 60%, transparent);
  cursor: pointer;
}
.audio-mute:hover {
  background: color-mix(in oklch, var(--fg) 16%, transparent);
}

/* A LOCKED arsenal row: the hack's name in its (dimmed) colour with an ENCRYPTED tag — the roster is
   visible from level 1, its numbers withheld until the level-up draft unlocks it. */
.hack-locked {
  grid-column: 1 / -1; /* span the stats-table grid — one full-width row per locked hack */
  display: flex;
  justify-content: space-between;
  padding: 5px 14px;
  border-bottom: 1px solid color-mix(in oklch, var(--dim) 18%, transparent);
}
.hack-locked-name {
  color: color-mix(in oklch, var(--theme) 38%, var(--locked));
  letter-spacing: 0.08em;
}
.hack-locked-tag {
  color: var(--locked);
  font-size: 13px;
  letter-spacing: 0.22em;
}

/* ---- DATABASE (threat intel) ---- */
.db {
  padding-bottom: 12px;
}
.db-empty {
  padding: 8px 14px;
  color: var(--dim);
}
/* A live contact: sigil · name+tags · trail (the level tell) · live energy gauge. */
.db-contact {
  display: grid;
  grid-template-columns: 30px 1fr auto auto;
  align-items: center;
  gap: 10px;
  padding: 4px 14px;
  border-bottom: 1px solid color-mix(in oklch, var(--dim) 18%, transparent);
}
.db-sigil {
  font-family: 'Monomaniac One', 'VT323', monospace;
  font-size: 21px;
  text-align: center;
  color: var(--theme);
  text-shadow: 0 0 8px color-mix(in oklch, var(--theme) 45%, transparent);
}
.db-cname {
  color: var(--theme);
  letter-spacing: 0.08em;
}
.db-tag {
  margin-left: 8px;
  padding: 0 5px;
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--color-slate-400);
  border: 1px solid color-mix(in oklch, var(--color-slate-400) 50%, transparent);
}
.db-tag.elite {
  color: var(--bytes);
  border-color: color-mix(in oklch, var(--bytes) 55%, transparent);
}
.db-tag.boss {
  color: var(--danger);
  border-color: var(--danger);
  font-weight: 700;
}
.db-trail {
  font-size: 13px;
  letter-spacing: 0.1em;
  color: var(--dim);
}
.db-hp {
  display: flex;
  align-items: center;
  gap: 8px;
}
.db-hp b {
  font-weight: 400;
  min-width: 4ch;
  text-align: right;
  color: var(--fg);
}
.seg-bar.hp {
  width: 80px;
  height: 8px;
}
/* An archetype / affix / rule entry: lead column + description. */
.db-arch,
.db-affix,
.db-rule {
  display: grid;
  grid-template-columns: 30px 110px 1fr;
  align-items: center;
  gap: 10px;
  padding: 5px 14px;
  border-bottom: 1px solid color-mix(in oklch, var(--dim) 18%, transparent);
}
.db-rule {
  grid-template-columns: 140px 1fr;
}
.db-aname {
  color: var(--theme, var(--fg));
  letter-spacing: 0.1em;
}
.db-affix .db-aname,
.db-rule .db-aname {
  color: var(--color-slate-300);
}
.db-adesc {
  color: var(--dim);
  font-size: 15px;
}
.db-adesc b {
  font-weight: 400;
  color: var(--color-slate-300);
}

/* The affix PREVIEW CHIP: a small square whose border ANIMATES like the on-board tell it documents —
   the database teaches the visual language by speaking it. Neutral tone (in play the border wears the
   archetype colour); reduced-motion freezes every chip to its lit state. */
.affix-chip {
  width: 14px;
  height: 14px;
  justify-self: center;
  color: var(--color-slate-300);
  border: 2px solid currentColor;
}
.affix-chip.shielded {
  box-shadow:
    0 0 0 1px var(--bg),
    0 0 0 3px currentColor;
}
.affix-chip.splitting {
  outline: 1px solid currentColor;
  outline-offset: 2px;
}
.affix-chip.explosive {
  animation: chip-throb 0.8s ease-in-out infinite;
}
.affix-chip.vampiric {
  animation: chip-vamp 1.3s ease-in infinite;
}
.affix-chip.summoner {
  animation: chip-ping 1.5s ease-out infinite;
}
.affix-chip.jammer {
  animation: chip-ping 0.75s ease-out infinite;
}
.affix-chip.kamikaze {
  animation: chip-blink 0.4s steps(1, end) infinite;
}
.affix-chip.phantom {
  animation: chip-phase 2.2s ease-in-out infinite;
}
@keyframes chip-throb {
  50% {
    box-shadow: 0 0 9px currentColor;
  }
}
@keyframes chip-ping {
  0% {
    box-shadow: 0 0 0 0 currentColor;
  }
  100% {
    box-shadow: 0 0 0 7px transparent;
  }
}
@keyframes chip-vamp {
  0% {
    box-shadow: inset 0 0 0 0 transparent;
  }
  100% {
    box-shadow: inset 0 0 0 5px currentColor;
  }
}
@keyframes chip-blink {
  50% {
    border-color: transparent;
  }
}
@keyframes chip-phase {
  50% {
    opacity: 0.15;
  }
}
/* The elected affliction's word beside a hack name, in its own status colour (the brackets around it stay
   the hack's --theme colour, as plain text). */
.hack-affix {
  color: var(--affix);
}

/* HELP tab: a key → action reference list. */
.menu-help {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.menu-help-row {
  display: flex;
  gap: 1ch;
}
.menu-help-key {
  flex: none;
  width: 12ch;
  color: var(--fg);
}
.menu-help-act {
  color: var(--color-slate-400);
}

/* A locked-tab placeholder (e.g. the ROLLER tab before the roller hack is unlocked). */
.menu-locked {
  color: var(--locked);
  font-style: italic;
}

/* The ROLLER tab panel: hosts the write-once banner editor (see roller-editor.ts). menu.ts shows it
   only on the ROLLER tab; the editor is built/torn down on entry/exit, not per frame. */
#menu-tab-roller {
  display: none; /* toggled to flex by menu.ts on the ROLLER tab */
  flex: 1;
  min-height: 0;
  padding: 14px;
  overflow: auto;
}

/* The editor column: hint → lane inputs → the two live travel previews → actions. The editor ink
   (sky family — EDITOR_CELL_* in config/palette.ts) mirrors how the typed text reads on the board. */
.roller {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.roller-hint {
  color: var(--dim);
  font-size: 14px;
  letter-spacing: 0.06em;
}

/* One native input per roller lane: a board-like text line (monospace, generous tracking) in the
   editor ink. Native editing — caret, selection, paste — is the whole point. Each input is sized to
   EXACTLY its character budget (--cols, set by roller-editor.ts): one monospace char advances
   1ch + the 0.35em tracking, plus the horizontal padding/borders — so the box never suggests room
   beyond what the banner can actually hold. */
#roller-lines {
  display: flex;
  flex-direction: column;
  align-items: flex-start; /* the lanes hug their own width instead of stretching to the column */
  gap: 4px;
}
.roller-lane {
  width: calc(var(--cols, 16) * (1ch + 0.35em) + 22px);
  max-width: 100%;
  font-family: 'VT323', monospace;
  font-size: 22px;
  letter-spacing: 0.35em;
  padding: 4px 10px;
  color: var(--color-sky-200);
  background: var(--color-slate-950);
  border: 1px solid color-mix(in oklch, var(--color-sky-200) 30%, transparent);
  outline: none;
}
.roller-lane:focus {
  border-color: var(--color-sky-200);
  box-shadow: 0 0 10px color-mix(in oklch, var(--color-sky-200) 30%, transparent);
}

/* The two travel previews, side by side: each a cell grid of the message exactly as the roller will
   paint it (the VERTICAL one is the derived transpose). Gaps render as dim sockets so the painted
   SHAPE reads, not just the text. */
.roller-previews {
  display: flex;
  gap: 18px;
  align-items: flex-start;
}
.roller-prev-tag {
  color: var(--color-slate-500);
  font-size: 12px;
  letter-spacing: 0.22em;
  margin-bottom: 5px;
}
.roller-prev-grid {
  display: inline-block;
}
.rp-row {
  display: flex;
  gap: 1px;
  margin-bottom: 1px;
}
.rp-cell {
  width: 14px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  color: var(--color-slate-950);
  background: var(--color-sky-200);
}
.rp-cell.gap {
  background: color-mix(in oklch, var(--color-sky-200) 8%, transparent);
}
.roller-prev-empty {
  color: var(--locked);
  font-style: italic;
  font-size: 14px;
}

.roller-note {
  color: var(--dim);
  font-size: 13px;
}

/* Wave/boss CEREMONY banners (#banner): transient centre-screen announcements built by hud/banners.ts.
   The container is a fixed, centred, non-interactive layer above the board/HUD but below the blocking
   modals (gameover 55, start 60); it is empty except for the ~2.2s a .banner-msg is mounted. The fade
   animation duration matches BANNER_MS in banners.ts, which removes the node when it elapses. */
#banner {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.banner-msg {
  font-family: 'VT323', monospace;
  font-size: clamp(2rem, 6vw, 4.5rem);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  text-align: center;
  animation: banner-fade 2200ms ease-out forwards;
}
/* WAVE CLEARED — the positive payoff, in the emerald memory tone (matches the green REGROUP phase tell). */
.banner-clear {
  color: var(--memory);
  text-shadow: 0 0 18px color-mix(in oklch, var(--memory) 60%, transparent);
}
/* BOSS WAVE / BOSS DESTROYED — danger red (matches the red BOSS phase tell in the CORE panel). */
.banner-boss {
  color: var(--danger);
  text-shadow: 0 0 22px color-mix(in oklch, var(--danger) 70%, transparent);
}
/* WAVE n — a normal wave opening: the SUBTLE tell, deliberately smaller and dimmer than the red boss
   alarm (a neutral cyan accent, a softer glow). It marks the start of an ordinary wave — notably the
   moment a newcomer crosses out of the safe spawn ring — without the boss-grade theatrics. */
.banner-wave {
  color: var(--accent);
  font-size: clamp(1.5rem, 4vw, 3rem);
  text-shadow: 0 0 12px color-mix(in oklch, var(--accent) 40%, transparent);
}
/* ESCAPED — the relief beat when a wave is shaken off by fleeing (it does NOT clear). Safety green (the
   same tone as the SAFE ZONE HUD tell), a touch smaller than the boss alarm: a calm "you got away". */
.banner-safe {
  color: var(--memory);
  font-size: clamp(1.6rem, 4.5vw, 3.2rem);
  text-shadow: 0 0 14px color-mix(in oklch, var(--memory) 50%, transparent);
}
@keyframes banner-fade {
  0% {
    opacity: 0;
    transform: scale(0.92);
  }
  12% {
    opacity: 1;
    transform: scale(1);
  }
  78% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(1.04);
  }
}

/* OS-level "reduce motion" (DESIGN_SYSTEM.md §2.3): the kinetic theatrics collapse to their lit end
   states — the hub boot plays no power-on (the panel just appears), the prompt cursor holds solid,
   ceremony banners cut instead of fading (banners.ts still unmounts them on schedule), the level-tab
   pulse and the UNLOCK rainbows hold still. The ASCII busy spinner stays (it is information — "still
   working" — not decoration), and gameplay blinks stay (they reveal what is under a cursor). The
   canvas side drops the camera shake and the surge flash — see platform/motion.ts. */
@media (prefers-reduced-motion: reduce) {
  #start.boot .start-modal,
  #start.boot .start-modal > *,
  .start-prompt::after,
  .banner-msg,
  .pts-chip,
  .hud-alarm,
  .oc-live,
  .affix-chip,
  .card.unlock::before,
  .card.unlock::after,
  .card.unlock .card-tag {
    animation: none;
  }
}
