/* LanguageQuest overrides on top of WebSlides.
   Kept separate from static/webslides/ (vendored, untouched library). */

/* Palette, pulled from the acacia tree photo. Used to retint WebSlides'
   default blue/black theme below. */
:root {
  --toasted-almond: #de9151;
  --scarlet-fire: #e93c0c;
  --shadow-grey: #2e2e3a;
  --autumn-ember: #bc5d2e;
  --silver: #bbb8b2;
}

/* webslides.css sets body's own background to #f7f9fb (near-white) as a
   deliberate elastic-scroll/bounce mitigation — its own comment says as
   much ("Disable elastic scrolling/bounce"). Every section here is dark
   (.bg-apple's gradient), but body itself was never re-themed to match,
   so that near-white shows through during exactly the moments that rule
   exists for: a mobile rubber-band overscroll past a slide's edge, or the
   brief gap in WebSlides' own native slide-to-slide transition — read as
   "a white slide" flashing through. Matching it to the app's own dark
   theme fixes that without touching WebSlides' own JS or transition code. */
html,
body {
  background-color: var(--shadow-grey);
}

/* The three "top menu" icon buttons — .audio-toggle (top-left, fixed,
   every page), .tour-reopen and .score-mini-expand (top-right,
   per-section) — share this same frosted-glass treatment: whatever's
   behind them blurs, without the button itself gaining an opaque or
   tinted fill (background stays `none` on each, unchanged). That keeps
   their "nice transparency" exactly as it is while still keeping any text
   that does end up scrolling past one of them legible-through rather than
   raggedly overlapped — the practical version of a sticky bar's own
   frosted look, just shaped like these circles instead of a full-width
   strip. Harmless (and unnoticeable) wherever nothing's actually behind
   the circle to blur. -webkit- prefix for Safari. */
.audio-toggle,
.tour-reopen,
.score-mini-expand {
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* webslides.css's own `html, body, input, select, textarea` rule lists
   'Roboto' before 'San Francisco'. Roboto is the one Google Font this app
   actually loads (base.html), so it always wins that stack wherever it's
   used — meaning the site's stated "everything's San Francisco now" intent
   (see CLAUDE.md) wasn't actually true anywhere that inherited the plain
   default. Overriding the same selector group here, San-Francisco-first,
   fixes it site-wide in one place rather than patching every element that
   happened to copy the old (wrong) stack. Elements that need to win back
   from a more specific font-family (e.g. .roulette-reel-row.settled, which
   overrides away from Cave Paintings) still have to name this same stack
   explicitly — inheritance alone can't reach past a same-element override. */
html, body, input, select, textarea {
  font-family: 'San Francisco', helvetica, arial, sans-serif;
}

/* We drive navigation with our own buttons, not WebSlides' built-in
   arrows/counter — so it must never be shown. */
#navigation {
  display: none !important;
}

/* WebSlides classes like .button set their own `display`, which otherwise
   beats the browser's default [hidden] rule at equal specificity. Make
   `hidden` always win so JS can rely on the plain hidden attribute/property. */
[hidden] {
  display: none !important;
}

/* Fade veil for in-deck slide transitions — see base.html's
   window.fadeToSlide. Sits above everything (including modals) since it's
   only ever visible for the ~400ms of a deliberate transition. */
#slide-veil {
  background: var(--shadow-grey);
  inset: 0;
  opacity: 0;
  pointer-events: none;
  position: fixed;
  transition: opacity .4s ease;
  z-index: 300;
}

#slide-veil.visible {
  opacity: 1;
  pointer-events: auto;
}

/* Young Serif for the big hero title only — every other heading level (and
   everything else) is San Francisco, WebSlides' own default (see
   .bg-apple in webslides.css, which loads a real @font-face for it and is
   on every one of our sections) — Barlow Condensed was removed throughout
   this file so nothing overrides that default anymore. */
h1 {
  font-family: 'Young Serif', serif;
}

/* WebSlides' default .text-intro runs large (2.4rem) in the body font —
   bring it down to normal copy size. */
.text-intro {
  font-size: 1.8rem;
  line-height: 2.6rem;
}

/* Slide background: WebSlides' default .bg-apple is a blue-black gradient —
   swap it for the shadow-grey from our palette so the dimmed acacia photo
   (span.background.dark) reads warm instead of blue. */
.bg-apple {
  background: var(--shadow-grey);
}

/* Buttons (e.g. the "Enter" CTA) — WebSlides defaults to blue (#44d). */
.button,
.button:hover,
button[type='submit'],
input[type='submit'] {
  background-color: var(--autumn-ember);
  border-color: var(--autumn-ember);
}

/* Fixed: .ghost (webslides.css: `background: none`) was never actually
   showing through on a `.button.ghost` — both are single-class selectors
   (same specificity), and .button's own `background-color` above, loaded
   later (custom.css after webslides.css), silently won regardless of
   `.ghost` being present. Every "ghost" button in this app (the welcome
   modal's "No storage", .roulette-choice, .results-reflection-choice, an
   earlier On/Off button-pair version of #audio-settings-modal, ...) had
   been rendering fully solid this whole time — never surfaced visually
   before because most of them aren't shown right next to a genuinely
   solid sibling for the difference to read as obviously wrong the way it
   did once two adjacent "On"/"Off" pills needed to look different from
   each other (that pair was since replaced by a proper toggle switch —
   see .audio-toggle-switch below — but the underlying .ghost bug it
   surfaced is app-wide). `.button.ghost` (0,2,0) beats the bare `.button`
   (0,1,0) above on specificity regardless of source order, restoring the
   transparent look
   — text/border color still come from .ghost's own `color: inherit`
   (webslides.css) against .button's existing `border-color`, so this only
   needed the missing background. */
.button.ghost {
  background: none;
}

.button:active,
button[type='submit']:active,
input[type='submit']:active {
  filter: brightness(.85);
}

/* WebSlides uppercases plain <button>/<input type=submit> elements — our
   own <button class="button"> (e.g. modal actions) should read the same as
   our <a class="button"> ones (e.g. "Enter"), not shout in caps. */
.button {
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
}

/* A disabled .button (e.g. #roster-claim-continue before every player has
   claimed a name) otherwise looks pixel-identical to an enabled one —
   nothing here overrides its full-strength fill color, so clicking it
   silently does nothing with no visual explanation why. Same dimmed
   treatment as .roulette-toggle:disabled/.score-mini-expand:disabled
   elsewhere in this file, so "can't click this right now" reads
   consistently everywhere in the app. */
.button:disabled {
  cursor: not-allowed;
  opacity: .35;
}

/* Small-caps kicker label above a slide's H1 (e.g. "The Atlas"). */
.kicker {
  color: var(--toasted-almond);
  font-size: 1.6rem;
  font-variant: small-caps;
  letter-spacing: .2rem;
  margin-bottom: 0;
}

/* Below WebSlides' own 1024px breakpoint, a section's own padding-top
   drops from a generous 12rem down to the same 2.4rem as every other
   edge (webslides.css) — plenty of desktop clearance, but not enough to
   keep a slide's kicker clear of .audio-toggle, which stays
   position:fixed at the same top-left 2.4rem corner regardless of scroll
   position. A short slide never reaches this (its content sits vertically
   centered, well clear of the top edge), but a tall one that needs
   scrolling on a small screen can land its kicker directly behind the
   icons once scrolled all the way up — confirmed by testing. Margin, not
   more section padding, so only the kicker (and whatever follows it)
   shifts down, not every slide's own vertical centering. */
@media (max-width: 1023px) {
  .kicker {
    margin-top: 5.6rem;
  }
}

/* Quest tiles (slide 2) */
.quest-tiles {
  margin-top: 3.2rem;
}

.quest-tile {
  text-align: center;
}

.quest-tile a {
  color: inherit;
  display: block;
  text-decoration: none;
}

.quest-tile a:hover h2 {
  color: var(--autumn-ember);
}

.quest-tile.locked {
  color: var(--silver);
  cursor: not-allowed;
  opacity: .6;
}

.quest-tile svg {
  display: block;
  height: 3.2rem;
  margin: 0 auto 1.2rem;
  width: 3.2rem;
}

.quest-tile:not(.locked) svg {
  color: var(--toasted-almond);
}

/* Icons: bundled as a local SVG sprite (see /icons for a browsable
   reference). Default to currentColor so they follow surrounding text. */
svg[class*='fa-'] {
  fill: currentColor;
}

/* An icon referenced inline within a sentence (e.g. "click the {icon}"),
   matching the accent color icons use everywhere else. */
.icon-inline {
  color: var(--toasted-almond);
}

/* Puzzle phase (quest decks): a jumble box to drag events out of, and a
   sequence box of numbered slots to drag them into. */
.puzzle {
  display: flex;
  flex-wrap: wrap;
  gap: 2.4rem;
  margin-top: 2.4rem;
}

.puzzle-box {
  background: rgba(0, 0, 0, .25);
  border-radius: .8rem;
  flex: 1 1 320px;
  overflow: hidden;
  padding: 1.6rem;
}

