/* ============================================================
   PartyGame — one stylesheet, no build step.
   ============================================================ */

:root {
  --bg: #0b0a14;
  --bg-2: #12102180;
  --panel: rgba(255, 255, 255, 0.045);
  --panel-strong: rgba(255, 255, 255, 0.08);
  --line: rgba(255, 255, 255, 0.11);
  --text: #f2f0ff;
  --muted: #a09cc0;
  --dim: #6f6a92;

  --pink: #ff4d8d;
  --violet: #9b5cff;
  --cyan: #34e0ff;
  --lime: #b6ff3d;
  --amber: #ffb340;
  --red: #ff4d4d;

  --grad: linear-gradient(105deg, var(--pink), var(--violet) 55%, var(--cyan));
  --radius: 18px;
  --radius-sm: 11px;
  --shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* The side rails, as numbers the page can lay itself out around. They give
     back width on smaller windows - two reserved columns at a fixed 232px is
     most of a 1000px screen, and the content is the point. The ceiling is for
     the opposite case: past ~1550px the content wraps have hit their own
     max-widths and are sitting in slack, so the rails may as well take some of
     it and give the chat room to breathe. 300px keeps the widest wrap
     (.play-wrap, 1240px) fitting on a 1900px screen. */
  --rail-w: clamp(190px, 15vw, 300px);
  --rail-gap: 14px;
  /* Every screen's breathing room at the top and bottom, measured against the
     window's HEIGHT rather than its width: 40px of each is 12% of a 1280x720
     window spent on nothing, on the screens where what is below the fold is
     the button. A variable because the final screen's sticky action bar has to
     cancel it out to reach the bottom of the window. */
  --screen-pad-y: clamp(14px, 2.8vh, 40px);
  /* Discord's wordmark glyph, inline so it costs no request and inherits
     currentColor through a mask. */
  --discord-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 127.14 96.36'%3E%3Cpath d='M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 032.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 064.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 032.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z' fill='%23fff'/%3E%3C/svg%3E");
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: 'Outfit', system-ui, -apple-system, 'Segoe UI', sans-serif;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

body.shake { animation: shake 0.42s var(--ease); }
@keyframes shake {
  0%,100% { transform: translate(0,0) }
  15% { transform: translate(-9px, 5px) rotate(-0.5deg) }
  30% { transform: translate(8px, -6px) rotate(0.4deg) }
  45% { transform: translate(-7px, -3px) }
  60% { transform: translate(5px, 4px) }
  80% { transform: translate(-3px, 2px) }
}

/* ---------- background ---------- */
#bg-orbs {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(60vw 60vw at 12% 8%, rgba(255,77,141,0.20), transparent 60%),
    radial-gradient(55vw 55vw at 88% 18%, rgba(155,92,255,0.20), transparent 60%),
    radial-gradient(65vw 65vw at 50% 105%, rgba(52,224,255,0.16), transparent 60%);
  animation: drift 24s ease-in-out infinite alternate;
}
@keyframes drift { to { transform: translate3d(0, -3%, 0) scale(1.06); } }

#fx {
  position: fixed; inset: 0; z-index: 900; pointer-events: none;
}

#app { position: relative; z-index: 1; height: 100%; }

/* ---------- screens ---------- */
.screen {
  position: absolute; inset: 0;
  display: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Reserve BOTH rails' columns rather than letting content slide underneath
     them. The wraps are centred inside this box with a max-width, so reserving
     only the right - as a sidebar layout normally would - pushed the whole
     page visibly off-centre. Equal padding keeps it centred and clear of both.
     A fixed element cannot be laid out around any other way.
     The vertical half is --screen-pad-y; see :root for why it is a variable
     and why it is measured against the window's height. */
  padding: var(--screen-pad-y) calc(var(--rail-w) + var(--rail-gap) + 14px);
}
.screen.active { display: block; animation: screenIn 0.4s var(--ease); }
@keyframes screenIn { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }

/* ---------- generic bits ---------- */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(16px, 2.4vw, 26px);
  backdrop-filter: blur(14px);
  box-shadow: var(--shadow);
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--dim);
}

.btn {
  font: inherit; font-weight: 700;
  padding: 12px 22px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  color: var(--text);
  cursor: pointer;
  transition: transform 0.15s var(--ease), box-shadow 0.2s, filter 0.2s, background 0.2s;
}
.btn:hover:not(:disabled) { transform: translateY(-2px); background: rgba(255,255,255,0.13); }
.btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn.primary {
  background: var(--grad); border: none; color: #12091c;
  box-shadow: 0 10px 30px rgba(155, 92, 255, 0.42);
}
.btn.primary:hover:not(:disabled) { filter: brightness(1.1); box-shadow: 0 16px 40px rgba(155,92,255,0.55); }
.btn.ghost { background: transparent; }
.btn.block { width: 100%; }
.btn.big { padding: clamp(11px, 2vh, 16px) 40px; font-size: clamp(1rem, 2.1vh, 1.15rem); }
.btn.tiny { padding: 7px 14px; font-size: 0.8rem; }

.field { display: block; margin-bottom: 14px; }
.field > span {
  display: block; font-size: 0.78rem; font-weight: 600;
  color: var(--muted); margin-bottom: 6px; letter-spacing: 0.02em;
}
.field > span em { color: var(--dim); font-style: normal; font-weight: 400; }
.field input {
  width: 100%; font: inherit; font-weight: 600;
  padding: 12px 14px;
  background: rgba(0,0,0,0.28);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text);
  transition: border-color 0.2s, box-shadow 0.2s;
}
.field input:focus {
  outline: none; border-color: var(--violet);
  box-shadow: 0 0 0 3px rgba(155,92,255,0.22);
}
.field.small { margin-bottom: 0; width: 92px; flex: none; }
.code-input {
  text-transform: uppercase; letter-spacing: 0.4em;
  font-family: 'JetBrains Mono', monospace; font-size: 1.3rem; text-align: center;
}

.toggle {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 0.82rem; font-weight: 600; color: var(--muted);
  cursor: pointer; user-select: none;
  padding: 8px 12px; border-radius: 999px;
  border: 1px solid var(--line); background: rgba(0,0,0,0.2);
  transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.toggle:has(input:checked) { color: var(--text); border-color: rgba(182,255,61,0.5); background: rgba(182,255,61,0.09); }
.toggle input { accent-color: var(--lime); width: 15px; height: 15px; cursor: pointer; }
/* A toggle with three answers. Same pill, so the row still reads as one row. */
.toggle.has-select { cursor: default; color: var(--text); gap: 4px; }
.toggle.has-select select {
  font: inherit; font-weight: 700; color: var(--lime); cursor: pointer;
  background: transparent; border: 0; outline: none; padding: 0 2px;
}
.toggle.has-select select option { color: #111; }

.opts-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; margin: 14px 0 18px; }
.opts-row.compact { margin: 0 0 14px; justify-content: center; gap: 7px; }
/* Six pills where there were five. Trimmed rather than wrapped onto another
   row, because a twenty-player lobby at 1366x768 has Leave sitting on the fold. */
.opts-row.compact .toggle { padding: 6px 10px; font-size: 0.78rem; gap: 6px; }
.opts-row.compact .field.small { width: 76px; }
.opts-row.compact .field.small input { padding: 8px 10px; }

.error-line { color: var(--red); font-weight: 600; min-height: 22px; text-align: center; margin-top: 12px; }

/* ============================================================
   HOME
   ============================================================ */
.home-wrap { max-width: 1080px; margin: 0 auto; }
.brand { text-align: center; margin: clamp(10px, 4vh, 44px) 0 26px; }

/* The first load. Every label on the home screen is written by main.js, and
   main.js is 125 modules - on a link as far away as Render's Oregon that is
   seconds after the stylesheet has painted every box empty, which read as a
   broken page (Chucho, 2026-10-08). Until main.js says it is done, only the
   logo shows, breathing; the rest fades in once it has words in it. The
   animation is the way out if main.js never says so: nothing stays hidden
   past 15 seconds. */
html.booting #screen-home .home-wrap > :not(.brand),
html.booting .tagline,
html.booting body > :not(#app):not(#bg-orbs):not(#fx) {
  opacity: 0; pointer-events: none;
  animation: boot-giveup 0.3s 15s forwards;
}
html.booting #screen-home .logo { animation: boot-breathe 1.1s ease-in-out infinite alternate; }
@keyframes boot-breathe { to { opacity: 0.45; } }
@keyframes boot-giveup { to { opacity: 1; pointer-events: auto; } }
html.booted #screen-home .home-wrap > *, html.booted .tagline { transition: opacity 0.35s ease; }
.logo {
  font-size: clamp(2.6rem, 8vw, 5rem); font-weight: 900; letter-spacing: -0.03em; line-height: 0.95;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 6px 26px rgba(155,92,255,0.4));
}
.logo span { -webkit-text-stroke: 2px rgba(255,255,255,0.85); color: transparent; -webkit-background-clip: initial; background: none; }
.tagline {
  color: var(--muted); font-weight: 600;
  /* Against the window's HEIGHT as well, for the same reason --screen-pad-y is:
     on a 720p laptop the whole home screen is 259px too tall, and a line of
     subtitle set for a 27" monitor is part of why. */
  margin-top: clamp(4px, 1vh, 8px); font-size: clamp(0.86rem, 1.5vh, 1.02rem);
}

.identity { margin-bottom: 20px; }
.identity-grid { display: grid; grid-template-columns: 240px 1fr; gap: 26px; align-items: start; }
.avatar-stage { display: flex; flex-direction: column; align-items: center; gap: 12px; }
#avatar-preview {
  width: 200px; height: 200px;
  border-radius: 26px;
  background: radial-gradient(circle at 50% 35%, rgba(255,255,255,0.10), rgba(0,0,0,0.32));
  border: 1px solid var(--line);
}

.customizer { display: flex; flex-direction: column; gap: 14px; }
.cust-group > span {
  display: block; font-size: 0.74rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dim); margin-bottom: 7px;
}
.chip-row { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  font: inherit; font-size: 0.8rem; font-weight: 600;
  padding: 7px 13px; border-radius: 999px;
  background: rgba(0,0,0,0.26); border: 1px solid var(--line); color: var(--muted);
  cursor: pointer; transition: all 0.15s var(--ease); text-transform: capitalize;
}
.chip:hover { color: var(--text); transform: translateY(-1px); }
.chip.on { background: var(--panel-strong); border-color: var(--violet); color: var(--text); box-shadow: 0 0 0 2px rgba(155,92,255,0.25); }
.swatch { width: 30px; height: 30px; border-radius: 50%; padding: 0; border: 2px solid transparent; cursor: pointer; transition: transform 0.15s var(--ease); }
.swatch:hover { transform: scale(1.14); }
.swatch.on { border-color: #fff; box-shadow: 0 0 0 3px rgba(255,255,255,0.22); transform: scale(1.1); }

.home-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
/* The strip below spans exactly these two cards, so both share one width. */
.home-actions, .id-strip { width: min(100%, 1080px); margin-left: auto; margin-right: auto; }
.action-card h2 { font-size: 1.2rem; margin-bottom: 14px; font-weight: 800; }
.home-foot { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-top: clamp(4px, 1.2vh, 10px); }
.conn { font-size: 0.78rem; color: var(--dim); font-weight: 600; }
.conn.ok { color: var(--lime); }
.conn.bad { color: var(--red); }

/* "v0.5.0 · what's new" and the list it opens (whatsNew.js). Quiet until there
   is something you have not read: then a dot. */
.ver-link {
  position: relative; font: inherit; font-size: 0.78rem; font-weight: 600; color: var(--dim);
  background: none; border: 0; padding: 2px 10px 2px 2px; cursor: pointer;
}
.ver-link:hover { color: var(--muted); text-decoration: underline; text-underline-offset: 3px; }
.ver-dot {
  position: absolute; top: 2px; right: 0; width: 7px; height: 7px; border-radius: 50%;
  background: var(--pink); box-shadow: 0 0 8px rgba(255, 77, 141, 0.8);
}
.ver-dot[hidden] { display: none; }
#ver-modal {
  position: fixed; inset: 0; z-index: 900;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
#ver-modal[hidden] { display: none; }
.ver-panel {
  position: relative; width: min(100%, 560px); max-height: 84vh;
  display: flex; flex-direction: column; gap: 12px;
  padding: 22px; border-radius: var(--radius);
  background: var(--bg2, #14112a); border: 1px solid var(--line);
  box-shadow: 0 30px 80px rgba(0,0,0,0.6);
}
.ver-title { margin: 0; font-size: 1.35rem; font-weight: 900; }
.ver-list { overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 18px; padding-right: 6px; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.18) transparent; }
.ver-head { display: flex; align-items: baseline; gap: 10px; margin: 0 0 4px; font-size: 1rem; }
.ver-v { font-family: 'JetBrains Mono', monospace; font-weight: 700; }
.ver-block.current .ver-v { color: var(--lime); }
.ver-date { color: var(--dim); font-size: 0.8rem; font-weight: 600; }
/* The older versions, folded to a line: a click opens one. */
details.ver-block > summary { cursor: pointer; list-style: none; color: var(--muted); }
details.ver-block > summary::-webkit-details-marker { display: none; }
details.ver-block > summary::before {
  content: '▸'; display: inline-block; width: 16px; font-size: 1.15rem; line-height: 1; color: var(--muted);
  transition: transform 0.15s var(--ease);
}
details.ver-block[open] > summary::before { transform: rotate(90deg); }
details.ver-block > summary:hover .ver-v { color: var(--text); }
details.ver-block[open] { padding-bottom: 4px; }
.ver-list:has(details) { gap: 8px; }
.ver-block.current { margin-bottom: 12px; }
/* A section per kind: a small header with a rule after it, in a dim tint of
   the kind's colour - not a capsule on every line. */
.ver-sec {
  display: flex; align-items: center; gap: 10px; margin: 12px 0 6px;
  font-size: 0.68rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase;
}
.ver-sec::after { content: ''; flex: 1; height: 1px; background: currentColor; opacity: 0.25; }
.ver-group.new .ver-sec { color: rgba(182, 255, 61, 0.62); }
.ver-group.change .ver-sec { color: rgba(52, 224, 255, 0.58); }
.ver-group.fix .ver-sec { color: rgba(255, 179, 64, 0.66); }
.ver-group.out .ver-sec { color: rgba(255, 77, 141, 0.62); }
.ver-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.ver-items li { position: relative; padding-left: 14px; font-size: 0.9rem; line-height: 1.45; color: var(--text); }
.ver-items li::before { content: '·'; position: absolute; left: 2px; color: var(--dim); font-weight: 900; }
.ver-build { margin: 0; text-align: right; color: var(--dim); font-size: 0.72rem; font-family: 'JetBrains Mono', monospace; }
.ver-build:empty { display: none; }

/* ============================================================
   LOBBY
   ============================================================ */
.lobby-wrap { max-width: 1100px; margin: 0 auto; padding-bottom: 40px; }
.lobby-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; margin-bottom: 16px; flex-wrap: wrap; }
.code-display {
  font-family: 'JetBrains Mono', monospace; font-size: clamp(2.4rem, 7vw, 3.6rem); font-weight: 700;
  letter-spacing: 0.16em; cursor: pointer;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
  transition: transform 0.15s var(--ease);
}
.code-display:hover { transform: scale(1.03); }
.lock-note { font-size: 0.8rem; color: var(--muted); font-weight: 600; }
.settings-readout { display: flex; gap: 7px; flex-wrap: wrap; justify-content: flex-end; }
.tag {
  font-size: 0.72rem; font-weight: 700; padding: 5px 11px; border-radius: 999px;
  background: rgba(255,255,255,0.07); border: 1px solid var(--line); color: var(--muted);
}
.tag.on { color: var(--lime); border-color: rgba(182,255,61,0.4); }

.lobby-taunt { color: var(--muted); font-style: italic; margin-bottom: 20px; font-size: 0.95rem; }

/* Twenty people is four rows of cards - 690px of them - between the room code
   and the Start button, which put Start 481px below the fold at 720p for the
   one person who has to press it. The grid scrolls inside itself past that
   point; the room's size is already on the games heading and the taunt. */
.player-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px; margin-bottom: 26px;
  /* The subtrahend is what the rest of the lobby costs above the fold - room
     code, taunt, toy strip and the whole host-controls block - measured at
     1280x720, which is the window where it is tightest. Not a plain `vh`
     fraction: at 720p the grid can only have one row of cards before Start
     goes under the fold, and on a 1080p monitor it can have two and a half,
     and one number cannot say both. layoutcheck holds it honest. */
  max-height: clamp(150px, calc(100vh - 553px), 380px);
  overflow-y: auto; scrollbar-width: thin;
}
.pcard {
  position: relative; text-align: center; padding: 16px 10px 14px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  transition: transform 0.2s var(--ease), border-color 0.2s;
  /* Cards are kept between renders now, so this plays once when somebody
     actually joins rather than on every room update. */
  animation: pop 0.35s var(--ease);
}
.pcard .pav { line-height: 0; }
@keyframes pop { from { opacity: 0; transform: scale(0.85) } to { opacity: 1; transform: none } }
.pcard:hover { transform: translateY(-3px); }
.pcard.me { border-color: rgba(52,224,255,0.5); box-shadow: 0 0 0 2px rgba(52,224,255,0.15); }
.pcard.off { opacity: 0.4; filter: grayscale(0.8); }
.pcard canvas { width: 84px; height: 84px; display: block; margin: 0 auto 8px; }
.pcard .pname { font-weight: 800; font-size: 0.98rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pcard .pmeta { font-size: 0.74rem; color: var(--dim); font-weight: 600; margin-top: 3px; }
/* A word, not a crown: the crown is what the last match's winner wears. */
.pcard .host-tag {
  position: absolute; top: 9px; right: 9px;
  font-size: 0.56rem; font-weight: 800; letter-spacing: 0.12em;
  padding: 3px 7px; border-radius: 999px;
  color: var(--muted); background: rgba(255,255,255,0.08); border: 1px solid var(--line);
}

.lobby-foot { text-align: center; margin-bottom: 28px; }
.waiting-note { color: var(--muted); font-weight: 600; }

.game-catalog { display: flex; flex-direction: column; gap: 18px; }
/* One section a group. The grid the whole catalogue used to be is per-section now. */
.ggroup-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
.ggroup.folded .ggroup-grid { display: none; }
.ggroup-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; border-bottom: 1px solid var(--line); padding-bottom: 8px; }
.ggroup.folded .ggroup-head { margin-bottom: 0; }
.ggroup-fold {
  display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
  font: inherit; color: var(--text); text-align: left; cursor: pointer;
  background: none; border: 0; padding: 4px 0;
}
.ggroup-chev { color: var(--dim); font-size: 0.8rem; transition: transform 0.2s var(--ease); }
.ggroup.folded .ggroup-chev { transform: rotate(-90deg); }
.ggroup-name { font-weight: 800; font-size: 1rem; }
.ggroup-count { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--dim); }
.ggroup.empty .ggroup-name, .ggroup.empty .ggroup-count { color: var(--dim); }
.ggroup-fold:hover .ggroup-name { color: var(--cyan); }
/* The host's shortcuts: whole sets across the top, all or none on each section. */
.gtools { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.gtools-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim); margin-right: 2px; }
.gchip {
  font: inherit; font-size: 0.8rem; font-weight: 700; cursor: pointer;
  padding: 7px 13px; border-radius: 999px; color: var(--muted);
  border: 1px solid var(--line); background: rgba(0,0,0,0.2);
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.gchip:hover:not(:disabled) { color: var(--text); border-color: var(--violet); }
.gchip.on { color: var(--text); border-color: rgba(182,255,61,0.5); background: rgba(182,255,61,0.09); }
.gchip.small { font-size: 0.7rem; padding: 4px 10px; }
.gchip:disabled { opacity: 0.35; cursor: default; }
/* The invite link, said out loud. It was only ever "click the code". */
.code-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.gcard { padding: 14px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-sm); }
.gcard .gi { font-size: 1.5rem; }
.gcard .gn { font-weight: 800; margin: 4px 0 2px; }
.gcard .gb { font-size: 0.8rem; color: var(--muted); line-height: 1.35; }

/* ============================================================
   INTRO
   ============================================================ */
/* Sized against the WINDOW'S HEIGHT, not its width.
   Everything here used to scale on vw, which on a 1440x900 laptop - the shape
   of screen this game is played on - produced a 112px icon and a 54px title
   above a card of rules, and pushed the Ready button and the countdown off the
   bottom of the screen. Measured across all 38 games: 21 of them scrolled. The
   one screen with a deadline on it is the last one that should need scrolling
   to find the button, so the type now shrinks when the window is short. */
.intro-wrap {
  max-width: 640px; margin: 0 auto; text-align: center;
  min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(5px, 1.1vh, 10px);
}
.intro-icon { margin-bottom: -6px; }
.intro-icon { font-size: clamp(2.6rem, 7.5vh, 5rem); line-height: 1.1; animation: bob 1.6s ease-in-out infinite; }
@keyframes bob { 50% { transform: translateY(-14px) rotate(-5deg) } }
.intro-name {
  font-size: clamp(1.7rem, 5vh, 3rem); line-height: 1.1; font-weight: 900; letter-spacing: -0.02em;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
}
.intro-blurb { color: var(--muted); font-size: clamp(0.9rem, 1.9vh, 1.05rem); max-width: 34ch; }
/* One card holding the rules and any notes, instead of four stacked boxes down
   the middle of the screen. The old layout had a pill per rule plus a handicap
   box plus an audio box, all competing for the same fifteen seconds, and the
   result was that people read the title and nothing else. */
.intro-card {
  width: min(100%, 560px);
  padding: clamp(11px, 1.8vh, 16px) 20px; border-radius: var(--radius-sm);
  background: var(--panel); border: 1px solid var(--line);
  text-align: left;
}
.intro-rules { list-style: none; margin: 0; display: flex; flex-direction: column; gap: clamp(6px, 1.1vh, 9px); }
.intro-rules li {
  font-size: clamp(0.83rem, 1.75vh, 0.92rem); color: var(--text); padding-left: 22px; position: relative;
}
/* A bullet rather than a bordered pill: three pills read as three buttons. */
.intro-rules li::before {
  content: ''; position: absolute; left: 4px; top: 0.55em;
  width: 7px; height: 7px; border-radius: 50%; background: var(--violet);
}
.intro-notes:empty { display: none; }
.intro-notes { display: flex; flex-direction: column; gap: 8px; }
.intro-notes > :not([hidden]) ~ * { margin-top: 0; }
/* Quiet. Being helped shouldn't be the loudest thing on the screen, and
   announcing it at rule-size told the whole room you were losing. */
.handicap-note {
  margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--dim); font-weight: 500; font-size: 0.76rem; font-style: italic;
}

.countdown {
  font-family: 'JetBrains Mono', monospace; font-size: 4rem; font-weight: 700;
  color: var(--cyan); margin-top: 8px;
}
.countdown.tick { animation: tick 0.5s var(--ease); }
@keyframes tick { from { transform: scale(1.5); opacity: 0.2 } to { transform: none; opacity: 1 } }

/* ============================================================
   PLAY
   ============================================================ */
.play-wrap { max-width: 1240px; margin: 0 auto; height: 100%; display: flex; flex-direction: column; }
.play-head { display: flex; align-items: center; gap: 16px; margin-bottom: 14px; }
.play-title { font-size: 1.05rem; display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.timer-bar { flex: 1; height: 9px; background: rgba(0,0,0,0.4); border-radius: 999px; overflow: hidden; border: 1px solid var(--line); }
.timer-fill { height: 100%; width: 100%; background: var(--grad); transition: width 0.1s linear; }
.timer-fill.low { background: linear-gradient(90deg, var(--red), var(--amber)); }
.timer-num { font-family: 'JetBrains Mono', monospace; font-weight: 700; width: 58px; text-align: right; }

.play-body { flex: 1; display: grid; grid-template-columns: 1fr 250px; gap: 16px; min-height: 0; }
.stage {
  position: relative; background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); overflow: hidden;
  /* Children must STRETCH, not centre. With align-items:center the .gstage
     inside gets its content height, its canvas holder's flex:1 then has no
     spare space to claim, and every canvas game collapses to nothing. */
  display: flex; align-items: stretch; justify-content: stretch;
  min-height: 480px;
}
.live-board { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
/* A game that shows the race on its own canvas (`ownBoard`) gets this column back. */
.own-board .play-body { grid-template-columns: 1fr; }
.own-board .live-board { display: none; }
.lrow {
  display: flex; align-items: center; gap: 9px; padding: 8px 10px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-sm);
  transition: border-color 0.2s;
}
.lrow.me { border-color: rgba(52,224,255,0.45); }
.lrow.done { opacity: 0.55; }
.lrow canvas { width: 34px; height: 34px; flex: none; }
.lrow .ln { font-size: 0.82rem; font-weight: 700; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lrow .lv { font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; font-weight: 700; color: var(--cyan); }
.lbar { height: 4px; background: rgba(0,0,0,0.4); border-radius: 999px; overflow: hidden; margin-top: 5px; }
.lbar > i { display: block; height: 100%; width: 0; background: var(--grad); transition: width 0.2s var(--ease); }
.lrow .lstack { flex: 1; min-width: 0; }

/* ============================================================
   RESULTS
   ============================================================ */
/* Every vertical measure on this screen is against the window's HEIGHT, not
   its width. Results is up for eight seconds and then leaves by itself, so
   anything below the fold is not read late - it is never read at all.
   Measured at five players: the screen ran 232px past the bottom of a
   1280x720 window before this. */
/* A column of a known height, so the one part that grows with the room - the
   table, one row per player - is the part that gives. See .results-table. */
.results-wrap {
  max-width: 760px; margin: 0 auto; padding-bottom: clamp(4px, 1.4vh, 40px);
  display: flex; flex-direction: column; height: 100%;
}
.results-title { font-size: clamp(1.5rem, 4.4vh, 2.6rem); font-weight: 900; margin-bottom: clamp(6px, 1.4vh, 14px); }
.taunt-stack { display: flex; flex-direction: column; gap: 8px; margin-bottom: 22px; }
/* Roasts render on the rows now, so this box is always empty on results - and
   an empty flex box still spends its margin. #final-taunts already had this. */
.taunt-stack:empty { display: none; }
.taunt-line {
  padding: 12px 18px; border-radius: var(--radius-sm); font-weight: 600; font-size: 0.98rem;
  background: rgba(255,77,141,0.10); border: 1px solid rgba(255,77,141,0.3); color: #ffc4da;
  animation: slideIn 0.45s var(--ease) backwards;
}
.taunt-line:nth-child(2) { animation-delay: 0.5s; }
.taunt-line:nth-child(3) { animation-delay: 1s; }
@keyframes slideIn { from { opacity: 0; transform: translateX(-18px) } to { opacity: 1; transform: none } }

/* This screen is on an 8-second server clock, so nothing below the fold is
   ever read - and a full room is twenty rows, which at 720p is 1282px of table
   in a 680px window. The table scrolls inside itself instead. Everyone's
   placing is also in the standings rail, expanded, for the whole of this
   screen. */
.results-table {
  display: flex; flex-direction: column; gap: clamp(3px, 0.7vh, 8px);
  margin-bottom: calc(clamp(8px, 1.8vh, 26px) - 10px);
  min-height: 0; overflow-y: auto; scrollbar-width: thin;
  /* Room for a row that is hit to flinch - shoved sideways and leaning -
     without poking out of the box: that used to flash both scrollbars, white,
     with every tomato. The negative margin keeps the rows where they were. */
  padding: 10px 12px; margin-inline: -12px; margin-top: -10px;
}
.rrow {
  display: flex; align-items: center; gap: 12px; padding: clamp(7px, 1.1vh, 12px) 16px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-sm);
  animation: slideIn 0.4s var(--ease) backwards;
}
.rrow.first { border-color: rgba(255,179,64,0.55); background: rgba(255,179,64,0.08); }
.rrow.last { border-color: rgba(255,77,77,0.4); }
/* The same cyan the live board and the standings rail use for you, and after
   .first/.last so winning or coming last still reads first. */
.rrow.me { border-color: rgba(52,224,255,0.45); }
.rrow canvas { width: clamp(32px, 4.6vh, 40px); height: clamp(32px, 4.6vh, 40px); flex: none; }
.rrow .rk { font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--dim); width: 26px; }
.rrow .rn { font-weight: 800; flex: 1; }
.rrow .rs { font-size: 0.8rem; color: var(--muted); font-weight: 600; }
.rrow .rp { font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--lime); min-width: 56px; text-align: right; }
.rrow .rp.zero { color: var(--red); }

.sub { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--dim); margin-bottom: 10px; }
.standings { display: flex; flex-direction: column; gap: clamp(4px, 0.7vh, 6px); }
.srow { display: flex; align-items: center; gap: 12px; padding: clamp(6px, 1.2vh, 10px) 16px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-sm); }
.srow canvas { width: clamp(26px, 3.7vh, 32px); height: clamp(26px, 3.7vh, 32px); flex: none; }
.srow .sn { flex: 1; font-weight: 700; }
.srow .ss { font-family: 'JetBrains Mono', monospace; font-weight: 700; }
.srow.lead { border-color: rgba(255,179,64,0.5); }
.srow.tail { opacity: 0.75; }

/* ============================================================
   PICK
   ============================================================ */
.pick-wrap { max-width: 860px; margin: 0 auto; min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 14px; }
.pick-taunt { font-size: clamp(1.3rem, 4vw, 2rem); font-weight: 800; line-height: 1.25; max-width: 26ch; }
.pick-sub { color: var(--muted); font-style: italic; }
.pick-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; width: 100%; margin-top: 10px; }
.pick-card {
  padding: 22px 16px; background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); cursor: default; text-align: center;
  transition: transform 0.2s var(--ease), border-color 0.2s, box-shadow 0.2s;
  font: inherit; color: inherit;
}
.pick-card.pickable { cursor: pointer; }
.pick-card.pickable:hover { transform: translateY(-5px) scale(1.02); border-color: var(--violet); box-shadow: 0 14px 40px rgba(155,92,255,0.34); }
.pick-card.chosen { border-color: var(--lime); box-shadow: 0 0 0 3px rgba(182,255,61,0.3); }
.pick-card .pi { font-size: 2.4rem; }
.pick-card .pn { font-weight: 800; font-size: 1.1rem; margin: 6px 0 4px; }
.pick-card .pb { font-size: 0.82rem; color: var(--muted); line-height: 1.35; }
.pick-card.raffle { border-top: 4px solid var(--slice); }
/* The wheel is the screen in a raffle, so the headline gives up its second line
   to it and the cards come in a little. */
.pick-wrap:has(.pick-wheel-wrap:not([hidden])) { gap: 10px; }
.pick-wrap:has(.pick-wheel-wrap:not([hidden])) .pick-taunt { max-width: none; font-size: clamp(1.2rem, 3.4vh, 1.8rem); }
.pick-wrap:has(.pick-wheel-wrap:not([hidden])) .pick-options { margin-top: 4px; }
.pick-wrap:has(.pick-wheel-wrap:not([hidden])) .pick-card .pi { font-size: 1.9rem; }
.pick-wheel-wrap { position: relative; width: clamp(180px, 42vh, 400px); aspect-ratio: 1; flex: none; margin-top: 8px; }
.pick-wheel {
  position: absolute; inset: 0; border-radius: 50%;
  border: 4px solid rgba(255,255,255,0.85);
  box-shadow: 0 10px 34px rgba(0,0,0,0.5), inset 0 0 0 2px rgba(0,0,0,0.25);
  will-change: transform;
}
.pick-wheel-peg {
  position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%;
  background: #fff; box-shadow: 0 0 0 1.5px rgba(0,0,0,0.35);
}
.pick-wheel-wrap.landed { animation: pick-landed 0.5s var(--ease); }
@keyframes pick-landed { 0% { transform: scale(1) } 30% { transform: scale(1.07) } 100% { transform: scale(1) } }
/* A unanimous vote's stamp: slammed on at an angle, over the wheel that did
   not need to spin. The words come from data-stamp so both languages work. */
.pick-wheel-wrap.stamped::after {
  content: attr(data-stamp); position: absolute; left: 50%; top: 50%; z-index: 3;
  padding: 0.18em 0.5em; border: 5px solid #ff4d8d; border-radius: 10px;
  color: #ff4d8d; background: rgba(12, 8, 26, 0.82);
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
  font-size: clamp(1.1rem, 4.6vh, 2.2rem); white-space: nowrap;
  transform: translate(-50%, -50%) rotate(-14deg);
  animation: pick-stamp 0.42s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
@keyframes pick-stamp {
  0% { transform: translate(-50%, -50%) rotate(-14deg) scale(2.6); opacity: 0 }
  100% { transform: translate(-50%, -50%) rotate(-14deg) scale(1); opacity: 1 }
}
.pick-wheel-icon {
  position: absolute; transform: translate(-50%, -50%);
  font-size: clamp(1.4rem, 5.4vh, 2.8rem); line-height: 1;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.55));
}
.pick-wheel-pointer {
  position: absolute; left: 50%; top: -12px; transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 14px solid transparent; border-right: 14px solid transparent;
  border-top: 26px solid var(--text);
  transform-origin: 50% 0; z-index: 1;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.6));
}
/* A click on a card. Re-added on every click, so hammering it rattles. */
.pick-card.poked { animation: pick-poke 0.3s var(--ease); }
.pick-card.poked .pi { animation: pick-hop 0.3s var(--ease); }
@keyframes pick-poke {
  0% { transform: scale(1) rotate(0deg); }
  28% { transform: scale(0.92, 0.88) rotate(var(--lean, 4deg)); }
  62% { transform: scale(1.05, 1.08) rotate(calc(var(--lean, 4deg) * -0.5)); }
  100% { transform: scale(1) rotate(0deg); }
}
@keyframes pick-hop {
  0% { transform: none; }
  40% { transform: translateY(-16px) scale(1.25) rotate(calc(var(--lean, 4deg) * -2)); }
  100% { transform: none; }
}
/* Heckling: what gets thrown on the results screen, and what it leaves. */
.heckle-shot {
  position: absolute; left: 0; top: 0; font-size: 1.9rem; line-height: 1;
  margin: -0.95rem 0 0 -0.95rem; will-change: transform;
  filter: drop-shadow(0 3px 4px rgba(0,0,0,0.5));
}
/* Where it lands: one canvas for every splat and petal (heckleFx.js). Under
   the things still in the air and under everybody's pointer. */
.heckle-fx { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: -1; }
/* A row that can be thrown at says so, and flinches when it is hit. */
.results-table.heckling .rrow { cursor: crosshair; }
.rrow.hit { animation: heckle-flinch 0.26s var(--ease); }
@keyframes heckle-flinch {
  0% { transform: none; }
  30% { transform: translateX(var(--shove, 7px)) rotate(var(--lean, 1deg)); }
  65% { transform: translateX(calc(var(--shove, 7px) * -0.5)); }
  100% { transform: none; }
}
.rrow .rhits { font-size: 0.78rem; font-weight: 700; color: var(--muted); white-space: nowrap; min-width: 0; }
.rrow .rhits:empty { display: none; }
.heckle-bar { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 10px; font-size: 0.78rem; font-weight: 600; color: var(--dim); }
.heckle-bar[hidden] { display: none; }
.heckle-bar button {
  font: inherit; font-size: 1.15rem; line-height: 1; padding: 5px 9px; cursor: pointer;
  border-radius: 999px; border: 1px solid var(--line); background: rgba(0,0,0,0.2);
  transition: transform 0.15s var(--ease), border-color 0.15s;
}
.heckle-bar button.on { border-color: var(--lime); background: rgba(182,255,61,0.1); transform: scale(1.12); }
/* The rules screen: somebody who is not ready can be poked, and it shows. */
.rface.pokeable { cursor: pointer; }
.rface.poked, .btn.poked { animation: heckle-rattle 0.3s var(--ease); }
@keyframes heckle-rattle {
  0% { transform: none; }
  20% { transform: translateX(-6px) rotate(-4deg); }
  45% { transform: translateX(6px) rotate(4deg); }
  70% { transform: translateX(-3px) rotate(-2deg); }
  100% { transform: none; }
}
/* Said on the line that was already there: a new one would make the match's
   rules screen a line taller than the playground's copy of it. */
.ready-note.poked-by { color: var(--amber); font-weight: 700; }

/* The crowd sits over the whole screen and never takes a click of its own. */
.pick-crowd { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 5; }
.pick-cursor {
  position: absolute; left: 0; top: 0; will-change: transform;
  transition: transform 0.09s linear;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.55));
  /* Seen through: it is somebody else's pointer, over cards you are reading. */
  opacity: 0.72;
}
.pick-cursor-face { position: absolute; left: 9px; top: 11px; display: block; }
.pick-cursor svg { display: block; }
.pick-cursor-name {
  position: absolute; left: 12px; top: 50px; white-space: nowrap;
  font-size: 0.68rem; font-weight: 800; line-height: 1; color: #0b0a14;
  background: var(--who); padding: 3px 7px; border-radius: 999px;
}
.pick-cursor-ring {
  position: absolute; left: -13px; top: -13px; width: 28px; height: 28px; border-radius: 50%;
  border: 3px solid var(--who); animation: pick-ring 0.42s ease-out forwards;
}
@keyframes pick-ring { from { transform: scale(0.3); opacity: 1; } to { transform: scale(1.7); opacity: 0; } }
.pick-pop {
  position: absolute; font-size: 1.7rem; line-height: 1; margin: -0.85rem 0 0 -0.85rem;
  animation: pick-pop 0.75s cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}
@keyframes pick-pop {
  0% { transform: translate(0, 0) scale(0.4) rotate(0deg); opacity: 0; }
  15% { opacity: 1; }
  100% { transform: translate(var(--dx, 0), var(--dy, -90px)) scale(1.25) rotate(var(--spin, 20deg)); opacity: 0; }
}
.pick-timer { width: 100%; max-width: 400px; height: 6px; background: rgba(0,0,0,0.4); border-radius: 999px; overflow: hidden; margin-top: 16px; }
.pick-timer-fill { height: 100%; width: 100%; background: var(--grad); transition: width 0.12s linear; }

/* ============================================================
   FINAL
   ============================================================ */
.final-wrap { max-width: 760px; margin: 0 auto; text-align: center; padding-bottom: clamp(8px, 2vh, 40px); }

