/* The Vault design-token layer -- colour roles (including the Spice
   heat and Depth water vocabularies), type scale, spacing, radii,
   elevation, motion, focus and touch minimums. Declares the vocabulary
   only; it restyles nothing on its own, and it keeps the seven legacy
   names below working by re-pointing them at tokens.

   @import rather than a <link> because this wave may not edit HTML.
   The intended final wiring is a <link> before style.css on every page
   (docs/handoff/F-wave1-needs-markup.md); adding it makes this import a
   cache hit rather than a conflict, so the two are safe together.
   MUST stay the first rule in this file -- @import is only valid before
   any other rule. */
@import url("vaultTokens.css");

/* The seven legacy names, re-pointed at tokens. Every existing rule in
   this file, in vaultControls.css and in every game screen keeps saying
   var(--gold) and now gets its value from one place. Each var() carries
   the original literal as its fallback, so if vaultTokens.css ever fails
   to load the app degrades to exactly the palette it had before the
   token layer existed rather than rendering unstyled. */
:root {
  --bg: var(--vault-ground, #0b0c14);
  --bg2: var(--vault-surface, #14162a);
  --gold: var(--vault-gold, #f3c94a);
  --text: var(--vault-text, #f5f4f2);
  --muted: var(--vault-text-muted, #9a9bb0);
  --good: var(--vault-yes, #4ade80);
  --bad: var(--vault-no, #f87171);
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overscroll-behavior: none;
}

#app {
  max-width: 480px;
  margin: 0 auto;
  min-height: 100vh;
  padding: 24px 20px 40px;
  display: flex;
  flex-direction: column;
  /* Session shell swipe gesture (master build brief Priority 4, item 4):
     tells the browser it may still handle vertical panning itself, but
     must NOT claim a horizontal drag as its own pan/scroll gesture -- #app
     has no horizontal overflow to scroll anyway, so this has no visible
     effect on its own, it only stops the browser swallowing the pointer
     sequence before client.js's own horizontal-swipe listener sees a
     pointerup. Without this, several mobile browsers cancel an in-progress
     pointer drag (dispatching pointercancel) the moment they decide it
     looks like a pan, which would make the real swipe-to-open/close
     gesture unreliable on exactly the touch devices it's for. */
  touch-action: pan-y;
}

.screen { display: flex; flex-direction: column; gap: 14px; flex: 1; animation: screen-fade-in 220ms ease; }
.hidden { display: none !important; }
/* Every screen switch re-triggers this on the newly-shown section (browsers
   restart CSS animations on display:none -> non-none) -- a purely additive
   fade-in with zero layout risk, since the outgoing screen still leaves
   flow instantly via display:none as before. Overnight competitor
   research (docs/research/OVERNIGHT_COMPETITOR_UX_PATTERNS.md #1) flagged
   the previous zero-transition hard-cut as the single most visible
   "prototype" tell against shipped party-game products. */
@keyframes screen-fade-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .screen { animation: none; } }

h1 {
  font-size: 44px;
  letter-spacing: 6px;
  text-align: center;
  color: var(--gold);
  margin: 30px 0 0;
}
.tagline { text-align: center; color: var(--muted); margin-top: -6px; margin-bottom: 10px; }

h2 { text-align: center; margin: 8px 0; }

.card {
  background: var(--bg2);
  border-radius: 14px;
  padding: 14px 16px;
}
.card label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.photo-preview-row { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.photo-preview-img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; border: 2px solid var(--gold); }
.ai-provider-row { display: flex; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
/* A CHOICE ROW SHOULD LOOK LIKE A CHOICE.
   .btn is `display:block; width:100%`, so every button in these flex
   rows took a 100% flex basis and wrapped onto its own line: "Quick /
   Standard / Deep" rendered as three stacked full-width bars, and so did
   Claude / ChatGPT / Other, and the four "How should Vault refer to you"
   options, and Copy / Open my AI. Six three-way choices, eighteen
   full-width bars, and a lobby you had to scroll five screens of.
   Letting them size to their content and share the line makes a
   three-way choice read as one control instead of three commands.
   Measured 2026-09-09 at 375x812: lobby height 2142px -> 1947px, no
   horizontal overflow, and because flex shrink stays on, adding the
   selected tick to the widest label makes its neighbours give way
   (Quick/Standard/Deep = 81+127+78px selected) rather than wrapping. */
.ai-provider-row .btn {
  width: auto;
  flex: 1 1 auto;
  min-width: 0;
  padding-left: 10px;
  padding-right: 10px;
}
/* Depth/provider selection. The selected LOOK deliberately no longer
   lives here: it is the platform-wide CHOICE = STAYS LIT state in
   vaultControls.css, applied via `class="btn ghost vault-choice depth-btn"`.
   The rule that used to sit here (`.depth-btn.on`, specificity 0,2,0) was
   silently beaten by `.btn.ghost` further down this same file (also 0,2,0,
   declared later), which is exactly why human testing on 2026-09-08 saw
   "DEEP is selected but only a tiny tick appears" -- see vaultControls.css's
   header for the full explanation and the specificity fix. */
.selected-summary { color: var(--good); font-weight: 700; }
.theatre-controls-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-bottom: 4px; }
.theatre-toggle-btn { flex: 0 0 auto; width: auto; background: var(--bg2); border: 1px solid #333654; color: var(--muted); font-size: 11px; font-weight: 600; padding: 8px 12px; border-radius: 10px; }
/* The ON look for these three toggles is now the platform-wide
   CHOICE = STAYS LIT state in vaultControls.css (they carry
   class="theatre-toggle-btn vault-choice" and are driven by
   VaultUiSelection.setSelected) -- one selected look, everywhere. */
.ai-connect-textarea { width: 100%; min-height: 90px; background: var(--bg); border: 1px solid var(--vault-edge-control, #5b60a0); border-radius: 10px; color: var(--text); font-size: 13px; padding: 10px; margin: 6px 0; resize: vertical; font-family: inherit; }
.ai-connect-textarea:focus { border-color: var(--gold); }
.ai-connect-ready { color: var(--good); font-weight: 700; }
.auto-connect-toggle { font-size: 12px; text-align: left; width: 100%; }
.auto-connect-steps { text-align: left; margin-top: 6px; line-height: 1.6; }
.card input {
  width: 100%;
  background: transparent;
  border: none;
  /* 3:1 boundary: this underline is the only thing that says "there is a
     field here", and #333654 measures 1.67:1 against the page. */
  border-bottom: 2px solid var(--vault-edge-control, #5b60a0);
  color: var(--text);
  font-size: 20px;
  padding: 6px 2px;
  /* A text field is a pointer target too: measured 303x37 before this. */
  min-height: var(--vault-touch-min, 44px);
  outline: none;
}
/* `outline: none` above kills the browser default. The platform focus
   ring in vaultControls.css puts a real one back for keyboard users
   (input:focus-visible, equal specificity, later stylesheet) -- stated
   here so the two are not read as contradicting each other. */
.card input:focus { border-bottom-color: var(--gold); }

.btn {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 16px;
  font-size: 17px;
  font-weight: 600;
  border-radius: 14px;
  border: none;
  background: #23264a;
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  text-align: center;
}
.btn.primary { background: var(--gold); color: #1a1408; }
.btn.ghost { background: transparent; border: 1px solid #333654; color: var(--muted); font-weight: 400; }
.btn:disabled { opacity: 0.5; }
.or { text-align: center; color: var(--muted); font-size: 13px; }

.hint { text-align: center; color: var(--muted); font-size: 13px; }
.error { color: var(--bad); text-align: center; font-size: 13px; min-height: 16px; }
/* Validation-detail line (2026-09-08, master build brief #33/#68): shows
   exactly which field/rule a rejected import failed on, so a real future
   failure is self-diagnosable on the spot instead of requiring a
   screenshot round-trip days later. Deliberately small/muted -- a
   supporting technical detail under the main honest error message, not a
   second alarming error of its own. */
.error-details { color: var(--muted); text-align: center; font-size: 11px; font-family: monospace; word-break: break-word; }

/* Grouped import-failure list (2026-09-08). One bullet per DISTINCT
   problem, in plain language, ABOVE the monospace technical detail --
   the diagnosis rather than the symptoms. A single real mistake (a genuine
   ChatGPT capsule using 0-100 percentages where Vault wants 0-1 decimals)
   used to arrive here as 43 near-identical clauses in one paragraph; these
   are the lines a player can actually act on, so they read as ordinary
   prose rather than validator output. */
.error-summary {
  margin: 4px 0 2px;
  padding-left: 20px;
  text-align: left;
  color: var(--bad);
  font-size: 12.5px;
  line-height: 1.45;
  list-style: disc;
}
.error-summary li { margin: 2px 0; }
/* A compact disclosure control sitting under an error -- small enough not
   to compete with the Import/Cancel buttons beside it. */
.btn.tiny { font-size: 11.5px; padding: 5px 10px; min-height: 0; align-self: center; }

#qrImg { width: 200px; height: 200px; align-self: center; border-radius: 10px; background: #fff; padding: 8px; }
#roomCode { color: var(--gold); letter-spacing: 4px; }

.player-list { display: flex; flex-direction: column; gap: 6px; }
.player-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; background: var(--bg2); padding: 10px 14px; border-radius: 10px; }
.player-row-photo { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; vertical-align: middle; margin-right: 8px; }
.player-row-identity { display: flex; align-items: center; }
.player-row .status { color: var(--good); font-size: 12px; }
.player-row .status.off { color: var(--muted); }
.checkin-row { margin: -2px 0 4px 8px; }
.checkin-row .btn { font-size: 11px; padding: 6px 10px; }
.checkin-qr-box { text-align: center; margin-top: 8px; }
.checkin-qr-box img { width: 140px; height: 140px; background: #fff; border-radius: 10px; padding: 6px; }
.checkin-qr-box .hint { font-size: 10px; word-break: break-all; }
.host-badge { color: var(--gold); font-size: 10px; font-weight: 800; letter-spacing: 1px; margin-left: 6px; vertical-align: middle; }
.vault-alias-tag { display: block; color: var(--muted); font-size: 11px; font-style: italic; margin-top: 2px; }
.capability-badge { font-size: 10px; font-weight: 700; letter-spacing: 0.5px; padding: 2px 6px; border-radius: 6px; margin-right: 8px; }
.capability-badge.cap-deep { background: #2a2410; color: var(--gold); }
.capability-badge.cap-fast { background: #16241c; color: var(--good); }
.capability-badge.cap-not_connected { background: transparent; color: var(--muted); border: 1px solid #333654; }

.round-label { text-align: center; color: var(--muted); font-size: 13px; letter-spacing: 2px; }

.option-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.option-grid.big { grid-template-columns: 1fr; }
.option-grid button {
  padding: 22px 14px;
  font-size: 18px;
  font-weight: 700;
  border-radius: 16px;
  border: 2px solid #333654;
  background: var(--bg2);
  color: var(--text);
  cursor: pointer;
}
/* Persistent colour selection (master build brief Priority 5): fill +
   colour + border + a light elevation lift, so a chosen answer stays
   visibly picked out, not just tinted. */
.option-grid button.selected { border-color: var(--gold); background: #2a2410; color: var(--gold); transform: translateY(var(--vault-lift-answer, -2px)); box-shadow: var(--vault-elev-answer, 0 4px 12px rgba(243, 201, 74, 0.3)); }
.option-grid button:disabled { opacity: 0.55; }
/* Without this, the button a player just picked immediately looks as dim
   as every OTHER (unpicked) disabled option the instant it locks -- since
   every button in the grid is disabled together on click (see client.js),
   plain :disabled above would otherwise win the cascade (equal specificity,
   later rule) and undo the whole point of .selected/.locked. */
.option-grid button.selected:disabled, .option-grid button.locked:disabled { opacity: 1; }

/* LOCK CLUNK (master build brief Priority 5): the settled state an answer
   moves into once it's genuinely locked (auto-locked on submission for
   MASK/BESTIES/GUESS WHO -- see client.js's lockAnswerButton) -- a
   distinct, subdued-but-still-filled look from the brighter momentary
   .selected fill, plus a quick scale-down-then-settle "clunk" on arrival.
   Kept at the same elevation as .selected (translateY(-2px)) rather than
   sinking back down, so a locked answer still visibly outranks the
   now-disabled options around it. */
.option-grid button.locked {
  border-color: var(--gold);
  background: var(--vault-gold-ink-deep, #211b0c);
  color: var(--vault-gold-soft, #d9b64a);
  font-weight: 700;
  transform: translateY(-2px);
  box-shadow: inset 0 0 0 1px var(--vault-gold-ring, rgba(243, 201, 74, 0.55));
  animation: lock-clunk 280ms ease-out;
}
/* 2026-09-09: the locked card's text was --muted grey (6.27:1 on this
   fill). That is the colour this codebase uses for "not important" --
   so the one answer the player had just committed to read as the most
   greyed-out thing on screen, which is the exact inversion of spec
   section 9's "LOCKED CHOICE: becomes visibly stronger". It is now a
   settled gold at 8.76:1 with an inset gold ring: quieter than the
   bright pre-lock fill, but unmistakably still theirs. */
@keyframes lock-clunk {
  0% { transform: translateY(-2px) scale(1); }
  45% { transform: translateY(-2px) scale(0.88); }
  75% { transform: translateY(-2px) scale(1.05); }
  100% { transform: translateY(-2px) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .option-grid button.locked { animation: none; }
}

/* THE VISIBLE 5-4-3-2-1 (theatre contract, spec section 8).
   Was a hard-coded 160px. That is roughly right on the 375px phone it
   was tuned on and wrong everywhere else -- the same markup renders on
   a laptop and on a television, where 160px stops reading as an event
   and starts reading as a large number. It is now fluid on vmin, so it
   occupies the same share of the screen on every device: measured
   157.5px at 375x812 (within 3px of the fixed 160px it replaces) and
   240px at 1280x800 and above.
   Tabular figures matter more than they sound: without them "4" and
   "1" have different advance widths and the digit visibly jumps
   sideways on every tick. */
.countdown-number {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--vault-type-countdown, clamp(110px, min(42vmin, 36vh), 240px));
  font-weight: 800;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-shadow: 0 0 40px rgba(243, 201, 74, 0.28);
}
/* READY FOR THE PER-TICK BEAT, currently inert. client.js's
   startLocalCountdown sets textContent and nothing else, so there is no
   class change for CSS to hang a per-tick animation on. Adding one line
   there (toggle "countdown-tick" off and back on around each
   textContent write) turns this on with no further CSS -- see
   docs/handoff/F-wave1-needs-markup.md. Deliberately NOT faked with an
   infinite 1s loop: an animation running free next to a real timer
   drifts out of phase within a couple of seconds and reads as cheap. */
.countdown-number.countdown-tick { animation: countdown-tick 420ms var(--vault-ease-out, ease-out); }
@keyframes countdown-tick {
  0% { transform: scale(1.18); opacity: 0.4; }
  60% { transform: scale(1); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .countdown-number.countdown-tick { animation: none; }
}

#screen-reveal { align-items: center; justify-content: center; text-align: center; }
/* BIG CHECK / BIG CROSS (spec section 8). Same reasoning as the
   countdown: measured 97x173px at 375x812 with the old fixed 130px,
   which is a good phone size and a small television one. Fluid on vmin
   makes it slightly bigger on a phone (157px) and genuinely giant on a
   shared screen (300px), from the same markup. */
.reveal-mark { font-size: var(--vault-type-mark, clamp(130px, 42vmin, 300px)); font-weight: 900; line-height: 1.1; }
.reveal-mark.correct { color: var(--good); text-shadow: 0 0 60px rgba(74, 222, 128, 0.35); }
.reveal-mark.wrong { color: var(--bad); text-shadow: 0 0 60px rgba(248, 113, 113, 0.35); }
.reveal-vault { font-size: 20px; color: var(--text); margin-top: 4px; }
.reveal-vault b { color: var(--gold); }

/* Giant reveal mark OVERLAY (master build brief Priority 5): MASK/BESTIES/
   GUESS WHO's own equivalent of Vault Verdict Classic's #revealMark above --
   reuses the exact same .reveal-mark class/sizing rather than a second,
   differently-sized mark, but as a layered near-full-screen overlay (see
   showGiantRevealMark() in client.js) rather than replacing those games'
   reveal screens, which show real per-player result content underneath
   that this must never block. pointer-events:none throughout its whole
   lifecycle (showing AND fading) -- it is a brief flourish that dismisses
   itself, never a modal a player has to act on. */
.reveal-mark-overlay {
  position: fixed;
  inset: 0;
  z-index: 470;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(4, 3, 10, 0.55);
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms ease;
}
.reveal-mark-overlay.reveal-mark-visible { opacity: 1; }
/* THE MARK LANDS, it does not fade up. Before this the backdrop faded
   and the glyph simply appeared inside it -- correct, but flat, and the
   moment it exists for ("BIG CHECK / BIG CROSS", spec section 8) is the
   loudest beat in a round. One overshoot-and-settle, ~320ms, no bounce
   after it: theatrical, not cartoonish. This hangs off the SAME
   .reveal-mark-visible class client.js already toggles, so it needs no
   JS change at all. */
.reveal-mark-overlay.reveal-mark-visible .reveal-mark {
  animation: reveal-mark-land var(--vault-motion-slow, 320ms) var(--vault-ease-clunk, cubic-bezier(0.34, 1.4, 0.64, 1));
}
@keyframes reveal-mark-land {
  0% { transform: scale(0.4); opacity: 0; }
  70% { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .reveal-mark-overlay { transition: none; }
  .reveal-mark-overlay.reveal-mark-visible .reveal-mark { animation: none; }
}

.confidence-bar { width: 80%; height: 10px; background: var(--bg2); border-radius: 6px; overflow: hidden; margin-top: 10px; }
.confidence-fill { height: 100%; background: var(--gold); width: 0%; transition: width 1.2s ease; }

.commentary-line { text-align: center; font-size: 22px; margin-top: 40px; padding: 0 10px; }
.callback-line { text-align: center; font-size: 13px; color: var(--gold); margin-top: 6px; padding: 0 10px; }
#autoAdvanceHint { margin-top: -6px; }

/* Device choreography PoC (docs/08 Part 11) -- overlay, sits above
   whatever screen is currently showing; the game underneath is
   untouched and simply reappears when the overlay is dismissed. */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(4, 3, 10, 0.96);
  z-index: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 18px;
  text-align: center;
  padding: 24px;
}
#devTriggerCallBtn.dev-only {
  position: fixed;
  bottom: 10px;
  left: 10px;
  right: 10px;
  z-index: 400;
  max-width: 440px;
  margin: 0 auto;
}

/* --- Shell header: professional platform, not a dev tool --- */
.shell-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 2px 14px;
  font-size: 12px;
  color: var(--muted);
  border-bottom: 1px solid #1c1e38;
  margin-bottom: 10px;
}
.shell-brand { color: var(--gold); font-weight: 800; letter-spacing: 2px; }
.shell-room { letter-spacing: 2px; }
.shell-game-name { margin-left: auto; color: var(--text); font-weight: 600; }
.shell-back-btn { background: transparent; border: 1px solid #333654; color: var(--muted); font-size: 11px; padding: 4px 10px; border-radius: 8px; cursor: pointer; }
.shell-wager-btn { background: transparent; border: 1px solid #333654; color: var(--text); font-size: 14px; padding: 3px 9px; border-radius: 8px; cursor: pointer; }
.shell-end-btn { background: transparent; border: 1px solid #5a2a2a; color: #d98a8a; font-size: 11px; padding: 4px 10px; border-radius: 8px; cursor: pointer; margin-left: 8px; }

.mix-bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; background: #201a08; border: 1px solid var(--gold); border-radius: 10px; padding: 8px 14px; margin-bottom: 10px; font-size: 12px; }
.mix-bar span:first-child { color: var(--gold); font-weight: 700; letter-spacing: 0.5px; }
.mix-bar .btn { font-size: 12px; padding: 6px 12px; }
.mix-finale-standings { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 420px; margin: 14px auto; }
.mix-finale-row { display: flex; justify-content: space-between; background: var(--bg2); padding: 12px 18px; border-radius: 10px; font-size: 16px; }
.mix-finale-row .pts { color: var(--gold); font-weight: 800; }
.mix-finale-row.winner { border: 2px solid var(--gold); background: #201a08; }

.connection-banner {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 400;
  background: var(--bad);
  color: #2a0e0e;
  font-weight: 700;
  font-size: 13px;
  text-align: center;
  padding: 8px 12px;
}

.wager-live-badge {
  position: fixed;
  top: 6px;
  right: 4vw;
  z-index: 300;
  background: var(--gold);
  color: #1a1408;
  font-weight: 800;
  font-size: 12px;
  padding: 6px 14px;
  border-radius: 20px;
  border: none;
  cursor: pointer;
  animation: wager-badge-pulse 1.6s ease-in-out infinite;
}
@keyframes wager-badge-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
/* The badge covers three states since the 2026-09-13 wager pass, not one:
   a proposal waiting on THIS player (needs-answer), a proposal waiting on
   someone else, and a locked-in live wager. needs-answer is the only one
   that is an instruction to the person holding the phone, so it is the
   only one that looks like a button rather than a status light. */
.wager-live-badge.needs-answer {
  background: var(--bad, #e2564d);
  color: #fff;
  box-shadow: 0 0 0 3px rgba(226, 86, 77, 0.28);
}

.wager-card { background: var(--bg2); border-radius: 18px; padding: 22px 20px; max-width: 420px; width: 100%; display: flex; flex-direction: column; gap: 14px; }
.wager-card h2 { margin: 0 0 4px; text-align: center; }
.wager-preset-list { display: flex; flex-direction: column; gap: 8px; }
.wager-preset-list button { padding: 14px 16px; font-size: 15px; font-weight: 600; border-radius: 12px; border: 2px solid #333654; background: var(--bg); color: var(--text); cursor: pointer; text-align: left; }
.wager-preset-list button .wager-preset-desc { display: block; font-size: 11px; font-weight: 400; color: var(--muted); margin-top: 2px; }
.wager-preset-list button.selected { border-color: var(--gold); }
.wager-scope-toggle { display: flex; gap: 8px; }
/* padding was 10px, which made these 140x40 -- under the touch minimum,
   measured at 375x812. 12px takes them to 44px exactly, without needing
   the expanded-hit-area trick, because unlike the shell header this row
   has the space. Used by BOTH the wager scope toggle and the SHOUT
   Recognised/Incognito toggle. */
.wager-scope-toggle .btn { flex: 1; padding: 12px 10px; font-size: 13px; }
.wager-scope-toggle .btn.selected { border-color: var(--gold); color: var(--gold); }
/* The one line that says why a wager action was refused. Amber rather
   than red: "too late for this round" is information, not an error the
   player did anything wrong to cause. */
.wager-notice { color: var(--gold); text-align: center; margin: 0; }
.wager-response-list { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.wager-response-chip { padding: 6px 12px; border-radius: 20px; font-size: 12px; background: var(--bg); color: var(--muted); }
.wager-response-chip.accepted { color: var(--good); border: 1px solid var(--good); }
.wager-response-chip.declined { color: var(--bad); border: 1px solid var(--bad); }
/* PENDING is "still deciding" and ABSENT is "wasn't reachable" -- two very
   different things that used to render identically, as unstyled grey. */
.wager-response-chip.pending { color: var(--text); border: 1px dashed #4a4e73; }
.wager-response-chip.absent { color: var(--muted); border: 1px dotted #4a4e73; font-style: italic; }

/* --- Live player rail (master brief §11): compact, always-legible row of
     every player's current-game score, tap to see session totals. --- */
/* padding was `2px 2px 12px`. `overflow-x: auto` forces overflow-y to
   compute to `auto` as well, so the rail is a scroll container that
   CLIPS its children vertically -- which quietly cut the rail toggle's
   expanded hit area down to 68x38. Balancing the padding gives the hit
   area room to reach 44px while costing the sacred GAME panel only 2px
   of height (44px rail -> 46px). */
.player-rail { display: flex; gap: 8px; overflow-x: auto; padding: 8px 2px; margin-bottom: 4px; }
.player-rail.hidden { display: none; }
.rail-chip { position: relative; flex: 0 0 auto; background: var(--bg2); border-radius: 10px; padding: 6px 10px; font-size: 12px; display: flex; align-items: center; gap: 6px; border: 1px solid transparent; transition: border-color 150ms ease, transform 150ms ease; }
.rail-chip .rail-name { font-weight: 600; }
.rail-photo { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; vertical-align: middle; margin-right: 4px; }
.rail-chip .rail-score { color: var(--gold); font-weight: 700; display: inline-block; }
/* Score/reveal animation (master build brief Priority 5): a brief
   scale-pulse on the score NUMBER itself, not just the existing floating
   "+N" .rail-delta bubble above -- applied by renderPlayerRail (client.js)
   only on the render where a player's score just went up, so it plays once
   per real change rather than looping. display:inline-block on the base
   .rail-score rule above is required for the scale transform to apply
   predictably (a bare inline span's transform support is inconsistent). */
.rail-score.rail-score-pop { animation: rail-score-pop 500ms ease; }
@keyframes rail-score-pop { 0% { transform: scale(1); } 40% { transform: scale(1.5); color: var(--good); } 100% { transform: scale(1); } }
/* SCORE REACTS VISIBLY (spec section 8). The number's own pop was easy
   to miss on a phone -- the rail chip is ~30px tall and the digit inside
   it is 12px, so at arm's length the only thing that moved was a
   character. The whole chip now takes one green ring as well, which is
   what a player actually sees out of the corner of their eye while they
   are looking at the reveal. :has() is a progressive enhancement: where
   it is unsupported the whole rule is dropped and the number's own pop
   still plays exactly as before. */
.rail-chip:has(.rail-score-pop) { animation: rail-chip-score 600ms var(--vault-ease-out, ease-out); }
@keyframes rail-chip-score {
  0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.6); }
  100% { box-shadow: 0 0 0 12px rgba(74, 222, 128, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .rail-score.rail-score-pop { animation: none; }
  .rail-chip:has(.rail-score-pop) { animation: none; }
}
.rail-chip.rail-locked { border-color: #333654; }
.rail-chip.rail-correct { border-color: var(--good); transform: scale(1.05); }
.rail-chip.rail-wrong { border-color: var(--bad); }
.rail-chip.rail-offline { opacity: 0.5; }
.rail-delta { position: absolute; top: -18px; right: -4px; color: var(--good); font-weight: 800; font-size: 12px; animation: rail-delta-rise 1.4s ease forwards; pointer-events: none; }
@keyframes rail-delta-rise { 0% { opacity: 0; transform: translateY(4px); } 20% { opacity: 1; transform: translateY(0); } 80% { opacity: 1; } 100% { opacity: 0; transform: translateY(-10px); } }
.rail-toggle { flex: 0 0 auto; background: transparent; border: 1px dashed #333654; color: var(--muted); border-radius: 10px; padding: 6px 10px; font-size: 11px; }

/* --- Game selector --- */
.game-list { display: flex; flex-direction: column; gap: 10px; }
.game-list button {
  padding: 20px 16px;
  font-size: 19px;
  font-weight: 700;
  border-radius: 16px;
  border: 2px solid #333654;
  background: var(--bg2);
  color: var(--text);
  cursor: pointer;
  text-align: left;
}
.game-list button:active { border-color: var(--gold); }
.game-list .game-tagline { display: block; font-size: 12px; font-weight: 400; color: var(--muted); margin-top: 4px; }

.display-qr { width: 180px; height: 180px; align-self: center; display: block; margin: 0 auto; background: #fff; border-radius: 10px; padding: 8px; }
.display-url { text-align: center; color: var(--muted); font-size: 12px; word-break: break-all; }

.settings-row { margin-bottom: 14px; }
.settings-row:last-of-type { margin-bottom: 4px; }
.settings-row label { display: flex; justify-content: space-between; font-size: 13px; color: var(--muted); margin-bottom: 6px; }
.settings-row input[type="range"] { width: 100%; accent-color: var(--gold); }

.standings-row { display: flex; justify-content: space-between; padding: 6px 0; }
.standings-row .pts { color: var(--gold); font-weight: 700; }

/* --- MASK --- */
.mask-target-label { font-weight: 700; color: var(--gold); }
.option-grid.mask-scale { grid-template-columns: 1fr; gap: 8px; }
.option-grid.mask-scale button { padding: 16px 14px; font-size: 16px; text-align: left; }

.predictor-status-list { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.predictor-chip { padding: 6px 12px; border-radius: 20px; font-size: 12px; background: var(--bg2); color: var(--muted); }
.predictor-chip.locked { color: var(--good); border: 1px solid var(--good); }
.predictor-chip.thinking { color: var(--muted); border: 1px solid #333654; }

.mask-locked-pulse {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 90px;
  color: var(--gold);
  animation: mask-pulse 0.7s ease-in-out infinite;
}
@keyframes mask-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }

.mask-reveal-answer {
  text-align: center;
  font-size: 24px;
  font-weight: 700;
  color: var(--gold);
  margin: 10px 0 6px;
}
.mask-predictor-results { display: flex; flex-direction: column; gap: 8px; }
.mask-predictor-row { display: flex; justify-content: space-between; align-items: center; background: var(--bg2); padding: 10px 14px; border-radius: 10px; }
.mask-predictor-row .mark { font-weight: 900; font-size: 18px; }
.mask-predictor-row .mark.correct { color: var(--good); }
.mask-predictor-row .mark.wrong { color: var(--bad); }
.mask-predictor-row.self { border: 1px solid var(--gold); background: #1c1a10; }

#screen-mask-verdict { align-items: center; justify-content: center; text-align: center; }
.mask-verdict-banner {
  font-size: 26px;
  font-weight: 900;
  padding: 18px 14px;
  border-radius: 16px;
  background: var(--bg2);
  border: 2px solid #333654;
}
/* Escalating theatre tiers -- gentle to alarming, matching device intensity 1-5 */
.mask-verdict-banner.tier-1, .mask-verdict-banner.tier-2 { color: var(--text); }
.mask-verdict-banner.tier-3 { color: var(--gold); border-color: var(--gold); }
.mask-verdict-banner.tier-4 { color: var(--bad); border-color: var(--bad); }
.mask-verdict-banner.tier-5 {
  color: #fff;
  background: #4a0d12;
  border-color: var(--bad);
  animation: mask-warning-flash 0.5s ease-in-out 3;
}
@keyframes mask-warning-flash { 0%, 100% { background: #4a0d12; } 50% { background: #8c1a24; } }
.mask-deep-dive-text { max-width: 90%; margin: 0 auto; }

/* --- QUICK RANK: progressive ladder, bottom (#N) to top (#1) --- */
.quickrank-ladder { display: flex; flex-direction: column-reverse; gap: 8px; }
.quickrank-rung { background: var(--bg2); border-radius: 12px; padding: 12px 16px; display: flex; align-items: center; gap: 12px; opacity: 0; animation: rung-in 350ms ease forwards; }
.quickrank-rung .rung-place { font-size: 22px; font-weight: 900; color: var(--muted); min-width: 34px; }
.quickrank-rung.rung-1 { border: 2px solid var(--gold); }
.quickrank-rung.rung-1 .rung-place { color: var(--gold); }
.quickrank-rung .rung-name { flex: 1; font-weight: 600; }
.quickrank-rung .rung-nodata { font-size: 11px; color: var(--muted); }
@keyframes rung-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

/* --- GUESS WHOSE PROFILE --- */
.gwp-clue-list { display: flex; flex-direction: column; gap: 10px; }
.gwp-clue { background: var(--bg2); border-radius: 12px; padding: 14px 16px; font-size: 16px; opacity: 0; animation: rung-in 350ms ease forwards; }
.gwp-clue .clue-num { color: var(--gold); font-weight: 800; margin-right: 6px; }
.gwp-points-hint { color: var(--gold); font-weight: 700; }

/* =====================================================================
   SESSION SHELL: "<- PEOPLE | GAME | WAGERS ->" (master build brief
   Priority 4 -- see docs/OVERNIGHT_MASTER_PRODUCT_BUILD_REPORT.md's own
   gap list, which named this shell, chevron nav, and active-player
   step-forward as not yet built). Purely additive over the existing
   #playerRail / #wagerOverlay surfaces -- see client.js's own section
   comment for exactly what's reused vs. new.
   ===================================================================== */

/* Off-canvas via transform + visibility, deliberately NOT the shared
   .hidden class above (display:none) -- .hidden would make the slide
   transition invisible (an instant cut, exactly the "prototype tell"
   the screen-fade-in animation at the top of this file was added to
   avoid). visibility+pointer-events keeps a closed panel out of the tab
   order and unclickable without fighting the transform transition the
   way display:none would. Toggled by the .side-panel-open modifier class
   client.js applies -- see setOpenSidePanel(). */
.side-panel {
  position: fixed;
  top: 0;
  bottom: 0;
  width: 84%;
  max-width: 340px;
  background: var(--bg2);
  z-index: 450;
  overflow-y: auto;
  padding: 18px 16px 24px;
  visibility: hidden;
  pointer-events: none;
  transition: transform 260ms ease, visibility 0s linear 260ms;
  box-shadow: 0 0 24px rgba(0, 0, 0, 0.5);
}
.side-panel.side-panel-open { visibility: visible; pointer-events: auto; transition: transform 260ms ease; }
.side-panel-left { left: 0; transform: translateX(-100%); border-right: 1px solid #1c1e38; }
.side-panel-left.side-panel-open { transform: translateX(0); }
.side-panel-right { right: 0; transform: translateX(100%); border-left: 1px solid #1c1e38; }
.side-panel-right.side-panel-open { transform: translateX(0); }
/* Respect prefers-reduced-motion (item 4's explicit requirement): the
   panel still opens/closes instantly via the transform change above, it
   just no longer animates getting there. */
@media (prefers-reduced-motion: reduce) {
  .side-panel, .side-panel.side-panel-open { transition: none; }
}

.side-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.side-panel-title { color: var(--gold); font-weight: 800; letter-spacing: 2px; font-size: 14px; }
.side-panel-close-btn { background: transparent; border: 1px solid #333654; color: var(--muted); font-size: 18px; line-height: 1; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; }

/* Real, generously hit-testable (30x64px), cursor-clickable chevrons
   fixed to the screen edges -- REAL <button> elements (item 3), not
   swipe-only affordances; the swipe/drag gesture (item 4) is an
   ADDITIONAL way to reach the same panels, never a replacement for
   these. Light-coloured LEFT / contrasting-colour RIGHT per the brief's
   own spec -- the colour difference doubles as a plain hint that the two
   sides do different things before a player has ever tapped either. */
.session-chevron {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 460;
  width: 30px;
  height: 64px;
  border: none;
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.session-chevron-left { left: 0; background: rgba(245, 244, 242, 0.16); color: var(--text); border-radius: 0 12px 12px 0; }
.session-chevron-right { right: 0; background: var(--gold); color: #1a1408; border-radius: 12px 0 0 12px; }

/* --- PEOPLE panel body: expanded player-rail (item 1) --- */
.people-panel-list { display: flex; flex-direction: column; gap: 8px; }
.people-panel-row { display: flex; align-items: center; gap: 10px; background: var(--bg); border: 1px solid transparent; border-radius: 12px; padding: 10px 12px; cursor: pointer; transition: border-color 150ms ease, transform 150ms ease; }
.people-panel-photo { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; background: var(--bg2); display: inline-block; }
.people-panel-identity { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.people-panel-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.people-panel-scores { font-size: 11px; color: var(--muted); }
.people-panel-scores b { color: var(--gold); }
.people-panel-row.rail-offline { opacity: 0.5; }

.people-profile-view { text-align: center; padding-top: 8px; position: relative; }
.people-profile-back-btn { position: absolute; top: -6px; left: 0; }
.people-profile-photo { width: 96px; height: 96px; border-radius: 50%; object-fit: cover; margin: 18px auto 10px; display: block; border: 2px solid var(--gold); }
.people-profile-name { font-size: 20px; font-weight: 800; }
.people-profile-badge { margin-top: 4px; }
.people-profile-scores { display: flex; justify-content: center; gap: 24px; margin-top: 18px; }
.people-profile-score-label { display: block; font-size: 11px; color: var(--muted); }
.people-profile-score-value { display: block; font-size: 22px; font-weight: 800; color: var(--gold); }

/* --- Active-player step-forward (item 5): generic across every game with
   a single spotlighted player -- see sessionShell.js's
   currentActivePlayerId(). Applied by client.js to the matching id in
   BOTH the existing compact rail chip and the new PEOPLE panel row; no
   chip carries either class when currentActivePlayerId() returns null
   (no game active, or the active game has nobody spotlighted). --- */
.rail-chip.rail-active { transform: translateY(-3px) scale(1.1); border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold), 0 4px 10px rgba(243, 201, 74, 0.35); z-index: 1; }
.people-panel-row.people-row-active { border-color: var(--gold); background: #1c1a10; transform: scale(1.03); box-shadow: 0 0 0 1px var(--gold); }

/* =====================================================================
   WHISPER / SHOUT (master build brief Priority 6). Backend/protocol were
   already real and tested the night before this task
   (src/social/whisperShoutEngine.js, server.js's whisper_send/shout_send
   handlers) -- everything here is purely the client surface.
   ===================================================================== */

/* --- WHISPER composer: lives inside the existing #peopleProfileView
   (see index.html's own comment on why this surface, not a per-row
   button/action sheet, was the "smaller, safer" choice). --- */
.people-whisper-btn { margin-top: 16px; }
.whisper-composer { margin-top: 12px; text-align: left; }
.whisper-textarea { min-height: 64px; }
.whisper-composer-buttons { display: flex; gap: 8px; }
.whisper-composer-buttons .btn { flex: 1; }

/* --- WHISPER silent visual notification: a small, self-dismissing card,
   NOT a modal (pointer-events: none throughout) -- the recipient never
   has to act on it, matching the brief's own "auto-dismissing" framing.
   The one-shot box-shadow keyframe is the brief's own "edge/light flash;
   visual pulse" -- a light gold ring that expands and fades once on
   arrival, layered on top of the plain opacity/transform fade so a
   whisper reads as visually distinct from the reveal-mark-overlay's own
   plain fade above (WHISPER is a light pulse; that overlay is a hard
   dimmed backdrop) -- appropriate to WHISPER being the quiet, private
   half of this pair. */
.whisper-toast {
  position: fixed;
  top: 14px;
  left: 50%;
  z-index: 465; /* above side panels (450) and chevrons (460), below the reveal-mark overlay (470) and the shout banner (480) -- a private notification should never outrank either */
  max-width: 88%;
  background: var(--bg2);
  border: 1px solid var(--gold);
  border-radius: 14px;
  padding: 10px 18px;
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -16px);
  transition: opacity 260ms ease, transform 260ms ease;
}
.whisper-toast.whisper-toast-visible {
  opacity: 1;
  transform: translate(-50%, 0);
  animation: whisper-edge-pulse 900ms ease-out 1;
}
@keyframes whisper-edge-pulse {
  0% { box-shadow: 0 0 0 0 rgba(243, 201, 74, 0.55); }
  100% { box-shadow: 0 0 0 16px rgba(243, 201, 74, 0); }
}
.whisper-toast-from { display: block; color: var(--gold); font-weight: 700; font-size: 12px; margin-bottom: 2px; }
.whisper-toast-text { display: block; font-size: 14px; }
@media (prefers-reduced-motion: reduce) {
  .whisper-toast { transition: none; }
  .whisper-toast.whisper-toast-visible { animation: none; }
}

/* --- SHOUT floating action button: a fixed FAB rather than a sixth icon
   crowding #shellHeader's single row (see index.html's own placement
   note) -- red/--bad background so it visually reads as the "loud,
   public, everyone sees this" action, distinct from the gold wager
   button. Positioned clear of the vertically-centred edge chevrons and
   the top-pinned wager live badge. --- */
.shout-fab-btn {
  position: fixed;
  right: 16px;
  bottom: 22px;
  z-index: 410;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: none;
  background: var(--bad);
  color: #2a0e0e;
  font-size: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 6px 16px rgba(248, 113, 113, 0.4);
}

.shout-cooldown-hint { color: var(--bad); }

/* --- SHOUT banner: public, prominent, NOT silent (client.js's
   shoutFanfare() plays a real sound cue alongside this) -- a dimmed,
   briefly-flashing backdrop behind a bordered card, genuinely more
   intrusive than the whisper toast above, matching the brief's own
   "may literally interrupt the game for a brief beat" framing while
   staying pointer-events:none throughout (never a modal a player must
   dismiss themselves -- it always self-clears). */
.shout-banner-overlay {
  position: fixed;
  inset: 0;
  z-index: 480; /* above the reveal-mark overlay (470) -- a public shout should never be hidden behind that game's own reveal flourish */
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 16vh;
  background: rgba(74, 15, 15, 0.001); /* effectively transparent until .shout-banner-visible's animation takes over -- keeps the opacity transition below from starting on a jarring solid tint */
  pointer-events: none;
  opacity: 0;
  transition: opacity 220ms ease;
}
.shout-banner-overlay.shout-banner-visible {
  opacity: 1;
  animation: shout-banner-flash 500ms ease-in-out 2;
}
@keyframes shout-banner-flash {
  0%, 100% { background: rgba(74, 15, 15, 0.32); }
  50% { background: rgba(140, 26, 36, 0.5); }
}
.shout-banner-card {
  background: var(--bg2);
  border: 2px solid var(--gold);
  border-radius: 18px;
  padding: 18px 22px;
  max-width: 88%;
  text-align: center;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
}
.shout-banner-from { display: block; color: var(--gold); font-weight: 800; letter-spacing: 0.5px; font-size: 12px; margin-bottom: 6px; text-transform: uppercase; }
.shout-banner-text { display: block; font-size: 20px; font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .shout-banner-overlay, .shout-banner-overlay.shout-banner-visible { transition: none; animation: none; }
}

/* =====================================================================
   REDUCED MOTION -- the animations that had no escape hatch
   =====================================================================
   Found by enumerating the live CSSOM on 2026-09-09: every @keyframes
   rule in the app, cross-checked against every
   `@media (prefers-reduced-motion: reduce)` block. Five animations were
   playing regardless of the preference, and three of them never stop:

     .wager-live-badge      wager-badge-pulse    INFINITE opacity pulse
     .mask-locked-pulse     mask-pulse           INFINITE opacity pulse
     .mask-verdict-banner.tier-5
                            mask-warning-flash   3x background flash
     .quickrank-rung        rung-in              entrance slide
     .gwp-clue              rung-in              entrance slide

   A permanently-pulsing badge is exactly what someone sets this
   preference to avoid. The information all of them carry survives
   without the movement.

   The two entrance animations MUST restore opacity by hand: their base
   rule is `opacity: 0` with `animation: ... forwards`, so `animation:
   none` on its own would leave the ladder rungs and the Guess Who clues
   permanently invisible -- the accessibility fix would have deleted the
   content. */
@media (prefers-reduced-motion: reduce) {
  .wager-live-badge { animation: none; }
  .mask-locked-pulse { animation: none; }
  .mask-verdict-banner.tier-5 { animation: none; }
  .quickrank-rung,
  .gwp-clue { animation: none; opacity: 1; }
}

/* VIS attribution on the landing screen (2026-09-13).
   Deliberately the quietest thing on the page: a player who scanned a QR is
   here to play, and a company link competing with START or JOIN would work
   against the product. Present, findable, never in the way. */
.vis-attribution {
  margin: -6px 0 22px;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.55;
}
.vis-attribution a {
  color: var(--muted, #8a8a8a);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.vis-attribution a:hover,
.vis-attribution a:focus-visible { border-bottom-color: currentColor; opacity: 1; }