/* Jumble box: cards are scattered at real random (x, y) coordinates —
   computed and kept collision-free in static/js/puzzle.js — rather than
   flex-wrapped, so it reads as an actual jumble/pile instead of a grid. */
.puzzle-jumble {
  min-height: 22rem;
  position: relative;
}

.puzzle-jumble .puzzle-card {
  position: absolute;
  transform: rotate(var(--r, 0deg));
  transition: box-shadow .2s ease, transform .2s ease;
}

/* A small lift on hover, on top of the card's own resting rotation — reads
   as picking the piece up off the pile rather than just a border change. */
.puzzle-jumble .puzzle-card:hover {
  transform: rotate(var(--r, 0deg)) translateY(-.3rem);
}

.puzzle-card {
  align-items: center;
  background: rgba(46, 46, 58, .82);
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .6rem;
  box-shadow: 0 .4rem .8rem rgba(0, 0, 0, .35);
  color: #fff;
  cursor: grab;
  display: flex;
  flex: 0 0 auto;
  font-size: 1.6rem;
  font-weight: 600;
  gap: .6rem;
  padding: 1.1rem 1.4rem;
  /* Placement is a Pointer Events drag (static/js/puzzle.js), mouse or
     touch — without this the browser treats a touch-drag on a card as a
     page scroll/pan gesture instead of handing it to our own handlers. */
  touch-action: none;
  user-select: none;
  white-space: nowrap;
}

.puzzle-card:hover {
  border-color: var(--autumn-ember);
}

/* Locked (correct) cards: the slot's own border already shows correctness —
   drop the card's border so it doesn't double up. */
.puzzle-card.locked,
.puzzle-card.locked:hover {
  border-color: transparent;
}

.puzzle-card.selected {
  box-shadow: 0 0 0 .3rem var(--toasted-almond);
}

/* Actively being dragged (puzzle.js's pointermove) — lifted well above
   everything else and drained of its own hit-testing so
   elementFromPoint() can see the slot/jumble underneath it instead of the
   card itself. cursor:grabbing over :grab reads as "currently held". */
.puzzle-card.dragging {
  box-shadow: 0 .8rem 1.6rem rgba(0, 0, 0, .45);
  cursor: grabbing;
  pointer-events: none;
  z-index: 50;
}

/* Whichever box is currently under a dragged card (puzzle.js's own
   dragOverTarget) — a quiet highlight standing in for the drop shadow a
   real object would cast, so the destination is never ambiguous mid-drag.
   .puzzle-slot already has its own dashed border, so drag-over there just
   recolors it; .puzzle-box has none, so the jumble needs one added. */
.puzzle-slot.drag-over {
  border-color: var(--toasted-almond);
}

.puzzle-jumble.drag-over {
  border: .15rem dashed var(--toasted-almond);
}

/* Filled in once the card locks in (see puzzle.js) — the label stays bold
   (.puzzle-card's own font-weight), the timestamp reads lighter beside it. */
.puzzle-card-timestamp {
  color: var(--silver);
  font-weight: 400;
}

.puzzle-card-timestamp:empty {
  display: none;
}

.puzzle-icon-btn {
  align-items: center;
  appearance: none;
  background: none;
  border: none;
  color: var(--toasted-almond);
  cursor: pointer;
  display: flex;
  height: 1.4rem;
  justify-content: center;
  line-height: 1;
  padding: 0;
}

.puzzle-icon-btn svg {
  height: 1.4rem;
  width: 1.4rem;
}

.puzzle-icon-btn:hover {
  color: var(--autumn-ember);
}

.puzzle-sequence {
  display: flex;
  flex-direction: column;
  gap: .8rem;
}

.puzzle-slot {
  align-items: center;
  border: .2rem dashed rgba(187, 184, 178, .4);
  border-radius: .4rem;
  display: flex;
  min-height: 4.4rem;
  padding: 0 1rem 0 3.6rem;
  position: relative;
}

/* Once a card is in a slot — correctly locked in, or mid-buzz about to
   bounce back to the jumble (bounceBack() in puzzle.js is what actually
   moves it back out; this covers the moment right before that, not just
   the locked-forever state) — the slot's own border already carries the
   correct/incorrect feedback, so the card's own resting-in-the-jumble
   background (.puzzle-card's own rgba fill) would otherwise show as a
   flat grey square behind it here. */
.puzzle-slot .puzzle-card {
  background: transparent;
  cursor: pointer;
  position: static;
  transform: none;
  width: 100%;
}

.puzzle-slot .puzzle-card.locked {
  cursor: default;
}

.puzzle-slot-number {
  color: var(--silver);
  font-size: 1.4rem;
  left: 1.2rem;
  position: absolute;
}

/* Correct: a quick confident pop, locked in for good. */
@keyframes puzzle-correct-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.06); }
  100% { transform: scale(1); }
}

.puzzle-slot.correct {
  animation: puzzle-correct-pop .4s ease-out;
  border-color: var(--toasted-almond);
  border-style: solid;
}

/* Incorrect: a buzzer shake, then the card bounces back to the jumble
   (see bounceBack() in puzzle.js) and this class is removed again. */
@keyframes puzzle-shake {
  10%, 90% { transform: translateX(-.2rem); }
  20%, 80% { transform: translateX(.4rem); }
  30%, 50%, 70% { transform: translateX(-.8rem); }
  40%, 60% { transform: translateX(.8rem); }
}

.puzzle-slot.incorrect {
  animation: puzzle-shake .9s;
  border-color: var(--scarlet-fire);
  border-style: solid;
}

.puzzle-slot.incorrect .puzzle-card {
  border-color: transparent;
}

/* Modal (event info / hint / completion). WebSlides has no dialog component of its own.
   z-index above .roulette-composite (250, also fixed) — otherwise that glowing
   fixed image paints in front of the modal, which is what made the scorecard
   modal look "transparent" and let the vote buttons behind it show through. */
.modal {
  align-items: center;
  display: flex;
  inset: 0;
  justify-content: center;
  position: fixed;
  z-index: 260;
}

.modal-backdrop {
  background: rgba(0, 0, 0, .7);
  inset: 0;
  position: absolute;
}

.modal-panel {
  background: var(--shadow-grey);
  border-radius: .8rem;
  color: #fff;
  max-width: 44rem;
  padding: 3.2rem;
  position: relative;
  width: 90%;
}

.modal-panel h3 {
  margin-bottom: 1.2rem;
}

/* The revealed correctInfo, shown under the description once an event is
   correctly placed — set apart with a divider so it reads as a separate,
   "unlocked" fact rather than more of the same description. */
#puzzle-modal-extra {
  border-top: .1rem solid rgba(187, 184, 178, .3);
  color: var(--toasted-almond);
  margin-top: 1.6rem;
  padding-top: 1.6rem;
}

/* Interim task screen (templates/_macros.html's interim_task()) — the
   transition slide shown between phases/tasks, shared across every
   quest/game. The gif itself is a full-bleed background (span.background,
   same treatment as every other slide's acacia photo). */

/* WebSlides fades in every element on slide entry at .6s (section * in
   webslides.css) — a touch slower here so the title doesn't rush in. */
.interim-task h1 {
  animation-duration: 1.2s;
}

.interim-task-description {
  font-size: 2.4rem;
}

/* display:flex + centered/wrapping so the Congratulations slide's two
   buttons (Show/Download My Findings) sit side by side — every other
   interim_task() screen has only ever had one button here, and a single
   flex child centers identically to a plain block one, so this is a
   backward-compatible generalization, not a behavior change for them. */
.interim-task-primary-wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 1.6rem;
  justify-content: center;
  margin-top: 2.4rem;
}

/* The description and both links start hidden — base.html's
   revealInterimTaskContent() adds .revealed to each once the gif's one
   playback cycle has finished, per data-gif-duration on the <section>.
   animation: none opts them out of the immediate "section *" fade above so
   they don't flash in early; they should only appear together, once,
   after the gif settles. */
.interim-task-description,
.interim-task-secondary,
.interim-task-primary {
  animation: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity .6s ease, transform .6s ease;
}

.interim-task-description.revealed,
.interim-task-secondary.revealed,
.interim-task-primary.revealed {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.interim-task-description {
  transform: translateY(.8rem);
}

/* Discreet "back" link, not a button — top-left, quiet until hovered.
   top sits below base.html's own persistent audio icons (.audio-toggles,
   position:fixed at the same 2.4rem/2.4rem corner) rather than at the
   same offset — this link used to render right on top of them. */
.interim-task-secondary {
  color: var(--silver);
  font-size: 1.5rem;
  left: 2.4rem;
  padding: .4rem;
  position: absolute;
  text-decoration: none;
  top: 7.6rem;
  transform: translateY(-.8rem);
}

.interim-task-secondary:hover {
  color: var(--autumn-ember);
}

.interim-task-primary {
  transform: translateY(.8rem);
}

.modal-close {
  background: none;
  border: none;
  color: var(--silver);
  cursor: pointer;
  font-size: 2.4rem;
  line-height: 1;
  padding: 0;
  position: absolute;
  right: 1.6rem;
  top: 1.6rem;
}

.modal-close:hover {
  color: var(--autumn-ember);
}

/* Investigation phase (templates/_investigation.html) — identical mechanic
   across every quest, parameterized by data/investigations/<slug>.json. */
.investigation-subtitle {
  color: var(--silver);
  font-size: 2.4rem;
  font-style: italic;
  margin-top: -.8rem;
}

.theory-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 2.4rem;
  margin-top: 3.2rem;
}