.final-title { font-size: clamp(1.7rem, 5vh, 3rem); font-weight: 900; margin: clamp(4px, 1vh, 10px) 0 clamp(10px, 2.4vh, 24px); background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
.podium { display: flex; align-items: flex-end; justify-content: center; gap: 12px; margin-bottom: clamp(10px, 2.6vh, 26px); }
.plinth { display: flex; flex-direction: column; align-items: center; gap: 8px; animation: rise 0.6s var(--ease) backwards; }
.plinth:nth-child(1) { animation-delay: 0.5s } .plinth:nth-child(2) { animation-delay: 0.2s } .plinth:nth-child(3) { animation-delay: 0.8s }
@keyframes rise { from { opacity: 0; transform: translateY(50px) } to { opacity: 1; transform: none } }
.plinth canvas { width: clamp(56px, 8.4vh, 76px); height: clamp(56px, 8.4vh, 76px); }
/* The crowning: see crowning.js. The crown and the knocked-off hat are canvases
   the avatar's own size laid exactly over it, so they line up to the pixel. */
.plinth-face { position: relative; line-height: 0; }
.plinth-face .crown-falling, .plinth-face .hat-knocked { position: absolute; left: 0; top: 0; pointer-events: none; }
.crown-falling { animation: crown-fall 2.6s cubic-bezier(0.35, 0, 0.2, 1) 1.5s both; filter: drop-shadow(0 6px 10px rgba(255,210,63,0.5)); }
@keyframes crown-fall {
  0% { transform: translateY(-260%) rotate(-32deg) scale(1.5); opacity: 0; }
  12% { opacity: 1; }
  78% { transform: translateY(-14%) rotate(7deg) scale(1.08); }
  90% { transform: translateY(5%) rotate(-3deg) scale(1.02, 0.94); }
  100% { transform: none; opacity: 1; }
}
.hat-knocked { animation: hat-knocked 0.85s cubic-bezier(0.2, 0.7, 0.4, 1) forwards; }
@keyframes hat-knocked {
  0% { transform: none; opacity: 1; }
  100% { transform: translate(90px, -70px) rotate(200deg); opacity: 0; }
}
.plinth-face.crowned { animation: crowned 0.5s var(--ease); }
/* The camera going on: a jolt, as if the flash had gone off in their face. */
.plinth-face.snapped { animation: snapped 0.45s var(--ease); }
@keyframes snapped { 0% { transform: scale(1); filter: brightness(2.2); } 35% { transform: scale(1.08) rotate(-3deg); } 100% { transform: none; filter: none; } }
/* The moment it lands: rays behind the head, once, turning as they fade. */
.plinth-face { isolation: isolate; }
.plinth-face.crowned::before {
  content: ''; position: absolute; left: 50%; top: 18%; width: 260%; aspect-ratio: 1;
  translate: -50% -50%; z-index: -1; border-radius: 50%; pointer-events: none;
  background: repeating-conic-gradient(from 0deg, rgba(255,214,80,0.55) 0deg 9deg, rgba(255,214,80,0) 9deg 24deg);
  -webkit-mask-image: radial-gradient(circle, #000 0 22%, transparent 68%);
  mask-image: radial-gradient(circle, #000 0 22%, transparent 68%);
  animation: crown-rays 1.8s ease-out forwards;
}
@keyframes crown-rays {
  0% { opacity: 0; scale: 0.2; rotate: 0deg; }
  18% { opacity: 1; scale: 1; }
  100% { opacity: 0; scale: 1.25; rotate: 70deg; }
}
@keyframes crowned { 0% { transform: none; } 35% { transform: translateY(5px) scale(1.06, 0.92); } 100% { transform: none; } }
.plinth .base {
  width: 100px; border-radius: 12px 12px 0 0; display: flex; align-items: flex-start; justify-content: center;
  padding-top: 8px; font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 1.3rem;
  background: var(--panel-strong); border: 1px solid var(--line); border-bottom: none;
}
/* The three plinths keep their proportions to each other on a short window;
   they are the picture of who won, and 110/80/56 flat is a third of a 720px
   window spent before the standings start. */
.plinth.p1 .base { height: clamp(72px, 12.2vh, 110px); background: linear-gradient(180deg, rgba(255,179,64,0.35), transparent); border-color: rgba(255,179,64,0.55); }
.plinth.p2 .base { height: clamp(52px, 8.9vh, 80px); }
.plinth.p3 .base { height: clamp(36px, 6.2vh, 56px); }
.plinth .pnm { font-weight: 800; font-size: 0.92rem; }
.final-hint { margin-top: 10px; font-size: 0.8rem; color: var(--dim); }

/* Pinned to the bottom of the window rather than left at the end of the page.
   These three buttons are how the evening continues, and they sit under a
   podium, a standings table and a highlight reel: measured at 348px below the
   fold on a 1280x720 window, with nothing on screen to suggest scrolling. The
   screen still scrolls; the buttons just never leave. */
.final-actions {
  display: flex; gap: 12px; justify-content: center; margin-top: clamp(12px, 2.6vh, 26px);
  /* Sticky sticks to the scrollport's CONTENT box, so bottom:0 leaves the
     screen's own bottom padding showing under the bar with the reel scrolling
     through it. Cancel that padding out and put it back inside the bar, so the
     buttons keep their distance from the edge of the window and the fade below
     them reaches it. */
  position: sticky; bottom: calc(-1 * var(--screen-pad-y)); z-index: 2;
  padding: 26px 0 var(--screen-pad-y);
  background: linear-gradient(180deg, rgba(11,10,20,0), var(--bg) 62%);
  transition: opacity 0.35s ease;
}
/* Until the crown is on and the camera has gone off. See onMatchEnd. */
.final-actions.held { visibility: hidden; opacity: 0; pointer-events: none; }

/* ============================================================
   CHAT + TOASTS
   ============================================================ */
/* A card in the left rail, under the settings. It used to be a bare stack of
   floating bubbles pinned to the corner, which is why it read as something
   spilled onto the page rather than part of it. */
#chat-rail { display: none; flex-direction: column; min-height: 0; }
#chat-rail.on { display: flex; }
#chat-log {
  display: flex; flex-direction: column; gap: 5px;
  padding: 8px; max-height: 190px; overflow-y: auto; scrollbar-width: thin;
}
#chat-log:empty { display: none; }
.cmsg {
  font-size: 0.8rem; padding: 6px 9px; border-radius: 9px;
  background: rgba(255,255,255,0.04);
  animation: slideIn 0.25s var(--ease);
}
.cmsg b { color: var(--cyan); }
#chat-form { padding: 8px; border-top: 1px solid var(--line); }
#chat-form input {
  width: 100%; font: inherit; font-size: 0.82rem; padding: 8px 12px;
  border-radius: 999px; background: rgba(0,0,0,0.45); border: 1px solid var(--line); color: var(--text);
}
#chat-form input:focus { outline: none; border-color: var(--violet); }

/* Clear of the right rail, not on top of it.
 *
 * `right: 16px` put every toast squarely over the standings dock, which lives
 * at the top of the rail band and is the one panel in the app that is supposed
 * to be readable at a glance all match. Two stacked toasts covered it entirely.
 * So the stack stops one --rail-gap short of the rail, which lines it up with
 * the content column the screens already reserve. */
#toast-stack {
  position: fixed; top: 16px; z-index: 1000;
  right: calc(var(--rail-w) + var(--rail-gap) + 24px);
  display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
}
.toast {
  padding: 12px 18px; border-radius: var(--radius-sm); font-weight: 700; font-size: 0.9rem;
  background: rgba(20,16,36,0.95); border: 1px solid var(--line); box-shadow: var(--shadow);
  animation: toastIn 0.35s var(--ease); max-width: 320px;
}
.toast.bad { border-color: rgba(255,77,77,0.5); color: #ffb3b3; }
.toast.good { border-color: rgba(182,255,61,0.5); color: var(--lime); }
@keyframes toastIn { from { opacity: 0; transform: translateX(30px) } to { opacity: 1; transform: none } }
.toast.out { animation: toastOut 0.3s var(--ease) forwards; }
@keyframes toastOut { to { opacity: 0; transform: translateX(30px) } }

/* ============================================================
   IN-GAME WIDGETS (shared by minigames)
   ============================================================ */
/* flex:1 rather than height:100% - a percentage height needs a parent with a
   definite height, and .stage only has a min-height. */
.gstage {
  width: 100%; flex: 1 1 auto; min-height: 0; align-self: stretch;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* Tight padding: every pixel here comes straight off the canvas. */
  gap: 12px; padding: 14px; text-align: center;
}
/* The holder every canvas game drops its <canvas> into. */
.gstage > .canvas-host {
  width: 100%; flex: 1 1 auto; min-height: 260px;
  display: flex; align-items: center; justify-content: center;
}
.gstage canvas { border-radius: var(--radius-sm); touch-action: none; }
/* What fitBox() builds. A transform takes up no space, so the frame reserves
   the scaled size and the field itself is taken out of flow inside it. */
.fit-frame { position: relative; flex: none; }
.fit-frame > * { position: absolute; top: 0; left: 0; }
.gbig { font-size: clamp(2rem, 7vw, 3.4rem); font-weight: 900; letter-spacing: -0.02em; }
.ghint { color: var(--muted); font-size: 0.9rem; }
.gstat { font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--cyan); font-size: 1.1rem; }

/* word sprint */
.ws-words { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; justify-content: center; max-width: 720px; min-height: 60px; }
.ws-word { font-size: 1.15rem; font-weight: 600; color: var(--dim); transition: all 0.2s var(--ease); }
.ws-word.cur { font-size: clamp(2rem, 6vw, 3rem); font-weight: 900; color: var(--text); letter-spacing: -0.01em; }
.ws-word.cur .ok { color: var(--lime); }
.ws-word.cur .bad { color: var(--red); text-decoration: underline wavy var(--red); }
.ws-input {
  font: inherit; font-size: 1.5rem; font-weight: 700; text-align: center; letter-spacing: 0.02em;
  padding: 12px 22px; border-radius: 999px; width: min(420px, 80vw);
  background: rgba(0,0,0,0.4); border: 2px solid var(--line); color: var(--text);
}
.ws-input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 4px rgba(155,92,255,0.2); }
.ws-input.wrong { border-color: var(--red); animation: shake 0.3s; }

/* arrow storm */
.as-row { display: flex; gap: 14px; justify-content: center; }
.as-arrow {
  width: clamp(56px, 9vw, 84px); height: clamp(56px, 9vw, 84px);
  display: flex; align-items: center; justify-content: center;
  font-size: clamp(1.8rem, 5vw, 2.6rem);
  border-radius: 16px; background: rgba(0,0,0,0.35); border: 2px solid var(--line);
  color: var(--dim); transition: all 0.12s var(--ease);
}
.as-arrow.lit { background: var(--grad); color: #12091c; border-color: transparent; transform: scale(1.14); box-shadow: 0 0 34px rgba(155,92,255,0.6); }
.as-arrow.ok { border-color: var(--lime); color: var(--lime); }
.as-arrow.bad { border-color: var(--red); color: var(--red); animation: shake 0.3s; }
.as-pads { display: grid; grid-template-columns: repeat(3, 60px); grid-template-rows: repeat(2, 60px); gap: 8px; margin-top: 8px; }
.as-pad { border-radius: 12px; background: rgba(0,0,0,0.35); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; cursor: pointer; color: var(--muted); font-family: inherit; }
.as-pad:active { background: var(--panel-strong); transform: scale(0.94); }

/* reflex */
.rf-panel {
  width: min(520px, 84%); height: min(300px, 46vh); border-radius: 22px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  font-size: 1.5rem; font-weight: 800; cursor: pointer; user-select: none;
  border: 2px solid var(--line); transition: background 0.06s;
}
.rf-panel.wait { background: rgba(255,77,77,0.18); color: #ff9d9d; }
.rf-panel.go { background: rgba(182,255,61,0.85); color: #14210a; border-color: var(--lime); }
.rf-panel.early { background: rgba(255,179,64,0.2); color: var(--amber); }
.rf-panel.rest { background: rgba(0,0,0,0.3); color: var(--muted); }
/* The number that decides the round, kept where you can watch it. Quiet until
   there is one - an empty "best: —" is just furniture. */
.rf-best {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: 1.05rem; color: var(--lime);
  letter-spacing: 0.02em; margin-top: 2px;
}
.rf-best span { color: var(--muted); font-weight: 400; font-size: 0.82rem; letter-spacing: 0.06em; }
.rf-best[hidden] { display: none; }

/* odd one out */
.oo-grid { display: grid; gap: 5px; }
.oo-tile { border-radius: 7px; cursor: pointer; border: none; padding: 0; transition: transform 0.08s; }
.oo-tile:active { transform: scale(0.94); }
.oo-grid.locked .oo-tile { cursor: not-allowed; }

/* generic overlays */
.g-overlay {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px;
  background: rgba(11,10,20,0.86); backdrop-filter: blur(6px); z-index: 5; text-align: center; padding: 20px;
}
.g-overlay .gbig { color: var(--lime); }
.splat {
  position: absolute; z-index: 6; pointer-events: none;
  font-size: 2.2rem; font-weight: 900; color: var(--red);
  text-shadow: 0 3px 0 rgba(0,0,0,0.5);
  animation: splat 0.7s var(--ease) forwards;
}
@keyframes splat {
  0% { opacity: 0; transform: translate(-50%,-50%) scale(0.3) rotate(-14deg) }
  35% { opacity: 1; transform: translate(-50%,-50%) scale(1.25) rotate(6deg) }
  100% { opacity: 0; transform: translate(-50%,-90%) scale(1) rotate(-3deg) }
}

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width: 900px) {
  .identity-grid { grid-template-columns: 1fr; }
  .home-actions { grid-template-columns: 1fr; }
  .play-body { grid-template-columns: 1fr; }
  .live-board { flex-direction: row; overflow-x: auto; }
  .lrow { min-width: 150px; }
  #chat-rail { display: none !important; }
  /* Two columns of chrome is most of a phone screen, so the chat goes away
     entirely, the rails narrow, and they overlay rather than reserving - which
     is what they did before any of this and is survivable at this size. */
  :root { --rail-w: 186px; }
  .screen { padding: clamp(16px, 3vw, 40px); }
  .pod-list { max-height: 26vh; }
}
@media (max-width: 560px) {
  .as-pads { grid-template-columns: repeat(3, 52px); grid-template-rows: repeat(2, 52px); }
  .stage { min-height: 340px; }
}

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

/* ============================================================
   LANGUAGE SWITCH / TOPBAR / TOOLTIPS
   ============================================================ */
/* Holds only the dev links now that language moved to the rail, so on a
   deployed site it is empty - collapse it rather than leave a gap. */
.topbar { display: flex; justify-content: flex-end; align-items: center; gap: 12px; flex-wrap: wrap; }
.topbar:has(> :only-child[hidden]) { display: none; }
.lang-switch { display: inline-flex; gap: 4px; padding: 3px; border-radius: 999px; background: rgba(0,0,0,0.3); border: 1px solid var(--line); }
.lang-btn {
  font: inherit; font-size: 0.78rem; font-weight: 700; cursor: pointer;
  padding: 6px 12px; border-radius: 999px; border: none;
  background: transparent; color: var(--dim); transition: all 0.15s var(--ease);
}
.lang-btn:hover { color: var(--text); }
.lang-btn.on { background: var(--panel-strong); color: var(--text); box-shadow: 0 0 0 1px rgba(255,255,255,0.12); }

.dev-link {
  font-size: 0.78rem; font-weight: 700; color: var(--dim); text-decoration: none;
  padding: 7px 14px; border-radius: 999px; border: 1px dashed var(--line);
  transition: all 0.15s var(--ease);
}
.dev-link:hover { color: var(--cyan); border-color: var(--cyan); }

/* ── The rail card's rows ───────────────────────────────────────────── */

/* Discord's blurple, deliberately. It is the one place in the app that borrows
   another product's colour, and that is the point: you should recognise it
   without reading it. The mark is an inline SVG behind a mask, so it inherits
   currentColor and costs no request - same reasoning as every other graphic. */
.discord-link {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 15px; border-radius: 999px;
  font-size: 0.8rem; font-weight: 700; text-decoration: none;
  color: #c3c9ff;
  background: rgba(88, 101, 242, 0.16);
  border: 1px solid rgba(88, 101, 242, 0.45);
  backdrop-filter: blur(10px);
  transition: all 0.15s var(--ease);
}
.discord-link:hover {
  color: #fff; background: rgba(88, 101, 242, 0.9);
  border-color: rgba(88, 101, 242, 1); transform: translateY(-1px);
}
.discord-mark {
  width: 17px; height: 13px; flex: none;
  background: currentColor;
  -webkit-mask: var(--discord-mark) center / contain no-repeat;
  mask: var(--discord-mark) center / contain no-repeat;
}

/* No invite configured. Shown rather than hidden, because a control that
   vanishes leaves you wondering whether you broke it or never set it - and
   the answer, locally, is always "never set it". Greyed out says so. */
.discord-link.off {
  color: var(--dim); background: rgba(10,9,22,0.7);
  border-color: var(--line); cursor: default; opacity: 0.65;
}
.discord-link.off:hover {
  color: var(--dim); background: rgba(10,9,22,0.7);
  border-color: var(--line); transform: none;
}

/* The language switcher fills its row instead of being a pill inside a card. */
#lang-dock { padding: 0; }
#lang-dock .lang-switch {
  display: flex; width: 100%; gap: 0; padding: 0;
  background: transparent; border: none; border-radius: 0;
}
#lang-dock .lang-btn { flex: 1; border-radius: 0; padding: 10px 6px; }
#lang-dock .lang-btn.on { box-shadow: none; background: rgba(255,255,255,0.07); }

.tip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; flex: none; margin-left: 2px;
  border-radius: 50%; border: 1px solid var(--line);
  font-size: 0.66rem; font-weight: 800; font-style: normal;
  color: var(--dim); cursor: help; user-select: none;
  transition: all 0.15s var(--ease);
}
.tip:hover, .tip:focus-visible { color: var(--cyan); border-color: var(--cyan); outline: none; transform: scale(1.15); }

#tip-pop {
  white-space: pre-line;
  position: fixed; z-index: 1200; pointer-events: none;
  padding: 12px 15px; border-radius: var(--radius-sm);
  background: rgba(16,13,30,0.97); border: 1px solid var(--violet);
  box-shadow: 0 14px 44px rgba(0,0,0,0.6);
  font-size: 0.83rem; line-height: 1.45; color: var(--text);
  opacity: 0; transform: translateY(-5px); transition: opacity 0.14s, transform 0.14s;
}
#tip-pop.on { opacity: 1; transform: none; }

/* ============================================================
   LOBBY: minigame toggles
   ============================================================ */
.games-head { display: flex; align-items: center; gap: 8px; }
.games-head em { font-style: normal; font-family: 'JetBrains Mono', monospace; color: var(--dim); letter-spacing: 0; }
.games-head em.warn { color: var(--amber); }

.gcard { position: relative; text-align: left; font: inherit; color: inherit; }
.gcard.toggleable { cursor: pointer; transition: transform 0.15s var(--ease), border-color 0.2s, opacity 0.2s; }
.gcard.toggleable:hover { transform: translateY(-3px); border-color: var(--violet); }
.gcard.off { opacity: 0.34; filter: grayscale(0.85); }
.gcard.on { border-color: rgba(182,255,61,0.28); }
/* A game that joined this week: a pink corner tab, top left, so it never sits
   on the amber badges top right and reads from across the lobby. */
.gnew {
  position: absolute; top: -7px; left: -7px; z-index: 1; pointer-events: none;
  font-size: 0.62rem; font-weight: 900; text-transform: uppercase; letter-spacing: 0.1em;
  padding: 3px 8px; border-radius: 999px; color: #1a0b16;
  background: linear-gradient(135deg, #ff5d8f, #ffb020);
  box-shadow: 0 3px 10px rgba(255, 93, 143, 0.45);
}
.test-game { position: relative; }
.gbadge {
  position: absolute; top: 8px; right: 8px;
  font-size: 0.6rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
  padding: 3px 7px; border-radius: 999px;
  background: rgba(255,179,64,0.16); border: 1px solid rgba(255,179,64,0.4); color: var(--amber);
}

/* ============================================================
   INTRO: ready-up
   ============================================================ */
.ready-block { display: flex; flex-direction: column; align-items: center; gap: clamp(7px, 1.4vh, 12px); margin-top: clamp(2px, 0.7vh, 6px); }
/* A room with ready-up switched off hides this block. It did not disappear -
   see the note on .stale-bar - so the setting turned the button off without
   taking it off the screen. */
.ready-block[hidden] { display: none; }
#btn-ready.done { background: rgba(182,255,61,0.16); color: var(--lime); box-shadow: none; border: 1px solid rgba(182,255,61,0.5); }
.ready-faces { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.rface {
  position: relative; opacity: 0.4; filter: grayscale(0.9);
  transition: opacity 0.25s var(--ease), filter 0.25s, transform 0.25s var(--ease);
}
.rface.in { opacity: 1; filter: none; transform: translateY(-3px); }
.rface canvas { display: block; }
.rtick {
  position: absolute; bottom: -2px; right: -2px;
  width: 17px; height: 17px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.62rem; font-weight: 900;
  background: var(--bg); border: 1px solid var(--line); color: var(--dim);
}
.rface.in .rtick { background: var(--lime); color: #14210a; border-color: var(--lime); }
.ready-note { font-size: 0.85rem; color: var(--muted); font-weight: 600; }

.countdown-wrap { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: clamp(0px, 0.6vh, 4px); }
.countdown-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--dim); font-weight: 700; }
.countdown { font-size: clamp(1.7rem, 4.4vh, 2.6rem); line-height: 1.1; }
.countdown.urgent { color: var(--amber); }

/* ============================================================
   LIVE BOARD (clearer at-a-glance opponent progress)
   ============================================================ */
.live-board { gap: 7px; }
.lrow { position: relative; transition: border-color 0.2s, background 0.25s, transform 0.25s var(--ease); }
.lrow.lead { border-color: rgba(255,179,64,0.5); background: rgba(255,179,64,0.07); }
.lrow.me { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.06); }
.lrank {
  font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 700;
  color: var(--dim); width: 22px; flex: none; text-align: center;
}
.lrow.lead .lrank { color: var(--amber); }
.lv { transition: color 0.15s; }
.lv.bump { animation: lvBump 0.32s var(--ease); }
@keyframes lvBump {
  0% { transform: scale(1); }
  40% { transform: scale(1.32); color: var(--lime); }
  100% { transform: scale(1); }
}
.lbar > i { transition: width 0.25s var(--ease); }

/* ============================================================
   NEW MINIGAMES
   ============================================================ */

/* shared progress bar used by the timed-prompt games */
.ar-bar {
  width: min(420px, 80%); height: 7px; border-radius: 999px; overflow: hidden;
  background: rgba(0,0,0,0.45); border: 1px solid var(--line);
}
.ar-bar > i { display: block; height: 100%; width: 100%; background: var(--grad); }

/* arrow storm: shrink tiles when the sequence gets long */
.as-row { --as-scale: 1; flex-wrap: nowrap; max-width: 100%; }
.as-row.tight { gap: 5px; }
.as-arrow {
  width: calc(clamp(56px, 9vw, 84px) * var(--as-scale));
  height: calc(clamp(56px, 9vw, 84px) * var(--as-scale));
  font-size: calc(clamp(1.8rem, 5vw, 2.6rem) * var(--as-scale));
  border-radius: calc(16px * var(--as-scale));
  flex: none;
}

/* arrow rush */
.ar-arrow {
  font-size: clamp(5rem, 18vw, 9rem); line-height: 1;
  transition: transform 0.08s var(--ease), color 0.08s;
  color: var(--text);
}
.ar-arrow.live { animation: arPop 0.14s var(--ease); }
.ar-arrow.good { color: var(--lime); transform: scale(1.12); }
.ar-arrow.bad { color: var(--red); transform: scale(0.86) rotate(-8deg); }
@keyframes arPop { from { transform: scale(0.7); opacity: 0.3 } to { transform: none; opacity: 1 } }

/* stroop */
.st-word {
  font-size: clamp(3rem, 11vw, 6rem); font-weight: 900; letter-spacing: 0.02em;
  text-shadow: 0 4px 24px rgba(0,0,0,0.5);
}
.st-options { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; max-width: 560px; }
.st-opt {
  font: inherit; font-weight: 800; font-size: 0.95rem;
  padding: 11px 20px; border-radius: 999px; cursor: pointer;
  background: rgba(0,0,0,0.35); border: 2px solid var(--line); color: var(--text);
  transition: transform 0.12s var(--ease), background 0.15s;
}
.st-opt:hover { transform: translateY(-2px); background: rgba(255,255,255,0.09); }
.st-opt:active { transform: scale(0.95); }

/* pairs */
.pr-arena { position: relative; }
.pr-board { display: grid; gap: 8px; perspective: 900px; }
/* The card is the hinge; the two faces are what you see. Rotating the parent
   and hiding whichever face points away is the whole animation - no per-frame
   JavaScript, and the browser keeps it on the compositor. */
.pr-card {
  position: relative; padding: 0; border: none; background: none;
  cursor: pointer; transform-style: preserve-3d;
  transition: transform 0.26s cubic-bezier(0.3, 0.8, 0.4, 1);
  /* Staggered a row at a time, so a twenty-card peek reads as a wave rather
     than one violent flicker. */
  transition-delay: calc(var(--row, 0) * 45ms);
}
.pr-card.up { transform: rotateY(180deg); }
.pr-card:active { transform: scale(0.95) rotateY(0deg); }
.pr-card.up:active { transform: scale(0.95) rotateY(180deg); }