.theory-card {
  background: transparent;
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .8rem;
  display: flex;
  flex: 1 1 280px;
  flex-direction: column;
  padding: 2.4rem;
}

.theory-card:hover {
  border-color: var(--autumn-ember);
}

.theory-card h3 {
  margin-bottom: 1.2rem;
}

.theory-card p {
  margin-bottom: 1.6rem;
}

/* Pushed to the card's bottom (this, not .theory-read-more itself, is the
   direct flex child of .theory-card now that the checkmark sits beside
   the button rather than being a second bottom-pinned element). */
.theory-actions {
  align-items: center;
  display: flex;
  gap: .8rem;
  margin-top: auto;
}

/* Revealed once this theory's "Read more" has been clicked
   (static/js/investigation.js) — a quiet "you've seen this one" mark,
   not a button, so no hover/press treatment. */
.theory-read-check {
  color: var(--toasted-almond);
  display: inline-flex;
}

.theory-read-check svg {
  height: 2rem;
  width: 2rem;
}

/* Was .investigation-continue — generalized once the Procedure slide
   (templates/quests/hand_or_voice.html) needed the same "continue" button
   treatment as the investigation phase's "Contemplate" button
   (_investigation.html) — see CLAUDE.md: extract a shared pattern once a
   second place actually needs it. */
/* min-height reserves a .button's own natural height (line-height: 4.8rem,
   webslides.css) — matters most for #investigation-continue-slot, whose
   only child starts `hidden`: without this the paragraph would collapse
   to nothing, both making the layout jump once "Contemplate" appears and
   leaving nothing for that slide's guided tour to highlight as "the empty
   space where it'll appear". Harmless everywhere else .slide-continue is
   used (Procedure's "Begin") since a real button already fills it. */
.slide-continue {
  margin-top: 3.2rem;
  min-height: 4.8rem;
  text-align: center;
}

#investigation-modal-references {
  color: var(--silver);
  font-size: 1.4rem;
  list-style: disc;
  margin-top: 1.6rem;
  padding-left: 2rem;
}

#investigation-modal-references li {
  margin-bottom: .4rem;
}

/* Roulette (templates/quests/hand_or_voice.html) — quest-specific, part of
   the Game phase. The problem spinner starts disabled/dimmed until the
   place spinner settles (its possibleProblems constrains what problems are
   even eligible — see static/js/roulette.js).

   Cave Paintings (static/fonts/cave-paintings-regular.ttf, vendored locally,
   user-supplied) is deliberately unreadable — every row uses it, spinning or
   at rest, so the reel reads as flickering pictograms, not text. Only the
   settled winning row (.roulette-reel-row.settled) switches to San
   Francisco — the site's own default font (see .bg-apple in
   webslides.css), not a special "reveal" font — so it's actually legible. */
@font-face {
  font-family: 'Cave Paintings';
  font-display: swap;
  src: url('../fonts/cave-paintings-regular.ttf') format('truetype');
}

/* Wider than the standard .wrap (90%) so .roulette-flex below has real
   room either side of its centerline. */
@media (min-width: 1024px) {
  .roulette-wrap {
    width: 96%;
  }
}

/* Outer container: centers the (spinners-or-text) + (image slot) pair as
   a unit. gap keeps a small deliberate breathing room at the centerline —
   without it the two flex:1 children's content would butt up against each
   other exactly at the line (see the children below, which each push
   their own content toward that shared edge). */
.roulette-flex {
  align-items: center;
  display: flex;
  gap: 2.4rem;
  justify-content: center;
}

/* Both slides inherit text-align:center from the section's .aligncenter —
   fine for the spinners (already center themselves via their own grid),
   wrong for the kicker (now sits above .roulette-flex at full width,
   would center into the image's half) and the discuss text (reads better
   left-justified as a real paragraph than centered line-by-line). */
/* #play-slide reuses this same "kicker above .roulette-flex" shape (see
   hand_or_voice.html) without literally being a .roulette-slide. */
.roulette-slide .kicker,
#play-slide .kicker {
  text-align: left;
}

#roulette-results,
.play-info {
  text-align: left;
}

/* Equal share of .roulette-flex's width (flex-basis:0 so it's a true
   50/50 split, not influenced by content length) — same on every slide
   that uses this layout (spinners on the spin slide, .roulette-results on
   discuss, .play-info on Play) so the card/image column lands in the same
   spot either way. flex-direction: column + default align-items:stretch is
   what lets each .roulette-spinner row below become wider than its own
   content, which is what gives justify-content:end below something to
   push against. */
.roulette-spinners,
#roulette-results,
.play-info {
  flex: 1 1 0;
}

.roulette-spinners {
  display: flex;
  flex-direction: column;
  gap: 2.4rem;
}

/* .roulette-spinners and .roulette-results cross-fade in place
   (roulette.js's crossFadeSwap()) rather than this being a real slide
   transition — opacity only; [hidden] (display:none) is toggled
   mid-transition, not before, since display:none can't itself be
   animated. */
.roulette-spinners,
.roulette-results,
.roulette-results-choices {
  transition: opacity .4s ease;
}

.roulette-fade-hidden {
  opacity: 0;
  pointer-events: none;
}

/* The other flex:1 share — holds the composite image directly (normal
   flow), pushed to this container's own right edge (justify-content:
   flex-end), i.e. the centerline — mirroring .roulette-spinner's
   justify-content:start above (.roulette-image-slot is the left-hand
   column; .roulette-spinners/.roulette-results the right-hand one — see
   the template's own comment on why). No position:fixed or JS
   position-syncing: spin and discuss are one slide now (see roulette.js),
   so there's no slide transition for the image to hold still through. */
.roulette-image-slot {
  align-items: center;
  display: flex;
  flex: 1 1 0;
  justify-content: flex-end;
}

/* Play slide's right column (hand_or_voice.html) — timer, card, and
   button row stacked. The column itself still hugs its own right edge
   (align-items:flex-end), same as the spin slide's image slot — but the
   timer/card/buttons are each their own width:40rem box (matching the
   card exactly), so centering any of them centers relative to the image
   specifically, not the (wider) flex:1 column as a whole. */
.play-card-slot {
  align-items: flex-end;
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: 1.2rem;
}

/* Deliberately large — this is the one thing a director glances up at mid-
   turn, it needs to read from across a room. tabular-nums keeps each digit
   the same width as the SS:MS countdown changes every frame, so the whole
   line doesn't visibly jitter side to side. .message (play.js, for "Ready"
   and "Pass to a new director!") drops back to a normal-reading size —
   those are phrases, not a number, and would otherwise wrap awkwardly at
   the countdown's size. .urgent (play.js's last WARNING_SECONDS) swaps to
   the same red used for the puzzle's wrong-slot buzzer feedback and gently
   pulses, alongside audio.js's once-per-second tick — the two are meant to
   land together. Centered (not right-aligned) within its own width:40rem
   box, so it sits centered above the image itself. */
.play-timer {
  color: var(--toasted-almond);
  /* Monospace, not font-variant-numeric:tabular-nums alone — San
     Francisco doesn't reliably honor that feature, which is what made the
     SS:MS countdown visibly jitter side to side as the MS digits changed
     (each digit glyph a slightly different width). A true monospace font
     guarantees fixed width regardless of font-feature support. */
  font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
  font-size: 4.8rem;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: .1rem;
  line-height: 1;
  margin-bottom: 0;
  text-align: center;
  text-transform: uppercase;
  transition: color .2s ease;
  width: 40rem;
}

.play-timer.message {
  font-family: inherit; /* back to the site default (San Francisco, via .bg-apple) — a phrase, not digits, doesn't need monospace */
  font-size: 1.8rem;
  font-weight: 400;
  letter-spacing: normal;
}

.play-timer.urgent {
  animation: play-timer-pulse .5s ease-in-out infinite;
  color: var(--scarlet-fire);
}

@keyframes play-timer-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.08); }
}

/* Same visual treatment as .roulette-composite below — both hold a
   settled image the same way — but normal flow, not position:fixed:
   nothing here needs to survive a slide transition, it's all one slide. */
.play-meaning-card {
  background: rgba(255, 255, 255, .75);
  border: .15rem solid rgba(187, 184, 178, .5);
  border-radius: 1.6rem;
  box-shadow: 0 0 3.2rem 1rem rgba(222, 145, 81, .9);
  height: 40rem;
  position: relative;
  width: 40rem;
}

#play-meaning-img {
  height: calc(100% - 3.2rem);
  left: 1.6rem;
  object-fit: contain;
  opacity: 0;
  position: absolute;
  top: 1.6rem;
  transition: opacity .8s ease;
  width: calc(100% - 3.2rem);
}

#play-meaning-img.visible {
  opacity: 1;
}

/* Below the card, same width:40rem box — centered rather than the
   full-width .slide-continue treatment used elsewhere, since these
   buttons belong specifically to the image above them, not the whole
   slide. */
.play-controls {
  display: flex;
  flex-wrap: wrap;
  gap: .8rem;
  justify-content: center;
  width: 40rem;
}

/* Young Serif — same as h1 — rather than the site's default San
   Francisco, so each game's name reads like a title, not a label. */
#play-round-title {
  font-family: 'Young Serif', serif;
  font-size: 2.4rem;
  margin-bottom: .8rem;
}

#play-round-description {
  margin-bottom: 0;
}

#play-round-results .score-bar-row:last-child {
  margin-bottom: 0;
}

/* line-height explicitly set, not left to webslides.css's own h3 default —
   that default is sized for h3's own (much larger, and viewport-dependent)
   font-size, not the smaller fixed size this overrides it to, and the
   mismatch reads as huge gaps between lines whenever this title actually
   wraps (it often does — an "X at the Y" combination of two data-driven
   titles, no fixed length). */
#roulette-results-title {
  font-size: 2.4rem;
  line-height: 1.25;
  margin-bottom: .8rem;
}

#roulette-results-problem-prompt,
#roulette-results-place-prompt {
  margin-bottom: .8rem;
}

.roulette-results-discuss {
  color: var(--silver);
  font-style: italic;
  margin-top: 1.2rem;
}

/* A sibling of .roulette-results, not nested inside it (see
   hand_or_voice.html) — spans the full .roulette-wrap width rather than
   being squeezed into .roulette-results' 46rem text column. Plain normal
   flow, not position:absolute: that's what guarantees it always renders
   below the discuss text above it, regardless of how tall that text ends
   up — an absolute-positioned guess at "the bottom" could overlap text of
   unexpected length (or the fixed composite image, if pinned low enough).
   Each button flexes to fill an equal share of the width, wrapping to a
   second row only if 4 don't fit at their minimum. */
.roulette-results-choices {
  display: flex;
  flex-wrap: wrap;
  gap: .8rem;
  justify-content: center;
  margin-top: 2.4rem;
}

/* flex: 0 (no grow) — buttons sit at their natural content width and
   cluster together in the center (see justify-content above), rather than
   stretching to fill the row and reading like space-between/space-around.
   Still wraps to a second row if 4 don't fit at their natural width. */
.roulette-choice {
  flex: 0 1 auto;
  font-size: 1.5rem;
  line-height: 3.2rem;
  min-width: 0;
  padding: 0 1.6rem;
}

.roulette-choice.selected {
  background-color: var(--autumn-ember);
  color: #fff;
}

/* Grid rather than nested flex rows: label/toggle/display each get their
   own area, so the label centers over the display's column (not skewed by
   the toggle beside it) and the toggle centers over the display's row (not
   skewed by the label above it) — two independent alignments flexbox can't
   give both at once from a single container. column-gap is deliberately
   tight — the toggle should read as attached to its reel, not floating
   apart from it. */
/* justify-content: start — .roulette-spinners (the parent) stretches each
   row to its own full width (flex column, default align-items:stretch),
   so once that width is wider than this row's own two auto-sized grid
   columns, this pushes them to the row's left edge — hugging the
   centerline (.roulette-spinners is the right-hand column — see
   .roulette-flex/.roulette-image-slot below) instead of sitting wherever
   they'd naturally fall. */
.roulette-spinner {
  align-items: center;
  column-gap: .6rem;
  display: grid;
  grid-template-areas:
    ". label"
    "toggle display";
  grid-template-columns: auto auto;
  justify-content: start;
  row-gap: .8rem;
}

.roulette-toggle {
  grid-area: toggle;
}

.roulette-display {
  grid-area: display;
}

/* Normal flow inside .roulette-image-slot — not position:fixed, unlike
   earlier this session, when spin and discuss were two separate slides and
   this needed to hold still across a real transition between them. Same
   visual treatment as .play-meaning-card below (same job: holding a
   settled image). Each settled entry's image fades in here, stacked on top
   of the other — same-size, transparent PNGs designed to compose into one
   coherent scene. White fill (75% opacity — the images are transparent
   and the page itself is dark); the box-shadow is a wide, soft blurred
   "border" (blur radius + spread, not a crisp line), reading even before
   either image has landed. */
.roulette-composite {
  background: rgba(255, 255, 255, .75);
  border: .15rem solid rgba(187, 184, 178, .5);
  border-radius: 1.6rem;
  box-shadow: 0 0 3.2rem 1rem rgba(222, 145, 81, .9);
  height: 40rem;
  position: relative;
  width: 40rem;
}

/* Explicit width/height (not just inset) — for a replaced element like
   <img>, position:absolute + inset alone doesn't reliably resolve a used
   size in every browser, which is what let images overflow the card. */
.roulette-composite-img {
  height: calc(100% - 3.2rem);
  left: 1.6rem;
  object-fit: contain;
  opacity: 0;
  position: absolute;
  top: 1.6rem;
  transition: opacity .8s ease;
  width: calc(100% - 3.2rem);
}

.roulette-composite-img.visible {
  opacity: 1;
}

/* It's a <p>, which webslides.css gives a default margin-bottom: 3.2rem —
   nearly 3x .roulette-spinner's own row-gap, and more than .roulette-spinners'
   gap between the two spinners. That's what made "The Problem" read closer
   to the place spinner above it than to its own reel below — killed here so
   the grid's row-gap is the only spacing in play. */
.roulette-label {
  color: var(--silver);
  font-size: 1.6rem;
  grid-area: label;
  letter-spacing: .1rem;
  margin: 0;
  text-align: center;
  text-transform: uppercase;
}

/* A real scrolling reel (see static/js/roulette.js): .roulette-reel-track
   holds many stacked .roulette-reel-row symbols and is moved with a single
   `transform: translateY()`, fast-linear then eased to a stop — same two-
   phase technique as https://github.com/momokang/slotmachine (reimplemented
   natively rather than pulling in jQuery for one effect). The window's mask
   fades rows out near the top/bottom edges, so whichever row is centered
   reads as "in focus" and its neighbors read as "faded" automatically —
   no per-row JS bookkeeping needed. Fixed width regardless of content: the
   window/track never resize, and rows clip long text with an ellipsis. */
.roulette-display {
  background: rgba(0, 0, 0, .25);
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .8rem;
  overflow: hidden;
  padding: 0 1.6rem;
  width: 26rem;
}

/* Below WebSlides' own 768px breakpoint (the same one its typography/grid
   rules switch on — see webslides.css), .roulette-flex's two flex:1 0
   halves — spinners-or-text plus the image slot on Roulette,
   .play-info/.play-card-slot on Play — no longer have room to sit side by
   side: each holds a fixed 40rem (400px) box, so the pair needs ~850px+
   before either 50%-share column can actually fit its content without
   squeezing/overflowing. Stack them instead, and cap every fixed-40rem/26rem
   box (composite image, meaning card, timer, controls, reel display) down
   to whatever's smaller of its usual size or a viewport-relative width —
   min() keeps them all still matching each other exactly, just capped
   together, rather than fixing one arbitrary smaller size by hand. */
@media (max-width: 767px) {
  .roulette-flex {
    flex-direction: column;
  }

  /* The centerline-hugging alignment (flex-start/flex-end) only makes
     sense side by side — centered reads correctly once stacked. */
  .roulette-image-slot,
  .play-card-slot {
    align-items: center;
    justify-content: center;
  }

  .roulette-composite,
  .play-meaning-card {
    height: min(40rem, 88vw);
    width: min(40rem, 88vw);
  }

  .play-timer,
  .play-controls {
    width: min(40rem, 88vw);
  }

  .roulette-display {
    width: min(26rem, 80vw);
  }
}

.roulette-reel-window {
  height: 9rem; /* 3 row-heights */
  mask-image: linear-gradient(to bottom, transparent 0%, black 35%, black 65%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 35%, black 65%, transparent 100%);
  overflow: hidden;
  position: relative;
}

.roulette-reel-row {
  align-items: center;
  color: var(--silver);
  display: flex;
  font-family: 'Cave Paintings', serif;
  font-size: 1.8rem;
  height: 3rem;
  justify-content: center;
  overflow: hidden;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The settled result — real text, in the site's own default font (San
   Francisco — see the html/body override near the top of this file)
   rather than the illegible reel font — .roulette-reel-row above sets
   Cave Paintings directly on this same element, so inherit wouldn't reach
   past it; this has to name the site's default stack explicitly to win
   back over it. */
.roulette-reel-row.settled {
  color: var(--toasted-almond);
  font-family: 'San Francisco', helvetica, arial, sans-serif;
  font-size: 2.2rem;
}

.roulette-toggle {
  align-items: center;
  background: none;
  border: .15rem solid var(--toasted-almond);
  border-radius: 50%;
  color: var(--toasted-almond);
  cursor: pointer;
  display: flex;
  flex-shrink: 0;
  height: 4rem;
  justify-content: center;
  width: 4rem;
}

.roulette-toggle svg {
  height: 1.6rem;
  width: 1.6rem;
}

.roulette-toggle:hover {
  border-color: var(--autumn-ember);
  color: var(--autumn-ember);
}