.pr-face {
  position: absolute; inset: 0; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  /* Scales with the card instead of a fixed size - the symbol is the thing you
     are trying to remember, so it gets the room. */
  font-size: calc(var(--card, 74px) * 0.62);
  line-height: 1; font-style: normal;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
.pr-back {
  background: var(--violet);
  border: 1px solid rgba(255,255,255,0.08);
  background-image: repeating-linear-gradient(45deg,
    rgba(255,255,255,0.05) 0 6px, transparent 6px 12px);
}
/* The colour is on the SYMBOL, not the card: a wall of coloured tiles is
   noisy, and a big bright glyph on a calm face is easier to hold in your head.
   It stays redundant with the shape either way, so it is never the only thing
   telling two cards apart. */
.pr-front {
  transform: rotateY(180deg);
  background: var(--panel-strong);
  border: 1px solid var(--line);
  color: hsl(var(--hue, 210) 88% 66%);
  text-shadow: 0 0 18px hsl(var(--hue, 210) 90% 58% / 0.45);
}
.pr-card.done { cursor: default; }
.pr-card.done .pr-front {
  background: rgba(182,255,61,0.14);
  border-color: rgba(182,255,61,0.55);
}

/* The "look now" sits over the board, because that is where you are looking. */
.pr-veil {
  position: absolute; inset: -10px; border-radius: 14px;
  background: rgba(6,5,14,0.72); backdrop-filter: blur(2px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; z-index: 4; transition: opacity 0.2s;
}
.pr-veil.gone { opacity: 0; pointer-events: none; }
.pr-veil-text { font-size: 1.5rem; font-weight: 800; letter-spacing: 0.02em; }
.pr-veil-num {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: 4.4rem; line-height: 1; color: var(--text);
}

/* The peek's own clock, so the look has a visible end. */
.pr-bar {
  width: min(430px, 80%); height: 6px; border-radius: 999px;
  background: rgba(255,255,255,0.08); overflow: hidden;
}
.pr-bar > i { display: block; height: 100%; width: 0%; background: var(--grad); }

/* minesweeper */
.ms-hud {
  display: flex; align-items: center; gap: 18px;
  font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 1.1rem;
}
.ms-lives { color: #ff6b8b; letter-spacing: 2px; }
.ms-left { color: var(--muted); }
.ms-count { color: var(--text); }
.ms-board { display: grid; gap: 3px; }
.ms-cell {
  padding: 0; border-radius: 6px; cursor: pointer;
  border: 1px solid rgba(255,255,255,0.10);
  background: var(--violet);
  font-family: 'JetBrains Mono', monospace; font-weight: 800;
  /* The digit is the entire information content of a square, so it gets most
     of the square rather than a polite fraction of it. */
  font-size: calc(var(--cell, 56px) * 0.52);
  line-height: 1; color: var(--text);
}
.ms-cell:hover { border-color: rgba(255,255,255,0.3); }
.ms-cell:active { transform: scale(0.94); }
/* An opened square is sunken and quiet - the closed ones are what you still
   have to think about, so they are the ones that stand out. */
.ms-cell.open {
  background: rgba(255,255,255,0.045);
  border-color: rgba(255,255,255,0.05);
  cursor: default;
}
.ms-cell.flag { color: #ffd23f; }
.ms-cell.mine {
  background: rgba(255,77,109,0.22);
  border-color: rgba(255,77,109,0.6);
}
/* Colour repeats the digit, it never replaces it: 1 and 2 are still 1 and 2 if
   you cannot tell these two blues apart. */
.ms-cell.n1 { color: #6db3ff; }
.ms-cell.n2 { color: #7ee787; }
.ms-cell.n3 { color: #ff8a8a; }
.ms-cell.n4 { color: #c4a4ff; }
.ms-cell.n5 { color: #ffb84d; }
.ms-cell.n6 { color: #5fe0d0; }
.ms-cell.n7 { color: #ffffff; }
.ms-cell.n8 { color: #9aa0b5; }

/* snake */
.sn-stage { gap: 10px; }
.sn-hud {
  display: flex; align-items: center; gap: 18px;
  font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 1.05rem;
}
.sn-best { color: var(--lime); }
.sn-len { color: var(--muted); }
.sn-brakes { color: var(--amber); }
/* No brakes left, and nothing where the count was - the gap would read as a
   layout bug rather than as an empty pocket. */
.sn-brakes.low { color: var(--dim); }

.sn-board {
  display: grid; gap: 1px;
  padding: 4px; border-radius: 10px;
  background: rgba(6,5,14,0.75); border: 1px solid var(--line);
}
.sn-cell { border-radius: 3px; background: rgba(255,255,255,0.025); }
.sn-cell.sn-body { background: #2f9e5a; border-radius: 4px; }
.sn-cell.sn-head { background: var(--lime); border-radius: 5px; }
.sn-cell.sn-apple { background: #ff4d6d; border-radius: 50%; }
.sn-cell.sn-dead { background: #ff4d6d; box-shadow: 0 0 0 2px rgba(255,255,255,0.6); }
/* The board itself flashes while a brake is holding, because the thing that
   stopped is the snake and looking at the HUD to find that out costs you the
   time the brake just bought. */
.sn-board.braking { box-shadow: 0 0 0 2px var(--amber); }

.sn-pad {
  display: grid; gap: 4px;
  grid-template-columns: repeat(3, 46px); grid-template-rows: repeat(2, 40px);
}
.sn-key {
  padding: 6px; cursor: pointer;
  border-radius: 10px; border: 1px solid rgba(255,255,255,0.12);
  background: var(--violet);
  display: flex; align-items: center; justify-content: center;
}
.sn-key:hover { border-color: rgba(255,255,255,0.35); }
.sn-key:active { transform: scale(0.92); }
/* A plus, laid out by hand: up over the middle, then the other three beneath. */
.sn-up { grid-column: 2; grid-row: 1; }
.sn-left { grid-column: 1; grid-row: 2; }
.sn-down { grid-column: 2; grid-row: 2; }
.sn-right { grid-column: 3; grid-row: 2; }

/* push your luck - one vault, everybody inside it */
.pl-wrap { gap: 14px; }
.pl-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.pl-vaultno { color: var(--cyan); font-size: 0.95rem; }
/* The head start, named for the whole round so 30 banked before the first crate
   is never mistaken for 30 somebody earned. Empty at tier 0, and empty is the
   normal case, so it takes no room when it has nothing to say. */
.pl-headstart { color: var(--lime); font-size: 0.85rem; font-weight: 700; }
.pl-headstart:empty { display: none; }

/* The vault, as two rows of six rather than a flex row breaking wherever the
   container happens to end. Twelve crates in a wrapping row came out nine and
   then three, which reads as a mistake rather than as a shape - and the shape
   is information here: half the vault above, half below, so how deep you are is
   a position you can see instead of a number you have to count to. */
.pl-crates {
  display: grid; grid-template-columns: repeat(6, 72px);
  gap: 8px; justify-content: center;
}
.pl-crate {
  width: 72px; height: 72px; border-radius: 12px;
  border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.05);
  display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 1.1rem;
  color: var(--muted);
  transition: background 0.14s var(--ease), border-color 0.14s, transform 0.14s var(--ease);
}
.pl-cval { line-height: 1; }
/* Open crates are filled, not merely outlined. The decision this game asks you
   to make is "how much further", and the only honest picture of how far in you
   already are is a solid block of them behind you. */
.pl-crate.gem {
  background: rgba(52,224,255,0.3); border-color: var(--cyan); color: #eafcff;
  box-shadow: inset 0 0 0 1px rgba(52,224,255,0.45);
  transform: translateY(-3px);
}
.pl-crate.alarm {
  background: rgba(255,77,77,0.24); border-color: var(--red); color: var(--text);
  font-size: 1.3rem;
}
/* What the vault still held when it shut. The only thing a locked vault can
   teach you is whether you left one crate early or five. */
.pl-crate.left { opacity: 0.34; border-style: dashed; }

/* Pot, button, odds. All three were on one row with three different baselines
   and no grouping, so the two numbers the decision actually turns on read as
   labels scattered around the button. They are a matched pair now - same
   caption treatment, same box, same baseline - with the button between them. */
.pl-mid { display: flex; align-items: stretch; gap: 18px; flex-wrap: wrap; justify-content: center; }
.pl-pot, .pl-risk {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  min-width: 132px; padding: 10px 14px; border-radius: 14px;
  border: 1px solid var(--line); background: rgba(255,255,255,0.03);
}
.pl-potcap, .pl-riskcap {
  font-size: 0.62rem; letter-spacing: 0.14em; color: var(--muted); font-weight: 700;
}
.pl-potnum {
  font-family: 'JetBrains Mono', monospace; font-weight: 900;
  font-size: clamp(2rem, 6vw, 2.9rem); line-height: 1; color: var(--dim);
}
.pl-pot.live .pl-potnum { color: var(--lime); }
.pl-risknum { font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2rem; line-height: 1; color: var(--muted); }
.pl-risknum.safe { color: var(--lime); font-size: 1.2rem; }
.pl-risknum.warm { color: var(--amber); }
.pl-risknum.hot { color: var(--red); }

.pl-bank {
  padding: 16px 34px; border-radius: 16px; border: 2px solid var(--lime);
  background: rgba(182,255,61,0.14); color: var(--lime);
  font: inherit; font-weight: 900; font-size: 1.15rem; letter-spacing: 0.08em; cursor: pointer;
  transition: transform 0.08s, background 0.14s;
}
.pl-bank:hover:not(:disabled) { background: rgba(182,255,61,0.24); }
.pl-bank:active:not(:disabled) { transform: scale(0.96); }
.pl-bank:disabled { opacity: 0.3; cursor: default; border-color: var(--line); color: var(--dim); background: transparent; }

/* Who is still in there with you. This is the half of the game that is not
   arithmetic, so it sits under the vault rather than off in the live board. */
.pl-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.pl-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.pl-chip .pl-av { display: flex; }
.pl-chip .pl-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-chip .pl-state {
  font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--muted);
}
.pl-chip.in { border-color: rgba(255,179,64,0.55); background: rgba(255,179,64,0.1); }
.pl-chip.in .pl-state { color: var(--amber); }
.pl-chip.out { opacity: 0.55; }
.pl-chip.caught { border-color: rgba(255,77,77,0.6); background: rgba(255,77,77,0.12); }
.pl-chip.caught .pl-state { color: var(--red); }
.pl-chip.me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

.pl-total { font-family: 'JetBrains Mono', monospace; font-weight: 800; color: var(--cyan); font-size: 1.05rem; }
.pl-wrap.locked .pl-crates { animation: plShut 0.34s var(--ease); }
@keyframes plShut {
  0% { transform: translateY(-4px); }
  40% { transform: translateY(3px); }
  100% { transform: translateY(0); }
}

/* shell game - one table, one shuffle, everybody calls it at once */
.sg-wrap { gap: 10px; }
.sg-head {
  display: flex; align-items: center; gap: 16px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.sg-dealno { color: var(--cyan); font-size: 0.95rem; }
.sg-worth { color: var(--amber); font-size: 0.85rem; }
.sg-seconds { display: flex; gap: 4px; font-size: 1rem; }
.sg-second.spent { opacity: 0.22; filter: grayscale(1); }

/* The table. Cups are absolutely placed by slot and slide between them, so the
   thing you are tracking is a real position on a real surface rather than a
   list that redraws - a redraw has no path, and the path is the game. Which is
   also why the surface is drawn at all: cups sliding through empty space are
   three shapes changing places, and a path needs a floor to be a path on. */
.sg-table {
  position: relative; width: 520px; max-width: 100%; height: 196px; margin: 2px auto;
  border-radius: 14px;
  background:
    radial-gradient(120% 70% at 50% 100%, rgba(155,92,255,0.16), transparent 70%),
    linear-gradient(180deg, transparent 55%, rgba(255,255,255,0.05) 100%);
}
.sg-table::after {
  content: ''; position: absolute; left: 4%; right: 4%; bottom: 30px; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.22), transparent);
}
.sg-cup {
  position: absolute; left: 6px; bottom: 30px; width: 92px; height: 132px;
  transition: transform 380ms ease-in-out;
}
/* This was 12%-to-3% white with a hairline border: three barely-there domes on
   a dark card, in the one game whose whole mechanic is following an object with
   your eyes. It is a solid object now - a lit top, a shaded base, a rim that
   catches the light and a shadow it sits in. */
.sg-dome {
  width: 100%; height: 100%; border-radius: 46px 46px 10px 10px;
  border: 1px solid rgba(255,255,255,0.30);
  background:
    linear-gradient(103deg, rgba(255,255,255,0.06) 0 32%, rgba(255,255,255,0.20) 46%, rgba(255,255,255,0.05) 62%),
    linear-gradient(180deg, #4a4166 0%, #332c4a 58%, #221d33 100%);
  box-shadow:
    inset 0 -14px 22px rgba(0,0,0,0.45),
    inset 0 2px 0 rgba(255,255,255,0.35),
    0 10px 18px rgba(0,0,0,0.45);
  transition: transform 0.18s var(--ease), border-color 0.14s, background 0.14s, opacity 0.14s;
}
.sg-cup.gone { display: none; }
.sg-cup.up .sg-dome { transform: translateY(-54px); }
/* Lifted by a second call: empty, and no longer callable. */
.sg-cup.out .sg-dome { opacity: 0.28; border-style: dashed; }
.sg-cup.picked .sg-dome { border-color: var(--violet); }
.sg-cup.had .sg-dome { border-color: var(--lime); background: rgba(182,255,61,0.16); }
.sg-cup.right .sg-dome { border-color: var(--lime); background: rgba(182,255,61,0.26); }
.sg-cup.wrong .sg-dome { border-color: var(--red); background: rgba(255,77,77,0.2); }
.sg-wrap.calling .sg-cup { cursor: pointer; }
.sg-wrap.calling .sg-cup:hover .sg-dome { border-color: rgba(255,255,255,0.4); }

.sg-ball {
  position: absolute; left: 0; bottom: 32px; width: 28px; height: 28px; margin-left: -14px;
  border-radius: 50%; background: radial-gradient(circle at 35% 30%, #fff, var(--amber) 60%, #b06f00);
  box-shadow: 0 0 14px rgba(255,179,64,0.55), 0 4px 6px rgba(0,0,0,0.5);
  opacity: 0; transition: opacity 0.16s;
}
.sg-ball.shown { opacity: 1; }

.sg-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* Who has committed, and - only once the cups are up - who was right. */
.sg-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.sg-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.sg-chip .sg-av { display: flex; }
.sg-chip .sg-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sg-chip .sg-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--muted); }
.sg-chip.ready { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.sg-chip.ready .sg-state { color: var(--cyan); }
.sg-chip.hit { border-color: rgba(182,255,61,0.6); background: rgba(182,255,61,0.12); }
.sg-chip.hit .sg-state { color: var(--lime); }
.sg-chip.missed { border-color: rgba(255,77,77,0.5); background: rgba(255,77,77,0.1); }
.sg-chip.missed .sg-state { color: var(--red); }

.sg-total { font-family: 'JetBrains Mono', monospace; font-weight: 800; color: var(--cyan); font-size: 1.05rem; }

/* where was it - one map for the room, and everybody's pins on it at the end */
.ww-wrap { gap: 9px; }
.ww-head {
  display: flex; align-items: center; gap: 16px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.ww-mapno { color: var(--cyan); font-size: 0.95rem; }
.ww-look {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.ww-look:disabled { opacity: 0.26; cursor: default; }
.ww-look[hidden] { display: none; }
.ww-total { color: var(--amber); font-size: 1.3rem; }

/* The window, draining. It is the only clock in the game, so it is map-wide. */
.ww-bar { width: 640px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.ww-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }
.ww-wrap.ww-revealing .ww-fill { background: var(--amber); }

/* The map keeps the server's aspect ratio exactly, because a click is scored by
   where it landed in that rectangle rather than by which cell it hit. */
.ww-map {
  position: relative; width: 640px; max-width: 100%; aspect-ratio: 1000 / 640;
  border-radius: 14px; border: 1px solid var(--line); background: var(--panel);
  overflow: hidden; cursor: crosshair;
}
.ww-wrap.ww-placing .ww-map { border-color: rgba(182,255,61,0.35); }
.ww-wrap.ww-looking .ww-map { cursor: default; box-shadow: inset 0 0 0 2px rgba(52,224,255,0.35); }
.ww-scene { position: absolute; inset: 0; }
/* Scenery, and why it is here: six dots on an empty field can only be
   remembered as coordinates. With landmarks people remember "left of the ring". */
/* At 6% white these were landmarks nobody could see, which makes them scenery
   that does not do the one job scenery has. Lifted until "left of the ring" is
   a thing you can actually say - still well under the icons, because they are
   the ground and the icons are the figure. */
.ww-blob { position: absolute; transform: translate(-50%, -50%); aspect-ratio: 1; }
.ww-s0 { border-radius: 50%; background: rgba(52,224,255,0.16); }
.ww-s1 { border-radius: 24%; background: rgba(155,92,255,0.17); }
.ww-s2 { border-radius: 50%; border: 3px solid rgba(255,255,255,0.16); }

/* The double shadow is not decoration: these are emoji, so their colours are
   whatever the font decided, and a white die on a pale blob vanished. A tight
   dark halo under any glyph makes every icon read on any piece of scenery. */
.ww-target {
  position: absolute; transform: translate(-50%, -50%); font-size: 1.8rem; line-height: 1;
  filter: drop-shadow(0 0 3px rgba(0,0,0,0.9)) drop-shadow(0 2px 6px rgba(0,0,0,0.7));
  animation: wwPop 0.18s var(--ease);
}
/* What "close enough" actually means, drawn at the size it really is. Nobody
   should have to infer the tolerance from their score. */
.ww-ring {
  position: absolute; transform: translate(-50%, -50%); aspect-ratio: 1;
  border-radius: 50%; border: 1px dashed rgba(255,255,255,0.2);
}
.ww-pin { position: absolute; transform: translate(-50%, -50%); font-size: 1.15rem; line-height: 1; }
.ww-pin.on { filter: drop-shadow(0 0 5px rgba(182,255,61,0.9)); }
.ww-pin.good, .ww-pin.bad { opacity: 0; transform: translate(-50%, -50%) scale(0.55); }
.ww-pin.up { opacity: 1; transform: translate(-50%, -50%) scale(1); transition: opacity 0.16s, transform 0.16s var(--ease); }
.ww-pts {
  position: absolute; left: 50%; top: 100%; transform: translateX(-50%);
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; font-weight: 800;
}
.ww-pin.good .ww-pts { color: var(--lime); }
.ww-pin.bad .ww-pts { color: var(--dim); }
/* Everybody else's guesses, on the same picture, once the map is over for all
   of you. This is the loud part of the game. */
.ww-ghost { position: absolute; transform: translate(-50%, -50%); opacity: 0.55; display: flex; }
@keyframes wwPop {
  0%   { transform: translate(-50%, -50%) scale(0.6); }
  60%  { transform: translate(-50%, -50%) scale(1.15); }
  100% { transform: translate(-50%, -50%) scale(1); }
}

.ww-tray { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; min-height: 40px; }
.ww-chip {
  width: 40px; height: 40px; padding: 0; font: inherit; font-size: 1.2rem;
  border-radius: 11px; border: 1px solid var(--line); background: var(--panel);
  color: var(--text); cursor: pointer; transition: transform 0.08s var(--ease), border-color 0.12s;
}
.ww-chip.on { border-color: var(--lime); background: rgba(182,255,61,0.14); transform: scale(1.08); }
.ww-chip.done { opacity: 0.4; }
.ww-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* How many pins everybody has down - never where, until the map is over. */
.ww-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.ww-who {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.ww-who .ww-av { display: flex; }
.ww-who .ww-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ww-who .ww-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--dim); }
.ww-who.ready { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.ww-who.ready .ww-state { color: var(--cyan); }
.ww-who.done { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.1); }
.ww-who.done .ww-state { color: var(--lime); }
.ww-who.me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* pipe connect - one board for the room, one tap, and a budget of quarter turns */
.pc-wrap { gap: 9px; }
.pc-wrap .pc-off { display: none; }
.pc-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.pc-boardno { color: var(--cyan); font-size: 0.9rem; }
.pc-wet { color: var(--text); font-size: 0.9rem; }
.pc-turns { color: var(--muted); font-size: 0.9rem; }
/* The budget is the game, so it changes colour before it runs out rather than
   after - five turns left is a decision, zero is a result. */
.pc-turns.pc-low { color: var(--amber); }
.pc-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

/* The window, draining. */
.pc-bar { width: 430px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.pc-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }
.pc-wrap.pc-sealed .pc-fill { background: var(--lime); }

.pc-grid { display: grid; gap: 3px; justify-content: center; }
.pc-tile {
  position: relative; padding: 0; font: inherit; border: 1px solid var(--line);
  border-radius: 8px; background: rgba(255,255,255,0.03); cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.pc-tile:hover { border-color: rgba(255,255,255,0.35); background: rgba(255,255,255,0.07); }
.pc-tile:active { border-color: var(--cyan); }
/* The pipes turn; the tile does not. One container per tile carrying every arm
   it has, rotated as a whole, is what makes a quarter turn a transform. */
.pc-pipes { position: absolute; inset: 0; transition: transform 0.13s var(--ease); }
/* Dry pipe was `--dim` (#6f6a92) on a 3%-white tile: a muted purple on a dark
   grey, and the board you spend the whole round READING is the board you cannot
   afford to misread - a tile you got wrong costs a turn, and turns are the
   scarcity this game is built on. Dry pipe is a light metal now; wet is still
   the loudest thing on the board. */
.pc-tile { background: rgba(255,255,255,0.055); }
.pc-core {
  position: absolute; left: 50%; top: 50%; width: 34%; height: 34%;
  transform: translate(-50%, -50%); border-radius: 3px; background: #b6b1d4;
}
.pc-arm { position: absolute; background: #b6b1d4; border-radius: 2px; display: none; }
.pc-arm.pc-on { display: block; }
.pc-arm.pc-n { left: 39%; width: 22%; top: 0; height: 58%; }
.pc-arm.pc-s { left: 39%; width: 22%; bottom: 0; height: 58%; }
.pc-arm.pc-w { top: 39%; height: 22%; left: 0; width: 58%; }
.pc-arm.pc-e { top: 39%; height: 22%; right: 0; width: 58%; }
/* Water is the only feedback in the game, so it is the loudest thing on it. */
.pc-tile.pc-wet { background: rgba(52,224,255,0.12); border-color: rgba(52,224,255,0.4); }
.pc-tile.pc-wet .pc-arm, .pc-tile.pc-wet .pc-core { background: var(--cyan); }
.pc-tile.pc-tap { border-color: var(--amber); box-shadow: 0 0 0 2px rgba(255,179,64,0.35); }
.pc-tile.pc-tap .pc-core { background: var(--amber); border-radius: 50%; }
.pc-tile.pc-tap.pc-wet .pc-core { background: var(--amber); }
.pc-wrap.pc-sealed .pc-tile { cursor: default; }
.pc-wrap.pc-sealed .pc-tile.pc-wet { background: rgba(182,255,61,0.14); border-color: rgba(182,255,61,0.45); }
.pc-wrap.pc-sealed .pc-tile.pc-wet .pc-arm { background: var(--lime); }

.pc-tools { display: flex; align-items: center; gap: 10px; }
.pc-undo {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.pc-undo:disabled { opacity: 0.28; cursor: default; }
.pc-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.pc-back.spent { opacity: 0.22; filter: grayscale(1); }

.pc-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* How much of the board everybody has wet - never which tile. */
.pc-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.pc-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.pc-chip .pc-av { display: flex; }
.pc-chip .pc-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pc-chip .pc-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--dim); }
.pc-chip.pc-going { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.pc-chip.pc-going .pc-state { color: var(--cyan); }
.pc-chip.pc-done { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.1); }
.pc-chip.pc-done .pc-state { color: var(--lime); }
.pc-chip.pc-me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* shape fit - one hole, the pieces that fill it, and no way to take one back */
.sf-wrap { gap: 9px; }
.sf-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.sf-no { color: var(--cyan); font-size: 0.9rem; }
.sf-cells { color: var(--text); font-size: 0.9rem; }
.sf-left { color: var(--muted); font-size: 0.9rem; }
/* The pieces are the scarcity, so the count changes colour while there is still
   something to do about it. */
.sf-left.sf-low { color: var(--amber); }
.sf-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

/* The window, draining. */
.sf-bar { width: 460px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.sf-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }
.sf-wrap.sf-filled .sf-fill { background: var(--lime); }

.sf-play { display: flex; align-items: flex-start; gap: 18px; }
.sf-grid { display: grid; gap: 3px; justify-content: center; }
/* A square outside the hole is still in the grid: the hole has a shape, and the
   shape is only readable against the box it was cut out of. */
.sf-sq {
  padding: 0; font: inherit; border-radius: 7px; cursor: pointer;
  border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.07);
  transition: background 0.1s, border-color 0.1s, transform 0.1s var(--ease);
}
.sf-sq.sf-off { border-color: transparent; background: transparent; cursor: default; }
/* Not part of THIS board at all - a square only a later, bigger hole needs.
   `sf-off` is a square of this board that is outside the hole and still has to
   hold its place in the rectangle; `sf-gone` must not, or the first board is
   drawn inside the last board's footprint. */
.sf-sq.sf-gone { display: none; }
.sf-sq:hover { border-color: rgba(255,255,255,0.4); }
.sf-sq.sf-off:hover { border-color: transparent; }
.sf-sq.sf-full { border-color: rgba(0,0,0,0.25); background: var(--piece, rgba(255,255,255,0.5)); }
/* Where the piece in your hand would land, and where it would not. */
.sf-sq.sf-ghost { background: rgba(255,255,255,0.26); border-color: rgba(255,255,255,0.6); }
.sf-sq.sf-bad { background: rgba(255,77,77,0.18); border-color: rgba(255,77,77,0.5); }
/* A square nothing left can reach. The one honest thing this game can tell you
   about a mistake: that you have made one, and not what to do instead. */
.sf-sq.sf-dead {
  background: repeating-linear-gradient(45deg, rgba(255,77,77,0.16) 0 4px, transparent 4px 8px);
  border-color: rgba(255,77,77,0.45);
}
/* The answer, once the window has shut and it cannot help anybody. */
.sf-sq.sf-sol {
  opacity: 0.4; border-style: dashed; border-color: rgba(255,255,255,0.4);
  background: var(--piece, transparent);
}
.sf-sq.sf-wildsq { background: rgba(155,92,255,0.6); border-color: rgba(155,92,255,0.85); }

/* One colour a piece, carried as a variable rather than a background, so the
   same class paints a square of the hole and a pip in the tray - which is the
   whole point of colouring them: the piece in your hand and the piece it
   became on the board are the same colour. */
.sf-p0 { --piece: #34e0ff; }
.sf-p1 { --piece: #b6ff3d; }
.sf-p2 { --piece: #ffb340; }
.sf-p3 { --piece: #ff6fae; }
.sf-p4 { --piece: #9b5cff; }
.sf-p5 { --piece: #4ee59a; }

/* The whole queue, top one first. Nothing is hidden in this game; what you
   cannot do is work all of it out before the window shuts. */
.sf-tray { display: flex; flex-direction: column; gap: 6px; }
/* Every slot is a slot, whether or not it is the one in your hand. Only the
   current one was framed before, so the other two read as loose pips floating
   next to the board rather than as the queue behind it. */
.sf-slot {
  padding: 5px; border-radius: 9px;
  border: 1px solid rgba(255,255,255,0.09); background: rgba(255,255,255,0.02);
}
.sf-slot.sf-now { border-color: rgba(52,224,255,0.55); background: rgba(52,224,255,0.08); }
.sf-slot.sf-empty { display: none; }
/* A pip is a square of the hole seen small. It cannot be the same size - three
   pieces at the hole's 56px would be taller than the card - but at 15px it was
   a quarter of it, and reading whether an S fits a gap meant rescaling the
   shape in your head first. Half is close enough to compare by eye. */
.sf-mini { display: grid; grid-template-columns: repeat(4, 21px); grid-auto-rows: 21px; gap: 2px; }
.sf-slot.sf-now .sf-mini { grid-template-columns: repeat(4, 30px); grid-auto-rows: 30px; }
.sf-pip { border-radius: 3px; background: transparent; }
.sf-pip.sf-on { background: var(--piece, var(--dim)); }
/* A piece with nowhere left to go: the one time the bin is not a mistake. */
.sf-wrap.sf-stuck .sf-slot.sf-now { border-color: rgba(255,77,77,0.6); background: rgba(255,77,77,0.1); }
.sf-wrap.sf-stuck .sf-bin { border-color: var(--amber); color: var(--amber); }

.sf-tools { display: flex; align-items: center; gap: 10px; }
.sf-rot, .sf-bin, .sf-wild {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.sf-rot:disabled, .sf-bin:disabled, .sf-wild:disabled { opacity: 0.28; cursor: default; }
.sf-wild.sf-hidden { display: none; }
.sf-wilds { display: flex; gap: 4px; font-size: 0.95rem; color: var(--violet); }
.sf-wpip.spent { opacity: 0.22; filter: grayscale(1); }

.sf-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* How much of the hole everybody has covered - never where. */
.sf-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.sf-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.sf-chip .sf-av { display: flex; }
.sf-chip .sf-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sf-chip .sf-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--dim); }
.sf-chip.sf-going { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.sf-chip.sf-going .sf-state { color: var(--cyan); }
.sf-chip.sf-done { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.1); }
.sf-chip.sf-done .sf-state { color: var(--lime); }
.sf-chip.sf-me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* drum line - one rhythm for the room, and everybody's taps under it at the end */
.dl-wrap { gap: 10px; }
.dl-head {
  display: flex; align-items: center; gap: 16px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.dl-no { color: var(--cyan); font-size: 0.95rem; }
.dl-undo {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.dl-undo:disabled { opacity: 0.26; cursor: default; }
.dl-undo[hidden] { display: none; }
.dl-total { color: var(--amber); font-size: 1.3rem; }

.dl-bar { width: 520px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.dl-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.06s linear; }
.dl-wrap.dl-playing .dl-fill { background: var(--lime); }
.dl-wrap.dl-revealing .dl-fill { background: var(--amber); }

/* The pad. One target, deliberately huge: a rhythm played by hunting for a
   small button is a rhythm played by the mouse rather than by the player.
   It is also the only thing on this screen, so it is drawn as an object rather
   than as a rectangle - a head lit where a stick would land, two tension rings,
   a rim that catches the light and a shadow it sits in. An empty rounded box
   with a word in it was the least designed thing in the collection, in the one
   game that has nothing else to look at. */
.dl-pad {
  position: relative; width: 380px; max-width: 100%; height: 214px; padding: 0;
  font: inherit; border-radius: 26px; border: 2px solid rgba(255,255,255,0.18);
  color: var(--text); cursor: pointer;
  background:
    radial-gradient(circle closest-side at 50% 47%, rgba(255,255,255,0.11), rgba(255,255,255,0.03) 62%, transparent 76%),
    radial-gradient(circle closest-side at 50% 47%, transparent 0 62%, rgba(255,255,255,0.13) 62% 64%, transparent 64%),
    radial-gradient(circle closest-side at 50% 47%, transparent 0 44%, rgba(255,255,255,0.07) 44% 46%, transparent 46%),
    linear-gradient(180deg, rgba(255,255,255,0.07), rgba(0,0,0,0.20));
  box-shadow:
    inset 0 2px 0 rgba(255,255,255,0.18),
    inset 0 -20px 34px rgba(0,0,0,0.34),
    0 12px 26px rgba(0,0,0,0.40);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  transition: transform 0.05s var(--ease), background 0.06s, border-color 0.12s, box-shadow 0.06s;
  touch-action: manipulation; user-select: none;
}
.dl-wrap.dl-listening .dl-pad { cursor: default; border-color: rgba(52,224,255,0.35); }
.dl-wrap.dl-playing .dl-pad { border-color: rgba(182,255,61,0.55); }
/* Struck by the pattern, and struck by you: two different colours, because
   during the reveal the room is arguing about which of them was which. */
.dl-pad.beat { background: rgba(52,224,255,0.22); transform: scale(0.98); }
.dl-pad.lit { background: rgba(182,255,61,0.24); transform: scale(0.97); }
.dl-padlabel { font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.14em; font-size: 1.3rem; }
.dl-count { font-family: 'JetBrains Mono', monospace; font-size: 0.95rem; color: var(--muted); }

/* How many beats there were - never a secret, because the game is the rhythm
   and not a counting test. Big enough to read from where the pad is, since
   during playback these are the only thing saying where in the bar you are. */
.dl-pips { display: flex; gap: 10px; min-height: 16px; }
.dl-pip { width: 14px; height: 14px; border-radius: 50%; background: rgba(255,255,255,0.12); transition: background 0.1s, transform 0.1s var(--ease), box-shadow 0.1s; }
.dl-pip.on { background: var(--cyan); transform: scale(1.3); box-shadow: 0 0 10px rgba(52,224,255,0.6); }
.dl-wrap.dl-playing .dl-pip.on { background: var(--lime); }

/* The reveal: the real rhythm on a line, and everybody's attempt under it. */
.dl-lines { display: flex; flex-direction: column; gap: 5px; width: 520px; max-width: 100%; }
.dl-line { display: flex; align-items: center; gap: 9px; }
.dl-line.me .dl-who { color: var(--text); }
.dl-who {
  display: flex; align-items: center; gap: 5px; width: 118px; flex: none;
  font-size: 0.72rem; font-weight: 800; color: var(--dim);
  font-family: 'JetBrains Mono', monospace; letter-spacing: 0.04em;
}
.dl-nick { max-width: 62px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dl-pts { color: var(--amber); margin-left: auto; }
.dl-row {
  position: relative; flex: 1; height: 26px; border-radius: 8px;
  border: 1px solid var(--line); background: rgba(255,255,255,0.03); overflow: hidden;
}
.dl-tick {
  position: absolute; top: 4px; bottom: 4px; width: 3px; margin-left: -1px;
  border-radius: 2px; background: var(--muted); opacity: 0;
  transition: opacity 0.14s, transform 0.14s var(--ease); transform: scaleY(0.4);
}
.dl-line.dl-truth .dl-tick { opacity: 1; transform: none; background: var(--cyan); }
.dl-tick.up { opacity: 1; transform: none; }
/* The origin tap is never scored - the gaps are - so it is drawn as the mark it
   is rather than as a hit or a miss. */
.dl-tick.dl-o { background: var(--muted); }
.dl-tick.dl-tight { background: var(--lime); box-shadow: 0 0 6px rgba(182,255,61,0.8); }
.dl-tick.dl-near { background: var(--amber); }
.dl-tick.dl-off { background: var(--red); }
.dl-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* memory grid */
.mg-grid { display: grid; gap: 6px; }
.mg-cell {
  border-radius: 9px; border: none; padding: 0; font: inherit;
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  /* Follows the cell, which the module sizes against the card. A fixed 1.3rem
     was a number chosen for a 74px cell and looked lost in a 110px one. */
  font-size: calc(var(--cell, 74px) * 0.42);
  display: flex; align-items: center; justify-content: center;
  transition: background 0.12s, transform 0.08s;
}
.mg-cell.blank { background: rgba(255,255,255,0.03); }
.mg-cell.num { background: var(--panel-strong); color: var(--text); cursor: pointer; border: 1px solid var(--line); }
.mg-cell.num.masked { background: var(--violet); color: transparent; border-color: transparent; }
.mg-cell.num.masked:active { transform: scale(0.93); }
.mg-cell.num.got { background: rgba(182,255,61,0.9); color: #14210a; }

/* shopping list */
.sl-wrap { gap: 10px; width: min(620px, 100%); }
.sl-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; }
.sl-which { color: var(--cyan); font-size: 0.95rem; }
.sl-chances { font-size: 0.95rem; letter-spacing: 2px; }
.sl-total { font-family: 'JetBrains Mono', monospace; font-weight: 800; color: var(--cyan); font-size: 1.05rem; }
/* One bar for all three phases. Which phase it is doing is the colour, so the
   list going away is never a surprise. */
.sl-bar { width: 100%; height: 6px; border-radius: 4px; background: rgba(255,255,255,0.06); overflow: hidden; }
.sl-fill { height: 100%; width: 100%; background: var(--violet); }
.sl-fill.look { background: var(--amber); }
.sl-fill.shop { background: var(--cyan); }
.sl-fill.reveal { background: var(--muted); }
.sl-list {
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; align-content: center;
  min-height: 150px;
}
/* `display` in a class rule outranks the browser's own [hidden] rule, so both of
   these stayed on screen through every phase - the list was still readable while
   the shelf was open, which is the one thing this game is about not being. */
.sl-list[hidden], .sl-shelf[hidden] { display: none; }
.sl-item {
  font-weight: 800; font-size: clamp(1.1rem, 3.4vw, 1.7rem); letter-spacing: 0.01em;
  padding: 10px 18px; border-radius: 12px;
  background: var(--panel-strong); border: 1px solid var(--line);
}
.sl-shelf { display: grid; gap: 6px; width: 100%; }
.sl-tile {
  font: inherit; font-weight: 600; font-size: 0.92rem; padding: 12px 4px;
  border-radius: 9px; border: 1px solid var(--line); background: var(--panel-strong);
  color: var(--text); cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background 0.12s, transform 0.08s, opacity 0.12s;
}
.sl-tile:hover:not(:disabled) { border-color: rgba(255,255,255,0.4); }
.sl-tile:active:not(:disabled) { transform: scale(0.95); }
.sl-tile.got { background: rgba(182,255,61,0.9); color: #14210a; border-color: transparent; }
.sl-tile.bad { background: rgba(255,77,77,0.28); border-color: var(--red); }
/* Refused rather than taken: the tile is out of play and nothing was spent. */
.sl-tile.refused { opacity: 0.3; border-style: dashed; }
.sl-tile.missed { background: rgba(255,181,61,0.22); border-color: var(--amber); color: var(--amber); }
.sl-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* plate spinner */
.ps-wrap { justify-content: center; gap: 16px; }
.ps-row { display: flex; align-items: flex-end; justify-content: center; flex: 0 0 auto; }
/* Fixed column width straight out of the deal, so the gap between two plates is
   always wider than a plate - which is what makes a bigger plate an easier
   target rather than a fatter row. */
.ps-slot { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 190px; }
.ps-plate {
  /* border-box, so the radius the server dealt is the radius you have to hit -
     the perk is measured in pixels and the border must not quietly add two. */
  box-sizing: border-box;
  border-radius: 50%; border: 3px solid rgba(255,255,255,0.5); padding: 0; font: inherit;
  background: var(--cyan); color: #05202a; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.12s, border-color 0.12s, opacity 0.18s;
}
.ps-plate.ps-warm { background: var(--amber); color: #2a1a02; }
.ps-plate.ps-hot { background: var(--red); color: #2a0606; border-color: rgba(255,255,255,0.9); }
/* Smashed: the pole is still there and still yours, it just earns nothing until
   a new plate arrives. */
.ps-plate.ps-gone { opacity: 0; pointer-events: none; }
.ps-worth { font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 1rem; }
.ps-pole { width: 7px; flex: 1 1 auto; border-radius: 4px 4px 0 0; background: linear-gradient(180deg, var(--line), rgba(255,255,255,0.05)); }
.ps-total { font-size: clamp(1.6rem, 5vw, 2.6rem); font-weight: 900; }

/* wall ball */
.wb-wrap { justify-content: center; gap: 12px; }
/* Keeps .canvas-host so the collapse guard still applies; fitBox scales the
   court inside it. */
.wb-host { min-height: 200px; }
/* Fixed pixel court out of the deal: the perk is a paddle measured in pixels,
   so a court that stretched with the window would make the same paddle a
   different fraction of it on every screen in the room. */
.wb-court {
  position: relative; flex: 0 0 auto; overflow: hidden; touch-action: none; cursor: none;
  border: 2px solid var(--line); border-radius: var(--radius-sm); box-sizing: content-box;
  background: radial-gradient(120% 80% at 50% 0%, rgba(155,92,255,0.14), transparent 70%), var(--bg-2);
}
.wb-court.wb-lost { box-shadow: inset 0 0 0 2px rgba(255,77,77,0.55); }
/* The far end. Nothing hangs off it - it is there so the top of the court reads
   as a wall to bounce off rather than as the edge of a picture. */
.wb-wall { position: absolute; left: 0; right: 0; top: 0; height: 6px; background: linear-gradient(90deg, var(--violet), var(--cyan)); opacity: 0.75; }
/* The plate on the back wall. Amber, because it is the only thing in this game
   that is not the ball or the paddle, and because lime already means "the
   paying end of the paddle" here. It sits ON the wall strip rather than in
   front of it - it is a place, not an obstacle, and the ball's path is
   identical whether it is there or not. */
.wb-plate {
  position: absolute; left: 0; top: 0; height: 6px;
  background: var(--amber); border-radius: 0 0 4px 4px;
  box-shadow: 0 0 10px rgba(255,179,64,0.65);
  transition: transform 0.18s var(--ease), opacity 0.18s, background 0.18s;
}
/* Taken for this window: still visible, so you can see where it was and that
   there is nothing left on it, but plainly spent. */
.wb-plate.wb-took { background: rgba(255,179,64,0.22); box-shadow: none; }
.wb-plate.wb-hid { opacity: 0; }
.wb-ball {
  position: absolute; left: 0; top: 0; border-radius: 50%; will-change: transform;
  background: radial-gradient(circle at 35% 30%, #fff, var(--amber));
  box-shadow: 0 0 16px rgba(255,179,64,0.6);
}
.wb-ball.wb-hid { opacity: 0; }
/* Painted with the price list: the background gradient is set in the module,
   in pixels from the middle, so the bright band is exactly the part of the
   paddle that pays the most. */
.wb-paddle {
  position: absolute; left: 0; border-radius: 6px; will-change: transform;
  box-shadow: 0 0 14px rgba(182,255,61,0.35);
}
.wb-total { font-family: 'JetBrains Mono', monospace; font-size: clamp(1.4rem, 4vw, 2.2rem); font-weight: 900; }

/* untangle */
.ut-wrap { justify-content: center; gap: 10px; }
/* Fixed pixel board straight out of the deal: the dots sit on a grid the deal
   decided, so a board that stretched with the window would move every one of
   them. */
.ut-board { position: relative; flex: 0 0 auto; touch-action: none; user-select: none; }
.ut-lines { position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible; }
.ut-edge { stroke: rgba(155,92,255,0.75); stroke-width: 3; stroke-linecap: round; }
/* Tier 2 only: the lines that are actually in a crossing. */
.ut-edge.ut-bad { stroke: var(--red); stroke-width: 4; }
/* Tier 1 and up: a mark where two lines meet. */
.ut-x { fill: var(--red); opacity: 0.85; }
/* Somewhere a dot may be put down. Big enough to aim at, drawn as a speck. */
.ut-slot {
  position: absolute; border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, var(--line) 0 3px, transparent 3px);
}
.ut-node {
  position: absolute; border-radius: 50%; box-sizing: border-box; cursor: grab; z-index: 2;
  background: var(--cyan); border: 2px solid rgba(255,255,255,0.35);
  box-shadow: 0 2px 10px rgba(0,0,0,0.45);
  transition: background 0.1s, transform 0.06s;
}
.ut-node.ut-held { background: var(--lime); transform: scale(1.18); cursor: grabbing; }
/* Tier 2 only: this dot has at least one of its lines in a crossing, and the
   brighter ring is the dot that is in more than one - which is the one worth
   moving first. */
.ut-node.ut-hot { border-color: var(--red); box-shadow: 0 0 0 3px rgba(255,77,77,0.35); }
.ut-node.ut-hotter { box-shadow: 0 0 0 5px rgba(255,77,77,0.6); }
.ut-board.ut-solved .ut-edge { stroke: var(--lime); }
.ut-total { font-family: 'JetBrains Mono', monospace; font-size: clamp(1.4rem, 4vw, 2.2rem); font-weight: 900; }
.ut-sub { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--muted); }

/* target number - six tiles, four operations, one number to build */
.tn-wrap { gap: 9px; }
.tn-wrap .tn-off { display: none; }
.tn-wrap .tn-hidden { display: none; }
.tn-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.tn-puzzleno { color: var(--cyan); font-size: 0.9rem; }
.tn-closest { color: var(--muted); font-size: 0.9rem; }
.tn-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.tn-bar { width: 430px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.tn-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }
.tn-wrap.tn-exact .tn-fill { background: var(--lime); }

/* The number to build. The one thing on the screen you look at first. */
.tn-target {
  font-family: 'JetBrains Mono', monospace; font-weight: 900;
  font-size: clamp(2.2rem, 8vw, 3.4rem); letter-spacing: 0.02em; color: var(--amber);
  line-height: 1;
}
.tn-wrap.tn-exact .tn-target { color: var(--lime); }
.tn-wrap.tn-exact .tn-closest { color: var(--lime); }

.tn-tiles { display: flex; gap: 7px; justify-content: center; flex-wrap: wrap; }
.tn-tile {
  min-width: 74px; height: 58px; border-radius: 12px; box-sizing: border-box;
  border: 1px solid var(--line); background: rgba(255,255,255,0.055); color: var(--text);
  font-family: 'JetBrains Mono', monospace; font-size: 1.4rem; font-weight: 800;
  cursor: pointer; transition: background 0.1s, border-color 0.1s, transform 0.06s;
}
.tn-tile:hover:not(:disabled) { border-color: rgba(255,255,255,0.35); background: rgba(255,255,255,0.09); }
.tn-tile.tn-pick { border-color: var(--cyan); background: rgba(52,224,255,0.16); transform: translateY(-3px); }
.tn-tile:disabled { opacity: 0.4; cursor: default; }

/* One button per operation, each showing what it would actually make. */
.tn-ops { display: flex; gap: 7px; justify-content: center; }
.tn-op {
  min-width: 92px; padding: 6px 10px; border-radius: 12px; box-sizing: border-box;
  border: 1px solid var(--line); background: var(--panel); color: var(--muted);
  cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 2px;
  transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.tn-op .tn-glyph { font-size: 1.2rem; font-weight: 900; }
.tn-op .tn-out { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; min-height: 1.1em; }
.tn-op.tn-live { color: var(--text); border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.09); }
.tn-op.tn-live .tn-out { color: var(--cyan); }
.tn-op:disabled { opacity: 0.35; cursor: default; }

.tn-tools { display: flex; align-items: center; gap: 10px; }
.tn-undo {
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel); color: var(--text); cursor: pointer;
  font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.06em;
}
.tn-undo:disabled { opacity: 0.28; cursor: default; }
.tn-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.tn-back.spent { opacity: 0.22; filter: grayscale(1); }

.tn-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* How close everybody is - never what they built. */
.tn-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.tn-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.tn-chip .tn-av { display: flex; }
.tn-chip .tn-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tn-chip .tn-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--dim); }
.tn-chip.tn-going { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.tn-chip.tn-going .tn-state { color: var(--cyan); }
.tn-chip.tn-done { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.1); }
.tn-chip.tn-done .tn-state { color: var(--lime); }
.tn-chip.tn-me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* warehouse — eight cells by six at 50px is 400x300, well inside the play card */
.wh-wrap { gap: 9px; }
.wh-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.wh-roomno { color: var(--cyan); font-size: 0.9rem; }
.wh-moves { color: var(--muted); font-size: 0.9rem; }
.wh-moves.wh-low { color: var(--red); }
.wh-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.wh-bar { width: 400px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.wh-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }

/* The floor was eight 50px cells across - 400px of an 8x6 warehouse in a card
   that is 546px wide at 1280x720 and 946 at 1080p, so the game was drawn at
   about a third of the room it had. The cells are a fraction of the floor now
   and the floor takes the card, capped at 80px a cell: past that a crate is a
   large brown rectangle rather than a crate. `aspect-ratio` is what keeps a
   cell square without anyone having to state its height. */
.wh-grid {
  display: grid; grid-template-columns: repeat(8, 1fr); grid-template-rows: repeat(6, 1fr);
  width: min(100%, 640px); max-height: 100%; aspect-ratio: 8 / 6;
  border-radius: 12px; overflow: hidden; background: rgba(255,255,255,0.03);
}
.wh-wrap.wh-over .wh-grid { opacity: 0.65; }
.wh-cell {
  position: relative; box-sizing: border-box;
  border: 1px solid rgba(255,255,255,0.045);
  display: flex; align-items: center; justify-content: center;
}
.wh-cell.wh-wall { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.1); }

/* A mark: the ring a crate is going to. Drawn under everything else. */
.wh-cell.wh-mark::before {
  content: ''; position: absolute; inset: 26%; border-radius: 50%;
  border: 2px dashed rgba(182,255,61,0.7);
}
/* The perk: ground a crate never comes off. */
.wh-cell.wh-dead {
  background: repeating-linear-gradient(45deg,
    rgba(255,77,77,0.16) 0 5px, transparent 5px 10px);
}

/* The crate itself, and the two states it stops moving in. */
.wh-box { width: 0; height: 0; border-radius: 6px; transition: width 0.07s, height 0.07s; }
.wh-cell.wh-crate .wh-box {
  width: 68%; height: 68%;
  background: linear-gradient(160deg, #d8a05a, #a06a2c);
  box-shadow: inset 0 0 0 2px rgba(0,0,0,0.28);
}
.wh-cell.wh-locked .wh-box {
  background: linear-gradient(160deg, #cdff6a, #7ec21a);
  box-shadow: inset 0 0 0 2px rgba(0,0,0,0.3), 0 0 10px rgba(182,255,61,0.45);
}
.wh-cell.wh-lost .wh-box {
  background: linear-gradient(160deg, #6b3030, #3d1c1c);
  box-shadow: inset 0 0 0 2px rgba(255,77,77,0.5);
}
.wh-cell.wh-lost::after {
  content: '✕'; position: absolute; color: var(--red); font-weight: 900; font-size: 1.1rem;
}

/* You. A ring rather than a block, so a crate under it still reads. */
.wh-cell.wh-you::after {
  content: ''; position: absolute; width: 44%; height: 44%; border-radius: 50%;
  border: 3px solid var(--violet); box-shadow: 0 0 10px rgba(155,92,255,0.6);
}

.wh-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

.wh-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.wh-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.wh-chip .wh-av { display: flex; }
.wh-chip .wh-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wh-chip .wh-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--dim); }
.wh-chip.wh-going { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.wh-chip.wh-going .wh-state { color: var(--cyan); }
.wh-chip.wh-clear { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.1); }
.wh-chip.wh-clear .wh-state { color: var(--lime); }
.wh-chip.wh-me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* lob */
.lb-wrap { justify-content: center; gap: 10px; }
/* Fixed pixel court out of the deal, like Wall Ball's: the whole game is a drag
   measured in pixels against a green measured in pixels, so a court that
   stretched with the window would be a different game on every screen. */
/* The court is played at 440x250 and drawn bigger: `transform` on the court,
   size reserved by the frame, because a transform takes up no space. Everything
   inside - the tee dot, the price tags, the aim line, the ball - scales with
   it, including hit testing, so nothing inside needs to know. */
/* Keeps .canvas-host so the collapse guard still applies; fitBox scales the
   court inside it. */
.lb-host { min-height: 200px; }
.lb-court {
  transform-origin: 0 0;
  position: relative; flex: 0 0 auto; overflow: hidden; touch-action: none; user-select: none;
  border: 2px solid var(--line); border-radius: var(--radius-sm); box-sizing: content-box;
  /* Dusk: a low sun on the right and the sky warming towards the horizon. */
  background:
    radial-gradient(circle at 84% 30%, rgba(255,226,160,0.95) 0 13px, rgba(255,170,120,0.28) 14px 34px, rgba(255,140,120,0.08) 35px 70px, transparent 71px),
    linear-gradient(180deg, #110c2c 0%, #24184f 45%, #4a2c6c 78%, #7b4674 100%);
  cursor: crosshair;
}
.lb-court.lb-miss { box-shadow: inset 0 0 0 2px rgba(255,77,77,0.5); }
/* Everything painted is scenery: it never takes the pointer from the court. */
.lb-hills, .lb-cloud, .lb-ground, .lb-water, .lb-green, .lb-rough, .lb-pin, .lb-tag,
.lb-tee, .lb-shadow, .lb-ball, .lb-wind, .lb-pop { pointer-events: none; }
.lb-hills {
  position: absolute; left: -10px; right: -10px; height: 48px;
  background:
    radial-gradient(ellipse 90px 34px at 18% 100%, #2c1d4d 98%, transparent 100%),
    radial-gradient(ellipse 120px 44px at 55% 100%, #251842 98%, transparent 100%),
    radial-gradient(ellipse 100px 30px at 90% 100%, #2c1d4d 98%, transparent 100%);
}
.lb-cloud {
  position: absolute; left: 0; width: 64px; height: 16px; opacity: 0.2; will-change: transform;
  background:
    radial-gradient(circle at 30% 60%, #fff 0 8px, transparent 9px),
    radial-gradient(circle at 55% 40%, #fff 0 10px, transparent 11px),
    radial-gradient(circle at 78% 62%, #fff 0 7px, transparent 8px);
}
/* The fairway: mown in stripes, earth underneath. */
.lb-ground {
  position: absolute; left: 0; right: 0; bottom: 0;
  border-top: 2px solid #6fe89a;
  background:
    repeating-linear-gradient(90deg, #2f9a5e 0 22px, #2a8b55 22px 44px) top / 100% 9px no-repeat,
    linear-gradient(180deg, #3a2940 9px, #251b30 100%);
}
/* Past the green: a long ball lands here for nothing, and should look it. */
.lb-water {
  position: absolute; bottom: 0; border-radius: 3px 3px 0 0;
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.18) 0 6px, transparent 6px 18px) top / 36px 2px repeat-x,
    linear-gradient(180deg, #4b95ff, #1d3f8f);
  animation: lb-ripple 2.4s linear infinite;
}
@keyframes lb-ripple { to { background-position: 36px 0, 0 0; } }
/* The plateau. The strip along its top is the price list drawn to scale: dull
   at the front where a landing pays 14, bright at the back lip where it pays
   54, and the same at every tier because the payout is a function of where the
   ball landed and nothing else. Underneath it is a bank of earth, which is
   what a ball short of the green runs into. */
.lb-green {
  position: absolute; border-radius: 3px 3px 0 0;
  background:
    linear-gradient(90deg, #2f8f5d, #b6ff3d) top / 100% 7px no-repeat,
    repeating-linear-gradient(180deg, rgba(0,0,0,0.14) 0 2px, transparent 2px 10px),
    linear-gradient(180deg, #7a5140 7px, #4a3029 100%);
  box-shadow: 0 -2px 14px rgba(182,255,61,0.28), inset 2px 0 0 rgba(255,255,255,0.08);
}
/* Tier 1 and up: the strip of rough behind the green that stops a ball which
   would otherwise be in the pond for nothing. Longer grass, amber-topped
   because it pays - less than the green, more than the water. */
.lb-rough {
  position: absolute; border-radius: 0 3px 0 0;
  background:
    repeating-linear-gradient(90deg, #d9a441 0 3px, #b8862f 3px 6px) top / 100% 7px no-repeat,
    linear-gradient(180deg, #6a4838 7px, #432c27 100%);
}
/* The pin at the back lip, and its flag - which blows the way the wind does. */
.lb-pin { position: absolute; width: 2px; height: 34px; background: #f1eefc; border-radius: 1px; }
.lb-cloth {
  position: absolute; top: 1px; left: 2px; width: 16px; height: 10px; transform-origin: 0 50%;
  background: #ff4d8d; clip-path: polygon(0 0, 100% 50%, 0 100%);
  animation: lb-flap calc(1.4s - var(--gust, 0) * 0.3s) ease-in-out infinite alternate;
}
.lb-cloth[data-wind="-1"] { left: auto; right: 0; transform-origin: 100% 50%; clip-path: polygon(100% 0, 0 50%, 100% 100%); }
.lb-cloth[data-wind="0"] { animation: none; transform: rotate(72deg); }
@keyframes lb-flap { from { transform: scaleX(0.82) skewY(-6deg); } to { transform: scaleX(1) skewY(5deg); } }
.lb-tag {
  position: absolute; font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; font-weight: 800;
  color: #fff; background: rgba(10,8,24,0.72); padding: 0 3px; border-radius: 3px;
}
.lb-tag.lb-back { color: #d9ff9a; }
/* The tee: a peg the ball sits on. */
.lb-tee {
  position: absolute; width: 10px; height: 10px;
  background: linear-gradient(90deg, transparent 4px, #f4efe6 4px 6px, transparent 6px) bottom / 100% 5px no-repeat;
}
.lb-ball {
  position: absolute; left: 0; top: 0; border-radius: 50%; will-change: transform; z-index: 2;
  background: radial-gradient(circle at 34% 30%, #ffffff, #e2e0f0 55%, #a8a5c4);
  box-shadow: 0 1px 2px rgba(0,0,0,0.55), 0 0 8px rgba(255,255,255,0.35);
}
.lb-shadow {
  position: absolute; left: 0; top: 0; width: 14px; height: 4px; border-radius: 50%;
  background: rgba(0,0,0,0.6); transform-origin: 50% 50%; will-change: transform;
}
.lb-trail { fill: none; stroke: rgba(255,255,255,0.6); stroke-width: 2; stroke-dasharray: 0.5 6; stroke-linecap: round; }
.lb-pop {
  position: absolute; transform: translateX(-50%); z-index: 3;
  font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: 900; color: #d9ff9a;
  text-shadow: 0 1px 3px rgba(0,0,0,0.8); animation: lb-pop 1.1s ease-out forwards;
}
.lb-pop.lb-pop-miss { color: #ffb3c1; }
@keyframes lb-pop { from { opacity: 1; margin-top: 0; } to { opacity: 0; margin-top: -26px; } }
/* The drag, drawn as the drag: which way and how far pulled back, never where
   the ball will land. */
.lb-aim { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
.lb-line { stroke: var(--cyan); stroke-width: 2; stroke-dasharray: 5 4; }
.lb-tip { fill: var(--cyan); }
/* Hovering rather than holding: the same guide, plainly not committed to. */
.lb-aim.lb-ghost { opacity: 0.42; }
/* ...and gone once the ball is. .lb-ghost outranked .lb-hid on its own, so a
   shot aimed by hovering and then clicking left the guide on screen for the
   whole flight. */
.lb-aim.lb-hid { opacity: 0; }
.lb-trails { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
.lb-wind {
  position: absolute; left: 50%; top: 6px; transform: translateX(-50%); z-index: 2;
  padding: 1px 10px; border-radius: 999px; background: rgba(8,6,22,0.55);
  font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; font-weight: 800; white-space: nowrap;
  letter-spacing: 0.18em; color: var(--cyan);
}
.lb-wind.lb-calm { color: var(--dim); letter-spacing: 0.06em; }
.lb-hid { opacity: 0; }
/* Width is set from the court's scale in the module, so the bar is exactly as
   wide as the court above it whatever the window is. */
.lb-bar { max-width: 100%; height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.lb-fill { height: 100%; background: var(--cyan); }
.lb-total { font-family: 'JetBrains Mono', monospace; font-size: clamp(1.4rem, 4vw, 2.2rem); font-weight: 900; }
.lb-sub { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--muted); }

/* The price list. Dim, because it is a fact about the grid rather than about
   the player - but on screen, because it changes every grid and it is half of
   every decision in the game. */
/* What this grid has cost and paid so far, signed. Two fields on one line used
   to have two different scopes with nothing saying so - one grid-local, one
   round-cumulative - so this one carries its scope in its own label. */


/* The perk: water this player's own soundings have already ruled out. */
/* A sounding, and the number it came back with. */
/* A reading that was measuring a wreck you have since landed on. The number
   stays - it is still true, and taking it away would be taking information off
   the screen - but it goes quiet, because it has already been spent. */
/* A mark: a claim, made and kept. */
/* A wreck nobody named, once the grid opens. */




/* unfold — eight cells by eight at 38px with a 2px gap is 318x318, which is the
   smallest board in the set on purpose: every square has to be readable as
   paper or as a hole from the next chair, and a bigger sheet buys nothing
   because the difficulty is in the folds rather than in the area. */
.uf-wrap { gap: 9px; }
.uf-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.uf-sheetno { color: var(--cyan); font-size: 0.9rem; }
.uf-marks { color: var(--muted); font-size: 0.9rem; }
.uf-marks.uf-low { color: var(--red); }
.uf-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.uf-bar { width: 318px; max-width: 100%; height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.uf-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }

/* The paper. `position: relative` because the fold lines are drawn over it. */
.uf-paper {
  position: relative;
  display: grid; grid-template-columns: repeat(8, 38px); grid-template-rows: repeat(8, 38px);
  gap: 2px; padding: 6px; border-radius: 12px; background: rgba(255,255,255,0.03);
}
.uf-cell {
  position: relative; box-sizing: border-box; border-radius: 4px;
  background: rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.05);
  cursor: pointer; transition: background 0.12s, opacity 0.25s;
}
/* Paper under the fold: still there, and not what you are looking at. */
.uf-cell.uf-under { opacity: 0.42; }
/* The folded stack, which is the only part a punch goes through. */
.uf-cell.uf-stack { background: rgba(255,255,255,0.16); border-color: rgba(255,255,255,0.14); }
/* A punch: a hole you were given, and the only free one on the sheet. */
.uf-cell.uf-punch::after {
  content: ''; position: absolute; inset: 8px; border-radius: 50%;
  background: #0b0a14; box-shadow: inset 0 0 0 2px rgba(255,255,255,0.35);
}
/* A mark that found one, and a mark that did not. */
.uf-cell.uf-hit { background: rgba(182,255,61,0.28); border-color: rgba(182,255,61,0.6); }
.uf-cell.uf-hit::after {
  content: ''; position: absolute; inset: 8px; border-radius: 50%;
  background: #0b0a14; box-shadow: inset 0 0 0 2px rgba(182,255,61,0.8);
}
.uf-cell.uf-miss { background: rgba(255,77,77,0.2); border-color: rgba(255,77,77,0.5); }
.uf-cell.uf-miss::after {
  content: '✕'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--red); font-weight: 900; font-size: 0.95rem;
}
/* The reveal, once the window shuts: every hole nobody marked. */
.uf-cell.uf-hole::after {
  content: ''; position: absolute; inset: 8px; border-radius: 50%;
  background: #0b0a14; box-shadow: inset 0 0 0 2px rgba(52,224,255,0.75);
}
.uf-wrap.uf-open .uf-cell { cursor: default; }

/* The perk: a fold line, drawn where the paper turned. */
.uf-line {
  position: absolute; pointer-events: none;
  left: 0; top: 0; width: calc(100% - 12px); height: 0;
  margin: 6px; border-top: 2px dashed rgba(255,179,64,0.8);
}
.uf-line.uf-vert {
  width: 0; height: calc(100% - 12px);
  border-top: 0; border-left: 2px dashed rgba(255,179,64,0.8);
}
.uf-line.uf-hid { display: none; }

.uf-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

.uf-roster { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.uf-chip {
  display: flex; align-items: center; gap: 7px; padding: 5px 10px 5px 5px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel);
  font-size: 0.78rem; font-weight: 700;
}
.uf-chip .uf-av { display: flex; }
.uf-chip .uf-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uf-chip .uf-state { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--dim); }
.uf-chip.uf-going { border-color: rgba(52,224,255,0.5); background: rgba(52,224,255,0.1); }
.uf-chip.uf-going .uf-state { color: var(--cyan); }
.uf-chip.uf-done { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.1); }
.uf-chip.uf-done .uf-state { color: var(--lime); }
.uf-chip.uf-me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* drift — the field is a canvas that scales to the card, so everything here is
   the strip of numbers above it: which pad, what is in the tank, and what has
   been banked. The tank goes red under six tenths of a second of engine, which
   is about one correction and nothing after it. */
.df-wrap { gap: 9px; }
.df-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.df-padno { color: var(--cyan); font-size: 0.9rem; }
.df-fuel { color: var(--muted); font-size: 0.9rem; }
.df-fuel.df-low { color: var(--red); }
.df-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.df-bar { width: min(100%, 560px); height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.df-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }

/* The field keeps .canvas-host so the collapse guard still applies; this only
   caps how wide it grows, because a ship 11px across on an 820px field has to
   stay readable as a ship. */
.canvas-host.df-host { width: min(100%, 820px); }

.df-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* mirror — the board is a canvas that scales to the card, so everything here is
   the strip of numbers above it: which wave, what a spark is worth in it, how
   many are still out there, and what has been banked. */
.mr-wrap { gap: 9px; }
.mr-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.mr-waveno { color: var(--violet); font-size: 0.9rem; }
.mr-left { color: var(--muted); font-size: 0.9rem; }
.mr-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.mr-bar { width: min(100%, 560px); height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.mr-fill { height: 100%; width: 100%; background: var(--violet); transition: width 0.08s linear; }

/* The board keeps .canvas-host so the collapse guard still applies; this only
   caps how wide it grows, because a bay is half of it and a spark 12px across
   has to stay a spark in the half you are not looking at. */
.canvas-host.mr-host { width: min(100%, 760px); }

.mr-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* legs — the runner and the track are one canvas; the strip above it is the
   distance, which is the score, and the four keys lit while they are held, so
   anybody watching over a shoulder can see what the hands are doing. */
.lg-wrap { gap: 9px; }
.lg-head {
  display: flex; align-items: center; gap: 18px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.06em;
}
.lg-dist { color: var(--amber); font-size: 1.6rem; min-width: 7ch; text-align: right; }
.lg-falls { color: var(--muted); font-size: 0.9rem; min-width: 9ch; }
.lg-keys { display: flex; gap: 6px; }
.lg-key {
  width: 34px; height: 34px; border-radius: 7px; display: grid; place-items: center;
  border: 1px solid var(--line); background: rgba(255,255,255,0.05); color: var(--muted);
  font-size: 1rem; transition: background 0.05s, color 0.05s, transform 0.05s;
}
.lg-key.on { background: var(--cyan); color: #0b0a18; border-color: var(--cyan); transform: translateY(2px); }
.canvas-host.lg-host { width: min(100%, 820px); }

/* tightrope — the meter and the rope are both inside one canvas, so that the
   markers the perk draws sit in the same coordinates as the needle they are
   predicting. Everything here is the strip of numbers above it: how far out you
   are, what a plank is worth there, and what has been banked. */
.tr-wrap { gap: 9px; }

/* The scene keeps .canvas-host so the collapse guard still applies; this only
   caps how wide it grows, because the meter is 600px of it and a needle three
   pixels wide has to stay readable against a marker nine wide. */
.canvas-host.tr-host { width: min(100%, 760px); }


/* slice — the field is a canvas that scales to the card, so everything here is
   the strip of numbers above it: what is in the hand, how many bubbles have
   been cut and how long the round has left, and what has been banked. The hand
   goes red when it is empty, because an empty hand is the only reason a stroke
   can be refused. */
.sl-wrap { gap: 9px; }
.sl-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.sl-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.sl-bar { width: min(100%, 560px); height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.sl-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }

/* The field keeps .canvas-host so the collapse guard still applies; this only
   caps how wide it grows, because a bubble 14px across on an 820px field has to
   stay something you can aim a line through. The cursor is a crosshair for the
   same reason: the whole card is one aiming surface. */
.canvas-host.sl-host { width: min(100%, 820px); cursor: crosshair; touch-action: none; }

.sl-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

/* milk run — the map is a plain positioned box rather than a canvas, because
   every doorstep has to be a real click target for the pointer path the whole
   collection is tested through, and because the crate count on a marker is
   text. 720x440 sits inside the 760px card with a margin either side. */


/* The map keeps one 720x440 coordinate space and the host scales it, so a
   narrower card is the same game smaller rather than a different one. */
/* The line already driven, and the line that was there to be driven. Both are
   one SVG over the whole map, so a route crossing itself still reads. */

/* The perk: a circle at exactly the distance the fuel left will carry you. */


/* A can of fuel: no crates at all, and the only stop on the map that pays in
   range rather than in points. */
/* Out of reach on the fuel that is left. Still clickable — the float will set
   off and stop in a field, which is the whole point of knowing. */
/* On the best loop, drawn once the run is over. */
/* The perk's second half: what this hop costs, written under the marker. */




/* pursuit — a grid of cells rather than a canvas, because every square has to
   be a real pointer target for the click path and because a stack's value is
   text. 11x8 at 54px is 594x432, which sits inside the 760px card. */
.pr-wrap { gap: 9px; }
.pr-head {
  display: flex; align-items: center; gap: 14px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.08em;
}
.pr-vaultno { color: var(--cyan); font-size: 0.9rem; }
.pr-moves { color: var(--muted); font-size: 0.9rem; }
.pr-moves.pr-low { color: var(--red); }
.pr-total { color: var(--amber); font-size: 1.3rem; padding-left: 12px; border-left: 1px solid var(--line); }

.pr-bar { width: min(100%, 560px); height: 5px; border-radius: 3px; background: rgba(255,255,255,0.07); overflow: hidden; }
.pr-fill { height: 100%; width: 100%; background: var(--cyan); transition: width 0.08s linear; }

/* The floor keeps one cell-sized coordinate space and the host scales it, so a
   narrower card is the same puzzle smaller rather than a different one. */
.pr-host { width: min(100%, 594px); }
.pr-field {
  position: relative; transform-origin: top left;
  border-radius: 14px; background: rgba(255,255,255,0.03);
  border: 1px solid var(--line); overflow: hidden;
  /* The grid itself, so a step is visibly one square. */
  background-image:
    linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px);
}
/* The route walked, and the route that was there to walk. */
.pr-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
.pr-walked { fill: none; stroke: rgba(182,255,61,0.6); stroke-width: 3; stroke-linejoin: round; stroke-linecap: round; }
.pr-best { fill: none; stroke: rgba(255,179,64,0.85); stroke-width: 2.5; stroke-dasharray: 7 6; stroke-linejoin: round; }
/* Once the vault is over the route you walked steps back so the one you missed
   reads over the top of it — both are on the same squares more often than not. */
.pr-wrap.pr-over .pr-walked { stroke: rgba(182,255,61,0.3); }

/* A pillar. The one thing the guard's rule cannot cope with. */
.pr-wall {
  position: absolute; z-index: 0;
  background: rgba(155,92,255,0.16);
  border: 1px solid rgba(155,92,255,0.35);
  border-radius: 6px;
}

.pr-loot {
  position: absolute; transform: translate(-50%, -50%);
  width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 0.95rem;
  background: rgba(255,179,64,0.16); border: 2px solid rgba(255,179,64,0.6); color: var(--amber);
  pointer-events: none; z-index: 2;
  transition: opacity 0.12s, border-color 0.12s;
}
.pr-loot.pr-took { opacity: 0.28; border-color: rgba(255,255,255,0.2); color: var(--dim); }
/* On the best route, drawn once the vault is over. */
.pr-loot.pr-onbest { box-shadow: 0 0 0 2px rgba(255,179,64,0.5); }

/* The perk. A ring on each square you could step to, red where the reply lands
   on it; and at tier 2 a ghost on the square the guard would answer with. */
.pr-mark {
  position: absolute; transform: translate(-50%, -50%);
  width: 42px; height: 42px; border-radius: 9px;
  border: 2px dashed rgba(52,224,255,0.5); pointer-events: none; z-index: 1;
}
.pr-mark.pr-fatal { border-color: rgba(255,77,77,0.85); background: rgba(255,77,77,0.14); border-style: solid; }
.pr-ghost {
  position: absolute; transform: translate(-50%, -50%);
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid rgba(255,77,77,0.45); pointer-events: none; z-index: 1;
}
.pr-ghost.pr-fatal { border-color: rgba(255,77,77,0.9); background: rgba(255,77,77,0.25); }
.pr-hid { display: none; }

.pr-you {
  position: absolute; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 6px;
  background: var(--lime); box-shadow: 0 0 14px rgba(182,255,61,0.6);
  pointer-events: none; z-index: 4;
  transition: left 0.09s linear, top 0.09s linear;
}
.pr-you.pr-nabbed { background: var(--red); box-shadow: 0 0 14px rgba(255,77,77,0.7); }
.pr-guard {
  position: absolute; transform: translate(-50%, -50%);
  width: 26px; height: 26px; border-radius: 50%;
  background: rgba(255,77,77,0.25); border: 2px solid var(--red);
  pointer-events: none; z-index: 3;
  transition: left 0.09s linear, top 0.09s linear;
}
.pr-guard.pr-got { background: var(--red); }

.pr-note { min-height: 1.3em; font-weight: 700; color: var(--muted); }

.pr-roster { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.pr-chip {
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
  border-radius: 999px; background: var(--panel); border: 1px solid var(--line);
}
.pr-av { width: 30px; height: 30px; }
.pr-name { font-weight: 800; font-size: 0.85rem; }
.pr-state { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--dim); }
.pr-chip.pr-going { border-color: rgba(182,255,61,0.5); background: rgba(182,255,61,0.1); }
.pr-chip.pr-going .pr-state { color: var(--lime); }
.pr-chip.pr-out { border-color: rgba(255,77,77,0.45); background: rgba(255,77,77,0.08); }
.pr-chip.pr-out .pr-state { color: var(--red); }
.pr-chip.pr-me { box-shadow: 0 0 0 2px rgba(155,92,255,0.5); }

/* herd — one field box the pens are placed in by percentage, at the field's own
   aspect ratio, so a distance on screen is the distance the walk uses. The dots
   under a pen are how many bodies it wants and the number is what it pays: both
   are drawn at every tier, because they are the deal. The perk is the ring —
   how far you can still walk before the bell — which is a fact about the clock
   and your own legs rather than about the field, and the only thing on this
   screen nobody can read off it. */


/* the perk: a pen the bell will go before you can get to it */
@keyframes hd-paid { 0% { transform: translate(-50%, -50%) scale(1.16); } 100% { transform: translate(-50%, -50%) scale(1); } }

/* the commons — one lake box the marks are placed in by percentage, so the
   same deal reads the same at any card width. The shoal is a disc sized to
   what is actually in the water: it is the honest signal every tier has, and
   the perk prints numbers on top of it rather than replacing it. */



/* the perk: where this net stops filling in the water it is in */

/* don't press it */
.dp-panel {
  width: min(560px, 88%); min-height: 190px; border-radius: 22px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  border: 3px solid var(--line); cursor: pointer; user-select: none;
  transition: background 0.12s, border-color 0.12s;
}
.dp-order { font-size: clamp(1.5rem, 5vw, 2.4rem); font-weight: 900; letter-spacing: -0.01em; text-align: center; padding: 0 16px; }

/* math blitz */
.mb-sum { font-family: 'JetBrains Mono', monospace; font-size: clamp(2.4rem, 9vw, 4.4rem); font-weight: 700; letter-spacing: 0.02em; }
.mb-input {
  font: inherit; font-family: 'JetBrains Mono', monospace; font-size: 2rem; font-weight: 700;
  text-align: center; width: min(260px, 70vw); padding: 12px 20px;
  border-radius: 16px; background: rgba(0,0,0,0.4); border: 2px solid var(--line); color: var(--text);
}
.mb-input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 4px rgba(155,92,255,0.2); }
.mb-input.wrong { border-color: var(--red); }
.mb-input:disabled { opacity: 0.5; }

/* ============================================================
   TURN-BASED GAMES
   ============================================================ */
.turn-stage { gap: 14px; }
.turn-stage.panic { animation: panicPulse 0.55s ease-in-out infinite; }
@keyframes panicPulse { 50% { background: rgba(255,77,77,0.07); } }

.bp-syllable {
  font-size: clamp(3rem, 13vw, 7rem); font-weight: 900; letter-spacing: 0.1em;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
  line-height: 1;
}
.bp-fuse { width: min(460px, 80%); height: 9px; border-radius: 999px; overflow: hidden; background: rgba(0,0,0,0.45); border: 1px solid var(--line); }
.bp-fuse > i { display: block; height: 100%; width: 100%; background: var(--grad); }
.bp-input {
  font: inherit; font-size: 1.4rem; font-weight: 700; text-align: center;
  width: min(420px, 82vw); padding: 12px 22px; border-radius: 999px;
  background: rgba(0,0,0,0.4); border: 2px solid var(--line); color: var(--text);
}
.bp-input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 4px rgba(155,92,255,0.2); }
.bp-input:disabled { opacity: 0.35; }
.bp-input.wrong { border-color: var(--red); animation: shake 0.3s; }
.bp-status { font-weight: 800; font-size: 1.05rem; min-height: 26px; color: var(--muted); }
.bp-status.mine { color: var(--lime); }
.bp-status.bad { color: var(--red); }
.bp-used { font-size: 0.82rem; color: var(--dim); min-height: 20px; font-family: 'JetBrains Mono', monospace; }

.hp-bomb { font-size: clamp(4rem, 15vw, 7rem); line-height: 1; will-change: transform; }
.hp-bomb.panic { filter: drop-shadow(0 0 22px rgba(255,77,77,0.85)); }

.tr-roster { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 4px; }
.tr-card {
  font: inherit; color: inherit; cursor: default;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 10px 14px; min-width: 96px; border-radius: var(--radius-sm);
  background: var(--panel); border: 2px solid var(--line);
  transition: transform 0.15s var(--ease), border-color 0.2s, opacity 0.25s;
}
.tr-card.me { background: rgba(52,224,255,0.07); }
.tr-card.current { border-color: var(--amber); box-shadow: 0 0 0 3px rgba(255,179,64,0.22); transform: translateY(-4px); }
.tr-card.out { opacity: 0.32; filter: grayscale(1); }
.tr-card.pickable { cursor: pointer; }
.tr-card.pickable:hover { border-color: var(--pink); transform: translateY(-6px) scale(1.04); }
.tr-av { line-height: 0; }
.tr-av canvas { display: block; }
.tr-card.out .tr-av { filter: grayscale(1); }
.tr-name { font-weight: 800; font-size: 0.88rem; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tr-lives { font-size: 0.8rem; letter-spacing: 1px; color: var(--pink); }
.tr-card.boom { animation: boom 0.5s var(--ease); }
@keyframes boom {
  0% { transform: scale(1) }
  25% { transform: scale(1.22) rotate(-5deg); border-color: var(--red) }
  60% { transform: scale(0.94) rotate(3deg) }
  100% { transform: scale(1) }
}

/* ============================================================
   DEV PLAYGROUND
   ============================================================ */
.dev-wrap { max-width: 1100px; margin: 0 auto; padding-bottom: 40px; }
.dev-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 6px; }
.dev-sub { color: var(--muted); margin-bottom: 20px; }
.dev-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); gap: 12px; }
.dev-card { text-align: left; font: inherit; color: inherit; cursor: pointer; position: relative; }
.dev-card:hover { transform: translateY(-3px); border-color: var(--violet); }
.dev-play {
  max-width: 1240px; margin: 0 auto;
  /* Fill the viewport so canvas games get the same room they do in a match,
     instead of being squeezed into a short fixed box. */
  min-height: 100%; display: flex; flex-direction: column;
}
/* The play card inside is the match's .play-wrap verbatim, which sizes itself
   with height:100% against a parent with a definite height. This one is a flex
   column, so it gets its share the flex way instead - and needs width:100%
   spelled out, because .play-wrap centres itself with `margin: 0 auto` and an
   auto cross-axis margin switches a flex item's stretch off. Without it the
   card shrank to fit its contents: a 250px live board, a gap, and a stage two
   pixels wide. */
.dev-play > .play-wrap { flex: 1 1 auto; min-height: 0; width: 100%; }
/* The playground's own controls: the way out, the handicap tier, replay, and
   the score the round came back with. A match has none of them - the round
   starts whether you like it or not and the score goes on the results screen -
   so none of them can live in the column the game is drawn in.
 *
 * They live in the rail band instead: every .screen reserves --rail-w on both
 * sides for the chat and standings rails, and on this page both bands are
 * empty. Above the card they were not free. A toolbar is 35px and its margin
 * is 14px, and .play-wrap fills what is left, so every game was being judged in
 * a stage 49px shorter than the one it ships in - 574x591 where a match gives
 * 574x640 - and 140px shorter once the score panel appeared under it, which is
 * a different aspect ratio, on a page whose entire job is to tell you whether a
 * board fits. tools/layoutcheck.js now measures both stages and fails if they
 * differ. */
.dev-dock {
  position: fixed; left: var(--rail-gap); top: var(--screen-pad-y); z-index: 500;
  width: calc(var(--rail-w) + var(--rail-gap));
  max-height: calc(100vh - 2 * var(--screen-pad-y));
  overflow-y: auto;
  display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
}
/* The fake-room picker is hidden for games without stand-ins. */
.dev-h-select[hidden] { display: none; }
.dev-h-select {
  display: inline-flex; flex-wrap: wrap; gap: 4px; padding: 3px;
  border-radius: 999px; background: rgba(0,0,0,0.3); border: 1px solid var(--line);
}
.dev-h-btn {
  font: inherit; font-size: 0.78rem; font-weight: 700; cursor: pointer;
  padding: 6px 13px; border-radius: 999px; border: none; background: transparent; color: var(--dim);
}
.dev-h-btn.on { background: var(--panel-strong); color: var(--lime); }
.dev-result {
  align-self: stretch; margin-top: 4px; padding: 14px 16px; border-radius: var(--radius);
  background: var(--panel); border: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 4px;
}
/* Same trap as .stale-bar: without this, an empty panel sat under the play card
   for the whole round and only filled in when the round ended. */
.dev-result[hidden] { display: none; }
.dev-score { font-family: 'JetBrains Mono', monospace; font-size: 2rem; font-weight: 700; color: var(--lime); }
.dev-note { color: var(--muted); font-size: 0.88rem; }
.dev-turn-note {
  padding: 14px 18px; border-radius: var(--radius-sm);
  background: rgba(255,179,64,0.1); border: 1px solid rgba(255,179,64,0.35);
  color: var(--amber); font-size: 0.88rem; max-width: 640px; text-align: center;
}

@media (max-width: 900px) {
  .tr-roster { gap: 7px; }
  .tr-card { min-width: 78px; padding: 8px 10px; }
}

/* ============================================================
   LEAD-IN COUNTDOWN
   ============================================================ */
.leadin {
  position: absolute; inset: 0; z-index: 4;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  background: rgba(11,10,20,0.55); backdrop-filter: blur(3px);
}
.leadin-icon { font-size: clamp(3rem, 10vw, 5rem); animation: bob 1.4s ease-in-out infinite; }
.leadin-name { font-weight: 800; font-size: 1.2rem; color: var(--muted); }
.leadin-num {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: clamp(4rem, 16vw, 8rem); line-height: 1; color: var(--cyan);
}
.leadin-num.go { color: var(--lime); }
.leadin-num.tick { animation: tick 0.45s var(--ease); }

/* ============================================================
   ARROWS (shared by Arrow Storm and Arrow Rush)
   ============================================================ */
.as-arrow svg, .as-pad svg, .ar-arrow svg { width: 62%; height: 62%; }
.ar-dot { color: var(--dim); font-weight: 700; font-size: 0.6em; }
.as-arrow.lit { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.4); transform: scale(1.14); box-shadow: 0 0 34px rgba(155,92,255,0.55); }
.as-arrow.ok { border-color: var(--lime); }
.as-arrow.bad { border-color: var(--red); animation: shake 0.3s; }
.as-pad { padding: 0; }
.as-pad svg { width: 58%; height: 58%; }

.ar-arrow {
  width: clamp(150px, 34vw, 240px); height: clamp(150px, 34vw, 240px);
  display: flex; align-items: center; justify-content: center;
  border-radius: 28px; background: rgba(0,0,0,0.3); border: 3px solid var(--line);
  transition: transform 0.09s var(--ease), border-color 0.09s, background 0.09s;
}
.ar-arrow svg { width: 74%; height: 74%; }
.ar-arrow.live { animation: arPop 0.13s var(--ease); border-color: rgba(255,255,255,0.28); }
.ar-arrow.good { border-color: var(--lime); background: rgba(182,255,61,0.12); transform: scale(1.07); }
.ar-arrow.bad { border-color: var(--red); background: rgba(255,77,77,0.14); transform: scale(0.93) rotate(-4deg); }

/* ============================================================
   STROOP (colour swatches, not words)
   ============================================================ */
.st-options { gap: 14px; }
.st-swatch {
  width: clamp(64px, 13vw, 104px); height: clamp(64px, 13vw, 104px);
  border-radius: 20px; border: 3px solid rgba(255,255,255,0.22);
  cursor: pointer; padding: 0;
  transition: transform 0.12s var(--ease), box-shadow 0.15s, border-color 0.15s;
}
.st-swatch:hover { transform: translateY(-4px) scale(1.05); border-color: #fff; box-shadow: 0 12px 34px rgba(0,0,0,0.5); }
.st-swatch:active { transform: scale(0.94); }

/* ============================================================
   PERFECT CIRCLE / NUMBER HUNT / STOP THE BAR
   ============================================================ */
.nh-target {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: clamp(2.4rem, 8vw, 3.6rem); line-height: 1; color: var(--cyan);
}
.nh-grid { display: grid; gap: 6px; }
.nh-grid.locked { opacity: 0.6; }
.nh-cell {
  border-radius: 10px; border: 1px solid var(--line); padding: 0;
  font: inherit; font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 1.15rem;
  background: var(--panel-strong); color: var(--text); cursor: pointer;
  transition: background 0.12s, transform 0.08s, opacity 0.2s;
}
.nh-cell:hover:not(:disabled) { background: rgba(255,255,255,0.14); }
.nh-cell:active:not(:disabled) { transform: scale(0.93); }
.nh-cell.got { background: rgba(182,255,61,0.22); color: var(--lime); opacity: 0.4; cursor: default; }
.nh-cell.bad { background: rgba(255,77,77,0.35); border-color: var(--red); }


/* ============================================================
   ANAGRAM / REVERSE TYPE
   ============================================================ */
.ag-word {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: clamp(1.8rem, 6.5vw, 3.2rem); letter-spacing: 0.08em;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
}
.ag-input {
  font: inherit; font-size: 1.5rem; font-weight: 700; text-align: center;
  width: min(420px, 82vw); padding: 12px 22px; border-radius: 999px;
  background: rgba(0,0,0,0.4); border: 2px solid var(--line); color: var(--text);
}
.ag-input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 4px rgba(155,92,255,0.2); }
.ag-input.wrong { border-color: var(--red); animation: shake 0.3s; }
.ag-input:disabled { opacity: 0.5; }

.rt-word {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: clamp(2rem, 8vw, 3.6rem); letter-spacing: 0.12em; color: var(--text);
}
.rt-arrow { font-size: 1.6rem; color: var(--dim); letter-spacing: 0.3em; margin-top: -6px; }

@media (max-width: 560px) {
  .ar-arrow { width: 46vw; height: 46vw; }
}

/* ============================================================
   DON'T PRESS IT — instruction on top, subject below
   ============================================================ */
.dp-panel {
  gap: 14px; padding: 22px 20px;
  transition: background 0.15s, border-color 0.15s;
}
.dp-panel.go { border-color: rgba(182,255,61,0.55); background: rgba(182,255,61,0.07); }
.dp-panel.stop { border-color: rgba(255,77,77,0.55); background: rgba(255,77,77,0.07); }
.dp-panel.right { border-color: var(--lime); }
.dp-panel.wrong { border-color: var(--red); }
.dp-subject { display: flex; align-items: center; justify-content: center; gap: 12px; min-height: 64px; }
.dp-swatch { border-radius: 16px; border: 3px solid rgba(255,255,255,0.25); }
.dp-swatch.big { width: 96px; height: 64px; }
.dp-swatch.small { width: 62px; height: 58px; }
.dp-num {
  font-family: 'JetBrains Mono', monospace; font-weight: 700;
  font-size: clamp(2rem, 6vw, 2.8rem); color: var(--text); line-height: 1;
}
.dp-dots { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; max-width: 260px; }
.dp-dot { width: 20px; height: 20px; border-radius: 50%; background: var(--cyan); display: block; }
.dp-pair { display: flex; gap: 16px; }

/* ============================================================
   ANAGRAM — reveal the answer
   ============================================================ */
.ag-reveal {
  min-height: 24px; font-weight: 700; font-size: 0.95rem; letter-spacing: 0.06em;
  font-family: 'JetBrains Mono', monospace; color: transparent;
}
.ag-reveal.skipped { color: var(--amber); animation: slideIn 0.3s var(--ease); }

/* ============================================================
   HIDDEN ROUND CLOCK (games that set hideClock)
   ============================================================ */
.play-head.clockless .timer-bar,
.play-head.clockless .timer-num { visibility: hidden; }

/* ============================================================
   AUDIO CONTROLS
   ============================================================ */
/* The dock sits opposite the chat rail so the two never fight for a corner. */
/* The last row of the rail card, so it draws no frame of its own. Its opacity
   and its collapse behaviour still belong to it. */
#audio-dock { overflow: hidden; }
#audio-dock.muted .aud-dock-head { color: var(--dim); }

.aud-dock-head {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 12px; cursor: pointer;
  font: inherit; color: var(--dim); text-align: left;
  background: rgba(255,255,255,0.04); border: none;
  transition: color 0.16s;
}
.aud-dock-head:hover { color: #fff; }
.aud-icon { font-size: 0.95rem; line-height: 1; }
.aud-dock-title {
  font-size: 0.62rem; font-weight: 800;
  letter-spacing: 0.14em; text-transform: uppercase;
}
/* A caret drawn in CSS, so it can rotate without a second glyph. */
.aud-dock-head::after {
  content: ''; margin-left: auto;
  width: 6px; height: 6px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.18s var(--ease);
}
#audio-dock.closed .aud-dock-head::after { transform: rotate(-135deg) translate(-2px, -2px); }

.aud-dock-body {
  padding: 8px 12px 11px;
  display: grid; gap: 2px;
  transition: max-height 0.22s var(--ease), opacity 0.18s, padding 0.22s var(--ease);
  max-height: 160px;
}
#audio-dock.closed .aud-dock-body {
  max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0;
}

.aud-row {
  display: grid; grid-template-columns: 34px 1fr auto;
  align-items: center; gap: 9px;
  padding: 4px 0;
  transition: opacity 0.16s;
}
.aud-row.off { opacity: 0.45; }
.aud-label { font-size: 0.76rem; font-weight: 600; white-space: nowrap; }

/* A switch, not another emoji: the old control drew the same note glyph for on
   and for off, so there was no way to read the state. */
.aud-switch {
  width: 34px; height: 19px; flex: none; padding: 0; cursor: pointer;
  border-radius: 999px; border: 1px solid var(--line);
  background: rgba(255,255,255,0.08);
  transition: background 0.16s var(--ease), border-color 0.16s;
}
.aud-switch .aud-knob {
  display: block; width: 13px; height: 13px; border-radius: 50%;
  margin: 2px; background: var(--dim);
  transition: transform 0.16s var(--ease), background 0.16s;
}
.aud-switch.on {
  background: linear-gradient(90deg, var(--violet), var(--cyan));
  border-color: transparent;
}
.aud-switch.on .aud-knob { transform: translateX(15px); background: #fff; }

.audio-range {
  -webkit-appearance: none; appearance: none;
  width: 64px; height: 5px; border-radius: 999px; cursor: pointer;
  background: linear-gradient(90deg, var(--violet), var(--cyan));
}
.audio-range:disabled { opacity: 0.3; cursor: not-allowed; }
.audio-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 13px; height: 13px; border-radius: 50%;
  background: #fff; border: 1px solid var(--bg); cursor: grab;
}
.audio-range::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 1px solid var(--bg);
}

/* ============================================================
   PLAYER ACTIVITY GLOW
   --glow is 0..1, so a microphone level can drive this later.
   ============================================================ */
.lrow { --glow: 0; }
.lrow.pulse { animation: rowPulse 0.5s var(--ease); }
.lrow.pulse-bad { animation: rowPulseBad 0.5s var(--ease); }
@keyframes rowPulse {
  0% { box-shadow: 0 0 0 0 rgba(182,255,61,0); }
  30% { box-shadow: 0 0 0 3px rgba(182,255,61,calc(0.45 * var(--glow))); border-color: rgba(182,255,61,0.6); }
  100% { box-shadow: 0 0 0 0 rgba(182,255,61,0); }
}
@keyframes rowPulseBad {
  0% { box-shadow: 0 0 0 0 rgba(255,77,77,0); }
  30% { box-shadow: 0 0 0 3px rgba(255,77,77,calc(0.5 * var(--glow))); border-color: rgba(255,77,77,0.65); }
  100% { box-shadow: 0 0 0 0 rgba(255,77,77,0); }
}

/* ============================================================
   CHAT — a real panel off the play screen, a discreet rail during it
   ============================================================ */
/* Fill the rail, never a hard width. `.screen` pads the page by exactly
   --rail-w, so a fixed 300px here is a promise the layout never made: at 1440px
   the rail is 216px and the panel hung 124px over the content, across the
   settings row and the first game card. Extra room off the play screen is
   bought in height instead - see below - which costs the content nothing. */
#chat-rail { width: 100%; }
#chat-log {
  display: flex; flex-direction: column; gap: 4px;
  max-height: 190px; overflow-y: auto; scrollbar-width: thin;
  padding-right: 4px;
  overscroll-behavior: contain;
}
#chat-log::-webkit-scrollbar { width: 6px; }
#chat-log::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.16); border-radius: 999px; }
.cmsg { word-break: break-word; }
.cmsg.mine b { color: var(--lime); }

/* Off the play screen there is time, so give it height. Width is the rail's to
   decide - taking more of it is what put the panel on top of the lobby. */
body:not(.in-game) #chat-log { max-height: min(46vh, 420px); }
body:not(.in-game) #chat-form input { font-size: 0.88rem; padding: 10px 14px; }

@media (max-width: 900px) {
  .audio-range { width: 56px; }
}

/* ============================================================
   LIGHTS OUT + COUNT THE DOTS
   ============================================================ */
/* The cap was 420px, which is the board this game shipped with and about two
   thirds of the card it sits in. 100% keeps it inside the card at any width,
   60vh keeps it off the bottom of a short window, and 620 is where a lit cell
   stops reading as a light and starts reading as a panel. */
.lo-board {
  display: grid; gap: 10px;
  width: min(72vh, 100%, 620px); aspect-ratio: 1;
  margin: 0 auto;
}
.lo-cell {
  border-radius: 12px; cursor: pointer; padding: 0;
  border: 1px solid var(--line);
  background: rgba(255,255,255,0.05);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
  transition: background 0.12s var(--ease), box-shadow 0.14s var(--ease), transform 0.09s var(--ease);
}
.lo-cell:hover { background: rgba(255,255,255,0.11); }
.lo-cell:active { transform: scale(0.95); }
.lo-cell.on {
  background: radial-gradient(circle at 32% 28%, #ffe98a, #ffb020 62%, #e07a00);
  border-color: rgba(255,220,120,0.75);
  box-shadow: 0 0 22px rgba(255,180,40,0.5), inset 0 1px 0 rgba(255,255,255,0.5);
}

/* The Ladder. One wall, everybody on it, the rungs running the whole width. */
.ld-wrap { display: flex; flex-direction: column; align-items: stretch; gap: 10px; width: 100%; }
.ld-wall {
  display: flex; align-items: stretch; gap: 4px; overflow-x: auto; padding: 6px 6px 0;
  border-radius: var(--radius); border: 1px solid var(--line); background: rgba(6,5,14,0.9);
  /* The rungs, across every column, at fifteenths of the track. */
  --ld-track: clamp(260px, 42vh, 380px);
}
.ld-gutter { display: flex; flex-direction: column-reverse; flex: 0 0 74px; height: var(--ld-track); position: sticky; left: 0; z-index: 2; background: rgba(6,5,14,0.96); }
.ld-lab { flex: 1; display: flex; align-items: center; justify-content: space-between; padding: 0 6px 0 4px; font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--dim); border-top: 1px solid transparent; }
.ld-lab-n { font-weight: 700; }
.ld-lab.next { color: var(--text); }
.ld-lab.next .ld-lab-odds { color: var(--amber); font-weight: 700; }
.ld-lab.safe.next .ld-lab-odds { color: var(--lime); }
.ld-col { display: flex; flex-direction: column; align-items: center; gap: 3px; flex: 0 0 48px; }
.ld-col.me { flex: 0 0 96px; position: sticky; left: 74px; z-index: 1; background: rgba(6,5,14,0.96); border-right: 1px solid var(--line); margin-right: 4px; }
.ld-track {
  position: relative; width: 100%; height: var(--ld-track);
  display: flex; flex-direction: column-reverse;
  background: repeating-linear-gradient(to top, rgba(255,255,255,0.11) 0 1px, transparent 1px calc(100% / 15));
}
.ld-rung { flex: 1; border-top: 2px solid transparent; transition: background 0.15s; }
.ld-rung.held { background: rgba(182,255,61,0.1); border-top-color: rgba(182,255,61,0.55); }
.ld-rung.next { background: rgba(155,92,255,0.18); border-top-color: var(--violet); }
.ld-fig { position: absolute; left: 50%; bottom: 0; width: 28px; height: 28px; margin: 0 0 -4px -14px; transition: bottom 0.25s var(--ease); z-index: 1; }
.ld-col.me .ld-fig { width: 40px; height: 40px; margin-left: -20px; margin-bottom: -6px; }
.ld-fig.fell { animation: ld-fall 0.6s var(--ease); }
@keyframes ld-fall { 0% { transform: none; } 30% { transform: translateY(-10px) rotate(-14deg); } 100% { transform: none; } }
.ld-col.fell .ld-fig { opacity: 0.35; filter: grayscale(1); }
.ld-col.banked .ld-fig { filter: drop-shadow(0 0 5px rgba(182,255,61,0.7)); }
.ld-rank { font-size: 0.6rem; font-weight: 800; letter-spacing: 0.06em; color: var(--dim); padding: 1px 6px; border-radius: 999px; border: 1px solid transparent; }
.ld-rank.first { color: var(--amber); border-color: rgba(255,179,64,0.5); }
.ld-tag { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--muted); min-height: 1em; white-space: nowrap; }
.ld-col.banked .ld-tag { color: var(--lime); } .ld-col.fell .ld-tag { color: var(--red); }
.ld-name { font-size: 0.64rem; color: var(--dim); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-bottom: 4px; }
.ld-col.me .ld-name { color: var(--cyan); font-weight: 700; }
.ld-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: center; }
.ld-climb, .ld-bank { font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; white-space: nowrap; }
.ld-bank { border: 1px solid rgba(182,255,61,0.5); background: rgba(182,255,61,0.1); color: var(--lime); }
.ld-standing { grid-column: 1 / -1; text-align: center; font-weight: 800; color: var(--cyan); font-size: 0.9rem; }
.ld-pass { height: 5px; border-radius: 999px; background: rgba(255,255,255,0.1); overflow: hidden; }
.ld-pass-fill { height: 100%; background: var(--grad); }
.ld-note { min-height: 1.3em; text-align: center; font-weight: 800; color: var(--amber); opacity: 0; transition: opacity 0.2s; }
.ld-note.on { opacity: 1; }

/* Add It Up. One board that does everything - shows the numbers, takes the
   answer, says what it was - under the round at a glance. `is-*` on the board
   is the only state there is; what each one shows is decided here. */
.au-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
/* What you have answered so far, and the score. Only the chips of the shared
   score panel: the board itself says what the last one was. Both step right
   back while numbers are flashing - nobody adding wants more numbers to read. */
.au-panel { width: min(100%, 640px); transition: opacity 0.25s; }
.au-panel .sp-verdict { display: none; }
.au-stat { transition: opacity 0.25s; }
.au-wrap.adding .au-panel, .au-wrap.adding .au-stat { opacity: 0.18; }

.au-board {
  position: relative; width: min(100%, 640px); height: clamp(220px, 40vh, 360px);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  border-radius: var(--radius); border: 1px solid var(--line);
  background: radial-gradient(ellipse at 50% 38%, rgba(124,92,255,0.13), rgba(6,5,14,0.94) 70%);
  overflow: hidden; transition: border-color 0.25s, box-shadow 0.25s;
}
.au-board.is-answer { border-color: rgba(155,92,255,0.7); box-shadow: 0 0 0 3px rgba(155,92,255,0.16); }
.au-board.is-result.good { border-color: rgba(182,255,61,0.6); }
.au-board.is-result.warn { border-color: rgba(255,179,64,0.55); }
.au-board.is-result.bad { border-color: rgba(255,77,77,0.5); }
.au-caption {
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--dim);
  min-height: 1.1em;
}
.au-number {
  font-size: clamp(5.5rem, 24vh, 12rem); font-weight: 900; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--text); opacity: 0;
  /* Holds its height while blank, so nothing jumps between two numbers. */
  min-height: 1em;
}
.au-number.on { opacity: 1; animation: au-pop 0.2s var(--ease); }
.au-number.tint0 { color: var(--cyan); } .au-number.tint1 { color: var(--pink); } .au-number.tint2 { color: var(--amber); }
.is-result .au-number { font-size: clamp(4.2rem, 17vh, 8rem); }
.is-result.good .au-number { color: var(--lime); } .is-result.warn .au-number { color: var(--amber); } .is-result.bad .au-number { color: var(--red); }
@keyframes au-pop { from { transform: scale(0.55); opacity: 0; } to { transform: none; opacity: 1; } }
.au-verdict { font-weight: 800; font-size: 1.02rem; min-height: 1.3em; color: var(--muted); }
.is-result.good .au-verdict { color: var(--lime); } .is-result.warn .au-verdict { color: var(--amber); } .is-result.bad .au-verdict { color: var(--red); }
/* The answer is typed where the numbers were. Nowhere else, and only then. */
.au-form { display: none; align-items: center; gap: 10px; }
.is-answer .au-form { display: flex; }
.is-answer .au-number, .is-answer .au-verdict { display: none; }
.au-input {
  width: 3.4ch; box-sizing: content-box; text-align: center; padding: 4px 18px;
  font-family: inherit; font-size: clamp(4.2rem, 17vh, 8rem); font-weight: 900; line-height: 1.05;
  font-variant-numeric: tabular-nums; color: var(--text); caret-color: var(--violet);
  background: transparent; border: 0; border-bottom: 4px solid rgba(155,92,255,0.6); border-radius: 0;
}
.au-input:focus { outline: none; border-bottom-color: var(--violet); }
.au-go {
  font: inherit; font-size: 1.3rem; font-weight: 800; cursor: pointer; color: #0b0a14;
  width: 46px; height: 46px; border-radius: 50%; border: 0; background: var(--grad);
}
/* One pip a number, filling as they go: how far through, never how much. */
.au-pips { position: absolute; left: 0; right: 0; bottom: 18px; display: flex; gap: 7px; justify-content: center; }
.is-answer .au-pips, .is-result .au-pips { opacity: 0; }
.au-pip { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.14); transition: background 0.15s, transform 0.15s; }
.au-pip.on { background: var(--violet); transform: scale(1.15); }
.au-bar { position: absolute; left: 0; right: 0; bottom: 0; height: 6px; background: transparent; }
.is-answer .au-bar { background: rgba(255,255,255,0.1); }
.au-bar-fill { height: 100%; width: 0; background: #7c5cff; }
.au-board:not(.is-answer) .au-bar-fill { width: 0 !important; }

.cd-form { display: flex; gap: 8px; justify-content: center; align-items: center; }
.cd-input {
  width: 130px; text-align: center;
  font-family: 'JetBrains Mono', monospace; font-size: 1.5rem; font-weight: 700;
  padding: 8px 12px; border-radius: 12px; color: var(--text);
  background: rgba(0,0,0,0.45); border: 1px solid var(--line);
}
.cd-input:focus { outline: none; border-color: var(--violet); }
.cd-input:disabled { opacity: 0.35; }

/* ============================================================
   SOUND-REQUIRED NOTICE
   ============================================================ */
.audio-note {
  margin: 10px 0 0;
  padding: 9px 14px; border-radius: 10px;
  font-size: 0.84rem; font-weight: 600;
  background: rgba(124,92,255,0.14); border: 1px solid rgba(124,92,255,0.4);
}
.audio-note.urgent {
  background: rgba(255,77,77,0.16); border-color: rgba(255,77,77,0.55);
  animation: pulseWarn 1.6s ease-in-out infinite;
}
@keyframes pulseWarn { 0%,100% { opacity: 1; } 50% { opacity: 0.62; } }
.gbadge.audio { background: rgba(124,92,255,0.22); border-color: rgba(124,92,255,0.5); }

/* ============================================================
   CONVEYOR · CODEBREAKER · RANK THEM · PERFECT PITCH
   ============================================================ */
/* Big and centred: these are the entire interface for the game, so they get to
   look like it rather than like a fallback for people without a keyboard. */
.cv-btns { display: flex; gap: 18px; justify-content: center; }
.cv-btn {
  min-width: 190px; font-size: 1.05rem; font-weight: 800;
  letter-spacing: 0.06em; padding: 14px 22px;
}
.cv-btn.bin { background: rgba(255,77,94,0.16); border-color: rgba(255,77,94,0.55); color: #ff8a95; }
.cv-btn.bin:hover { background: rgba(255,77,94,0.28); }
.cv-btn.keep { background: rgba(61,220,132,0.16); border-color: rgba(61,220,132,0.55); color: #7ff0ad; }
.cv-btn.keep:hover { background: rgba(61,220,132,0.28); }

.cb-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.cb-rows { display: flex; flex-direction: column; gap: 6px; max-height: 210px; overflow-y: auto; width: min(100%, 360px); }
.cb-row { display: flex; align-items: center; gap: 7px; justify-content: center; }
.cb-peg { width: 22px; height: 22px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.35); }
.cb-marks { font-family: 'JetBrains Mono', monospace; font-size: 0.95rem; color: var(--dim); letter-spacing: 2px; margin-left: 6px; }
.cb-current { display: flex; gap: 10px; }
.cb-slot {
  width: 46px; height: 46px; border-radius: 12px; cursor: pointer; padding: 0;
  border: 2px dashed var(--line); background: rgba(255,255,255,0.04);
}
.cb-slot.filled { border-style: solid; border-color: rgba(255,255,255,0.35); }
.cb-palette { display: flex; gap: 9px; flex-wrap: wrap; justify-content: center; }
.cb-pick {
  width: 38px; height: 38px; border-radius: 50%; cursor: pointer; padding: 0;
  border: 2px solid rgba(255,255,255,0.2); transition: transform 0.12s var(--ease);
}
.cb-pick:hover { transform: scale(1.14); }

.rt-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.rt-prompt { margin: 0; font-size: clamp(1.05rem, 2.6vw, 1.5rem); text-align: center; }
.rt-slots { display: flex; flex-direction: column; gap: 6px; width: min(100%, 460px); }
.rt-slot {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 13px; border-radius: 11px;
  border: 1px dashed var(--line); background: rgba(255,255,255,0.03);
}
.rt-slot.filled { border-style: solid; background: rgba(124,92,255,0.16); cursor: pointer; }
.rt-num { font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--cyan); width: 16px; }
.rt-label { font-weight: 600; }
.rt-ends { display: flex; justify-content: space-between; font-size: 0.72rem; color: var(--dim); padding: 0 4px; }
.rt-pool { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 620px; }
.rt-item { font-size: 0.88rem; }

.pp-wrap, .cd-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.pp-warn {
  padding: 9px 14px; border-radius: 12px; font-weight: 600; font-size: 0.88rem;
  background: rgba(255,77,77,0.16); border: 1px solid rgba(255,77,77,0.55);
}
.pp-warn[hidden] { display: none; }
/* Fixed-height playfields rather than stretchy ones. These two games want most
   of the screen given to the feedback panel underneath, which is where the
   learning happens - the field only has to be big enough to read.
   They keep .canvas-host so the collapse guard still applies; the explicit
   height simply overrides its flex growth. */
.canvas-host.pp-field, .canvas-host.cd-field {
  width: min(100%, 760px); flex: 0 0 auto; min-height: 0;
}
.canvas-host.pp-field { height: clamp(150px, 22vh, 240px); }
.canvas-host.cd-field { height: clamp(180px, 30vh, 300px); }
.pp-panel, .cd-panel { width: min(100%, 620px); }
.pp-dial { display: flex; align-items: center; gap: 14px; width: min(100%, 620px); }
.pp-slider { flex: 1; }

/* ============================================================
   SHARED SCORE PANEL  (Count the Dots, Perfect Pitch)
   ============================================================ */
.sp-box {
  width: min(100%, 620px); margin: 0 auto;
  padding: 16px 18px; border-radius: 18px;
  background: rgba(0,0,0,0.32); border: 1px solid var(--line);
}
.sp-box.flash { animation: spFlash 0.45s var(--ease); }
@keyframes spFlash {
  from { border-color: rgba(124,92,255,0.8); transform: scale(1.015); }
  to { border-color: var(--line); transform: none; }
}
.sp-verdict { min-height: 96px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; }
.sp-row { display: flex; align-items: center; justify-content: center; gap: 22px; }
.sp-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.sp-lab { font-size: 0.6rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; color: var(--dim); }
.sp-val { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: clamp(1.9rem, 6vw, 3rem); line-height: 1; }
.sp-cell.right .sp-val { color: var(--cyan); }
.sp-arrow { font-size: 1.6rem; color: var(--dim); }
.sp-note { font-size: 1rem; font-weight: 700; min-height: 1.3em; }
.sp-note.good { color: var(--lime); }
.sp-note.bad { color: var(--red); }
.sp-note.warn { color: var(--amber); }
.sp-cap { text-align: center; font-size: 0.7rem; color: var(--dim); margin-top: 4px; }
.sp-strip {
  display: flex; gap: 6px; flex-wrap: wrap; justify-content: center;
  margin-top: 12px; min-height: 26px;
}
.sp-chip {
  font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; font-weight: 700;
  padding: 4px 9px; border-radius: 8px;
  background: rgba(255,255,255,0.05); border: 1px solid var(--line); color: var(--dim);
  transition: transform 0.2s var(--ease);
}
.sp-chip.good { color: var(--lime); border-color: rgba(182,255,61,0.35); }
.sp-chip.bad { color: var(--red); border-color: rgba(255,77,77,0.35); }
.sp-chip.warn { color: var(--amber); border-color: rgba(255,210,61,0.35); }
.sp-chip.latest {
  transform: scale(1.14); color: #fff;
  background: rgba(124,92,255,0.22); border-color: rgba(124,92,255,0.6);
}

/* ============================================================
   CHAT: AVATARS + COLLAPSE
   ============================================================ */
.cmsg { display: flex; align-items: flex-start; gap: 7px; }
.cmsg-av { flex: none; line-height: 0; margin-top: 1px; }
.cmsg-body { min-width: 0; }

#chat-toggle {
  width: 100%; text-align: left;
  font: inherit; font-size: 0.68rem; font-weight: 800;
  letter-spacing: 0.12em; text-transform: uppercase;
  padding: 9px 12px; cursor: pointer;
  color: var(--dim); background: rgba(255,255,255,0.04); border: none;
  transition: color 0.16s, background 0.16s;
}
#chat-toggle:hover { color: #fff; }

/* Collapsed to just the button. There is nothing to say mid-round and a column
   of messages beside a timing game is one more thing moving in the corner of
   your eye. */
#chat-rail.collapsed #chat-log,
#chat-rail.collapsed #chat-form { display: none; }

/* Collapsed is not muted - something has to say the room is talking. */
#chat-rail.unread #chat-toggle {
  color: #fff; background: rgba(124,92,255,0.35);
  animation: chatPing 1.4s ease-in-out infinite;
}
@keyframes chatPing {
  0%, 100% { box-shadow: 0 0 0 0 rgba(124,92,255,0.5); }
  50% { box-shadow: 0 0 0 7px rgba(124,92,255,0); }
}

/* ============================================================
   PODIUM / STANDINGS
   ============================================================ */
/* Two rails, one down each side, and the page reserves both.

   `margin-top: auto` and a bottom anchor rather than `justify-content:
   space-between`: space-between with one visible child puts that child at the
   TOP, so on the home screen - which has no standings and no chat - the
   controls used to jump to the top of the window and drop to the bottom the
   moment you entered a lobby. */
#left-rail {
  position: fixed; left: var(--rail-gap); bottom: var(--rail-gap); z-index: 500;
  width: var(--rail-w);
  max-height: calc(100vh - 2 * var(--rail-gap));
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: stretch; gap: 10px;
  pointer-events: none;              /* the space between them stays click-through */
}
/* Both fade back during a round rather than competing with the game. */
body.in-game #settings-dock, body.in-game #discord-link, body.in-game #feedback-dock { opacity: 0.4; }
body.in-game #settings-dock:hover, #settings-dock:focus-within,
body.in-game #discord-link:hover, body.in-game #feedback-dock:hover { opacity: 1; }
#feedback-dock { transition: opacity 0.2s var(--ease); }
/* The standings sit alone on the right. The extra offset clears the scroll
   bar, which belongs to .screen and is drawn at the viewport's edge - a fixed
   element at right:14px lands on top of it. */
#right-rail {
  position: fixed; right: calc(var(--rail-gap) + 10px);
  top: var(--rail-gap); bottom: var(--rail-gap); z-index: 480;
  width: var(--rail-w);
  display: flex; flex-direction: column; align-items: stretch; gap: 10px;
  pointer-events: none;
}
/* Pinned to the bottom whether or not the standings are above it. An auto
   margin rather than `justify-content: space-between`, which puts a lone
   child at the TOP - which is how these ended up at the top of the home
   screen and the bottom of a lobby. */
#settings-dock { margin-top: auto; }
/* The feedback bubble rides on top of the settings box: it takes the auto
   margin, and the box sits right under it. */
#feedback-dock { margin-top: auto; display: flex; justify-content: flex-end; }
#feedback-dock[hidden] { display: none; }
#feedback-dock:not([hidden]) + #settings-dock { margin-top: 0; }
#left-rail > *, #right-rail > * { pointer-events: auto; }

/* The two panels share one frame so they read as panels rather than as loose
   controls that happened to land near each other. */
#settings-dock, #chat-rail {
  border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line);
  background: rgba(10,9,22,0.82);
  backdrop-filter: blur(10px);
  transition: opacity 0.2s var(--ease);
}
.rail-row {
  display: flex; align-items: center;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
}
/* `display` in a class rule outranks the browser's own [hidden] rule, and the
   language row is hidden whenever there is only one language to switch to. */
.rail-row[hidden] { display: none; }

/* The host's end-match button. Deliberately quiet - it sits in the rail for a
   whole match and the one thing it must never be is easy to hit by accident. */
.rail-abort {
  width: 100%; font: inherit; font-size: 0.78rem; font-weight: 700;
  padding: 7px 10px; border-radius: var(--radius-sm);
  background: transparent; border: 1px solid var(--line); color: var(--dim);
  cursor: pointer; transition: color 0.14s, border-color 0.14s, background 0.14s;
}
.rail-abort:hover { color: var(--red); border-color: rgba(255,77,77,0.5); background: rgba(255,77,77,0.08); }
/* Shrinkable, so a full room's standings scroll rather than running off the
   bottom of the window. */
#podium-dock { display: none; min-height: 0; flex: 0 1 auto; overflow: hidden; }
#podium-dock.on { display: block; }

.pod {
  border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line); background: rgba(10,9,22,0.84);
  backdrop-filter: blur(10px);
  transition: opacity 0.2s var(--ease);
}
.pod.empty { display: none; }
/* Present but out of the way mid-round, like the audio dock. */
body.in-game .pod { opacity: 0.45; }
body.in-game .pod:hover { opacity: 1; }

.pod-head {
  display: flex; align-items: center; width: 100%;
  padding: 8px 12px; cursor: pointer;
  font: inherit; text-align: left; color: var(--dim);
  background: rgba(255,255,255,0.04); border: none;
  transition: color 0.16s;
}
.pod-head:hover { color: #fff; }
.pod-title { font-size: 0.62rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
.pod-head::after {
  content: '📌'; margin-left: auto; font-size: 0.7rem; opacity: 0.25;
}
.pod.pinned .pod-head::after { opacity: 1; }

/* Ten players expanded is taller than a lot of laptop screens, so cap it and
   let it scroll rather than running off the bottom. */
.pod-list {
  padding: 6px 8px 8px; display: flex; flex-direction: column; gap: 2px;
  max-height: min(48vh, 430px); overflow-y: auto; scrollbar-width: thin;
}
.pod-row {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 7px; border-radius: 9px;
  border: 1px solid transparent;
}
.pod-row.me { background: rgba(124,92,255,0.18); border-color: rgba(124,92,255,0.45); }
.pod-row.gone { opacity: 0.4; }
.pod-rank {
  width: 20px; flex: none; text-align: center;
  font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; font-weight: 700; color: var(--dim);
}
.pod-row.top .pod-rank { font-size: 0.95rem; }
.pod-av { flex: none; line-height: 0; }
.pod-nick {
  flex: 1; min-width: 0; font-size: 0.8rem; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pod-score { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; font-weight: 700; color: var(--cyan); }
.pod-gap { text-align: center; color: var(--dim); font-size: 0.7rem; line-height: 0.8; }

/* Direction of travel is the information, so it gets a colour. */
.pod-row.rose { background: rgba(182,255,61,0.2); border-color: rgba(182,255,61,0.5); }
.pod-row.fell { background: rgba(255,77,77,0.16); border-color: rgba(255,77,77,0.4); }
.pod-row.rose, .pod-row.fell { transition: background 0.5s var(--ease), border-color 0.5s; }

@media (max-width: 900px) {
  .pod { width: 186px; }
  .pod-nick { font-size: 0.74rem; }
}

/* ============================================================
   SCREEN PREVIEW  (/screens.html only)
   ============================================================ */
#sv-bar {
  position: sticky; top: 0; z-index: 900;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 6px 16px;
  background: rgba(8,7,18,0.92); border-bottom: 1px solid var(--line);
  backdrop-filter: blur(10px);
}
.sv-home { font-size: 0.78rem; font-weight: 700; color: var(--dim); text-decoration: none; }
.sv-home:hover { color: #fff; }
/* Flat tabs in three groups, not a row of pills: it is a list of places in the
   order you reach them, and an underline says "you are here" without shouting. */
#sv-tabs { display: flex; align-items: center; gap: 6px 22px; flex-wrap: wrap; flex: 1 1 auto; }
.sv-group { display: flex; align-items: center; gap: 2px; }
.sv-group + .sv-group { padding-left: 20px; border-left: 1px solid var(--line); }
.sv-group-name {
  font-size: 0.6rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--dim); opacity: 0.75; margin-right: 8px;
}
.sv-tab {
  font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer;
  padding: 6px 9px; color: var(--muted);
  background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0;
  transition: color 0.15s, border-color 0.15s;
}
.sv-tab:hover { color: #fff; }
.sv-tab.on { color: #fff; border-bottom-color: var(--cyan); }
.sv-opt { font-size: 0.72rem; color: var(--dim); display: flex; align-items: center; gap: 5px; }
.sv-opt select {
  font: inherit; color: #fff; padding: 2px 4px;
  background: rgba(255,255,255,0.08); border: 1px solid var(--line); border-radius: 6px;
}
#sv-note {
  padding: 8px 16px; font-size: 0.76rem; color: var(--dim);
  border-bottom: 1px solid var(--line);
}
/* The screens are `position: absolute; inset: 0` inside an `#app` that is
   `height: 100%`. On the real page that resolves against a full-height body;
   here it resolved against an auto-height div, so every screen collapsed to
   zero and the page looked empty. The preview has to give #app a real box. */
body.sv-page { display: flex; flex-direction: column; min-height: 100vh; }
#sv-mount { position: relative; flex: 1 1 auto; min-height: 0; }
#sv-mount > #app { position: absolute; inset: 0; height: auto; }
body.sv-bare #sv-mount { min-height: 100vh; }
.sv-stub {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 320px; border-radius: 16px;
  border: 2px dashed var(--line); color: var(--dim);
  font-size: 0.85rem; letter-spacing: 0.08em; text-transform: uppercase;
}
/* Hide the preview's own furniture for a clean screenshot. */
body.sv-bare #sv-bar, body.sv-bare #sv-note { display: none; }
.topbar-links { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
/* The playground and preview links are hidden off localhost. Today the topbar
   above them collapses because they are its only child, which is a rule about
   the parent doing the work of a rule about this element. */
.topbar-links[hidden] { display: none; }
/* The podium is fixed at top:14px, which on this page lands under the preview
   toolbar. Push it clear so it can actually be seen and screenshotted - but
   only while the toolbar is there. With the chrome off, this page is meant to
   be the game to the pixel, and 82px of the rail's height is exactly the kind
   of difference every measurement taken here would silently carry. */
body.sv-page #right-rail { top: 96px; }
body.sv-page.sv-bare #right-rail { top: var(--rail-gap); }
.sv-tab:focus { outline: none; }
.sv-tab:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.picker-name { color: var(--cyan); font-weight: 800; }
#btn-reroll { margin: 14px auto 0; display: block; }
#btn-reroll[hidden] { display: none; }
/* Your own row, always findable at a glance. */
.lrow.me {
  background: rgba(124,92,255,0.2);
  border-color: rgba(124,92,255,0.55);
}
.lrow.me .ln { color: #fff; font-weight: 800; }


/* ============================================================
   IDENTITY CHIP + MODAL
   ============================================================ */
.id-chip {
  display: flex; align-items: center; gap: 12px; cursor: pointer;
  margin: 0 auto; padding: 8px 18px 8px 8px; border-radius: 999px;
  font: inherit; color: var(--text);
  background: var(--panel); border: 1px solid var(--line);
  transition: border-color 0.16s, transform 0.12s var(--ease);
}
.id-chip:hover { border-color: var(--violet); transform: translateY(-1px); }
.id-face { line-height: 0; flex: none; }
.id-text { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.id-nick { font-weight: 800; font-size: 1rem; }
.id-nick.unset { color: var(--dim); font-weight: 600; }
.id-edit { font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--dim); }

#identity-modal {
  position: fixed; inset: 0; z-index: 900;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
#identity-modal[hidden] { display: none; }

/* 💬 Feedback: the button in the rail and its dialog, which borrows the
   identity editor's backdrop and close button. */
/* A speech bubble: rounded, with its tail at the bottom right. */
.rail-feedback {
  position: relative; margin-bottom: 7px;
  font: inherit; font-size: 0.85rem; font-weight: 800;
  padding: 8px 15px; border-radius: 16px 16px 4px 16px;
  background: var(--cyan); border: 0; color: #06121a;
  box-shadow: 0 8px 22px rgba(0,0,0,0.4);
  cursor: pointer; transition: transform 0.14s var(--ease), box-shadow 0.14s;
}
.rail-feedback::after {
  content: ''; position: absolute; right: 10px; bottom: -7px;
  border: 7px solid transparent; border-bottom: 0; border-top-color: var(--cyan);
  border-right-width: 0;
}
.rail-feedback:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(52,224,255,0.3); }

/* The one-time hint: a chat bubble to the left of the button, its tail
   pointing at it. Placed from the button's own position, so it follows the
   rail wherever the rail ends up. */
.fb-hint-bubble {
  position: fixed; z-index: 880; max-width: 270px;
  display: flex; align-items: flex-start; gap: 8px;
  padding: 11px 12px 11px 14px; border-radius: 16px 16px 4px 16px;
  background: var(--cyan); color: #06121a; font-size: 0.86rem; font-weight: 700; line-height: 1.35;
  box-shadow: 0 12px 34px rgba(0,0,0,0.5), 0 0 0 3px rgba(52,224,255,0.18);
  transform: translateY(-50%); animation: fbHintIn 0.35s var(--ease);
}
.fb-hint-bubble[hidden] { display: none; }
.fb-hint-bubble::after {
  content: ''; position: absolute; right: -8px; top: 50%; margin-top: -8px;
  border: 8px solid transparent; border-right: 0; border-left-color: var(--cyan);
}
.fb-hint-x {
  flex: none; font: inherit; font-size: 1rem; line-height: 1; padding: 0 2px;
  background: transparent; border: 0; color: #06121a; opacity: 0.6; cursor: pointer;
}
.fb-hint-x:hover { opacity: 1; }
@keyframes fbHintIn {
  from { opacity: 0; transform: translate(10px, -50%); }
  to { opacity: 1; transform: translate(0, -50%); }
}
#fb-modal {
  position: fixed; inset: 0; z-index: 900;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
#fb-modal[hidden] { display: none; }
.fb-panel {
  position: relative; width: min(100%, 520px); max-height: 88vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 12px;
  padding: 22px; border-radius: var(--radius);
  background: var(--bg2, #14112a); border: 1px solid var(--line);
  box-shadow: 0 30px 80px rgba(0,0,0,0.6);
  animation: pop 0.22s var(--ease);
}
.fb-title { margin: 0; font-size: 1.35rem; font-weight: 900; }
.fb-sub { margin: -6px 0 0; color: var(--muted); font-size: 0.9rem; }
/* The sheet in the typewriter: paper, ink, and a typewriter's type. */
.fb-text {
  width: 100%; resize: vertical; min-height: 140px;
  font-family: 'Courier New', Courier, monospace; font-size: 1rem; line-height: 1.55;
  padding: 14px 16px; border-radius: 4px;
  background: #fbf6e6; color: #26211a; border: 1px solid #d8ceb0;
  box-shadow: inset 0 2px 6px rgba(0,0,0,0.12);
}
.fb-text::placeholder { color: #9a8f74; }
.fb-text:focus { outline: none; border-color: #c9a85a; }

/* The letter meeting its end after Send (letterFx.js): a canvas over the page
   where the sheet was, above everything and never in the way of a click. */
.fb-letter { position: fixed; z-index: 950; pointer-events: none; }

.fb-shot { display: flex; align-items: center; gap: 12px; }
.fb-shot[hidden] { display: none; }
.fb-img { max-width: 160px; max-height: 110px; border-radius: var(--radius-sm); border: 1px solid var(--line); object-fit: contain; background: #fff; }
.fb-row { display: flex; align-items: center; gap: 10px; }
.fb-hint { color: var(--dim); font-size: 0.8rem; }
.fb-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 4px; }
.fb-status { margin-right: auto; font-size: 0.85rem; color: var(--muted); }
.fb-status[data-kind="bad"] { color: var(--pink); }
.id-backdrop { position: absolute; inset: 0; background: rgba(4,3,10,0.72); backdrop-filter: blur(4px); }
/* The chips scroll, the button does not. Scrolling the whole panel put "Looks
   good" below the fold on any laptop-height window - five groups of chips are
   taller than 88vh - so the one control you need to leave the editor was the
   one you had to go looking for. */
.id-panel {
  position: relative; width: min(100%, 720px);
  max-height: 88vh;
  display: flex; flex-direction: column;
  padding: 22px; border-radius: var(--radius);
  background: var(--bg2, #14112a); border: 1px solid var(--line);
  box-shadow: 0 30px 80px rgba(0,0,0,0.6);
  animation: pop 0.22s var(--ease);
}
.id-grid {
  display: grid; grid-template-columns: auto 1fr; gap: 24px; align-items: start;
  overflow-y: auto; min-height: 0;   /* min-height, or a grid in a flex column refuses to shrink */
  scrollbar-width: thin;
}
#btn-identity-done { flex: none; margin-top: 16px; }
@media (max-width: 640px) { .id-grid { grid-template-columns: 1fr; } }

/* Match settings fold away - they're set once and then never touched again,
   and they were the reason Create didn't fit on the screen. */
.adv > summary {
  cursor: pointer; font-size: 0.76rem; font-weight: 700; color: var(--dim);
  letter-spacing: 0.08em; text-transform: uppercase; padding: 6px 0;
  list-style: none;
}
.adv > summary::-webkit-details-marker { display: none; }
.adv > summary::before { content: '▸ '; }
.adv[open] > summary::before { content: '▾ '; }
.adv > summary:hover { color: #fff; }

/* No scrolling on the home screen: the two things it exists for should both
   be on the fold. */
/* One column, tight. The actions and the identity strip are one block, not two
   halves of a page with a hole between them.
   The gap is measured against the window's height, not left at a constant: on a
   720p laptop this screen was 259px too tall, so the bottom row of colour
   swatches - the thing the strip exists for - was below the fold. There is no
   padding under the column, because `justify-content: center` already leaves
   whatever room is going above and below it; the 40px that used to sit there
   was 40px of the deficit and bought nothing. */
.home-wrap {
  min-height: 100%; display: flex; flex-direction: column;
  justify-content: center; gap: clamp(7px, 1.4vh, 14px);
}
.home-actions + .error-line { margin: 0; }
.home-actions + .error-line:empty { display: none; }
.brand { margin: 0; }
.logo { font-size: clamp(2.2rem, min(7vw, 6.2vh), 4.2rem); }
/* The two cards are what the screen is for, so they are shrunk last and least -
   only the space between their parts moves, never the inputs or the buttons.
   `.panel`'s own padding is scaled off the window's WIDTH, which on a 1280x720
   laptop asks for the full 26px top and bottom on the one axis that has none to
   give; the horizontal half is left exactly as it was. */
.action-card { padding: clamp(12px, 2.4vh, 26px) clamp(16px, 2.4vw, 26px); }
.action-card h2 { font-size: clamp(1rem, 2.2vh, 1.2rem); margin-bottom: clamp(8px, 1.7vh, 14px); }
.action-card .field { margin-bottom: clamp(8px, 1.7vh, 14px); }
.action-card .field > span { margin-bottom: clamp(3px, 0.8vh, 6px); }
/* The test room button sits under Create room and needs air between the two. */
#btn-create-test { margin-top: clamp(8px, 1.4vh, 12px); }
.lobby-btns { display: flex; gap: 8px; }

/* ============================================================
   IDENTITY STRIP (home) + EDITOR OVERLAY (lobby)
   ============================================================ */
/* Directly beneath the two cards and exactly as wide - the gap that used to sit
   here was doing nothing except making the strip look like a different page. */
.id-strip {
  padding: clamp(8px, 1.5vh, 16px) 20px; border-radius: var(--radius-sm);
  background: var(--panel); border: 1px solid var(--line);
}
.id-strip-head {
  display: flex; align-items: center; gap: 18px;
  margin-bottom: clamp(8px, 1.5vh, 14px);
}
.id-strip .id-name { flex: 1; min-width: 0; }
/* A field in a row, not in a stack. The 14px of bottom margin every stacked
   field carries was being centred along with the field, so the name box sat 7px
   above the face and the Randomise button it is meant to line up with - and it
   made the row 14px taller than the tallest thing in it. */
.id-strip-head .field { margin-bottom: 0; }
.id-face { line-height: 0; flex: none; }
/* Compact chips, but a full-size face - it's the thing people came to fiddle
   with, and a thumbnail made it feel like an afterthought. */
.id-strip .customizer { gap: clamp(6px, 1.1vh, 10px); }
/* Label beside its chips rather than above them. Five one-word labels, each on
   its own 20px line, were 100px of the home screen - a third of what this strip
   costs - spent on five words. `max-content` sizes the column to the longest
   label in whichever language is loaded, so the five rows still line up and
   nothing has to be measured by hand. */
.id-strip .cust-group {
  display: grid; grid-template-columns: max-content 1fr;
  align-items: center; column-gap: 12px;
}
.id-strip .cust-group > span { font-size: 0.62rem; margin-bottom: 0; }
.id-strip .chip { padding: 5px 10px; font-size: 0.74rem; }
.id-strip .chip.swatch { width: 22px; height: 22px; }

.id-close {
  position: absolute; top: 10px; right: 12px; z-index: 2;
  width: 34px; height: 34px; padding: 0; cursor: pointer;
  font: inherit; font-size: 1.4rem; line-height: 1;
  border-radius: 50%; color: var(--dim);
  background: rgba(255,255,255,0.06); border: 1px solid var(--line);
  transition: color 0.16s, background 0.16s;
}
.id-close:hover { color: #fff; background: rgba(255,255,255,0.16); }

/* Your own lobby card is the edit button. */
.pcard.editable { cursor: pointer; position: relative; }
.pcard.editable:hover { border-color: var(--violet); }
.pedit {
  position: absolute; bottom: 8px; right: 8px;
  font-size: 0.8rem; opacity: 0; transition: opacity 0.16s;
}
.pcard.editable:hover .pedit { opacity: 1; }
.lobby-leave { margin-top: 10px; }
#final-taunts:empty { display: none; }
/* A roast under the player it's about, in their row. Two banners above the
   table read as a system message; the same words here read as someone in the
   room saying it. */
.rq {
  margin-top: 3px; font-size: 0.82rem; font-style: italic;
  color: #ff9ecb;
}

/* ============================================================
   LOBBY TOY
   ============================================================ */
/* A strip, not a stage - in the layout. It gets the height of one figure and no
   more, because anything taller starts competing with the Start button for the
   fold. The stage inside it is what grows, upward and over the player cards,
   once you click in: see .toy-stage. */
/* 7:1 to match the world in lobbyToy.js, so the drawing fills the strip
   instead of sitting letterboxed in the middle of it. Capped, because on a
   wide monitor a proportional strip becomes a stage. */
.toy-strip {
  position: relative;
  aspect-ratio: 7 / 1; max-height: 150px; min-height: 96px;
  margin: 14px 0 4px;
}
/* Pinned to the bottom of the strip, so opening it reveals the world upward and
   the floor never moves. Three strips tall when open, to match toyWorld.js. */
.toy-stage {
  position: absolute; left: 0; right: 0; bottom: 0; height: 100%;
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.18));
  border: 1px solid var(--line);
  overflow: hidden;
  transition: height 0.26s var(--ease), box-shadow 0.2s, border-color 0.2s;
}
.toy-strip.playing { z-index: 30; }
.toy-strip.playing .toy-stage {
  height: 300%;
  /* Opaque: it is lying over the player cards, and they should not show through. */
  background: linear-gradient(180deg, #17142e, #0e0c1c);
  border-color: rgba(52,224,255,0.55);
  box-shadow: 0 0 0 2px rgba(52,224,255,0.16), 0 -18px 50px rgba(0,0,0,0.55);
}
/* Focus is the whole interaction, so it has to be visible. The outline is the
   browser's own idea, replaced with something that matches the room. */
.toy-strip:focus { outline: none; }
.toy-strip:not(.playing) { cursor: pointer; }
.toy-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* Top right, out of the way of the figures, which stand on the floor line.
   Quiet: it is an instruction you need once. */
.toy-hint {
  position: absolute; right: 10px; top: 8px; z-index: 1;
  font-size: 0.7rem; font-weight: 600; color: var(--dim);
  /* A slab under it, like the name pills: the star drifts up here, and dim
     text over a bright star is no text at all. */
  padding: 2px 8px; border-radius: 999px; background: rgba(11, 10, 20, 0.6);
  pointer-events: none;
}
/* Exactly one of the two is up at any time: what to do, then how to do it. */
.toy-strip .toy-hint.keys,
.toy-strip.playing .toy-hint.idle { display: none; }
.toy-strip.playing .toy-hint.keys { display: block; color: var(--muted); }
/* The hint rides up with the top of the open stage. */
.toy-strip.playing .toy-hint { top: calc(-200% + 8px); }
.toy-hint { transition: top 0.26s var(--ease); }

/* ============================================================
   PRE-JOIN (invite links)
   ============================================================ */
/* The Join button never leaves the screen. Five groups of chips are taller
   than any laptop window, so the chips scroll inside the card and the button
   is pinned under them - same trick, and the same reason, as the identity
   editor. A screen whose whole point is that a link is one tap cannot make you
   scroll to find the tap. */
.join-wrap {
  height: 100%; width: min(100%, 760px); margin: 0 auto; padding: 16px 0;
  display: flex; flex-direction: column; justify-content: center; gap: 12px;
}
.join-head { text-align: center; }
/* Smaller than the home screen's: you are not here to admire the logo, and
   every pixel it takes comes off the customizer. */
.join-wrap .logo { font-size: clamp(1.9rem, 5vw, 3rem); }
.join-wrap .brand { margin: 0; }
/* The lobby's code is a button that copies the invite. This one is a statement
   of fact, so it should not invite a click that does nothing. */
#join-code { cursor: default; }
#join-code:hover { transform: none; }

.join-card { display: flex; flex-direction: column; gap: 16px; min-height: 0; }
.join-id {
  display: grid; grid-template-columns: auto 1fr; gap: 22px; align-items: start;
  overflow-y: auto; min-height: 0;  /* min-height, or a grid in a flex column refuses to shrink */
  scrollbar-width: thin;
}
#btn-join-go { flex: none; }
/* .field is display:block, which beats the browser's [hidden]. An open room
   must not ask for a password it doesn't have. */
#join-pass-field[hidden] { display: none; }
#join-preview {
  width: 180px; height: 180px;
  border-radius: 26px;
  background: radial-gradient(circle at 50% 35%, rgba(255,255,255,0.10), rgba(0,0,0,0.32));
  border: 1px solid var(--line);
}
/* Same compact chips as the home strip - five groups at full size push the
   Join button off a phone screen, which is the one thing this screen must not
   do. */
.join-card .customizer { gap: 10px; }
/* Labels stay ABOVE their chips here, unlike the home strip. Moving them beside
   was tried and measured: this column is narrower than the home strip's, so
   every chip row wraps to two lines and the scrollport ends up holding one
   group FEWER, cut mid-row. See the backlog entry on this customizer. */
.join-card .cust-group > span { font-size: 0.62rem; }
.join-card .chip { padding: 5px 10px; font-size: 0.74rem; }
.join-card .chip.swatch { width: 22px; height: 22px; }
.join-card .error-line:empty { display: none; margin: 0; }
.join-back { align-self: center; }

@media (max-width: 640px) {
  .join-id { grid-template-columns: 1fr; justify-items: center; }
  .join-id .identity-form { width: 100%; }
  #join-preview { width: 132px; height: 132px; }
}

/* Someone at the door: on the pre-join screen with a name and a face, but they
   haven't pressed the button yet. Dashed and breathing, so it reads as "on the
   way" rather than as a player who has somehow gone grey. */
.pcard.ghost {
  border-style: dashed;
  border-color: rgba(255,255,255,0.2);
  background: transparent;
  opacity: 0.6;
  animation: knocking 2.6s var(--ease) infinite;
}
.pcard.ghost:hover { transform: none; }
.pcard.ghost .pmeta { color: var(--cyan); }
@keyframes knocking {
  0%, 100% { opacity: 0.42; }
  50%      { opacity: 0.72; }
}

/* ============================================================
   OUT-OF-DATE PAGE
   ============================================================ */
/* Shown when the server reports a game this bundle cannot mount, which means
   this tab is older than the server it is talking to. Pinned to the top rather
   than dropped in as a toast: a toast that vanishes is exactly the wrong shape
   for a message whose whole content is "you need to do something". */
.stale-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center; gap: 12px;
  flex-wrap: wrap; padding: 9px 16px;
  background: linear-gradient(180deg, rgba(255,179,64,0.22), rgba(255,179,64,0.12));
  border-bottom: 1px solid rgba(255,179,64,0.5);
  backdrop-filter: blur(8px);
  font-size: 0.88rem; font-weight: 600; color: var(--text);
}
/* `display` in a class rule beats the browser's own `[hidden] { display:none }`,
   so without this the bar is on screen on every single page load: a 29px amber
   strip pinned to the top of the window with an empty button in the middle of
   it. Every other element on the page that is toggled with `hidden` and styled
   with `display` needs the same line, and most of them already had it. */
.stale-bar[hidden] { display: none; }
.stale-msg { color: var(--text); }
.stale-hint { color: var(--muted); font-size: 0.8rem; }
.stale-reload {
  font: inherit; font-weight: 800; letter-spacing: 0.04em;
  padding: 4px 14px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--amber); background: rgba(255,179,64,0.2); color: var(--amber);
}
.stale-reload:hover { background: rgba(255,179,64,0.32); }

/* The round this page could not mount. It replaces the stage rather than the
   screen, so the scoreboard, the clock and the results that follow all behave
   normally - the player misses one round instead of the match. */
.unknown-game { justify-content: center; gap: 14px; }
.unknown-face { font-size: 3rem; opacity: 0.7; }
.unknown-line { max-width: 30ch; color: var(--muted); font-weight: 600; line-height: 1.5; }

/* ============================================================
   THE RUNNING SCORE IN A GAME'S HUD
   ============================================================ */
/* Four of the newer games ended their HUD with a bare amber number after a
   divider - "BOARD 1/4  WET 1/9  TURNS 20 | 2" - and nothing on screen said
   what the 2 was. It is your score, and the two games that spell it out
   (Push Your Luck's BANKED, Shell Game's SCORE) are the ones nobody has to
   guess at.
   Done in CSS rather than in four JS files because the label is the same word
   in both languages this game ships: `pts` is the standard abbreviation for
   puntos as well as points, so there is nothing here for a translator to do. */
.ww-total, .pc-total, .sf-total, .dl-total {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding-left: 12px; border-left: 1px solid var(--line);
}
.ww-total::after, .pc-total::after, .sf-total::after, .dl-total::after {
  content: 'PTS';
  font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; font-weight: 800;
  letter-spacing: 0.14em; color: var(--muted);
}

/* ============================================================
   WAITING OUT A MATCH
   ============================================================ */
/* Turning up mid-match used to be an error message on the home screen. This is
   the room seen from the doorway: the scoreboard, how far along they are, what
   is on right now, and who else is stuck out here with you. */
.wait-wrap {
  width: min(560px, 100%); margin: 0 auto; padding: 28px 20px;
  display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center;
  /* Bounded, so the standings shrink rather than pushing the Leave button off
     the bottom - a full room is twenty rows and this screen has no clock on it,
     but nobody in the queue is going to scroll a scoreboard to find the way
     out. See .wait-wrap .standings. */
  height: 100%;
}
.wait-title { font-size: clamp(1.5rem, 4vw, 2.1rem); font-weight: 900; letter-spacing: -0.02em; }
.wait-sub { color: var(--muted); max-width: 46ch; line-height: 1.5; }

/* What the room is playing this second. Not a link and not a button - there is
   nothing to press, it is just the answer to "what are they doing in there". */
.wait-now {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 16px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--panel);
  font-weight: 700; font-size: 0.9rem;
}
.wait-now[hidden] { display: none; }
.wait-now-icon { font-size: 1.2rem; }
.wait-now-name { color: var(--muted); }

.wait-wrap .standings { width: 100%; min-height: 0; overflow-y: auto; scrollbar-width: thin; }

.wait-queue {
  font-family: 'JetBrains Mono', monospace; font-size: 0.78rem;
  letter-spacing: 0.06em; color: var(--dim); min-height: 1.2em;
}

.wait-actions { display: flex; gap: 10px; margin-top: 4px; }

/* Who turned up while the room was playing, on the final screen. Amber rather
   than the usual muted grey because it is the one thing on that screen that
   changes what the host should press next. */
.final-waiting {
  padding: 9px 18px; border-radius: 12px;
  border: 1px solid rgba(255,179,64,0.45); background: rgba(255,179,64,0.12);
  color: var(--text); font-weight: 700; font-size: 0.9rem;
}
.final-waiting[hidden] { display: none; }

/* The rules proof sheet on /screens.html. Not part of the game: a flat read of
   every game's copy, so it is styled for scanning rather than for atmosphere. */
#sv-rules {
  width: min(900px, 94vw); margin: 0 auto; display: grid; gap: 10px;
  /* The app pins `overflow: hidden` on html and body so a screen can never
     scroll - that is the rule layoutcheck exists to enforce. This sheet is a
     document rather than a screen, so it takes the leftover column height and
     scrolls inside itself, which leaves the page lock alone for every other
     tab. `min-height: 0` is what lets a flex child shrink below its content;
     without it the pane grows and the scrollbar never appears. */
  flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin;
  align-content: start; padding: 0 4px 60px;
}
/* An id selector outranks the UA stylesheet's `[hidden] { display: none }`, so
   without this the sheet renders on every tab instead of only its own. */
#sv-rules[hidden] { display: none; }
.sv-rules-head {
  font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--muted);
  letter-spacing: 0.5px; padding: 4px 2px 6px;
}
.sv-rules-card {
  background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08);
  border-radius: 12px; padding: 12px 14px;
}
/* Gated games are still worth reading, and still worth telling apart at a
   glance - the copy for a game nobody can play is not a priority. */
.sv-rules-gated { opacity: 0.55; border-style: dashed; }
.sv-rules-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.sv-rules-name { font-weight: 800; font-size: 1.05rem; }
.sv-rules-meta {
  font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--muted);
  white-space: nowrap;
}
.sv-rules-blurb { color: var(--muted); font-style: italic; margin: 2px 0 8px; font-size: 0.9rem; }
.sv-rules-list { margin: 0; padding-left: 20px; display: grid; gap: 3px; }
.sv-rules-list li { line-height: 1.45; }

/* ============================================================
   BLINK
   ============================================================ */
.bl-wrap { gap: 10px; }
.bl-head { font-size: 0.68rem; font-weight: 800; letter-spacing: 0.16em; color: var(--muted); }
/* Twenty tiles that share the card rather than each claiming a width off the
   viewport. The wall has to be one shape you can take in at a glance - a tile
   that wrapped, or that flex-shrink squeezed into a shape nobody drew, would be
   a tile you look at instead of look past, which is the whole game. */
/* Wider than tall, and narrower than the card. Twenty square tiles at the full
   574px of the play card is 446px of wall before the hint, the ticket strip and
   the total are counted, which overflows a 639px stage by 94px. A 5:4 tile on a
   470px wall is 304px and reads exactly the same. */
.bl-grid { display: grid; gap: 8px; width: min(100%, 470px); }
.bl-tile {
  aspect-ratio: 5 / 4; display: flex; align-items: center; justify-content: center;
  min-width: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 10px;
  background: repeating-linear-gradient(45deg, rgba(255,255,255,0.05) 0 6px, rgba(255,255,255,0.02) 6px 12px);
}
/* No transition anywhere on a tile, on purpose. A 120ms fade on the glyph or on
   the border would leak the change across the blank as a smear, and the blank
   is the only reason this game is hard. Everything here snaps. */
.bl-tile.dark { background: rgba(255,255,255,0.03); border-color: rgba(255,255,255,0.06); }
.bl-tile.answer { background: rgba(182,255,61,0.18); border-color: var(--lime); }
.bl-tile.wrong { background: rgba(255,84,112,0.16); border-color: var(--pink); }
.bl-face { font-size: clamp(1.1rem, 2.4vw, 2rem); line-height: 1; }
.bl-panel { width: min(100%, 620px); }
.bl-total { font-variant-numeric: tabular-nums; }

/* --- Swarm ------------------------------------------------------------- */
.sw-wrap { gap: 9px; }
.sw-head {
  font-size: clamp(0.8rem, 2.1vh, 1rem); font-weight: 800; letter-spacing: 0.14em; color: var(--muted);
  padding: 4px 14px; border-radius: 999px; background: rgba(255, 255, 255, 0.04); border: 1px solid transparent;
  transition: color 0.2s, border-color 0.2s, background 0.2s;
}
/* What the round wants from you right now, in the colour of the thing you are
   doing it to: the call buttons, your lit dots, the field, the dots you name.
   It was one line of small grey capitals, and the phases are the game. */
.sw-wrap[data-phase="call"] .sw-head { color: var(--amber); border-color: rgba(255, 179, 64, 0.35); background: rgba(255, 179, 64, 0.08); }
.sw-wrap[data-phase="mark"] .sw-head { color: var(--lime); border-color: rgba(182, 255, 61, 0.4); background: rgba(182, 255, 61, 0.08); }
.sw-wrap[data-phase="run"] .sw-head { color: var(--cyan); border-color: rgba(52, 224, 255, 0.4); background: rgba(52, 224, 255, 0.08); }
.sw-wrap[data-phase="pick"] .sw-head { color: var(--pink); border-color: rgba(255, 77, 141, 0.45); background: rgba(255, 77, 141, 0.08); }
/* The field is one shape you take in at a glance and it has to fit the card
   whole. 620x380 units at 500px wide is 306px of field, which leaves room for
   the call row, the ticket strip and the total inside a 639px stage - the
   mistake Blink paid for was sizing the play area off the viewport instead.

   The slot is what the stage squeezes when it is shorter than that - a 13"
   laptop in a real match, where the field used to be squashed to 500x271
   while the dots were still placed for 306, so the bottom ones ran out of it
   (Elsa, 2026-10-08). The field sizes off the slot (container units), so it
   gets smaller and keeps its shape instead. */
.sw-slot {
  flex: 0 1 306px; min-height: 160px; width: min(100%, 500px);
  container-type: size; display: flex; justify-content: center;
}
.sw-field {
  position: relative; height: min(100cqh, 100cqw * 380 / 620); aspect-ratio: 620 / 380;
  border: 1px solid var(--line); border-radius: 14px; overflow: hidden;
  /* A honeycomb, faint enough to be texture: it is a swarm. It is the same
     under every dot and moves with none of them, so it cannot be used to find
     one again. */
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(155, 92, 255, 0.12), transparent 70%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='48.5' viewBox='0 0 28 48.5'%3E%3Cpath d='M14 0 L28 8.08 L28 24.25 L14 32.33 L0 24.25 L0 8.08 Z M14 32.33 L14 48.5' fill='none' stroke='rgba(255,179,64,0.07)' stroke-width='1'/%3E%3C/svg%3E") 0 0 / 28px 48.5px,
    rgba(255, 255, 255, 0.03);
  transition: border-color 0.25s, box-shadow 0.25s;
}
.sw-wrap[data-phase="mark"] .sw-field { border-color: rgba(182, 255, 61, 0.45); }
.sw-wrap[data-phase="run"] .sw-field { border-color: rgba(52, 224, 255, 0.5); box-shadow: 0 0 22px rgba(52, 224, 255, 0.12); }
.sw-wrap[data-phase="pick"] .sw-field { border-color: rgba(255, 77, 141, 0.5); }
/* Every dot is the same dot. Nothing here may vary with the index - a dot that
   looked different is a dot you could find again without having tracked it. */
.sw-dot {
  position: absolute; left: 0; top: 0; border-radius: 50%;
  background: rgba(255, 255, 255, 0.72);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
  cursor: pointer;
}
/* No transition on transform, on purpose: the field is stepped at a fixed 20ms
   and a browser interpolating between two steps would be inventing a path. */
.sw-dot.live { cursor: default; }
/* Yours, lit, and pulsing so the few you have to follow jump out of the twelve.
   The ring pulses; the dot does not change size, so where it is stays exact. */
.sw-dot.mark { background: var(--lime); box-shadow: 0 0 0 3px rgba(182, 255, 61, 0.35); animation: sw-mark 0.7s ease-in-out infinite alternate; }
@keyframes sw-mark { to { box-shadow: 0 0 0 7px rgba(182, 255, 61, 0.12), 0 0 14px rgba(182, 255, 61, 0.55); } }
/* Naming them: the dot under the pointer says it is the one a click takes. */
.sw-wrap[data-phase="pick"] .sw-dot:not(.pick):hover { box-shadow: 0 0 0 3px rgba(255, 77, 141, 0.55); }
.sw-dot.pick { background: var(--cyan); box-shadow: 0 0 0 3px rgba(52, 224, 255, 0.35); }
.sw-dot.right { background: var(--lime); box-shadow: 0 0 0 3px rgba(182, 255, 61, 0.45); }
.sw-dot.wrong { background: var(--pink); box-shadow: 0 0 0 3px rgba(255, 77, 141, 0.4); }
.sw-dot.missed { background: rgba(255, 255, 255, 0.18); box-shadow: 0 0 0 2px var(--lime); }
/* The window on the two phases that have one. Nothing else here is timed. */
.sw-bar { height: 3px; border-radius: 3px; background: var(--violet); opacity: 0; width: 0; }
.sw-bar.on { opacity: 0.75; }
.sw-calls { display: flex; gap: 8px; }
.sw-call {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  min-width: 58px; padding: 5px 10px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 11px;
  background: var(--panel); color: var(--text); font: inherit;
}
.sw-call:disabled { opacity: 0.35; cursor: default; }
/* The call is the decision, so while it is open the buttons say so. */
.sw-wrap[data-phase="call"] .sw-call:not(:disabled) { border-color: rgba(255, 179, 64, 0.45); box-shadow: 0 0 12px rgba(255, 179, 64, 0.12); }
.sw-wrap[data-phase="call"] .sw-call:not(:disabled):hover { border-color: var(--amber); background: rgba(255, 179, 64, 0.1); }
.sw-call.on { border-color: var(--lime); background: rgba(182, 255, 61, 0.14); }
.sw-n { font-size: 1.05rem; font-weight: 800; line-height: 1; }
.sw-pays { font-size: 0.6rem; letter-spacing: 0.08em; color: var(--muted); }
.sw-panel { width: min(100%, 620px); }
.sw-total { font-variant-numeric: tabular-nums; }

/* --- Dig --------------------------------------------------------------- */
.dg-wrap { gap: 9px; }
.dg-head { font-size: 0.68rem; font-weight: 800; letter-spacing: 0.14em; color: var(--muted); }
/* The shaft is read as a ruler, so its height is what matters and it must not
   grow past the card. 640x300 units at 620px wide is 291px of field, which
   leaves the control row, the ticket strip and the total inside the stage. */
.dg-field { width: min(100%, 620px); aspect-ratio: 640 / 300; }
.dg-controls { display: flex; gap: 10px; justify-content: center; flex: 0 0 auto; }
/* The pick is held rather than clicked, so it must not take a text selection
   or a long-press menu with it. */
.dg-pick, .dg-haul {
  min-width: 150px; padding: 7px 16px; font-weight: 800; letter-spacing: 0.08em;
  touch-action: none; user-select: none;
}
.dg-pick { border-color: var(--cyan); color: var(--cyan); }
.dg-haul { border-color: var(--line); color: var(--muted); }
.dg-pick:disabled, .dg-haul:disabled { opacity: 0.35; cursor: default; }
.dg-panel { width: min(100%, 620px); }
.dg-total { font-variant-numeric: tabular-nums; }

/* The test room's lobby: the toy, the settings and the catalogue of switches go,
   and a search over a list takes their place. Each of those sets its own display,
   so [hidden] needs saying for each of them or the attribute does nothing. */
.toy-strip[hidden], .games-head[hidden], .game-catalog[hidden],
.test-panel[hidden], .test-search[hidden], .test-list[hidden], .test-sort[hidden] { display: none; }
.test-tools { display: flex; gap: 10px; align-items: center; }
.test-tools .test-search { flex: 1 1 auto; }
.test-sort { display: flex; gap: 6px; flex: none; }
.test-party { flex: none; }
.test-party[hidden] { display: none; }
.test-panel { display: flex; flex-direction: column; gap: 12px; margin: 4px 0 28px; }
.test-note { color: var(--muted); font-weight: 600; text-align: center; }
/* The same box as every other field here. */
.test-search {
  width: 100%; font: inherit; font-weight: 600; padding: 12px 14px;
  background: rgba(0,0,0,0.28); border: 1px solid var(--line); border-radius: var(--radius-sm);
  color: var(--text); transition: border-color 0.2s, box-shadow 0.2s;
}
.test-search:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px rgba(155,92,255,0.22); }
.test-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
.test-game {
  display: grid; grid-template-columns: auto 1fr; column-gap: 10px; align-items: center;
  padding: 12px 14px; text-align: left; font: inherit; color: inherit; cursor: pointer;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.test-game:hover, .test-game:focus-visible { border-color: var(--cyan); outline: none; }
.test-game .gi { font-size: 1.5rem; grid-row: span 2; }
.test-game .gn { font-weight: 800; }
.test-game .gb { font-size: 0.8rem; color: var(--muted); line-height: 1.35; }
.test-none { color: var(--muted); text-align: center; }
/* The experimental games, after the rest: a heading across the whole grid and
   a dashed edge, so nobody mistakes one for a game already in the lobby. */
.test-section {
  grid-column: 1 / -1; margin: 12px 0 0; font-size: 0.85rem; font-weight: 800;
  letter-spacing: 0.04em; color: var(--amber);
}
.test-game.experimental { border-style: dashed; border-color: rgba(255,179,64,0.45); }
/* The shelved games, last and folded: a heading you click, across the whole
   grid, and quieter than the experimental one because nothing here is waiting
   to be tried. */
.test-fold {
  grid-column: 1 / -1; justify-self: start; margin: 12px 0 0; padding: 4px 0;
  font: inherit; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.04em;
  color: var(--muted); background: none; border: 0; cursor: pointer;
}
.test-fold:hover, .test-fold:focus-visible { color: var(--cyan); outline: none; }
.test-game.shelved { border-style: dashed; opacity: 0.8; }

/* Ghost Race - the pack along the top of the canvas, and above it where you
   are: which room, how far, how often you fell. */
.gr-wrap { gap: 9px; }
.gr-head {
  display: flex; align-items: baseline; gap: 16px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.06em;
}
.gr-room { color: var(--cyan); font-size: 0.9rem; min-width: 9ch; text-align: right; }
.gr-dist { color: var(--amber); font-size: 1.6rem; min-width: 6ch; text-align: center; }
.gr-falls { color: var(--muted); font-size: 0.9rem; min-width: 9ch; }
/* The canvas owns its size (fitCanvas); the host only has to be somewhere to put it. */
.gr-host { position: relative; }

/* Motocross - the pack along the top of the canvas, and above it how far you
   have got, with the clean landings on one side and the crashes on the other. */
.mx-wrap { gap: 9px; }
.mx-head {
  display: flex; align-items: baseline; gap: 16px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.06em;
}
.mx-clean { color: #4dffa8; font-size: 0.9rem; min-width: 11ch; text-align: right; }
.mx-dist { color: var(--amber); font-size: 1.6rem; min-width: 6ch; text-align: center; }
.mx-falls { color: var(--muted); font-size: 0.9rem; min-width: 11ch; text-align: left; }
/* The canvas owns its size (fitCanvas); the host only has to be somewhere to put it. */
.mx-host { position: relative; }

/* flap — the course is one canvas; above it, the pipes cleared, which is the
   score, and the crashes, which are what it cost. */
.fl-wrap { gap: 9px; }
.fl-head {
  display: flex; align-items: baseline; gap: 16px;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; letter-spacing: 0.06em;
}
.fl-count { color: var(--amber); font-size: 1.8rem; min-width: 3ch; text-align: right; }
.fl-crashes { color: var(--muted); font-size: 0.9rem; min-width: 10ch; }
.canvas-host.fl-host { width: min(100%, 640px); }

/* A probable screenshot on the results: a camera by the name, the line on a
   polaroid rather than in the roast's italics, and one flash of the screen.
   It is a guess, so it is a joke in its own frame - not a verdict in red. */
.rsnap { display: inline-block; margin-left: 5px; animation: rsnap-wiggle 0.5s ease-in-out 0.9s 2; }
@keyframes rsnap-wiggle { 0%, 100% { transform: rotate(0); } 30% { transform: rotate(-14deg); } 70% { transform: rotate(10deg); } }
.rpolaroid {
  display: inline-block; margin-top: 6px; max-width: 34ch; padding: 6px 10px 12px;
  background: #f6f3ea; color: #26213a; border-radius: 3px;
  font-size: 0.78rem; font-weight: 600; line-height: 1.35;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45), inset 0 0 0 1px rgba(0, 0, 0, 0.06);
  transform: rotate(-1.5deg); transform-origin: 0 0;
  animation: rpolaroid-in 0.5s var(--ease) 0.7s both;
}
@keyframes rpolaroid-in { from { opacity: 0; transform: rotate(-8deg) translateY(-6px) scale(0.92); } to { opacity: 1; transform: rotate(-1.5deg); } }
.snap-flash {
  position: fixed; inset: 0; z-index: 9999; pointer-events: none;
  background: #fff; animation: snap-flash 0.6s ease-out forwards;
}
@keyframes snap-flash { from { opacity: 0.85; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .rsnap, .rpolaroid { animation: none; }
  .snap-flash { display: none; }
}

/* The playground's screenshot check: what the match's chord watcher would say
   about every ⌘⇧ you press, live. */
.dev-snap {
  align-self: stretch; padding: 12px 14px; border-radius: var(--radius);
  background: var(--panel); border: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 8px;
}
.dev-snap-now { font-weight: 700; font-size: 0.86rem; color: var(--dim); }
.dev-snap-now[data-state="showing"] { color: var(--lime); }
.dev-snap-now[data-state="hidden"] { color: var(--muted); }
.dev-snap-log { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.dev-snap-log li {
  display: flex; flex-direction: column; gap: 1px; padding: 6px 9px; border-radius: 8px;
  font-size: 0.8rem; border: 1px solid var(--line); color: var(--muted);
}
.dev-snap-log li.yes { color: var(--lime); border-color: rgba(182, 255, 61, 0.45); background: rgba(182, 255, 61, 0.07); }
.dev-snap-log li.again { color: var(--amber); border-color: rgba(255, 179, 64, 0.4); background: rgba(255, 179, 64, 0.07); }
.dev-snap-keys { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 0.75rem; opacity: 0.85; }

/* blocks - an 8x8 board and three pieces on offer, all DOM. A cell's colour is
   --c, set per cell from the piece that filled it, so one rule draws them all. */
.bk-wrap { gap: 8px; }
.bk-head { display: flex; align-items: baseline; gap: 14px; font-family: 'JetBrains Mono', monospace; }
.bk-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.bk-lines { color: var(--muted); font-size: 0.85rem; font-weight: 700; letter-spacing: 0.06em; }
.bk-combo { color: var(--amber); font-weight: 900; font-size: 1rem; min-width: 9ch; text-align: left; }
.bk-combo.bk-hot { animation: bkPulse 0.6s ease-in-out infinite; }
@keyframes bkPulse { 50% { transform: scale(1.12); } }
.bk-host { min-height: 240px; }
.bk-field { position: relative; user-select: none; touch-action: none; }
.bk-board {
  position: absolute; top: 0; border-radius: 14px;
  background: rgba(10, 6, 24, 0.72); border: 1px solid var(--line);
  box-shadow: inset 0 0 24px rgba(155, 92, 255, 0.12);
}
.bk-cell {
  position: absolute; margin: 2px; border-radius: 6px;
  background: rgba(255, 255, 255, 0.045);
  transition: background 0.08s, box-shadow 0.12s;
}
.bk-cell.bk-on, .bk-block {
  background: var(--c);
  box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.35), inset 0 -4px 0 rgba(0, 0, 0, 0.25);
}
.bk-cell.bk-ghost { background: var(--c); opacity: 0.5; }
.bk-cell.bk-glow { box-shadow: 0 0 0 2px #fff, 0 0 14px rgba(255, 255, 255, 0.55); }
.bk-cell.bk-pop { animation: bkPop 0.28s ease-out forwards; }
@keyframes bkPop {
  0% { transform: scale(1); filter: brightness(1.8); }
  100% { transform: scale(0.2); opacity: 0; filter: brightness(2.4); }
}
.bk-board.bk-jammed .bk-cell.bk-on { filter: grayscale(0.8) brightness(0.7); }
.bk-banner {
  position: absolute; left: 0; right: 0; top: 44%; text-align: center; pointer-events: none;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.5rem; color: var(--red);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.8);
}
.bk-slot { position: absolute; display: flex; align-items: center; justify-content: center; cursor: grab; }
.bk-slot.bk-dead { opacity: 0.28; filter: grayscale(0.9); }
.bk-slot.bk-lifted .bk-piece { opacity: 0; }
.bk-piece { position: relative; pointer-events: none; }
.bk-block { position: absolute; border-radius: 5px; }
.bk-drag { position: absolute; display: none; pointer-events: none; z-index: 5; filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.5)); }
.bk-float {
  position: absolute; transform: translateX(-50%); pointer-events: none; z-index: 6;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.6rem; color: #fff;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); animation: bkFloat 0.9s ease-out forwards;
}
@keyframes bkFloat { from { opacity: 1; margin-top: 0; } to { opacity: 0; margin-top: -46px; } }

/* jigsaw - a board of tiles cut from one painted picture, and the picture
   itself beside it. A tile is a canvas in a div moved by transform, so a swap
   is two transitions. */
.jg-wrap { gap: 8px; }
.jg-host { min-height: 240px; }
.jg-field { position: relative; user-select: none; touch-action: none; }
.jg-board {
  position: absolute; top: 0; left: 0; border-radius: 12px;
  background: rgba(10, 6, 24, 0.72); box-shadow: 0 0 0 1px var(--line);
}
.jg-tile {
  position: absolute; top: 0; left: 0; cursor: grab; border-radius: 6px; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform 0.14s var(--ease), box-shadow 0.2s;
}
.jg-tile canvas { display: block; width: 100%; height: 100%; }
.jg-tile.jg-home { cursor: default; border-radius: 0; box-shadow: none; }
.jg-tile.jg-picked { z-index: 3; box-shadow: 0 0 0 3px var(--cyan), 0 6px 18px rgba(0, 0, 0, 0.5); }
.jg-tile.jg-held { z-index: 4; transition: none; cursor: grabbing; box-shadow: 0 0 0 3px var(--amber), 0 12px 24px rgba(0, 0, 0, 0.55); }
.jg-board.jg-whole { box-shadow: 0 0 0 3px var(--lime), 0 0 40px rgba(182, 255, 61, 0.45); }
.jg-float {
  position: absolute; left: 50%; top: 44%; transform: translateX(-50%); pointer-events: none; z-index: 6;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2.4rem; color: #fff;
  text-shadow: 0 3px 14px rgba(0, 0, 0, 0.8); animation: bkFloat 1.2s ease-out forwards;
}
.jg-side { position: absolute; top: 0; display: flex; flex-direction: column; align-items: center; gap: 12px; }
.jg-which { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.jg-thumb { border-radius: 10px; overflow: hidden; box-shadow: 0 0 0 2px var(--line), 0 8px 22px rgba(0, 0, 0, 0.45); }
.jg-thumb-img { display: block; width: 100%; height: 100%; }
.jg-points { font-size: clamp(1.6rem, 4vw, 2.4rem); font-weight: 900; }
.jg-swaps { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; white-space: nowrap; font-weight: 700; color: var(--muted); letter-spacing: 0.04em; }

/* maze - one canvas; above it, which maze and the points. */
.mz-wrap { gap: 8px; }
.mz-head { display: flex; align-items: baseline; gap: 16px; }
.mz-which { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.mz-points { font-size: clamp(1.4rem, 4vw, 2.1rem); font-weight: 900; min-width: 3ch; }
.canvas-host.mz-host { width: min(100%, 900px); }

/* spot the difference - two panels of absolutely placed emoji. A colour change
   is a filter and a turned-round one a transform, so the two sides are the
   same markup with a class or two different. */
.sd-wrap { gap: 8px; }
.sd-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; justify-content: center; }
.sd-which, .sd-tally { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.sd-tally { color: var(--lime); }
.sd-points { font-size: clamp(1.4rem, 4vw, 2.1rem); font-weight: 900; min-width: 3ch; }
.sd-points.sd-neg { color: var(--red); }
.sd-skip { flex: none; }
.sd-skip:disabled { opacity: 0.35; cursor: default; }
.sd-host { min-height: 220px; }
.sd-field { position: relative; user-select: none; touch-action: none; cursor: crosshair; }
.sd-panel {
  position: absolute; top: 0; overflow: hidden; border-radius: 14px;
  background:
    radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.14), transparent 45%),
    linear-gradient(160deg, var(--ga), var(--gb));
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12), 0 10px 26px rgba(0, 0, 0, 0.4);
}
.sd-item {
  position: absolute; transform: translate(-50%, -50%); line-height: 1; pointer-events: none;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.35));
}
.sd-item.sd-flip { transform: translate(-50%, -50%) scaleX(-1); }
.sd-ring {
  position: absolute; transform: translate(-50%, -50%); border-radius: 50%; pointer-events: none;
  border: 4px solid var(--lime); box-shadow: 0 0 14px rgba(182, 255, 61, 0.7);
  animation: sdRing 0.3s ease-out;
}
.sd-ring.sd-missed { border-color: var(--red); box-shadow: 0 0 14px rgba(255, 77, 77, 0.7); border-style: dashed; }
@keyframes sdRing { from { transform: translate(-50%, -50%) scale(1.6); opacity: 0; } }
.sd-miss {
  position: absolute; transform: translate(-50%, -50%); pointer-events: none;
  font-weight: 900; font-size: 1.6rem; color: var(--red); text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
  animation: sdMiss 0.65s ease-out forwards;
}
@keyframes sdMiss { to { opacity: 0; transform: translate(-50%, -90%); } }
.sd-float {
  position: absolute; left: 50%; top: 42%; transform: translateX(-50%); pointer-events: none; z-index: 6;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2.2rem; color: #fff;
  text-shadow: 0 3px 14px rgba(0, 0, 0, 0.8); animation: bkFloat 1.1s ease-out forwards;
}

/* rock paper scissors - three screens. THE BILL is the stage's matchups
   dropping in, yours last. THE STAIRCASE is a step per number of wins, each
   indented a little more so they climb to the trophy; pairs still playing
   stand on theirs with little hands. THE MATCH is two plates and two fists.
   Every hand, big or little, is two layers - the outer one bounces (idle, the
   three shakes, the strike), the inner glyph is turned to face the other side
   - so the two transforms never fight. */
.rp-wrap { gap: 8px; justify-content: flex-start; overflow-y: auto; overflow-x: hidden; }
.rp-head { font-family: 'JetBrains Mono', monospace; font-weight: 900; letter-spacing: 0.08em; color: var(--amber); font-size: 0.95rem; text-align: center; }
.rp-sub { color: var(--muted); font-weight: 700; font-size: 0.9rem; min-height: 1.2em; text-align: center; }
.rp-screen { width: min(100%, 860px); display: flex; flex-direction: column; align-items: center; gap: 12px; flex: none; }
.rp-screen[hidden], .rp-final[hidden] { display: none; }
.rp-screen.rp-enter { animation: rpEnter 0.38s var(--ease); }
@keyframes rpEnter { from { opacity: 0; transform: scale(0.94); } }

/* the bill: the stage's matchups dropping in, yours last and biggest */
.rp-poster-screen, .rp-stairs-screen, .rp-match-screen { gap: 14px; }
/* The bill and the staircase take the height the head leaves and sit in the
   middle of it; the game sizes the steps to fill it (up to a point). */
.rp-poster-screen, .rp-stairs-screen { flex: 1 0 auto; justify-content: center; }
.rp-bill { width: 100%; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 8px; }
.rp-bout {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 10px; padding: 8px 14px;
  border-radius: 12px; background: var(--panel); border: 1px solid var(--line); animation: rpDrop 0.35s var(--ease) both;
}
@keyframes rpDrop { from { opacity: 0; transform: translateY(-18px) scale(1.06); } }
.rp-bout-side { display: flex; align-items: center; gap: 8px; min-width: 0; }
.rp-bout-right { flex-direction: row-reverse; text-align: right; }
.rp-bout-side canvas { display: block; flex: none; }
.rp-bout-side .rp-bot { font-size: 1.7rem; }
.rp-bout-name { font-weight: 800; font-size: 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-bout-vs { display: flex; flex-direction: column; align-items: center; line-height: 1; }
.rp-bout-tag { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; font-weight: 800; color: var(--muted); letter-spacing: 0.06em; }
.rp-bout-x { font-family: 'JetBrains Mono', monospace; font-weight: 900; color: var(--amber); font-size: 0.9rem; }
.rp-bout-final .rp-bout-tag { color: var(--amber); }
.rp-bout-mine {
  grid-column: 1 / -1; padding: 14px 20px; border-color: var(--cyan); box-shadow: 0 0 22px rgba(52, 224, 255, 0.3);
  animation: rpSlam 0.55s var(--ease) both;
}
.rp-bout-mine .rp-bout-name { font-size: 1.25rem; font-weight: 900; }
.rp-bout-mine .rp-bout-side .rp-bot { font-size: 3rem; }
.rp-bout-mine .rp-bout-x { font-size: 1.8rem; }
.rp-bout-mine .rp-bout-tag { font-size: 0.8rem; }
@keyframes rpSlam {
  from { opacity: 0; transform: scale(1.7); }
  60% { opacity: 1; transform: scale(0.97); }
  80% { transform: scale(1.02) rotate(-0.6deg); }
}

/* the staircase: a step for every number of wins, climbing right to the trophy */
.rp-stairs { width: 100%; display: flex; flex-direction: column; }
.rp-step {
  display: flex; align-items: flex-end; gap: 10px; min-height: var(--step-h, 64px); padding: 6px 8px 5px;
  border-bottom: 3px solid rgba(201, 182, 255, 0.28); border-left: 3px solid rgba(201, 182, 255, 0.28);
}
.rp-step-top { border-color: rgba(255, 210, 63, 0.55); }
.rp-step-label {
  flex: none; width: 80px; padding-bottom: 4px; font-family: 'JetBrains Mono', monospace; font-size: 0.7rem;
  font-weight: 900; letter-spacing: 0.08em; color: var(--muted); text-align: left;
}
.rp-step-top .rp-step-label { color: var(--amber); }
.rp-step-lane { flex: 1; display: flex; flex-wrap: wrap; align-items: flex-end; gap: 6px 8px; min-width: 0; }
.rp-tok, .rp-duel-end { display: flex; flex-direction: column; align-items: center; gap: 1px; width: 48px; }
.rp-tok canvas, .rp-duel-end canvas { display: block; }
.rp-tok .rp-bot, .rp-duel-end .rp-bot { font-size: 1.7rem; line-height: 30px; }
.rp-tok-name { max-width: 100%; font-size: 0.72rem; font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-tok-gd { font-family: 'JetBrains Mono', monospace; font-size: 0.64rem; font-weight: 800; color: var(--muted); }
.rp-tok-me .rp-tok-name, .rp-end-me .rp-tok-name { color: var(--cyan); }
.rp-tok-me canvas, .rp-end-me canvas { border-radius: 50%; box-shadow: 0 0 0 2px var(--cyan), 0 0 12px rgba(52, 224, 255, 0.6); }
.rp-tok-bot { opacity: 0.7; }
.rp-duel {
  display: flex; align-items: center; padding: 4px 5px; border-radius: 12px;
  background: rgba(10, 6, 24, 0.7); border: 1px solid var(--line);
}
.rp-duel-mine { border-color: var(--cyan); }
.rp-duel-end.rp-end-out { opacity: 0.45; }
.rp-duel-hand { display: grid; place-items: center; width: 30px; line-height: 1; }
.rp-duel .rp-glyph { font-size: 1.75rem; }
.rp-dh0 .rp-glyph { transform: scaleX(-1) rotate(-90deg); }
.rp-dh1 .rp-glyph { transform: rotate(-90deg); }
.rp-duel-hand.rp-idle { animation: rpIdleSmall 1.6s ease-in-out infinite; }
.rp-duel-hand.rp-shaking { animation: rpShakeSmall 0.9s ease-in-out; }
.rp-dh0.rp-strike { animation: rpStrike0 0.45s var(--ease); filter: drop-shadow(0 0 8px rgba(182, 255, 61, 0.9)); }
.rp-dh1.rp-strike { animation: rpStrike1 0.45s var(--ease); filter: drop-shadow(0 0 8px rgba(182, 255, 61, 0.9)); }
.rp-duel-hand.rp-struck { animation: rpStruck 0.5s ease-out; opacity: 0.4; }
@keyframes rpIdleSmall { 50% { transform: translateY(2px); } }
@keyframes rpShakeSmall {
  0%, 33%, 66%, 100% { transform: translateY(0); }
  16%, 50%, 83% { transform: translateY(-9px); }
}
@keyframes rpStrike0 { 40% { transform: translateX(9px) scale(1.3); } }
@keyframes rpStrike1 { 40% { transform: translateX(-9px) scale(1.3); } }
@keyframes rpStruck { 35% { transform: rotate(-16deg) translateY(2px); } 65% { transform: rotate(9deg); } }
.rp-duel-score { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; font-weight: 900; color: var(--cyan); min-width: 2.6ch; text-align: center; }

/* the match */
.rp-card {
  width: min(100%, 680px); display: grid; grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "p0 . p1" "h0 mid h1"; align-items: center; gap: 8px 14px; padding: 16px 18px;
  border-radius: 20px; border: 1px solid var(--line);
  background: radial-gradient(ellipse at 50% 60%, rgba(155, 92, 255, 0.16), transparent 65%), var(--panel);
}
.rp-plate { display: flex; align-items: center; gap: 10px; transition: opacity 0.25s; }
.rp-p0 { grid-area: p0; justify-self: start; }
.rp-p1 { grid-area: p1; justify-self: end; flex-direction: row-reverse; text-align: right; }
.rp-p1 .rp-plate-text { align-items: flex-end; }
.rp-p0 .rp-plate-text { align-items: flex-start; text-align: left; }
.rp-plate-text { display: flex; flex-direction: column; gap: 3px; }
.rp-face { width: 56px; height: 56px; display: grid; place-items: center; flex: none; }
.rp-bot { font-size: 2.6rem; line-height: 1; }
.rp-name { font-weight: 900; max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-dots { display: flex; gap: 5px; }
.rp-dots i { width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--line); }
.rp-dots i.on { background: var(--lime); border-color: var(--lime); box-shadow: 0 0 8px rgba(182, 255, 61, 0.6); }
.rp-note { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; font-weight: 800; color: var(--muted); min-height: 1em; letter-spacing: 0.04em; }
.rp-plate.rp-lost { opacity: 0.45; }
.rp-plate.rp-won .rp-name { color: var(--lime); }
.rp-hand { display: grid; place-items: center; line-height: 1; filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.45)); }
.rp-h0 { grid-area: h0; justify-self: end; }
.rp-h1 { grid-area: h1; justify-self: start; }
.rp-glyph { display: block; font-size: clamp(4.6rem, 12vw, 7rem); }
/* Turned to face each other, thumb on top: checked on the raised fist, the
   same turn works for the open hand and the two fingers. */
.rp-h0 .rp-glyph { transform: scaleX(-1) rotate(-90deg); }
.rp-h1 .rp-glyph { transform: rotate(-90deg); }
.rp-hand.rp-idle { animation: rpIdle 1.6s ease-in-out infinite; }
.rp-hand.rp-shaking { animation: rpShake 0.9s ease-in-out; }
.rp-hand.rp-open { animation: rpPop 0.28s var(--ease); }
.rp-hand.rp-win { filter: drop-shadow(0 0 22px rgba(182, 255, 61, 0.9)); }
.rp-hand.rp-lose { opacity: 0.45; filter: grayscale(0.6); }
@keyframes rpIdle { 50% { transform: translateY(5px); } }
@keyframes rpShake {
  0%, 33%, 66%, 100% { transform: translateY(0); }
  16%, 50%, 83% { transform: translateY(-26px); }
}
@keyframes rpPop { from { transform: scale(0.55); } }
.rp-mid { grid-area: mid; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 150px; }
.rp-count { font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2.6rem; color: var(--cyan); min-height: 1.2em; line-height: 1.2; }
.rp-status { font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 0.9rem; text-align: center; min-height: 1.2em; }
.rp-bar { width: 150px; height: 6px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); overflow: hidden; }
.rp-bar i { display: block; height: 100%; width: 0; background: var(--cyan); }
.rp-bar.rp-late i { background: var(--red); }
.rp-picks { display: flex; gap: 12px; }
.rp-pick {
  display: grid; justify-items: center; gap: 2px; padding: 10px 18px; min-width: 104px; cursor: pointer;
  font: inherit; color: inherit; background: var(--panel); border: 2px solid var(--line); border-radius: 14px;
  transition: transform 0.08s, border-color 0.15s, opacity 0.15s;
}
.rp-picks.rp-armed .rp-pick { border-color: var(--violet); }
.rp-pick:hover:not(:disabled) { transform: translateY(-3px); border-color: var(--lime); }
.rp-pick:disabled { opacity: 0.35; cursor: default; }
.rp-pick-hand { font-size: 2rem; }
.rp-pick-name { font-weight: 800; font-size: 0.85rem; }
.rp-pick-key { font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--muted); }

/* the final standings */
.rp-final { display: grid; gap: 4px; width: min(100%, 440px); }
.rp-final.rp-final-two { width: 100%; grid-auto-flow: column; grid-template-columns: 1fr 1fr; column-gap: 12px; }
.rp-row { display: grid; grid-template-columns: 3ch 28px 1fr auto; align-items: center; gap: 10px; padding: 3px 10px; border-radius: 10px; background: var(--panel); border: 1px solid var(--line); }
.rp-row.rp-mine { border-color: var(--cyan); }
.rp-row-place { font-size: 1.1rem; text-align: center; font-weight: 900; }
.rp-row-name { display: flex; flex-direction: column; min-width: 0; font-weight: 800; text-align: left; line-height: 1.15; }
.rp-row-record { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--muted); white-space: nowrap; }
.rp-row-note { font-size: 0.66rem; font-weight: 800; color: var(--amber); }
.rp-row-note:empty { display: none; }

/* gems - an 8x8 board of DOM gems. A gem keeps its element while it moves, so
   a swap and a fall are transitions on its transform; each kind is a colour and
   a cut, so it reads without the colour too. */
.gm-wrap { gap: 8px; }
.gm-head { display: flex; align-items: baseline; gap: 14px; font-family: 'JetBrains Mono', monospace; }
.gm-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.gm-chain { color: var(--amber); font-weight: 900; font-size: 1rem; min-width: 11ch; text-align: left; }
.gm-chain.gm-hot { animation: bkPulse 0.4s ease-in-out infinite; }
.gm-host { min-height: 240px; }
.gm-field { position: relative; user-select: none; touch-action: none; }
.gm-board {
  position: absolute; top: 0; left: 0; overflow: hidden; border-radius: 14px; cursor: pointer;
  background: rgba(10, 6, 24, 0.72); border: 1px solid var(--line);
  box-shadow: inset 0 0 24px rgba(155, 92, 255, 0.12);
}
.gm-cell { position: absolute; background: rgba(255, 255, 255, 0.035); }
.gm-cell.gm-dark { background: rgba(255, 255, 255, 0.07); }
.gm-layer { position: absolute; inset: 0; pointer-events: none; }
.gm-gem {
  position: absolute; top: 0; left: 0; border-radius: 10px;
  transition-property: transform; transition-timing-function: cubic-bezier(0.35, 0, 0.6, 1);
}
.gm-face {
  position: absolute; inset: 3px;
  background:
    radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.85) 0 9%, transparent 10%),
    linear-gradient(150deg, rgba(255, 255, 255, 0.45), transparent 45%, rgba(0, 0, 0, 0.35)),
    var(--c);
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.45));
}
.gm-k0 { --c: #ff4d6d; clip-path: polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%); }
.gm-k1 { --c: #ff9f2b; clip-path: polygon(25% 6%, 75% 6%, 100% 50%, 75% 94%, 25% 94%, 0 50%); }
.gm-k2 { --c: #ffe14d; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.gm-k3 { --c: #3ddc84; clip-path: polygon(50% 4%, 100% 94%, 0 94%); }
.gm-k4 { --c: #4dabff; border-radius: 50%; }
.gm-k5 { --c: #b36bff; border-radius: 22%; transform: scale(0.86) rotate(45deg); }
.gm-gem.gm-sel { box-shadow: 0 0 0 3px #fff, 0 0 16px rgba(255, 255, 255, 0.6); }
.gm-gem.gm-hint { animation: gmHint 0.7s ease-in-out infinite; }
@keyframes gmHint { 50% { box-shadow: 0 0 0 3px var(--lime), 0 0 18px var(--lime); } }
.gm-gem.gm-pop .gm-face { animation: gmPop 0.16s ease-out forwards; }
@keyframes gmPop {
  0% { transform: scale(1); filter: brightness(1.8); }
  100% { transform: scale(1.5); opacity: 0; filter: brightness(2.6); }
}
.gm-gem.gm-new { animation: gmIn 0.35s ease-out; }
@keyframes gmIn { from { opacity: 0; } }
.gm-board.gm-shuffling .gm-gem { filter: grayscale(0.8) brightness(0.6); }
.gm-banner {
  position: absolute; left: 0; right: 0; top: 44%; text-align: center; pointer-events: none;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.4rem; color: var(--amber);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.8);
}
.gm-float {
  position: absolute; transform: translateX(-50%); pointer-events: none; z-index: 6;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.4rem; color: #fff;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7); animation: bkFloat 0.9s ease-out forwards;
}

/* 2048 - a 4x4 board of DOM tiles. A tile keeps its element while it slides,
   so a move is a transition on its transform; the colour climbs with the
   number, and the number is always written on it. */
.tw-wrap { gap: 8px; }
.tw-head { display: flex; align-items: baseline; gap: 16px; font-family: 'JetBrains Mono', monospace; }
.tw-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.tw-best { color: var(--amber); font-weight: 900; font-size: 1rem; min-width: 9ch; }
.tw-left { color: var(--muted); font-weight: 800; font-size: 0.95rem; min-width: 13ch; }
.tw-left.tw-low { color: var(--pink); }
.tw-host { min-height: 240px; }
.tw-field { position: relative; user-select: none; touch-action: none; }
.tw-board {
  position: absolute; inset: 0; overflow: hidden; border-radius: 16px;
  background: rgba(10, 6, 24, 0.72); border: 1px solid var(--line);
  box-shadow: inset 0 0 24px rgba(155, 92, 255, 0.12);
}
.tw-cell { position: absolute; border-radius: 10px; background: rgba(255, 255, 255, 0.05); }
.tw-layer { position: absolute; inset: 0; pointer-events: none; }
.tw-tile {
  position: absolute; top: 0; left: 0; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2.6rem;
  color: #1a1030; background: var(--c, #fff); box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28);
  transition-property: transform; transition-timing-function: cubic-bezier(0.3, 0, 0.5, 1);
}
.tw-tile.tw-long { font-size: 1.9rem; }
.tw-v2 { --c: #e8e2ff; }
.tw-v4 { --c: #cfc3ff; }
.tw-v8 { --c: #ffb340; }
.tw-v16 { --c: #ff8c3a; }
.tw-v32 { --c: #ff6a4d; color: #fff; }
.tw-v64 { --c: #ff4d6d; color: #fff; }
.tw-v128 { --c: #ffe14d; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 0 16px rgba(255, 225, 77, 0.45); }
.tw-v256 { --c: #b6ff3d; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 0 20px rgba(182, 255, 61, 0.5); }
.tw-v512 { --c: #34e0ff; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 0 24px rgba(52, 224, 255, 0.55); }
.tw-v1024 { --c: #9b5cff; color: #fff; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 0 28px rgba(155, 92, 255, 0.6); }
.tw-v2048 { --c: #ff4d8d; color: #fff; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.28), 0 0 34px rgba(255, 77, 141, 0.7); }
.tw-v4096 { --c: #f2f0ff; box-shadow: 0 0 34px rgba(255, 255, 255, 0.7); }
.tw-tile.tw-new { animation: twIn 0.16s ease-out; }
@keyframes twIn { from { opacity: 0; scale: 0.3; } }
.tw-tile.tw-merge { animation: twMerge 0.16s ease-out; }
@keyframes twMerge { 50% { scale: 1.16; } }
.tw-tile.tw-gone { animation: twGone 0.5s ease-in forwards; }
@keyframes twGone { to { opacity: 0; scale: 0.4; filter: grayscale(1); } }
.tw-banner {
  position: absolute; left: 0; right: 0; top: 44%; text-align: center; pointer-events: none; z-index: 5;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.4rem; color: var(--amber);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
}
.tw-float {
  position: absolute; transform: translateX(-50%); pointer-events: none; z-index: 6;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.3rem; color: #fff;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8); animation: bkFloat 0.8s ease-out forwards;
}
.tw-tools { display: flex; align-items: center; gap: 10px; }
.tw-tools.tw-off { visibility: hidden; }
.tw-undo {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.tw-undo:disabled { opacity: 0.28; cursor: default; }
.tw-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.tw-back.spent { opacity: 0.22; filter: grayscale(1); }

/* ---------- Ball Sort ---------- */
.bs-wrap { gap: 8px; }
.bs-head { display: flex; align-items: baseline; gap: 16px; font-family: 'JetBrains Mono', monospace; }
.bs-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.bs-step { color: var(--amber); font-weight: 900; font-size: 1rem; min-width: 18ch; }
.bs-host { min-height: 220px; }
.bs-field { position: relative; user-select: none; touch-action: none; cursor: pointer; }
.bs-board {
  position: absolute; inset: 0; overflow: hidden; border-radius: 16px;
  background: rgba(10, 6, 24, 0.72); border: 1px solid var(--line);
  box-shadow: inset 0 0 24px rgba(52, 224, 255, 0.1);
}
.bs-tubes, .bs-layer { position: absolute; inset: 0; pointer-events: none; }
.bs-tube {
  position: absolute; border: 3px solid rgba(255, 255, 255, 0.34); border-top: 0;
  border-radius: 0 0 30px 30px; background: rgba(255, 255, 255, 0.05);
  transition: border-color 0.2s, box-shadow 0.2s;
}
.bs-tube.bs-sel { border-color: rgba(255, 255, 255, 0.75); }
.bs-tube.bs-shut { border-color: var(--amber); box-shadow: 0 0 18px rgba(255, 225, 77, 0.35); }
.bs-tube.bs-shut::before {
  content: ''; position: absolute; left: -7px; right: -7px; top: -9px; height: 10px;
  border-radius: 5px; background: var(--amber);
}
.bs-tube.bs-no { animation: bsNo 0.3s ease-in-out; }
@keyframes bsNo { 25% { translate: -5px 0; } 75% { translate: 5px 0; } }
.bs-ball {
  position: absolute; top: 0; left: 0; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, rgba(255, 255, 255, 0.75), var(--c, #fff) 38%, var(--c, #fff) 62%, rgba(0, 0, 0, 0.35));
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  transition-property: transform; transition-timing-function: cubic-bezier(0.3, 0, 0.5, 1);
}
.bs-ball.bs-up { z-index: 2; box-shadow: 0 10px 14px rgba(0, 0, 0, 0.45); }
.bs-c0 { --c: #ff4d6d; }
.bs-c1 { --c: #ff9f2b; }
.bs-c2 { --c: #ffe14d; }
.bs-c3 { --c: #3ddc84; }
.bs-c4 { --c: #34e0ff; }
.bs-c5 { --c: #4d6bff; }
.bs-c6 { --c: #b36bff; }
.bs-c7 { --c: #f2f0ff; }
.bs-banner {
  position: absolute; left: 0; right: 0; top: 42%; text-align: center; pointer-events: none; z-index: 5;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.4rem; color: var(--amber);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
}
.bs-tools { display: flex; align-items: center; gap: 10px; }
.bs-btn {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.bs-btn:disabled { opacity: 0.28; cursor: default; }
.bs-restart.bs-hot { border-color: var(--amber); color: var(--amber); animation: bkPulse 0.6s ease-in-out infinite; }
.bs-undo { color: var(--cyan); }
.bs-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.bs-back.spent { opacity: 0.22; filter: grayscale(1); }

/* wanted! - the poster and the count in the head, the crowd under them,
   scaled by fitBox. A face is an SVG moved by transform every frame, so
   anything that scales the caught one goes on the svg inside it. A wrong face
   blurs the whole crowd grey while it is frozen. */
.wt-wrap { gap: 8px; }
.wt-head { display: flex; align-items: center; gap: 22px; }
.wt-tally { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; font-family: 'JetBrains Mono', monospace; }
.wt-which { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.wt-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.wt-host { min-height: 220px; }
.wt-poster {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 6px 10px 8px; border-radius: 5px; color: #3b2412;
  background: linear-gradient(170deg, #f3dfb3, #d9b77c);
  box-shadow: 0 0 0 2px #8a5a2b, 0 8px 18px rgba(0, 0, 0, 0.45);
  transform: rotate(-2deg);
}
.wt-title { font-family: Georgia, 'Times New Roman', serif; font-weight: 900; font-size: 1.15rem; letter-spacing: 0.06em; line-height: 1; }
.wt-mug {
  width: 84px; height: 84px; border-radius: 4px; background: rgba(255, 255, 255, 0.35);
  box-shadow: inset 0 0 0 2px rgba(59, 36, 18, 0.5);
}
.wt-mug svg { width: 100%; height: 100%; display: block; }
.wt-caption { font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 0.6rem; letter-spacing: 0.04em; text-align: center; }
.wt-stamp {
  position: absolute; left: 50%; top: 50%; padding: 1px 7px; border: 3px solid #c8202a; border-radius: 5px;
  color: #c8202a; background: rgba(243, 223, 179, 0.6); font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 0.95rem;
  transform: translate(-50%, -50%) rotate(-14deg) scale(1.8); opacity: 0; pointer-events: none;
  transition: transform 0.18s ease-out, opacity 0.18s ease-out;
}
.wt-poster.wt-got .wt-stamp { opacity: 0.95; transform: translate(-50%, -50%) rotate(-14deg) scale(1); }
.wt-crowd {
  overflow: hidden; border-radius: 14px; cursor: crosshair; user-select: none; touch-action: none;
  background:
    radial-gradient(circle at 25% 15%, rgba(255, 255, 255, 0.1), transparent 45%),
    linear-gradient(160deg, #3d2c52, #1f1a33);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12), 0 10px 26px rgba(0, 0, 0, 0.4);
}
.wt-faces, .wt-over { position: absolute; inset: 0; pointer-events: none; }
.wt-face { position: absolute; left: 0; top: 0; width: 48px; height: 48px; will-change: transform; }
.wt-face svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.4)); transition: transform 0.2s ease-out; }
.wt-crowd.wt-frozen .wt-faces { filter: grayscale(1) blur(3px) brightness(0.5); }
.wt-crowd.wt-caught .wt-face { opacity: 0.18; transition: opacity 0.2s; }
.wt-crowd.wt-caught .wt-face.wt-got { opacity: 1; }
.wt-face.wt-got svg { transform: scale(1.7); }
.wt-face.wt-got::after {
  content: ''; position: absolute; inset: -18px; border-radius: 50%;
  border: 4px solid var(--lime); box-shadow: 0 0 16px rgba(182, 255, 61, 0.7);
}
.wt-banner {
  position: absolute; left: 0; right: 0; top: 44%; text-align: center; z-index: 5;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.5rem; color: var(--red);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
}
.wt-miss {
  position: absolute; transform: translate(-50%, -50%); z-index: 6;
  font-weight: 900; font-size: 1.8rem; color: var(--red); text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
  animation: sdMiss 1.2s ease-out forwards;
}
.wt-float {
  position: absolute; transform: translate(-50%, -100%); z-index: 6;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.8rem; color: var(--lime);
  text-shadow: 0 3px 14px rgba(0, 0, 0, 0.8); animation: wtFloat 0.8s ease-out forwards;
}
@keyframes wtFloat { to { opacity: 0; transform: translate(-50%, -190%); } }

/* ---------------- Whack-a-Mole ---------------- */
.wm-wrap { gap: 8px; }
.wm-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; }
.wm-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.wm-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.wm-host { min-height: 220px; }
.wm-field {
  overflow: hidden; border-radius: 14px; cursor: pointer; user-select: none; touch-action: none;
  background:
    radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.12), transparent 45%),
    linear-gradient(170deg, #5aa844, #2f7a30);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12), 0 10px 26px rgba(0, 0, 0, 0.4);
}
.wm-hole { position: absolute; width: 180px; height: 160px; transition: filter 0.15s; }
.wm-field.wm-stunned .wm-hole { filter: grayscale(1) blur(3px) brightness(0.55); }
.wm-pit {
  position: absolute; left: 26px; top: 100px; width: 128px; height: 40px; border-radius: 50%;
  background: radial-gradient(ellipse at 50% 35%, #0e0906, #2b1a0f 70%, #4a3020);
}
.wm-slot { position: absolute; left: 36px; top: 0; width: 108px; height: 121px; overflow: hidden; }
.wm-pop {
  position: absolute; left: 4px; bottom: 0; width: 100px; height: 100px;
  transform: translateY(108%); transition: transform 0.11s ease-in;
}
.wm-pop svg { width: 100%; height: 100%; display: block; }
.wm-hole.wm-up .wm-pop { transform: translateY(0); transition: transform 0.08s ease-out; }
.wm-hole.wm-bonked .wm-pop { transition: transform 0.2s ease-in 0.06s; }
.wm-pop .wm-dizzy { display: none; }
.wm-hole.wm-bonked .wm-dizzy { display: inline; }
.wm-hole.wm-bonked .wm-eyes { display: none; }
.wm-spark { animation: wmSpark 0.16s steps(2) infinite; transform-origin: 79px 9px; }
@keyframes wmSpark { 50% { transform: scale(0.6); opacity: 0.7; } }
.wm-lip {
  position: absolute; left: 20px; top: 119px; width: 140px; height: 24px;
  border-radius: 0 0 70px 70px / 0 0 24px 24px;
  background: linear-gradient(180deg, #7a5233, #56381f);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.25);
}
.wm-key {
  position: absolute; right: 8px; top: 126px; width: 26px; height: 26px; border-radius: 6px;
  display: grid; place-items: center; font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 0.95rem;
  color: #fff; background: rgba(0, 0, 0, 0.42); box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.3);
}
.wm-mallet {
  position: absolute; left: 118px; top: 22px; width: 64px; height: 80px; z-index: 3;
  transform-origin: 50% 96%; opacity: 0; pointer-events: none;
}
.wm-mallet svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.4)); }
.wm-mallet.wm-swing { animation: wmSwing 0.26s ease-out forwards; }
.wm-hole.wm-stuck .wm-mallet { animation: wmStuck 0.35s ease-out forwards; }
@keyframes wmSwing {
  0% { opacity: 1; transform: rotate(25deg); }
  40% { opacity: 1; transform: rotate(-58deg); }
  70% { opacity: 1; transform: rotate(-50deg); }
  100% { opacity: 0; transform: rotate(-30deg); }
}
@keyframes wmStuck {
  0% { opacity: 1; transform: rotate(25deg); }
  30% { opacity: 1; transform: rotate(-82deg); }
  50% { transform: rotate(-78deg); }
  70% { transform: rotate(-83deg); }
  90% { opacity: 1; transform: rotate(-80deg); }
  100% { opacity: 0; transform: rotate(-80deg); }
}
.wm-hole.wm-boom::after {
  content: ''; position: absolute; left: 30px; top: 10px; width: 120px; height: 120px; border-radius: 50%; z-index: 4;
  background: radial-gradient(circle, #fff6c8, #ffb03d 38%, rgba(255, 80, 40, 0.6) 62%, transparent 72%);
  animation: wmBoom 0.5s ease-out forwards; pointer-events: none;
}
@keyframes wmBoom { from { transform: scale(0.3); opacity: 1; } to { transform: scale(1.5); opacity: 0; } }
.wm-over { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
.wm-banner {
  position: absolute; left: 0; right: 0; top: 44%; text-align: center;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.6rem; color: #ff8a3d;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
}
.wm-float {
  position: absolute; transform: translate(-50%, -100%);
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.7rem; color: var(--lime);
  text-shadow: 0 3px 14px rgba(0, 0, 0, 0.8); animation: wtFloat 0.7s ease-out forwards;
}

/* ---------------- Piano Tiles ---------------- */
.pt-wrap { gap: 8px; }
.pt-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.pt-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.pt-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.pt-song { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem; color: var(--text); opacity: 0.85; }
.pt-stars { font-weight: 900; color: #ffd23f; white-space: nowrap; }
.pt-speed { font-size: 0.9rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.pt-host { min-height: 260px; }
.pt-field {
  overflow: hidden; border-radius: 12px; cursor: pointer; user-select: none; touch-action: none;
  background: #f3f4f7;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.14), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.pt-belt { position: absolute; inset: 0; transition: filter 0.12s; }
.pt-field.pt-frozen .pt-belt { filter: saturate(0.4) brightness(0.9); }
.pt-row { position: absolute; left: 0; top: 0; width: 400px; height: 140px; will-change: transform; }
.pt-cell {
  position: absolute; top: 0; width: 100px; height: 140px; box-sizing: border-box;
  background: #f7f8fa; border-left: 1px solid #d5d8e0; border-bottom: 1px solid #e3e5ea;
  display: grid; place-items: center;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.05rem; letter-spacing: 0.06em; color: #fff;
}
.pt-cell:first-child { border-left: 0; }
.pt-cell.pt-black { background: linear-gradient(180deg, #2a2d36, #121318); border-bottom-color: #000; }
.pt-cell.pt-played { background: linear-gradient(180deg, #c9ccd6, #b5b9c5); border-bottom-color: #a4a8b4; transition: background 0.12s; }
.pt-cell.pt-start, .pt-cell.pt-resume { box-shadow: inset 0 0 0 4px var(--cyan); animation: ptPulse 0.8s ease-in-out infinite; }
.pt-cell.pt-dropped { background: #e8453c; animation: ptBlink 0.25s steps(2) infinite; }
.pt-cell.pt-wrong { background: #ff5a4f; animation: ptBlink 0.25s steps(2) infinite; }
@keyframes ptPulse { 50% { box-shadow: inset 0 0 0 9px var(--cyan); } }
@keyframes ptBlink { 50% { filter: brightness(1.35); } }
.pt-key {
  position: absolute; bottom: 10px; width: 100px; text-align: center; pointer-events: none; z-index: 2;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.15rem; color: rgba(120, 126, 142, 0.85);
}
.pt-key.pt-key-on { animation: ptKey 0.18s ease-out; }
@keyframes ptKey { from { color: var(--cyan); transform: scale(1.35); } }
.pt-belt.pt-shake { animation: ptShake 0.3s ease-out; }
@keyframes ptShake {
  20% { transform: translateX(-6px); }
  50% { transform: translateX(5px); }
  80% { transform: translateX(-3px); }
}
.pt-over { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.pt-banner {
  position: absolute; left: 0; right: 0; top: 22%; text-align: center; opacity: 0;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.5rem; color: #ffd23f;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.pt-banner.pt-show { animation: ptBanner 1.4s ease-out forwards; }
@keyframes ptBanner {
  0% { opacity: 0; transform: translateY(12px) scale(0.9); }
  15% { opacity: 1; transform: translateY(0) scale(1.05); }
  75% { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-10px); }
}

/* ---- Breakout ---- */
.bo-wrap { gap: 8px; }
.bo-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.bo-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.bo-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.bo-mult { font-size: 1.2rem; font-weight: 900; color: var(--muted); }
.bo-mult[data-m="2"] { color: var(--cyan); }
.bo-mult[data-m="3"] { color: #ffd23f; text-shadow: 0 0 10px rgba(255, 210, 63, 0.55); }
.bo-mult.bo-mult-up { animation: boMult 0.35s ease-out; }
@keyframes boMult { from { transform: scale(1.6); } }
.bo-wall { margin-left: auto; font-size: 0.9rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.bo-host { min-height: 260px; }
.bo-field {
  overflow: hidden; border-radius: 12px; cursor: none; user-select: none; touch-action: none;
  background: radial-gradient(120% 70% at 50% 0%, rgba(155, 92, 255, 0.16), transparent 70%), var(--bg-2);
  box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.bo-field.bo-lost { animation: boLost 0.5s ease-out; }
@keyframes boLost { from { box-shadow: 0 0 0 2px var(--line), inset 0 -40px 40px -20px rgba(255, 77, 77, 0.7); } }
.bo-brick {
  position: absolute; box-sizing: border-box; border: 2px solid rgba(0, 0, 0, 0.35); border-radius: 5px;
  box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.35), inset 0 -3px 0 rgba(0, 0, 0, 0.2);
}
.bo-brick.bo-r0 { background: #ff4d5e; }
.bo-brick.bo-r1 { background: #ff9a3c; }
.bo-brick.bo-r2 { background: #ffd23f; }
.bo-brick.bo-r3 { background: #5ee06a; }
.bo-brick.bo-r4 { background: #34b8ff; }
.bo-brick.bo-off { display: none; }
.bo-brick.bo-pop { animation: boPop 0.2s ease-out forwards; pointer-events: none; }
@keyframes boPop { to { opacity: 0; transform: scale(1.35); filter: brightness(2); } }
.bo-field.bo-new .bo-brick { animation: boIn 0.45s ease-out; }
@keyframes boIn { from { opacity: 0; transform: translateY(-24px); } }
.bo-ball {
  position: absolute; left: 0; top: 0; border-radius: 50%; will-change: transform; z-index: 2;
  background: radial-gradient(circle at 35% 30%, #fff, #d9e6ff);
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.7);
}
.bo-ball.bo-hid { opacity: 0; }
.bo-paddle {
  position: absolute; left: 0; border-radius: 6px; will-change: transform; z-index: 2;
  background: linear-gradient(180deg, #e9fbff, var(--cyan));
  box-shadow: 0 0 14px rgba(52, 224, 255, 0.45);
}
/* Which way the ball on the paddle will go: a short dotted line up from it. */
.bo-aim {
  position: absolute; left: 0; top: 0; width: 0; height: 56px; margin-top: -56px;
  border-left: 3px dotted rgba(255, 255, 255, 0.6); transform-origin: 0 100%; pointer-events: none;
}
.bo-aim.bo-hid { display: none; }
.bo-over { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.bo-banner {
  position: absolute; left: 0; right: 0; top: 42%; text-align: center; opacity: 0;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.6rem; color: #ffd23f;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.bo-banner.bo-show { animation: ptBanner 1.4s ease-out forwards; }

/* ---- Invaders ---- */
.iv-wrap { gap: 8px; }
.iv-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.iv-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.iv-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 4ch; }
.iv-mult { font-size: 1.2rem; font-weight: 900; color: var(--muted); }
.iv-mult[data-m="2"] { color: var(--cyan); }
.iv-mult[data-m="3"] { color: #7dff5a; text-shadow: 0 0 10px rgba(125, 255, 90, 0.5); }
.iv-mult[data-m="4"] { color: #ffd23f; text-shadow: 0 0 12px rgba(255, 210, 63, 0.65); }
.iv-mult.iv-mult-up { animation: boMult 0.35s ease-out; }
.iv-mult.iv-mult-drop { animation: ivDrop 0.45s ease-out; }
@keyframes ivDrop { 0% { color: #ff4d5e; transform: translateX(-4px); } 25% { transform: translateX(4px); } 50% { transform: translateX(-2px); } }
.iv-wave { margin-left: auto; font-size: 0.9rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.iv-host { min-height: 280px; }
.iv-field {
  overflow: hidden; border-radius: 12px; cursor: crosshair; user-select: none; touch-action: none;
  background:
    radial-gradient(1px 1px at 12% 18%, rgba(255, 255, 255, 0.7), transparent),
    radial-gradient(1px 1px at 38% 62%, rgba(255, 255, 255, 0.5), transparent),
    radial-gradient(1px 1px at 71% 30%, rgba(255, 255, 255, 0.6), transparent),
    radial-gradient(1px 1px at 88% 74%, rgba(255, 255, 255, 0.45), transparent),
    radial-gradient(1px 1px at 55% 12%, rgba(255, 255, 255, 0.5), transparent),
    #05060f;
  box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.iv-field.iv-hit { animation: ivHit 0.5s ease-out; }
@keyframes ivHit { from { box-shadow: 0 0 0 2px var(--line), inset 0 -60px 50px -20px rgba(255, 77, 77, 0.75); } }
.iv-ground { position: absolute; left: 0; right: 0; height: 2px; background: #3cff6a; opacity: 0.7; }
.iv-formation { position: absolute; left: 0; top: 0; will-change: transform; }
.iv-formation.iv-gone { visibility: hidden; }
.iv-formation.iv-new { animation: ivIn 0.5s ease-out; }
@keyframes ivIn { from { opacity: 0; } }
.iv-alien { position: absolute; line-height: 0; }
.iv-alien svg, .iv-cannon svg, .iv-ufo svg, .iv-bomb svg, .iv-boom svg { display: block; }
.iv-alien.iv-dead { visibility: hidden; }
.iv-k0 { color: #ff6ad5; }
.iv-k1 { color: #5ae0ff; }
.iv-k2 { color: #7dff5a; }
.iv-alien .iv-b, .iv-formation.iv-f2 .iv-alien .iv-a { display: none; }
.iv-formation.iv-f2 .iv-alien .iv-b { display: inline; }
.iv-cannon { position: absolute; left: 0; line-height: 0; color: #3cff6a; will-change: transform; z-index: 2; }
.iv-cannon .iv-b, .iv-cannon.iv-down .iv-a { display: none; }
.iv-cannon.iv-down .iv-b { display: inline; }
.iv-cannon.iv-down { color: #ff4d5e; animation: ivBlink 0.2s steps(2) infinite; }
@keyframes ivBlink { 50% { opacity: 0.35; } }
.iv-shot { position: absolute; left: 0; top: 0; background: #fff; box-shadow: 0 0 6px rgba(255, 255, 255, 0.8); will-change: transform; z-index: 2; }
.iv-bomb { position: absolute; left: 0; top: 0; line-height: 0; color: #fff4a8; will-change: transform; z-index: 2; }
.iv-ufo { position: absolute; left: 0; line-height: 0; color: #ff4d5e; will-change: transform; filter: drop-shadow(0 0 5px rgba(255, 77, 94, 0.7)); }
.iv-shot.iv-gone, .iv-bomb.iv-gone, .iv-ufo.iv-gone { display: none; }
.iv-over { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.iv-boom { position: absolute; line-height: 0; animation: ivBoom 0.26s steps(1) forwards; }
@keyframes ivBoom { to { opacity: 0; } }
.iv-float {
  position: absolute; transform: translateX(-50%); font-family: 'JetBrains Mono', monospace; font-weight: 900;
  font-size: 0.95rem; color: #fff; text-shadow: 0 1px 4px #000; white-space: nowrap; animation: ivFloat 0.7s ease-out forwards;
}
.iv-float.iv-float-big { font-size: 1.3rem; color: #ffd23f; animation-duration: 1.1s; }
@keyframes ivFloat { to { opacity: 0; transform: translate(-50%, -26px); } }
.iv-banner {
  position: absolute; left: 0; right: 0; top: 42%; text-align: center; opacity: 0;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.6rem; color: #7dff5a;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.iv-banner.iv-show { animation: ptBanner 1.4s ease-out forwards; }

/* ---- Fruit Slash ---- */
.fs-wrap { gap: 8px; }
.fs-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.fs-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.fs-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.fs-tally { margin-left: auto; font-size: 0.9rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.fs-host { min-height: 280px; }
.fs-board {
  cursor: crosshair; user-select: none; touch-action: none;
  box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.fs-wrap.fs-out .fs-board { cursor: not-allowed; }
/* On the whole stage rather than the canvas, whose size fitCanvas owns. */
.fs-wrap.fs-shake .fs-host { animation: fsShake 0.4s ease-out; }
@keyframes fsShake {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-6px, 3px); }
  40% { transform: translate(5px, -4px); }
  60% { transform: translate(-3px, 2px); }
  80% { transform: translate(2px, -1px); }
}

/* ---- Red Light ---- */
.rl-wrap { gap: 8px; }
.rl-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.rl-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.rl-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.rl-laps { font-size: 1rem; font-weight: 900; color: #ffd23f; letter-spacing: 0.05em; white-space: nowrap; }
.rl-light {
  margin-left: auto; align-self: center; padding: 4px 12px; border-radius: 999px; white-space: nowrap;
  font-size: 0.9rem; font-weight: 900; letter-spacing: 0.06em;
}
.rl-light[data-c="green"] { background: #3ddc84; color: #0b2a17; box-shadow: 0 0 14px rgba(61, 220, 132, 0.45); }
.rl-light[data-c="red"] { background: #ff3b4e; color: #fff; box-shadow: 0 0 14px rgba(255, 59, 78, 0.55); }
.rl-host { min-height: 300px; }
.rl-field {
  overflow: hidden; border-radius: 12px; user-select: none; touch-action: none; cursor: pointer;
  background:
    linear-gradient(90deg, transparent 168px, rgba(255, 255, 255, 0.12) 168px 272px, transparent 272px),
    linear-gradient(180deg, #e2c893, #caa56c);
  box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.rl-field.rl-hit { animation: rlHit 0.6s ease-out; }
@keyframes rlHit { from { box-shadow: 0 0 0 2px var(--line), inset 0 0 70px 10px rgba(255, 40, 60, 0.75); } }
/* The painted wall behind her: sky and clouds, as in the show. */
.rl-sky {
  position: absolute; left: 0; top: 0; right: 0; height: 204px;
  background:
    radial-gradient(38px 16px at 70px 70px, #fff 98%, transparent),
    radial-gradient(30px 14px at 100px 62px, #fff 98%, transparent),
    radial-gradient(44px 18px at 360px 96px, #fff 98%, transparent),
    radial-gradient(28px 13px at 330px 88px, #fff 98%, transparent),
    radial-gradient(30px 12px at 220px 150px, rgba(255, 255, 255, 0.9) 98%, transparent),
    linear-gradient(180deg, #6cc3f0, #a9ddf7 80%, #c9b27f 80%, #d7bd88);
}
.rl-tree { position: absolute; left: 120px; top: 34px; width: 200px; height: 170px; }
.rl-tree svg, .rl-doll-side svg, .rl-runner svg { display: block; width: 100%; height: 100%; }
.rl-doll { position: absolute; left: 160px; top: 48px; width: 120px; height: 152px; z-index: 1; }
.rl-doll-side { position: absolute; inset: 0; transform-origin: 50% 50%; }
/* Her front is drawn over her back, so the middle of a turn never shows both. */
.rl-doll-back { z-index: 0; }
.rl-doll-front { z-index: 1; transform: scaleX(0); }
.rl-eyes { fill: #1b1411; }
.rl-field.rl-look .rl-eyes { fill: #ff2436; filter: drop-shadow(0 0 3px #ff2436); }
.rl-song {
  position: absolute; left: 0; right: 0; top: 8px; z-index: 3;
  display: flex; justify-content: center; gap: 4px; pointer-events: none;
}
.rl-word {
  padding: 3px 6px; border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 900;
  background: rgba(10, 30, 50, 0.45); color: rgba(255, 255, 255, 0.55);
}
.rl-word.rl-sung { background: #3ddc84; color: #0b2a17; }
.rl-word.rl-word-last.rl-sung { background: #ff3b4e; color: #fff; }
.rl-line { position: absolute; left: 0; right: 0; height: 4px; margin-top: -2px; background: #ff3b4e; box-shadow: 0 0 8px rgba(255, 59, 78, 0.6); }
.rl-start { position: absolute; left: 0; right: 0; height: 0; border-top: 3px dashed rgba(255, 255, 255, 0.85); }
.rl-mark {
  position: absolute; left: 0; right: 0; height: 0; border-top: 1px dashed rgba(90, 60, 30, 0.3);
  text-align: right; padding-right: 8px; font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 800;
  line-height: 14px; color: rgba(90, 60, 30, 0.6);
}
/* While she is looking: the yard goes red and her stare lands on you. */
.rl-stare {
  position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.12s;
  background: radial-gradient(90% 70% at 50% 16%, rgba(255, 30, 50, 0.28), rgba(255, 30, 50, 0.08) 70%);
}
.rl-field.rl-look .rl-stare { opacity: 1; }
.rl-beam {
  position: absolute; left: 219px; top: 88px; width: 3px; height: 0; opacity: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 36, 54, 0.9), rgba(255, 36, 54, 0.15));
}
.rl-field.rl-look .rl-beam { opacity: 1; }
.rl-runner { position: absolute; left: 0; top: 0; width: 44px; height: 62px; z-index: 2; will-change: transform; }
.rl-leg, .rl-arm { transform-box: fill-box; transform-origin: 50% 0; }
.rl-runner.rl-run .rl-leg-l, .rl-runner.rl-run .rl-arm-r { animation: rlStride 0.26s ease-in-out infinite alternate; }
.rl-runner.rl-run .rl-leg-r, .rl-runner.rl-run .rl-arm-l { animation: rlStride 0.26s ease-in-out infinite alternate-reverse; }
@keyframes rlStride { from { transform: scaleY(1.08) translateY(-2px); } to { transform: scaleY(0.82) translateY(1px); } }
.rl-runner.rl-run { filter: drop-shadow(0 3px 2px rgba(0, 0, 0, 0.25)); }
.rl-runner.rl-stuck { filter: drop-shadow(0 0 6px #ff2436) saturate(0.4); }
.rl-over { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
.rl-banner {
  position: absolute; left: 0; right: 0; top: 46%; text-align: center; opacity: 0;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.6rem; color: #ffd23f;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.rl-banner[data-kind="bad"] { color: #ff4d5e; }
.rl-banner.rl-show { animation: ptBanner 1.4s ease-out forwards; }

/* ---- Falling Blocks ---- */
.fb-wrap { gap: 8px; }
.fb-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; width: min(100%, 460px); }
.fb-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.fb-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 4ch; text-align: left; }
.fb-lines { font-size: 0.95rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.fb-tools { margin-left: auto; align-self: center; display: flex; align-items: center; gap: 8px; }
.fb-tools.fb-off { visibility: hidden; }
.fb-undo {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
}
.fb-undo:disabled { opacity: 0.28; cursor: default; }
.fb-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.fb-back.spent { opacity: 0.22; filter: grayscale(1); }
.fb-host { position: relative; min-height: 300px; }
.fb-board { box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45); }
/* On the stage, not the canvas, whose size fitCanvas owns. */
.fb-wrap.fb-shake .fb-host { animation: fsShake 0.4s ease-out; }
/* Placed over the middle of the well by the script. */
.fb-banner {
  position: absolute; left: 50%; top: 40%; transform: translateX(-50%); opacity: 0; pointer-events: none; z-index: 4;
  white-space: nowrap; font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.5rem; color: #fff;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.fb-banner[data-kind="one"] { font-size: 1.15rem; color: #cfe9ff; }
.fb-banner[data-kind="many"] { color: #35d6f5; }
.fb-banner[data-kind="four"] { font-size: 1.9rem; color: #ffd23f; }
.fb-banner[data-kind="out"] { color: #ff4d5e; }
.fb-banner.fb-show { animation: fbBanner 1.2s ease-out forwards; }
@keyframes fbBanner {
  0% { opacity: 0; transform: translate(-50%, 12px) scale(0.9); }
  15% { opacity: 1; transform: translate(-50%, 0) scale(1.06); }
  75% { opacity: 1; transform: translate(-50%, 0) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -12px); }
}

/* ---- Darts ---- */
.dt-wrap { gap: 8px; }
.dt-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.dt-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.dt-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.dt-slots { display: flex; gap: 6px; }
.dt-slot {
  min-width: 3.4ch; padding: 2px 6px; border-radius: 6px; text-align: center;
  font-size: 0.95rem; font-weight: 900; color: var(--muted); background: var(--panel-strong);
}
.dt-slot[data-k="s"] { color: var(--text); }
.dt-slot[data-k="d"] { color: #5ee06a; }
.dt-slot[data-k="t"] { color: #ffd23f; }
.dt-slot[data-k="b"] { color: #ff6b6b; }
.dt-slot[data-k="x"] { color: var(--pink); text-decoration: line-through; }
.dt-visit { margin-left: auto; font-size: 0.9rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.dt-host { min-height: 280px; }
.dt-field {
  position: relative; overflow: hidden; border-radius: 12px; cursor: none; user-select: none; touch-action: none;
  background: radial-gradient(60% 60% at 50% 50%, #2a3a52, #10151f 75%);
  box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45);
}
.dt-svg { display: block; overflow: visible; }
.dt-nums { font-family: 'JetBrains Mono', monospace; pointer-events: none; }
.dt-darts { pointer-events: none; }
.dt-aim { pointer-events: none; }
/* How wide the hand is swinging right now, round the pointer. */
.dt-reach { fill: none; stroke: rgba(255, 255, 255, 0.35); stroke-width: 0.8; stroke-dasharray: 3 3; }
.dt-ptr { fill: rgba(255, 255, 255, 0.55); }
.dt-cross { fill: none; }
.dt-under { stroke: rgba(0, 0, 0, 0.85); stroke-width: 3.2; }
.dt-over-line { stroke: #fff; stroke-width: 1.4; }
.dt-dot { fill: #fff; stroke: #000; stroke-width: 0.5; }
.dt-aim[data-phase="settle"] .dt-over-line { stroke: #cfe9ff; }
.dt-aim[data-phase="steady"] .dt-over-line { stroke: #8dff5a; }
.dt-aim[data-phase="steady"] .dt-dot { fill: #8dff5a; }
.dt-aim[data-phase="steady"] .dt-reach { stroke: rgba(141, 255, 90, 0.55); }
.dt-aim[data-phase="shake"] .dt-over-line { stroke: #ff7a45; }
.dt-aim[data-phase="shake"] .dt-dot { fill: #ff7a45; }
.dt-aim[data-phase="shake"] .dt-reach { stroke: rgba(255, 122, 69, 0.6); }
.dt-over { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.dt-float {
  position: absolute; transform: translateX(-50%); white-space: nowrap; pointer-events: none;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.05rem; color: #fff;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.95), 0 0 2px #000; animation: dtFloat 0.9s ease-out forwards;
}
.dt-float[data-k="d"] { color: #5ee06a; }
.dt-float[data-k="t"] { color: #ffd23f; font-size: 1.25rem; }
.dt-float[data-k="b"] { color: #ff6b6b; font-size: 1.25rem; }
.dt-float[data-k="x"] { color: var(--pink); }
@keyframes dtFloat { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, -26px); } }
.dt-banner {
  position: absolute; left: 0; right: 0; top: 42%; text-align: center; opacity: 0;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2.2rem; color: #fff;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.dt-banner[data-kind="low"] { color: #9aa4b2; }
.dt-banner[data-kind="ton"] { color: #ffd23f; font-size: 2.6rem; }
.dt-banner[data-kind="max"] { color: #ffd23f; font-size: 1.9rem; text-shadow: 0 0 18px rgba(255, 210, 63, 0.8), 0 2px 12px #000; }
.dt-banner.dt-show { animation: ptBanner 1.2s ease-out forwards; }

/* ---- Bubble Shooter ---- */
.bb-wrap { gap: 8px; }
.bb-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; }
.bb-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.bb-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; }
.bb-board-no { font-size: 0.9rem; font-weight: 900; color: #ffd23f; letter-spacing: 0.05em; white-space: nowrap; }
/* Five pips: a lit one for every shot that popped nothing; the fifth brings a row. */
.bb-strikes { display: flex; align-items: center; gap: 4px; margin-left: auto; align-self: center; }
.bb-strike-label { font-size: 0.72rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); margin-right: 2px; }
.bb-pip {
  width: 10px; height: 10px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.12); box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.28);
}
.bb-pip.bb-on { background: #ff5a6e; box-shadow: 0 0 6px rgba(255, 90, 110, 0.7); }
.bb-strikes.bb-warn .bb-pip:not(.bb-on) { animation: bbPip 0.7s ease-in-out infinite alternate; }
@keyframes bbPip { to { box-shadow: inset 0 0 0 1.5px #ff5a6e, 0 0 8px rgba(255, 90, 110, 0.6); } }
.bb-shots { font-size: 0.9rem; font-weight: 900; letter-spacing: 0.05em; white-space: nowrap; color: var(--muted); }
.bb-shots.bb-low { color: #ff8a3d; }
.bb-host { min-height: 300px; }
.bb-canvas {
  cursor: crosshair; user-select: none; touch-action: none; border-radius: 10px;
  box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45);
}

/* ---- Rush Hour ---- */
/* The car park is DOM in a fitBox field: a tarmac board inside a kerb, a gap
   in the right-hand kerb on the red car's row, and vehicles moved by
   transform. A held vehicle follows the pointer with no transition; let go,
   it settles on its square over ANIM.slide. */
.rh-wrap { gap: 8px; }
.rh-head { display: flex; align-items: baseline; gap: 16px; font-family: 'JetBrains Mono', monospace; }
.rh-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 3ch; text-align: right; }
.rh-step { color: var(--amber); font-weight: 900; font-size: 1rem; white-space: nowrap; }
.rh-moves { color: var(--muted); font-weight: 800; font-size: 0.95rem; white-space: nowrap; }
.rh-moves.rh-over { color: var(--pink); }
.rh-host { min-height: 260px; }
.rh-field { position: relative; user-select: none; touch-action: none; }
.rh-walls {
  position: absolute; border-radius: 18px;
  background: linear-gradient(160deg, #6f7483, #4b4f5c);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45), inset 0 2px 0 rgba(255, 255, 255, 0.18);
}
.rh-lot {
  position: absolute; border-radius: 6px;
  background-color: #2b2e38;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.07) 2px, transparent 2px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.07) 2px, transparent 2px);
  background-size: 64px 64px; background-position: -1px -1px;
  box-shadow: inset 0 0 18px rgba(0, 0, 0, 0.6);
}
/* The gap: tarmac cut through the kerb, and an arrow out. */
.rh-exit {
  position: absolute; display: flex; align-items: center; justify-content: flex-end; padding-right: 6px;
  background: linear-gradient(90deg, #2b2e38, rgba(43, 46, 56, 0));
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 0.62rem; letter-spacing: 0.08em;
  color: var(--lime); text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); pointer-events: none;
}
.rh-layer { position: absolute; inset: 0; overflow: hidden; }
.rh-layer.rh-in .rh-car { animation: rhIn 0.45s ease-out backwards; }
@keyframes rhIn { from { opacity: 0; filter: blur(4px); } }
.rh-car {
  position: absolute; left: 0; top: 0; border-radius: 12px; cursor: grab;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0) 45%), var(--c, #888);
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.35), inset 0 0 0 2px rgba(0, 0, 0, 0.25);
  transition-property: transform; transition-timing-function: cubic-bezier(0.25, 0.8, 0.4, 1);
}
/* The roof: a darker pane across the middle, the way the board game's cars read from above. */
.rh-car::after {
  content: ''; position: absolute; border-radius: 6px;
  background: rgba(10, 14, 30, 0.42); box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12);
}
.rh-h::after { top: 10px; bottom: 10px; left: 28%; right: 22%; }
.rh-v::after { left: 10px; right: 10px; top: 28%; bottom: 22%; }
.rh-truck { border-radius: 8px; }
.rh-truck.rh-h::after { left: 8px; right: auto; width: 26%; }
.rh-truck.rh-v::after { top: 8px; bottom: auto; height: 26%; }
.rh-held { z-index: 3; cursor: grabbing; box-shadow: 0 10px 16px rgba(0, 0, 0, 0.5), inset 0 0 0 2px rgba(255, 255, 255, 0.55); }
.rh-red { --c: #e8283c; z-index: 2; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.35), 0 0 14px rgba(232, 40, 60, 0.55), inset 0 0 0 2px rgba(0, 0, 0, 0.25); }
.rh-arrow {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 1;
  font-size: 1.1rem; font-weight: 900; color: #fff; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); pointer-events: none;
}
.rh-gone { transition-timing-function: cubic-bezier(0.55, 0, 0.9, 0.4); }
.rh-c0 { --c: #3ddc84; }
.rh-c1 { --c: #34a4ff; }
.rh-c2 { --c: #ffd23f; }
.rh-c3 { --c: #b36bff; }
.rh-c4 { --c: #ff9f2b; }
.rh-c5 { --c: #2fd4d4; }
.rh-c6 { --c: #a0724e; }
.rh-c7 { --c: #ff7ab8; }
.rh-c8 { --c: #9bd14a; }
.rh-c9 { --c: #7f8ea3; }
.rh-c10 { --c: #e9e6f5; }
.rh-c11 { --c: #1f9e8a; }
.rh-banner {
  position: absolute; left: 0; right: 0; top: 40%; text-align: center; pointer-events: none; z-index: 5;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2rem; color: var(--lime);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95);
}
.rh-banner.rh-par { color: var(--amber); }
.rh-tools { display: flex; align-items: center; gap: 10px; min-height: 26px; }
.rh-btn {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--cyan); cursor: pointer;
}
.rh-btn:disabled { opacity: 0.28; cursor: default; }
.rh-undo { color: var(--cyan); }
.rh-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.rh-back.spent { opacity: 0.22; filter: grayscale(1); }

/* ---- Flow ---- */
/* The board is a canvas drawn on change, never on a loop. The banner sits
   over it in the host, and the win glow and the next board's fade are on the
   host too, never on the canvas, whose size fitCanvas owns. */
.fl-wrap { gap: 8px; }
.fl-head { display: flex; align-items: baseline; gap: 16px; font-family: 'JetBrains Mono', monospace; flex-wrap: wrap; justify-content: center; }
.fl-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 3ch; text-align: right; }
.fl-step { color: var(--amber); font-weight: 900; font-size: 1rem; white-space: nowrap; }
.fl-stats { color: var(--muted); font-weight: 800; font-size: 0.85rem; white-space: nowrap; }
.fl-moves { color: var(--muted); font-weight: 800; font-size: 0.85rem; white-space: nowrap; }
.fl-moves.fl-over { color: var(--pink); }
.fl-host { position: relative; min-height: 300px; }
.fl-canvas { cursor: pointer; user-select: none; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45); }
.fl-host.fl-win { animation: flWin 0.7s ease-out; }
@keyframes flWin { 30% { filter: brightness(1.45) saturate(1.3); } }
.fl-host.fl-in { animation: flIn 0.35s ease-out; }
@keyframes flIn { from { opacity: 0; filter: blur(4px); } }
.fl-banner {
  position: absolute; left: 50%; top: 42%; transform: translateX(-50%); width: max-content; pointer-events: none; z-index: 5;
  padding: 4px 18px; border-radius: 999px; background: rgba(7, 8, 13, 0.78);
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2rem; color: var(--lime);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95);
}
.fl-banner:empty { display: none; }
.fl-banner.fl-perfect { color: var(--amber); }
.fl-tools { display: flex; align-items: center; gap: 10px; min-height: 26px; }
.fl-btn {
  font: inherit; font-weight: 800; letter-spacing: 0.06em; font-size: 0.72rem;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--cyan); cursor: pointer;
}
.fl-btn:disabled { opacity: 0.28; cursor: default; }
.fl-undo { color: var(--cyan); }
.fl-backs { display: flex; gap: 4px; font-size: 0.95rem; color: var(--cyan); }
.fl-back.spent { opacity: 0.22; filter: grayscale(1); }

/* Mahjong */
.mj-wrap { gap: 8px; }
.mj-head { display: flex; align-items: baseline; gap: 16px; font-family: 'JetBrains Mono', monospace; flex-wrap: wrap; justify-content: center; }
.mj-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 3ch; text-align: right; }
.mj-step { color: var(--amber); font-weight: 900; font-size: 1rem; white-space: nowrap; }
.mj-left { color: var(--muted); font-weight: 800; font-size: 0.85rem; white-space: nowrap; }
.mj-clean { color: var(--lime); font-weight: 800; font-size: 0.85rem; white-space: nowrap; }
.mj-clean.mj-lost { color: var(--pink); }
.mj-host { position: relative; min-height: 300px; }
.mj-canvas { cursor: pointer; user-select: none; border-radius: 10px; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45); }
.mj-host.mj-win { animation: mjWin 0.9s ease-out; }
@keyframes mjWin { 30% { filter: brightness(1.4) saturate(1.3); } }
.mj-host.mj-in { animation: mjIn 0.45s ease-out; }
@keyframes mjIn { from { opacity: 0; transform: translateY(-14px); } }
.mj-banner {
  position: absolute; left: 50%; top: 42%; transform: translateX(-50%); width: max-content; pointer-events: none; z-index: 5;
  padding: 4px 18px; border-radius: 999px; background: rgba(7, 8, 13, 0.8);
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.7rem; color: var(--lime);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95);
}
.mj-banner:empty { display: none; }
.mj-banner.mj-good { color: var(--amber); }
.mj-banner.mj-bad { color: var(--pink); font-size: 1.3rem; }

/* ---- Word Search ---- */
/* The square is DOM: a cell per letter over a layer of capsules, each
   rotated along its word's line. The shake on a wrong line uses the
   `translate` property so it never fights the capsule's own rotate. */
.ws-wrap { gap: 8px; }
.ws-head { display: flex; align-items: baseline; gap: 16px; font-family: 'JetBrains Mono', monospace; flex-wrap: wrap; justify-content: center; }
.ws-count { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 2ch; text-align: right; }
.ws-step { color: var(--amber); font-weight: 900; font-size: 1rem; white-space: nowrap; }
.ws-left { color: var(--muted); font-weight: 800; font-size: 0.85rem; white-space: nowrap; }
.ws-host { position: relative; min-height: 300px; }
.ws-field { position: relative; user-select: none; touch-action: none; }
.ws-board {
  position: absolute; left: 0; top: 0; border-radius: 12px; overflow: hidden; cursor: crosshair;
  background: linear-gradient(160deg, #1b2033, #12151f); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45), inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.ws-board.ws-in { animation: wsIn 0.35s ease-out; }
@keyframes wsIn { from { opacity: 0; filter: blur(4px); } }
.ws-board.ws-win { animation: wsWin 0.9s ease-out; }
@keyframes wsWin { 30% { filter: brightness(1.5) saturate(1.3); } }
.ws-caps, .ws-cells, .ws-over { position: absolute; inset: 0; pointer-events: none; }
.ws-cells { z-index: 2; }
.ws-over { z-index: 4; }
.ws-cell {
  position: absolute; display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; color: #eef1fb; line-height: 1;
}
.ws-board.ws-locked .ws-cells { opacity: 0.4; filter: grayscale(1); transition: opacity 0.1s; }
.ws-cell.ws-hint { color: var(--cyan); animation: wsHint 0.9s ease-in-out infinite; }
@keyframes wsHint {
  0%, 100% { box-shadow: inset 0 0 0 2px rgba(52, 224, 255, 0.25); border-radius: 50%; }
  50% { box-shadow: inset 0 0 0 4px rgba(52, 224, 255, 0.95), 0 0 16px rgba(52, 224, 255, 0.7); border-radius: 50%; }
}
.ws-cap { position: absolute; box-sizing: border-box; }
.ws-cap.ws-got {
  --c: #3ddc84;
  background: color-mix(in srgb, var(--c) 34%, transparent); border: 2px solid var(--c);
  animation: wsGot 0.3s ease-out;
}
@keyframes wsGot { from { opacity: 0; filter: brightness(2); } }
.ws-sel { z-index: 1; display: none; background: rgba(255, 255, 255, 0.16); border: 2px solid rgba(255, 255, 255, 0.7); }
.ws-sel.ws-on { display: block; }
.ws-sel.ws-wrong { background: rgba(255, 77, 109, 0.3); border-color: var(--pink); animation: wsShake 0.3s ease-out; }
.ws-sel.ws-fade { opacity: 0; transition: opacity 0.25s; }
@keyframes wsShake { 20% { translate: -5px 0; } 45% { translate: 5px 0; } 70% { translate: -3px 0; } 100% { translate: 0 0; } }
.ws-banner {
  position: absolute; left: 50%; top: 42%; transform: translateX(-50%); width: max-content; pointer-events: none;
  padding: 4px 18px; border-radius: 999px; background: rgba(7, 8, 13, 0.8);
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 2rem; color: var(--lime);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95);
}
.ws-banner:empty { display: none; }
.ws-float {
  position: absolute; transform: translate(-50%, -50%); pointer-events: none;
  font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.6rem; color: var(--lime);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.9); animation: wsFloat 0.9s ease-out forwards;
}
@keyframes wsFloat { to { opacity: 0; translate: 0 -40px; } }
.ws-list {
  position: absolute; left: 0; right: 0; display: flex; flex-wrap: wrap; align-content: flex-start; justify-content: center;
  gap: 8px 10px; padding-top: 2px;
}
.ws-word {
  --c: #3ddc84;
  font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 17px; letter-spacing: 0.06em; color: #eef1fb;
  padding: 5px 11px; border-radius: 999px; border: 1px solid var(--line); background: var(--panel-strong);
}
.ws-word.ws-got { color: var(--c); border-color: var(--c); text-decoration: line-through; text-decoration-thickness: 2px; opacity: 0.75; }
.ws-word.ws-hinted { color: var(--cyan); border-color: var(--cyan); box-shadow: 0 0 12px rgba(52, 224, 255, 0.55); }

/* ---- Pac-Man ---- */
.pm-wrap { gap: 8px; }
.pm-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; width: min(100%, 460px); flex-wrap: wrap; }
.pm-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.pm-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 4ch; text-align: left; color: #ffe01a; transition: color 0.2s; }
.pm-head.pm-blue .pm-count { color: #7f8cff; }
.pm-best { font-size: 0.95rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.pm-lives { display: flex; gap: 5px; align-self: center; }
.pm-life {
  width: 16px; height: 16px; border-radius: 50%;
  background: conic-gradient(#ffe01a 0 60deg, transparent 60deg 120deg, #ffe01a 120deg);
  transition: opacity 0.3s, filter 0.3s;
}
.pm-life.pm-lost { opacity: 0.18; filter: grayscale(1); }
.pm-maze { margin-left: auto; font-size: 0.85rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.pm-host { position: relative; min-height: 300px; }
.pm-board { box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45); border-radius: 4px; }
.pm-wrap.pm-hurt .pm-host { animation: fsShake 0.4s ease-out; }
.pm-banner {
  position: absolute; left: 50%; top: 42%; transform: translateX(-50%); opacity: 0; pointer-events: none; z-index: 4;
  white-space: nowrap; font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.5rem; color: #fff;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.pm-banner[data-kind="clear"] { color: #ffe01a; font-size: 1.8rem; }
.pm-banner[data-kind="over"] { color: #ff4d5e; }
.pm-banner.pm-show { animation: fbBanner 1.6s ease-out forwards; }

/* ---- Bowling ----
   The lane is a canvas; around it, the head (points, frame, the last ball's
   speed) and a score sheet of ten boxes the way an alley's screen draws them:
   the balls on top, the running total underneath. */
.bw-wrap { gap: 7px; }
.bw-head { display: flex; align-items: baseline; gap: 12px; font-family: 'JetBrains Mono', monospace; min-width: 0; width: min(100%, 460px); flex-wrap: wrap; }
.bw-label { font-size: 0.8rem; font-weight: 800; letter-spacing: 0.08em; color: var(--muted); }
.bw-count { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; min-width: 3ch; text-align: left; color: #ffd23f; }
.bw-frame { font-size: 0.85rem; font-weight: 800; color: var(--muted); white-space: nowrap; }
.bw-speed { margin-left: auto; font-size: 0.85rem; font-weight: 800; color: var(--cyan); white-space: nowrap; }
.bw-sheet {
  display: grid; grid-template-columns: repeat(9, 1fr) 1.45fr; width: min(100%, 460px);
  border: 2px solid var(--line); border-radius: 6px; overflow: hidden; background: rgba(0, 0, 0, 0.25);
  font-family: 'JetBrains Mono', monospace;
}
.bw-cell { border-right: 1px solid var(--line); min-width: 0; transition: background 0.2s; }
.bw-cell:last-child { border-right: none; }
.bw-cell.bw-now { background: rgba(52, 224, 255, 0.12); }
.bw-balls { display: flex; justify-content: flex-end; border-bottom: 1px solid var(--line); height: 18px; }
.bw-mark { width: 16px; font-size: 0.72rem; font-weight: 800; line-height: 18px; text-align: center; border-left: 1px solid var(--line); color: var(--text); }
.bw-mark[data-k="x"] { color: #ffd23f; }
.bw-mark[data-k="s"] { color: var(--cyan); }
.bw-mark[data-k="p"] { color: #ff6fa8; border-radius: 50%; box-shadow: inset 0 0 0 1px #ff6fa8; }
.bw-sum { height: 20px; font-size: 0.82rem; font-weight: 900; line-height: 20px; text-align: center; }
.bw-host { position: relative; min-height: 300px; }
.bw-host.bw-strike { animation: bwStrike 0.5s ease-out; }
.bw-lane { box-shadow: 0 0 0 2px var(--line), 0 10px 26px rgba(0, 0, 0, 0.45); border-radius: 4px; cursor: grab; }
.bw-banner {
  position: absolute; left: 50%; top: 22%; transform: translateX(-50%); opacity: 0; pointer-events: none; z-index: 4;
  white-space: nowrap; font-family: 'JetBrains Mono', monospace; font-weight: 900; font-size: 1.5rem; color: #fff;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.95), 0 0 2px #000;
}
.bw-banner[data-kind="strike"] { color: #ffd23f; font-size: 2rem; }
.bw-banner[data-kind="spare"] { color: var(--cyan); font-size: 1.8rem; }
.bw-banner[data-kind="split"] { color: #ff6fa8; }
.bw-banner[data-kind="gutter"] { color: #9aa3b5; }
.bw-banner[data-kind="final"] { color: #ffd23f; font-size: 1.9rem; top: 40%; }
.bw-banner.bw-show { animation: fbBanner 1.4s ease-out forwards; }
@keyframes bwStrike {
  0% { transform: scale(1); }
  30% { transform: scale(1.025); }
  100% { transform: scale(1); }
}

/* ---------- Sketch Guess ---------- */
.sg-wrap { gap: 8px; }
.sg-head { display: flex; align-items: baseline; gap: 16px; width: min(100%, 720px); font-family: 'JetBrains Mono', monospace; }
.sg-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.sg-count { color: var(--muted); font-weight: 800; font-size: 0.95rem; margin-left: auto; }
.sg-title { font-size: clamp(1.3rem, 3vw, 1.8rem); font-weight: 900; letter-spacing: 0.02em; color: var(--text); line-height: 1.1; }
.sg-lit { color: var(--lime); }
.sg-bar { width: min(100%, 720px); height: 6px; border-radius: 999px; background: var(--panel-strong); overflow: hidden; }
.sg-fill { height: 100%; width: 0; background: var(--grad); border-radius: inherit; }
.sg-bar.sg-late .sg-fill { background: var(--pink); }
.sg-prompt { min-height: 1.4em; font-size: 0.95rem; font-weight: 600; color: var(--muted); }
.canvas-host.sg-host { width: min(100%, 720px); min-height: 220px; }
.sg-paper { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); cursor: crosshair; }
.sg-wrap:not([data-phase="draw"]) .sg-paper { cursor: default; }
.sg-controls { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; min-height: 44px; }
.sg-btn {
  font: inherit; font-weight: 800; letter-spacing: 0.05em; font-size: 0.95rem;
  padding: 9px 16px; border-radius: 12px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
  transition: transform 0.12s var(--ease), border-color 0.12s, background 0.12s;
}
.sg-btn:not(:disabled):hover { transform: translateY(-1px); border-color: var(--cyan); }
.sg-btn:disabled { cursor: default; opacity: 0.55; }
.sg-tool { font-size: 0.75rem; padding: 6px 14px; border-radius: 999px; color: var(--cyan); }
.sg-word { font-weight: 900; font-size: 1.15rem; padding: 10px 20px; border-radius: 12px; border: 1px solid var(--line); background: var(--panel-strong); }
.sg-word.sg-on { border-color: var(--lime); color: var(--lime); box-shadow: 0 0 0 2px rgba(182, 255, 61, 0.25); }
.sg-solo { font-size: 1.6rem; letter-spacing: 0.06em; }
.sg-opt { min-width: 150px; font-family: 'JetBrains Mono', monospace; text-align: left; }
.sg-opt.sg-mine { border-color: var(--cyan); color: var(--cyan); opacity: 1; }
.sg-opt.sg-answer { border-color: var(--lime); color: #0b0a14; background: var(--lime); opacity: 1; }
.sg-opt.sg-miss { border-color: var(--red); color: var(--red); opacity: 1; }
.sg-room { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; width: min(100%, 860px); }
.sg-chip {
  display: flex; align-items: center; gap: 6px; padding: 3px 10px 3px 4px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel); font-size: 0.85rem;
}
.sg-chip.sg-me { border-color: var(--violet); }
.sg-chip.sg-artist { border-color: var(--amber); background: rgba(255, 179, 64, 0.1); }
.sg-chip.sg-right { border-color: var(--lime); }
.sg-chip.sg-wrong { opacity: 0.6; }
.sg-chip-name { font-weight: 800; max-width: 9ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sg-chip-pts { font-family: 'JetBrains Mono', monospace; color: var(--cyan); font-weight: 700; }
.sg-chip-mark { min-width: 1.2em; }
.sg-nib {
  position: fixed; z-index: 50; pointer-events: none; transform: translate(-50%, -50%);
  border-radius: 50%; border: 2px solid var(--pink); background: rgba(255, 77, 141, 0.18);
}
.sg-nib[hidden] { display: none; }
.sg-paper.sg-nope { animation: sgNope 0.35s ease-out; }
@keyframes sgNope {
  0% { box-shadow: 0 0 0 4px var(--pink), 0 10px 30px rgba(0, 0, 0, 0.45); }
  100% { box-shadow: 0 0 0 0 transparent, 0 10px 30px rgba(0, 0, 0, 0.45); }
}

/* ---------- Minefield ---------- */
.mf-wrap { gap: 8px; }
.mf-head { display: flex; align-items: baseline; gap: 16px; width: min(100%, 720px); font-family: 'JetBrains Mono', monospace; }
.mf-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.mf-field { color: var(--cyan); font-weight: 800; font-size: 0.95rem; letter-spacing: 0.06em; }
.mf-count { color: var(--muted); font-weight: 800; font-size: 0.95rem; margin-left: auto; }
.mf-title { font-size: clamp(1.3rem, 3vw, 1.8rem); font-weight: 900; letter-spacing: 0.02em; color: var(--text); line-height: 1.1; }
.mf-lit { color: var(--lime); }
.mf-bar { width: min(100%, 720px); height: 6px; border-radius: 999px; background: var(--panel-strong); overflow: hidden; }
.mf-fill { height: 100%; width: 0; background: var(--grad); border-radius: inherit; }
.mf-bar.mf-late .mf-fill { background: var(--pink); }
.mf-prompt { min-height: 1.4em; font-size: 0.95rem; font-weight: 600; color: var(--muted); text-align: center; }
.canvas-host.mf-host { width: min(100%, 720px); min-height: 260px; }
.mf-board {
  position: relative; display: grid; grid-auto-rows: 44px; border-radius: 10px; overflow: hidden;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); user-select: none;
}
.mf-cell {
  position: relative; display: flex; align-items: center; justify-content: center;
  background: #2f6b3a; font-size: 24px; line-height: 1;
}
.mf-cell.mf-alt { background: #2a6234; }
.mf-cell.mf-start { background: #8b7651; }
.mf-cell.mf-start.mf-alt { background: #826e4a; }
.mf-cell.mf-finish { background: repeating-conic-gradient(#ececec 0 25%, #2b2b33 0 50%) 0 0 / 22px 22px; }
.mf-cell.mf-step::after {
  content: ''; width: 10px; height: 10px; border-radius: 50%; background: rgba(255, 255, 255, 0.32);
}
.mf-wrap[data-phase="plant"] .mf-cell.mf-plantable { cursor: pointer; }
.mf-wrap[data-phase="plant"] .mf-cell.mf-plantable:hover { box-shadow: inset 0 0 0 2px rgba(182, 255, 61, 0.6); }
.mf-cell.mf-own { box-shadow: inset 0 0 0 2px var(--lime); }
.mf-cell.mf-hint { box-shadow: inset 0 0 0 2px var(--cyan); animation: mfHint 1s ease-in-out infinite alternate; }
.mf-cell.mf-crater { background: radial-gradient(circle, #1a120a 0 45%, #3b2b18 75%); }
.mf-cell.mf-shown { background: #5a2a2a; }
.mf-cell.mf-shown.mf-alt { background: #522626; }
.mf-icon { pointer-events: none; filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.45)); }
.mf-who { position: absolute; right: 1px; bottom: 1px; display: flex; gap: 1px; }
.mf-times {
  position: absolute; left: 3px; top: 2px; font: 800 11px 'JetBrains Mono', monospace; color: #fff;
  text-shadow: 0 1px 2px #000;
}
.mf-runners { position: absolute; inset: 0; pointer-events: none; }
.mf-runner {
  position: absolute; transform: translate(-50%, -50%); opacity: 0.55; line-height: 0;
  transition: left 0.09s linear, top 0.09s linear, opacity 0.2s;
}
.mf-runner.mf-me { opacity: 1; z-index: 2; border-radius: 50%; box-shadow: 0 0 0 2px var(--lime), 0 4px 10px rgba(0, 0, 0, 0.5); }
.mf-runner.mf-done:not(.mf-me) { opacity: 0.85; }
.mf-runner.mf-dazed { animation: mfDazed 0.3s ease-in-out infinite alternate; }
.mf-pop {
  position: absolute; z-index: 3; transform: translate(-50%, -100%); pointer-events: none;
  font: 900 18px 'JetBrains Mono', monospace; color: var(--pink); text-shadow: 0 2px 4px #000;
  animation: mfPop 1.5s ease-out forwards;
}
.mf-pop.mf-good { color: var(--lime); }
@keyframes mfPop { from { opacity: 1; margin-top: 0; } to { opacity: 0; margin-top: -36px; } }
@keyframes mfHint { from { box-shadow: inset 0 0 0 2px var(--cyan); } to { box-shadow: inset 0 0 0 4px rgba(61, 220, 255, 0.35); } }
@keyframes mfDazed { from { transform: translate(-50%, -50%) rotate(-12deg); } to { transform: translate(-50%, -50%) rotate(12deg); } }
.mf-room { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; width: min(100%, 860px); }
.mf-chip {
  display: flex; align-items: center; gap: 6px; padding: 3px 10px 3px 4px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel); font-size: 0.85rem;
}
.mf-chip.mf-chip-me { border-color: var(--violet); }
.mf-chip.mf-chip-across { border-color: var(--lime); }
.mf-chip-name { font-weight: 800; max-width: 9ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mf-chip-pts { font-family: 'JetBrains Mono', monospace; color: var(--cyan); font-weight: 700; }
.mf-chip-mark { min-width: 1.2em; }

/* ---------- Trap Run ---------- */
.tr-wrap { gap: 8px; }
.tr-head { display: flex; align-items: baseline; gap: 16px; width: min(100%, 760px); font-family: 'JetBrains Mono', monospace; }
.tr-points { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 900; min-width: 4ch; text-align: right; }
.tr-lap { color: var(--cyan); font-weight: 800; font-size: 0.95rem; letter-spacing: 0.06em; }
.tr-count { color: var(--muted); font-weight: 800; font-size: 0.95rem; margin-left: auto; }
.tr-title { font-size: clamp(1.3rem, 3vw, 1.8rem); font-weight: 900; letter-spacing: 0.02em; color: var(--text); line-height: 1.1; }
.tr-lit { color: var(--lime); }
.tr-bar { width: min(100%, 760px); height: 6px; border-radius: 999px; background: var(--panel-strong); overflow: hidden; }
.tr-fill { height: 100%; width: 0; background: var(--grad); border-radius: inherit; }
.tr-bar.tr-late .tr-fill { background: var(--pink); }
.tr-prompt { min-height: 1.4em; font-size: 0.95rem; font-weight: 600; color: var(--muted); text-align: center; }
.canvas-host.tr-host { width: min(100%, 860px); min-height: 220px; }
.tr-host canvas { border-radius: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); }
.tr-wrap[data-phase="place"] .tr-host canvas { cursor: crosshair; }
.tr-tools { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; min-height: 40px; }
.tr-kind {
  font: inherit; font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 0.85rem; letter-spacing: 0.04em;
  padding: 8px 14px; border-radius: 12px; border: 1px solid var(--line);
  background: var(--panel-strong); color: var(--text); cursor: pointer;
  transition: transform 0.12s var(--ease), border-color 0.12s;
}
.tr-kind:hover { transform: translateY(-1px); border-color: var(--cyan); }
.tr-kind.tr-on { border-color: var(--lime); color: var(--lime); box-shadow: 0 0 0 2px rgba(182, 255, 61, 0.25); }
.tr-room { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; width: min(100%, 860px); }
.tr-chip {
  display: flex; align-items: center; gap: 6px; padding: 3px 10px 3px 4px;
  border-radius: 999px; border: 1px solid var(--line); background: var(--panel); font-size: 0.85rem;
}
.tr-chip.tr-chip-me { border-color: var(--violet); }
.tr-chip.tr-chip-home { border-color: var(--lime); }
.tr-chip-name { font-weight: 800; max-width: 9ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tr-chip-pts { font-family: 'JetBrains Mono', monospace; color: var(--cyan); font-weight: 700; }
.tr-chip-mark { min-width: 1.2em; }