.roulette-toggle:disabled {
  cursor: default;
  opacity: .35;
}

.roulette-toggle.spinning svg {
  animation: roulette-spin-icon 1s linear infinite;
}

@keyframes roulette-spin-icon {
  to { transform: rotate(360deg); }
}

/* Running score: one horizontal bar per theory + Inconclusive, built by
   renderScoreBars() in roulette.js off a single `tallies` object so the
   full scorecard modal (#scorecard-bars) and the spin slide's discreet
   mini widget (#score-mini-bars) can never drift apart. Base rule below is
   the full version (label + track + count); .score-bars--mini overrides it
   down to bars only. */
.score-bar-row {
  align-items: center;
  display: grid;
  gap: .8rem;
  grid-template-columns: 9rem 1fr 2.4rem;
  margin-bottom: .8rem;
}

.score-bar-row:last-child {
  margin-bottom: 0;
}

/* Director/round-result bars (play.js) skip the count column entirely —
   hovering the row shows the tally instead (native title attribute), and a
   legend above the list (#play-score-modal) explains the two colors — so
   there's nothing to put there. */
.score-bar-row--no-count {
  grid-template-columns: 9rem 1fr;
  position: relative; /* anchors the tap-reveal chip below */
}

/* Hovering shows the exact tally via the row's own `title` — there's no
   touch equivalent of hover, so play.js also toggles this class on tap,
   surfacing the same text (content: attr(title), rather than a duplicated
   string) as a small floating chip instead. */
.score-bar-row--no-count.revealed::after {
  background: var(--shadow-grey);
  border: .1rem solid rgba(187, 184, 178, .25);
  border-radius: .4rem;
  color: #fff;
  content: attr(title);
  font-size: 1.2rem;
  left: 0;
  padding: .3rem .6rem;
  position: absolute;
  top: 100%;
  white-space: nowrap;
  z-index: 20;
}

/* The mini widget sits near the screen's right edge (see .score-mini
   below) — anchor the chip to its own right edge too, or a long tally
   string would run off-screen. */
.score-bars--mini .score-bar-row--no-count.revealed::after {
  left: auto;
  right: 0;
}

.score-bar-label {
  color: var(--silver);
  font-size: 1.4rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* display:flex so a track can hold two adjacent fill segments side by
   side (play.js's director bars — guessed, then passed) as well as the
   original single-fill case (roulette's theory tally) — a lone flex child
   with its own width:X% renders exactly as it did as a block child, so
   this doesn't change the single-fill look. */
.score-bar-track {
  background: rgba(255, 255, 255, .1);
  border-radius: .4rem;
  display: flex;
  height: 1.2rem;
  overflow: hidden;
}

.score-bar-fill {
  background: var(--autumn-ember);
  border-radius: .4rem;
  display: block;
  height: 100%;
  transition: width .4s ease;
}

/* Director bars only (play.js) — two segments in one track, so each only
   rounds its own outer corner rather than both using the full-pill
   .score-bar-fill radius above (which would leave a rounded notch where
   the two meet in the middle). The track's own overflow:hidden still
   rounds whichever segment actually reaches the outer edge, so this holds
   up even when one segment is 0% width. */
.score-bar-fill--guessed {
  border-radius: .4rem 0 0 .4rem;
}

.score-bar-fill--passed {
  background: #000;
  border-radius: 0 .4rem .4rem 0;
}

.score-bar-count {
  color: #fff;
  font-size: 1.4rem;
  text-align: right;
}

/* Static key above the director-tally bars (#play-score-modal) explaining
   the two segment colors — same swatch colors as .score-bar-fill--guessed/
   --passed, kept as separate literal values rather than referencing those
   classes directly since a swatch here is just a small square, not an
   actual bar segment. */
.score-legend {
  display: flex;
  gap: 1.6rem;
  justify-content: center;
  margin-bottom: 1.6rem;
}

.score-legend-item {
  align-items: center;
  color: var(--silver);
  display: flex;
  font-size: 1.4rem;
  gap: .6rem;
}

.score-legend-swatch {
  border-radius: .2rem;
  display: inline-block;
  height: 1rem;
  width: 1rem;
}

.score-legend-swatch--guessed {
  background: var(--autumn-ember);
}

.score-legend-swatch--passed {
  background: #000;
}

/* justify-content:center, not just text-align:center — webslides.css has
   a `[class*="card-"]` rule (its own card-component styling) that
   incidentally matches this class too, purely because "scorecard-actions"
   contains the substring "card-", forcing display:flex with no
   justify-content of its own. text-align does nothing for a flex
   container's children (that's what justify-content is for), which is
   why the button sat flush left instead of centered — it only went
   unnoticed on the (wider) "Spin Again"/"Continue" button in
   #scorecard-modal, where the button nearly fills the row either way. */
.scorecard-actions {
  display: flex;
  justify-content: center;
  margin-top: 2.4rem;
  text-align: center;
}

/* Discreet widget (this same class backs both #score-mini on the spin
   slide and #play-score-mini on Play) — top-right corner, quiet until
   hovered, hidden until there's something to show. Bars only: label/count
   are dropped so it reads as a glance, not a report; the icon opens the
   full modal on demand. `right` sits inboard of the plain 2.4rem edge
   margin by a tour-reopen button's width plus its own .8rem gap (see
   .tour-reopen) — reserved so a guided-tour reopen button can share this
   same top-right row without the two ever overlapping, on any slide that
   ends up with both. */
.score-mini {
  align-items: center;
  display: flex;
  gap: .8rem;
  position: absolute;
  right: 7.2rem;
  top: 2.4rem;
  z-index: 10;
}

.score-bars--mini {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  width: 8rem;
}

.score-bars--mini .score-bar-row {
  display: block;
  margin-bottom: 0;
}

.score-bars--mini .score-bar-label,
.score-bars--mini .score-bar-count {
  display: none;
}

.score-bars--mini .score-bar-track {
  height: .5rem;
  width: 100%;
}

.score-mini-expand {
  align-items: center;
  background: none;
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: 50%;
  color: var(--silver);
  cursor: pointer;
  display: flex;
  flex-shrink: 0;
  height: 3.2rem;
  justify-content: center;
  width: 3.2rem;
}

.score-mini-expand svg {
  height: 1.4rem;
  width: 1.4rem;
}

.score-mini-expand:hover {
  border-color: var(--autumn-ember);
  color: var(--autumn-ember);
}

/* Play's version is disabled mid-turn (play.js) — same dimmed treatment
   as .roulette-toggle:disabled, so "can't click this right now" reads the
   same way in both places. */
.score-mini-expand:disabled {
  cursor: default;
  opacity: .35;
}

.score-mini-expand:disabled:hover {
  border-color: rgba(187, 184, 178, .5);
  color: var(--silver);
}

/* Cipher gate (templates/index.html) — proof-of-concept player identifier,
   checked against app.py's CIPHER_ALLOWLIST over the socket
   (cipher:check/cipher:result). See static/js/cipher.js. */
.cipher-gate {
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
  margin: 2.4rem auto 0;
  max-width: 40rem;
}

/* It's a <p>, which webslides.css gives a default margin-bottom: 3.2rem —
   killed here (same issue as .roulette-label elsewhere in this file);
   .cipher-gate's own gap above is what spaces it from .cipher-field now. */
.cipher-label {
  color: var(--silver);
  font-size: 1.6rem;
  margin-bottom: 0;
}

/* Column, not row — label above input above button, stacked, not
   side-by-side (which read as cramped/"right up against each other"). */
.cipher-field {
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
}

.cipher-input {
  background: rgba(0, 0, 0, .25);
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .4rem;
  color: #fff;
  font-size: 1.6rem;
  height: 4.8rem;
  padding: 0 1.2rem;
  /* 20rem (320px) is exactly the smallest common phone viewport (iPhone
     SE) — any surrounding padding at all pushes it into horizontal
     overflow, so it's capped relative to the viewport too rather than a
     fixed width. */
  width: min(20rem, 80vw);
}

/* background repeated here, not inherited from .cipher-input — webslides.css
   has a generic `input:focus { background: #fff }` that otherwise wins
   (higher-specificity-but-later doesn't help since it was never overridden
   here before): white background + this element's own white text made
   whatever was typed invisible. */
.cipher-input:focus {
  background: rgba(0, 0, 0, .25);
  border-color: var(--toasted-almond);
  outline: none;
}

.cipher-input.invalid {
  border-color: var(--scarlet-fire);
}

.cipher-modal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1.2rem;
  justify-content: center;
  margin-top: 2.4rem;
}

/* Re-opens a driver.js guided tour (puzzle.js/investigation.js/
   roulette.js/play.js) once it's been closed — same circular icon-button
   treatment (and same 3.2rem/1.4rem-icon size, below) as
   .score-mini-expand — this used to be a visibly larger 4rem/1.8rem, out
   of step with it. Top-right of its section, which is `position:relative`
   itself (webslides.css) so this doesn't need position:fixed or an
   overlay-block home — it's meant to disappear along with its own slide,
   same as any other in-section control. Same corner and `top` offset as
   .score-mini (below) so the two sit in one visual row wherever both
   exist on a slide — the roulette and Play slides both do, which is
   exactly why .score-mini reserves the room for this deliberately. */
.tour-reopen {
  align-items: center;
  background: none;
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: 50%;
  color: var(--silver);
  cursor: pointer;
  display: flex;
  height: 3.2rem;
  justify-content: center;
  position: absolute;
  right: 2.4rem;
  top: 2.4rem;
  width: 3.2rem;
}

.tour-reopen:hover {
  border-color: var(--autumn-ember);
  color: var(--autumn-ember);
}

.tour-reopen svg {
  height: 1.4rem;
  width: 1.4rem;
}

/* driver.js guided-tour popovers (static/js/driver.js / static/css/driver.css
   — vendored, see CLAUDE.md) — restyled to match the site's own dark theme
   instead of the library's default white box. Loaded after driver.css in
   base.html specifically so these overrides win the cascade (driver.css's
   own rules reset `all: unset` then set direct values on the very same
   selectors — an earlier same-specificity rule loses to that regardless of
   shorthand/longhand). driver.js itself also sets some layout inline
   (position/inset) via JS, which no stylesheet rule can override short of
   `!important` — not needed for anything here, since only color/type
   needed changing, not that positioning. */
.driver-popover {
  background: var(--shadow-grey);
  border: .1rem solid rgba(187, 184, 178, .25);
  border-radius: .8rem;
  box-shadow: 0 .4rem 2.4rem rgba(0, 0, 0, .5);
  font-family: 'San Francisco', helvetica, arial, sans-serif;
  max-width: 38rem;
  min-width: 30rem;
  padding: 2rem;
}

/* min-width: 30rem above is 480px — wider than any phone viewport (a
   320px-wide screen is narrower than the popover's own minimum), which
   left every guided tour popover overflowing the page on mobile
   regardless of touch input. Below WebSlides' own 768px breakpoint, drop
   the minimum and cap the width to whatever's smaller of the usual 30rem
   or a viewport-relative width instead. */
@media (max-width: 767px) {
  .driver-popover {
    min-width: unset;
    width: min(30rem, 90vw);
  }
}

/* Both driver.js's popover title AND footer are semantic <header>/<footer>
   tags, and webslides.css has its own generic `header, footer, #navigation
   { padding: 2.4rem; ... }` rule (styling ITS OWN slide headers/footers)
   that unintentionally matches these too, indenting their text 2.4rem from
   the popover's actual left edge instead of lining up with the
   description under/above it. Same leaking-vendor-selector bug as the
   footer's position:absolute below — different property, same cause. */
.driver-popover-title,
.driver-popover-footer {
  padding: 0;
}

.driver-popover-title {
  color: #fff;
  font-family: 'Young Serif', serif;
  font-size: 1.8rem;
  font-weight: 700;
}

.driver-popover-description {
  color: var(--silver);
  font-size: 1.4rem;
  line-height: 1.6;
  margin: 0;
}

.driver-popover-close-btn {
  color: var(--silver);
}

.driver-popover-close-btn:hover,
.driver-popover-close-btn:focus {
  color: var(--autumn-ember);
}

/* The real fix for the footer visually overlapping the description above
   it: driver.js's popover footer is a semantic <footer> tag, and
   webslides.css has its own generic `header, .ws-ready footer` rule
   (styling ITS OWN slide headers/footers) that unintentionally matches
   this one too and takes it out of flow with position:absolute; top:0;
   left:0 — landing it over the popover's other content instead of below
   it. driver.css itself never sets `position` on this selector, so nothing
   there was contesting that leak. Same class of bug as the .roulette-choice/
   zoomed-clone and [class*="card-"] issues elsewhere in this app: a
   vendored library's broad selector unintentionally catching an unrelated
   element. `.driver-popover-title` (a <header> tag) doesn't have the same
   problem — driver.css's own `.driver-popover-title{position:relative}` is
   a class selector, which already beats webslides.css's bare `header` tag
   selector on specificity alone. This one needs the extra
   `.driver-popover` ancestor qualifier for the same reason: webslides.css's
   `.ws-ready footer` is one class + one type selector (0,1,1) — a bare
   `.driver-popover-footer` (0,1,0) still loses to that regardless of load
   order, so this pairs it with its own parent class to reach (0,2,0). */
.driver-popover .driver-popover-footer {
  position: static;
}

.driver-popover-progress-text {
  color: var(--silver);
  font-size: 1.2rem;
}

.driver-popover-footer-btn {
  background: none;
  border: .1rem solid var(--autumn-ember);
  border-radius: .4rem;
  color: var(--autumn-ember);
  font-size: 1.3rem;
  padding: .5rem 1.2rem;
}

.driver-popover-footer-btn:hover,
.driver-popover-footer-btn:focus {
  background: rgba(188, 93, 46, .15);
}

/* The primary action (Next, or the relabelled Done button on the last
   step) gets the same solid-fill treatment as .button elsewhere in the
   app, so it reads as the one to press; Previous/Close stay outlined. */
.driver-popover-next-btn {
  background: var(--autumn-ember);
  color: #fff;
}

.driver-popover-next-btn:hover,
.driver-popover-next-btn:focus {
  background: var(--autumn-ember);
  filter: brightness(.85);
}

/* The little pointer triangle: driver.css leaves exactly one of these four
   border-*-color longhands unset per side (the other three are forced
   transparent by that same driver.css rule) — these just fill in that one
   remaining side with the popover's new background, additively, so the
   arrow still reads as "attached to" the popover post-recolor. */
.driver-popover-arrow-side-left { border-left-color: var(--shadow-grey); }
.driver-popover-arrow-side-right { border-right-color: var(--shadow-grey); }
.driver-popover-arrow-side-top { border-top-color: var(--shadow-grey); }
.driver-popover-arrow-side-bottom { border-bottom-color: var(--shadow-grey); }

/* Between-round transition (play.js's showRoundTransition(), driver.js's
   own one-off `.highlight()`, not a window.LQTour tour) — deliberately
   larger than the instructional tours' popovers, so it reads as "stop and
   notice this changed" rather than another how-to-play step. Only the
   title/description scale up; the footer (progress text, buttons) stays
   at driver.css's own size — this popover only ever has a close button,
   no progress to speak of. */
.lq-round-transition-popover .driver-popover-title {
  font-size: 2.6rem;
}

.lq-round-transition-popover .driver-popover-description {
  font-size: 1.8rem;
  line-height: 1.4;
}

/* Your Results (results.js) — wider than the standard .wrap (90%), same
   reasoning as .roulette-wrap: these panels read better with real room,
   even stacked in one column. */
@media (min-width: 1024px) {
  .results-wrap {
    width: 96%;
  }
}

/* A single vertical stack, not a grid — read top to bottom in a specific
   order (theory scores, which theory actually won, the round-by-round
   summary, then the full director breakdown), not scanned like a
   dashboard's worth of independent tiles. */
.results-dashboard {
  display: flex;
  flex-direction: column;
  gap: 1.6rem;
  margin: 2.4rem 0;
  text-align: left;
}

.results-panel {
  background: rgba(0, 0, 0, .2);
  border: .1rem solid rgba(187, 184, 178, .3);
  border-radius: .8rem;
  padding: 1.6rem 2rem;
}

.results-panel h3 {
  margin-bottom: 1.2rem;
}

/* <details> is also a .results-panel (the director-tallies one) — its own
   <summary> needs the marker/cursor treatment regular panels don't. The
   default disclosure triangle is kept (not suppressed via
   `summary { list-style: none }`) — plain and correctly conveys
   collapsed/expanded with no icon work needed. */
.results-panel summary {
  cursor: pointer;
}

.results-panel summary h3 {
  display: inline;
}

.results-panel[open] summary h3 {
  margin-bottom: 1.2rem;
}

/* The winning theory, restated right under its own scores (results.js) —
   title in the same toasted-almond as other "this is the answer" text
   (.roulette-results-discuss's own theory buttons, etc.), hypothesis below
   it in normal body copy. */
.results-winning-theory {
  border-top: .1rem solid rgba(187, 184, 178, .3);
  margin-top: 1.6rem;
  padding-top: 1.6rem;
}

.results-winning-theory h4 {
  color: var(--toasted-almond);
  margin-bottom: .4rem;
}

/* Labels here run longer than the compact modal bars this markup was
   built for ("R01 — Round Silent Faceoff", not just "Director 3") — a
   wider label column, scoped to this panel only so the original 9rem
   stays put everywhere else (#play-score-modal, #scorecard-modal). Same
   reasoning, smaller bump, for #play-round-modal's own round-by-round
   summary just below (also "R01 — ..." labels, but inside a narrower
   .modal-panel, not this full-width dashboard panel). */
.results-panel .score-bar-row {
  grid-template-columns: 18rem 1fr 2.4rem;
}

.results-panel .score-bar-row--no-count {
  grid-template-columns: 18rem 1fr;
}

#play-round-results .score-bar-row--no-count {
  grid-template-columns: 13rem 1fr;
}

.results-round-heading {
  color: var(--toasted-almond);
  font-size: 1.6rem;
  margin: 1.6rem 0 .8rem;
}

.results-round-heading:first-child {
  margin-top: 0;
}

/* "By Turn"/"By Director" sub-labels within one round's own director
   breakdown (renderDirectorsBreakdown(), results.js) — smaller and
   plainer than .results-round-heading itself, reading as a label on the
   bars right below it rather than another heading of its own. */
.results-breakdown-sublabel {
  color: var(--silver);
  font-size: 1.3rem;
  margin: 1.2rem 0 .4rem;
}

.results-question {
  margin-top: 2.4rem;
  text-align: center;
}

/* flex-wrap — three equal-weight ghost buttons (Yes/No/Inconclusive) can
   run wider than a narrow phone screen fits on one line; each still reads
   fine wrapped onto a second row, centered same as the first. */
.results-question-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1.6rem;
  justify-content: center;
  margin-top: 1.6rem;
}

.results-done {
  align-items: center;
  color: var(--toasted-almond);
  display: flex;
  gap: .8rem;
  justify-content: center;
  margin-top: 2.4rem;
}

.results-done svg {
  height: 2rem;
  width: 2rem;
}

/* Hand-off to the Congratulations/poster slide — hidden until
   finishReflection() (results.js) reveals it, same centered spacing as
   .results-question-actions just above. */
.results-continue-wrap {
  margin-top: 2.4rem;
  text-align: center;
}

/* #results-reflection-modal's own choice step ("which hypothesis...") —
   same ghost-button treatment as .roulette-choice, since it's the same
   underlying set of options (the roulette vote's own categories). */
.results-reflection-choices {
  display: flex;
  flex-wrap: wrap;
  gap: 1.2rem;
  justify-content: center;
  margin-bottom: 1.6rem;
}

.results-reflection-choice.selected {
  background: var(--autumn-ember);
  color: #fff;
}

/* Same dark-field treatment as .cipher-input, sized for a few sentences
   rather than one line. resize:vertical only — horizontal would let it
   spill past .modal-panel's own width. */
.results-reflection-textarea {
  background: rgba(0, 0, 0, .25);
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .4rem;
  color: #fff;
  font-family: inherit;
  font-size: 1.5rem;
  margin-bottom: 1.6rem;
  padding: 1.2rem;
  resize: vertical;
  width: 100%;
}

.results-reflection-textarea:focus {
  background: rgba(0, 0, 0, .25);
  border-color: var(--toasted-almond);
  outline: none;
}

/* The closing "Name Your Poster" step (results.js's askPosterTitle()) —
   two single-line fields stacked, same dark-field treatment as
   .results-reflection-textarea/.cipher-input above rather than a new look. */
.results-reflection-title-fields {
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
  margin-bottom: 1.6rem;
}

.results-reflection-input {
  background: rgba(0, 0, 0, .25);
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .4rem;
  color: #fff;
  font-family: inherit;
  font-size: 1.5rem;
  height: 4.4rem;
  padding: 0 1.2rem;
  width: 100%;
}

.results-reflection-input:focus {
  border-color: var(--toasted-almond);
  outline: none;
}

/* Play's turn-progress bar (play.js's renderTurnProgress()) — one segment
   per TURNS_PER_ROUND, pinned to the bottom of #play-slide regardless of
   its content's own height (see the template's own comment on this
   element for why it's a sibling of .wrap, not nested in it). */
.play-turn-progress {
  bottom: 2.4rem;
  display: flex;
  gap: .8rem;
  justify-content: center;
  left: 50%;
  position: absolute;
  transform: translateX(-50%);
}

.play-turn-progress-segment {
  background: rgba(255, 255, 255, .1);
  border-radius: .4rem;
  height: .8rem;
  transition: background .3s ease;
  width: 4rem;
}

/* A completed turn stays solid; the current one pulses (drawing the eye
   to "you are here") rather than just sitting solid like a done segment —
   an upcoming turn stays at the dim base color above. */
.play-turn-progress-segment.done {
  background: var(--autumn-ember);
}

.play-turn-progress-segment.current {
  animation: play-turn-progress-pulse 1.6s ease-in-out infinite;
  background: var(--autumn-ember);
}

@keyframes play-turn-progress-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .45; }
}

/* The Procedure's basicInstruction (data/quests/<slug>.json) embeds its own
   <ul> — invalid inside a <p>, so the browser auto-closes .text-intro right
   before it, meaning the paragraph isn't :last-child and still gets
   webslides.css's generic `p { margin-bottom: 3.2rem }`, on top of the
   list's own spacing right after — a visibly oversized gap between "Here's
   the basic gist of the game:" and the list itself. Zeroed here rather than
   on .text-intro generally, which needs that margin everywhere else it's
   used (this page's other text-intro paragraphs, the landing page, etc). */
.procedure-intro {
  margin-bottom: 0;
}

/* Roster setup (static/js/roster.js, index.html's #roster-claim-modal) —
   one row per generated player name. Scrolls internally past a handful of
   rows rather than growing the modal past a sane height for a big group. */
.roster-claim-list {
  display: flex;
  flex-direction: column;
  gap: .8rem;
  margin: 1.6rem 0;
  max-height: 40rem;
  overflow-y: auto;
}

.roster-claim-row {
  align-items: center;
  background: rgba(0, 0, 0, .2);
  border: .1rem solid rgba(187, 184, 178, .3);
  border-radius: .4rem;
  display: flex;
  gap: .8rem;
  padding: .8rem 1.2rem;
  transition: border-color .3s ease;
}

/* Claimed or edited — same "this one's settled" signal as
   .results-reflection-choice.selected elsewhere. The transition above is
   what actually makes this subtle: roster.js patches one row's own
   persistent wrapper in place rather than recreating it, so this class
   change animates smoothly on the real element instead of snapping in on
   a brand new one. */
.roster-claim-row.claimed {
  border-color: var(--autumn-ember);
}

/* Every row's children get swapped out (claim/edit buttons <-> checkmark,
   label <-> input) each time roster.js patches that one row — without
   this, they'd replay webslides.css's broad `section * { animation:
   fadeIn }` (meant for a slide's own one-time entrance), which is what
   made even the single row being claimed or edited visibly flash. Opting
   out here keeps the actual feedback to the deliberate, subtle
   border-color transition above. */
.roster-claim-row * {
  animation: none;
}

/* text-align:left explicitly — this modal sits inside a WebSlides
   .aligncenter section (index.html), which centers text in every
   descendant by default; a flex:1 span otherwise centers its own text
   within whatever width it's currently stretched to, so the name visibly
   shifted sideways the moment claiming swapped two icon-buttons for one
   check icon and the label's available width changed. */
.roster-claim-label {
  color: #fff;
  flex: 1;
  font-size: 1.6rem;
  overflow: hidden;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Claim/edit — same circular icon-button treatment as .score-mini-expand. */
.roster-claim-btn {
  align-items: center;
  background: none;
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: 50%;
  color: var(--silver);
  cursor: pointer;
  display: flex;
  flex-shrink: 0;
  height: 3.2rem;
  justify-content: center;
  width: 3.2rem;
}

.roster-claim-btn svg {
  height: 1.4rem;
  width: 1.4rem;
}

.roster-claim-btn:hover {
  border-color: var(--autumn-ember);
  color: var(--autumn-ember);
}

/* Once claimed/edited, the claim/edit buttons are replaced by this — a
   plain settled checkmark, not another clickable control (no "un-claim"
   once chosen, same "one path through" spirit as the game's other
   forced-choice modals). */
.roster-claim-check {
  align-items: center;
  color: var(--toasted-almond);
  display: flex;
  flex-shrink: 0;
  height: 3.2rem;
  justify-content: center;
  width: 3.2rem;
}

.roster-claim-check svg {
  height: 2rem;
  width: 2rem;
}

/* Same dark-field treatment as .cipher-input, inline in the row rather
   than its own fixed width. */
.roster-claim-input {
  background: rgba(0, 0, 0, .25);
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: .4rem;
  color: #fff;
  flex: 1;
  font-size: 1.6rem;
  height: 3.6rem;
  padding: 0 1.2rem;
}

.roster-claim-input:focus {
  background: rgba(0, 0, 0, .25);
  border-color: var(--toasted-almond);
  outline: none;
}

/* The generated name sits here as a placeholder (roster.js never
   pre-fills the real value — typing immediately replaces it, standard
   placeholder behavior), but with no color of its own it rendered the
   same solid white as real typed text, reading as "already-entered text"
   a player couldn't seem to select or delete. Dimming it here is the fix
   — visually a suggestion, not content, the same "prefill" cue every
   other placeholder-driven field uses. */
.roster-claim-input::placeholder {
  color: var(--silver);
  opacity: .6;
}

/* Whose turn it is (roulette.js/play.js, via roster.js's announce()) —
   empty and so invisible (bare <p>, no box of its own) whenever no roster
   was ever set up. Young Serif (the site's own h1 font) at a modest size —
   prominent enough to read as an announcement, not just another line of
   body copy, without pushing the slide's own content down much. White by
   default; only the player's own name (.turn-announcement-name, built by
   announce()) breaks from that into orange, so it's the one word in the
   sentence that visibly changes each turn. */
.roulette-turn-announcement,
.play-turn-announcement {
  color: #fff;
  font-family: 'Young Serif', serif;
  font-size: 2.2rem;
}

.turn-announcement-name {
  color: var(--autumn-ember);
}

.roulette-turn-announcement:empty,
.play-turn-announcement:empty {
  margin: 0;
}

.roulette-turn-announcement {
  margin-bottom: .8rem;
}

.play-turn-announcement {
  margin-bottom: 1.6rem;
}

/* Persistent audio controls (base.html, wired by static/js/audio-settings.js)
   — top-left, fixed to the viewport (not section-relative like
   .tour-reopen/.score-mini, which need to disappear along with their own
   slide) so the same two icons sit in the same spot on every page,
   surviving slide transitions the same way #slide-veil does. Above
   #slide-veil's own z-index (300) — "toggle anytime" includes while a
   modal is open, not just between slides. */
.audio-toggles {
  display: flex;
  gap: .8rem;
  left: 2.4rem;
  position: fixed;
  top: 2.4rem;
  z-index: 310;
}

/* Same circular icon-button treatment as .tour-reopen/.score-mini-expand —
   same size too (3.2rem/1.4rem icon, below), not the 4rem/1.8rem this used
   to be: next to .score-mini-expand these visibly read as oversized. */
.audio-toggle {
  align-items: center;
  background: none;
  border: .1rem solid rgba(187, 184, 178, .5);
  border-radius: 50%;
  color: var(--silver);
  cursor: pointer;
  display: flex;
  height: 3.2rem;
  justify-content: center;
  width: 3.2rem;
}

.audio-toggle:hover {
  border-color: var(--autumn-ember);
  color: var(--autumn-ember);
}

.audio-toggle svg {
  height: 1.4rem;
  width: 1.4rem;
}

/* Off — dimmed in place rather than swapping to a separate "muted" icon
   (fa-sprite has no muted variant of fa-music, and swapping only one of
   the two icons would read inconsistently) — same "dim, don't replace"
   treatment as .roulette-toggle:disabled elsewhere in this file. */
.audio-toggle.muted {
  opacity: .4;
}

/* index.html's one-time #audio-settings-modal — each row a label plus a
   real compact toggle switch (.audio-toggle-switch). Was originally a
   pair of full-size .button.ghost pills ("On"/"Off") — at that size, two
   of them side by side on two separate rows visibly overflowed the
   modal's own 44rem max-width; a proper small switch fixes both the size
   and the "which one is active" legibility in one move. */
.audio-settings-row {
  align-items: center;
  display: flex;
  justify-content: space-between;
  margin-top: 1.6rem;
}

.audio-settings-label {
  align-items: center;
  color: #fff;
  display: flex;
  font-size: 1.6rem;
  gap: .8rem;
}

.audio-settings-label svg {
  color: var(--toasted-almond);
  height: 1.6rem;
  width: 1.6rem;
}

/* Same shape as any native OS on/off switch — a pill track, a round knob
   that slides between its two ends. State lives in aria-pressed (set by
   audio-settings.js), not a class, so the control is announced correctly
   to assistive tech for free. */
.audio-toggle-switch {
  background: rgba(255, 255, 255, .15);
  border: none;
  border-radius: 999px;
  cursor: pointer;
  flex-shrink: 0;
  height: 2.4rem;
  padding: .2rem;
  position: relative;
  transition: background .2s ease;
  width: 4.4rem;
}

.audio-toggle-switch[aria-pressed="true"] {
  background: var(--autumn-ember);
}

.audio-toggle-knob {
  background: #fff;
  border-radius: 50%;
  display: block;
  height: 2rem;
  transition: transform .2s ease;
  width: 2rem;
}

.audio-toggle-switch[aria-pressed="true"] .audio-toggle-knob {
  transform: translateX(2rem);
}

/* The closing "Better Poster" (static/js/poster.js) — reuses the .modal
   shell (.modal/.modal-backdrop/.modal-panel) but needs to be much larger
   than the standard 44rem .modal-panel, since it's a whole poster rather
   than a small dialog; scrolls internally instead of ever pushing content
   off-screen. */
.poster-panel {
  max-height: 90vh;
  max-width: 104rem;
  overflow-y: auto;
}

.poster-headline {
  border-bottom: .1rem solid rgba(187, 184, 178, .3);
  margin-bottom: 2.4rem;
  padding-bottom: 2.4rem;
  text-align: center;
}

.poster-headline h1 {
  margin-bottom: .8rem;
}

/* The team's own subtitle (results.js's askPosterTitle()) — hidden
   entirely via poster.js when none was given, rather than rendering an
   empty line. Toasted-almond, between the (white) h1 and the (silver)
   auto-generated statement below — its own visual step between the two. */
#poster-headline-subtitle {
  color: var(--toasted-almond);
  font-size: 1.6rem;
  margin-bottom: .8rem;
}

#poster-headline-statement {
  color: var(--silver);
  font-size: 1.9rem;
}

.poster-grid {
  display: grid;
  gap: 2.4rem;
  grid-template-columns: 1fr 1fr;
}

@media (max-width: 900px) {
  .poster-grid {
    grid-template-columns: 1fr;
  }
}

.poster-block {
  background: rgba(255, 255, 255, .04);
  border-radius: .6rem;
  padding: 1.6rem 2rem;
}

.poster-block h3 {
  color: var(--autumn-ember);
  margin-bottom: 1.2rem;
}

#poster-scenario {
  margin-top: 1.2rem;
}

.poster-hypothesis,
.poster-round-rule,
.poster-reflection-row {
  margin-bottom: 1.2rem;
}

.poster-hypothesis:last-child,
.poster-round-rule:last-child,
.poster-reflection-row:last-child {
  margin-bottom: 0;
}

.poster-hypothesis h4,
.poster-round-rule h4 {
  margin-bottom: .4rem;
}

/* Winning theory called out among the three, same accent color as every
   other "this one won" label in this app (results.js's own
   .results-winning-theory heading). */
.poster-winner h4 {
  color: var(--autumn-ember);
}

/* Each round's own bar (poster.js's renderSingleBar()) sits right under
   that round's description text — a little separation from it, since
   .score-bar-row's own margin-bottom (custom.css, general rule) only ever
   adds space *after* a row, not before one. */
.poster-round-rule .score-bar-row {
  margin-top: .8rem;
}

/* No label column at all (unlike .score-bar-row--no-count, which still
   reserves 9rem for one) — poster.js's per-round bars don't repeat that
   round's own name, already stated in the heading right above. */
.score-bar-row--bar-only {
  grid-template-columns: 1fr;
}

/* Round-by-Round Results' own static legend (hand_or_voice.html) —
   explains what guessed/passed actually mean, not just which color is
   which (unlike #play-score-modal's own compact .score-legend, which
   assumes the reader already knows the game). A vertical stack of two
   short lines, not a centered horizontal row like .score-legend — there's
   real sentence-length text here, not just a word each. */
.poster-legend {
  border-bottom: .1rem solid rgba(187, 184, 178, .2);
  margin-bottom: 1.2rem;
  padding-bottom: 1.2rem;
}

.poster-legend p {
  align-items: baseline;
  color: var(--silver);
  display: flex;
  font-size: 1.3rem;
  gap: .6rem;
  margin-bottom: .4rem;
}

.poster-legend p:last-child {
  margin-bottom: 0;
}

.poster-legend strong {
  color: #fff;
  flex-shrink: 0;
}

.poster-legend .score-legend-swatch {
  flex-shrink: 0;
  margin-top: .3rem;
}

.poster-reflection-question {
  color: var(--silver);
  font-size: 1.4rem;
  margin-bottom: .2rem;
}

/* "Download My Findings" (poster.js) hands off to the browser's own print
   dialog rather than a vendored PDF library — window.print() + this
   isolates just the poster on the page. #poster-overlay is a direct child
   of <body> (the overlay block renders right after </main>, same level as
   #slide-veil/.audio-toggles), so hiding every other direct child leaves
   only the poster behind. Colors are overridden to print legibly on paper
   — the app's own dark theme (var(--silver) labels, #fff counts, a
   near-transparent bar track) would otherwise print as near-invisible
   light grey on white. */
@media print {
  body > *:not(#poster-overlay) {
    display: none !important;
  }

  #poster-overlay {
    background: #fff;
    display: block !important;
    inset: 0;
    position: static !important;
  }

  #poster-overlay .modal-backdrop,
  #poster-overlay .modal-close {
    display: none !important;
  }

  .poster-panel {
    background: #fff;
    box-shadow: none !important;
    color: #000;
    max-height: none;
    max-width: none;
    overflow: visible;
    width: 100%;
  }

  .poster-block {
    background: none;
    border: .1rem solid #ccc;
  }

  #poster-overlay #poster-headline-statement,
  #poster-overlay #poster-headline-subtitle,
  #poster-overlay .score-bar-label,
  #poster-overlay .score-bar-count,
  #poster-overlay .poster-legend p,
  #poster-overlay .poster-legend strong,
  #poster-overlay .poster-reflection-question {
    color: #000 !important;
  }

  #poster-overlay .score-bar-track {
    background: #eee;
  }
}
