/* Copyright (c) 2024-2026 Kilotronic LLC. All rights reserved. */

/* Material Symbols Rounded subsets (Apache-2.0) + the .ms-icon helper now
   live in ui/shared/shared.css (menu-strategy 2026-07-30) so the player app
   can render icon glyphs too, without duplicating the font. Built by
   scripts/subset_material_symbols.py. host.css and its callers rely on
   shared.css being loaded first (see host.html link order). */

/* The one full-width CTA cap (ui-style.md Width: full-width is `width:100%`
   capped, centered). It was previously the literal 320px repeated in three
   places and scoped to #auth-buttons / #phone-auth / #collecting-panel, which
   is how the config CTA (480px) and the shared tertiary ghost (432px) escaped
   it. One token, applied on the buttons themselves. */
:root {
  --cta-max: 320px;
  /* Reading width for recap blocks (final scores, per-round table, stat lines).
     The recap is read together at the end of a game, so it stays a centered
     column rather than spreading to the window on desktop and iPad. */
  --recap-col-max: 420px;
}

body {
  display: flex;
  flex-direction: column;
  height: 100vh; /* fallback for older browsers */
  height: 100dvh; /* dynamic: shrinks when Safari address bar is visible */
  overflow: hidden;
  background: radial-gradient(ellipse at 50% -10%, var(--bg-1) 0%, var(--bg-2) 65%);
  /* Touch-primary console: suppress the iOS long-press text-selection +
     callout menu and the tap highlight, so a stray long touch on a button
     never opens the system text-select context menu. Editable fields opt
     back in below. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* No overlay scroll-lock rule here, deliberately. `body` above is already
   `overflow: hidden` unconditionally — this console is a fixed app shell — so
   the old `body.overlay-open` class it replaced was pinning something already
   pinned. It cost a JS toggle, five call sites, and a regression suite guarding
   close paths against "leaking" a lock that changed nothing. Verified in the
   browser: with every dialog closed, computed body overflow is still hidden,
   from this base rule. The player app's body DOES scroll, but it never had the
   host-only class, so nothing regresses there; giving it a real lock via
   `body:has(dialog[open])` in shared.css is a separate, deliberate change. */

/* Editable fields keep native selection/editing despite the body opt-out. */
input,
textarea,
[contenteditable="true"] {
  -webkit-user-select: text;
  user-select: text;
}

/* Prose opts back in too. The body-level opt-out exists to stop a stray
   long-press on a BUTTON opening the iOS callout menu — it was never meant to
   make the rules unreadable-by-copy on a desktop browser, where the host may
   well want to paste the setup line into a group chat (owner, 2026-07-29
   playtest). Scoped to the read-only rules/overview text, so buttons, the
   marquee, and every gameplay control keep the touch-console behavior. */
#intro-overview,
#intro-rules,
#intro-also-known-as,
#intro-plays-like,
.intro-rounds,
#intro-themes,
#rules-howto,
#rules-ways,
#rules-deckinfo {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

/* ── Auth overlay ────────────────────────────────────────────────── */
#auth-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--bg-2);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Anchor the sign-in stack toward the upper third rather than dead center, so
     a short heading + button doesn't float in a tall-phone void. */
  justify-content: flex-start;
  padding-top: clamp(64px, 22vh, 200px);
  gap: 16px;
}
#auth-overlay.hidden {
  display: none;
}
#auth-overlay h2 {
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 32px;
}
#auth-overlay p {
  color: var(--text-muted);
  font-size: 14px;
  max-width: 400px;
  text-align: center;
}
/* Skin (fill, radius, press) lives on .btn/.btn--primary/.btn--secondary now
   (ui-style backlog #2 — .auth-btn moves to the 18px/14px-radius CTA scale
   instead of its old one-off 15px/6px-radius); auth-btn stays a layout hook. */
/* Standard stacked sign-in layout: one full-width button per line, matching the
   phone form's width below. Width is scoped to provider buttons so the phone
   form's inline input + "Send code" row (also .auth-btn) keeps its layout. */
#auth-buttons {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: var(--cta-max);
}
#auth-buttons .auth-btn {
  width: 100%;
}
#auth-buttons.hidden,
#phone-auth.hidden {
  display: none;
}
#phone-auth {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
  max-width: var(--cta-max);
}
.phone-form {
  display: flex;
  gap: 8px;
  width: 100%;
}
.phone-input {
  flex: 1;
  min-width: 0;
  padding: 10px 14px;
  font-size: 16px; /* ≥16px so iOS Safari doesn't zoom on focus */
  border-radius: var(--radius-sm);
  border: 1px solid var(--bg-1);
  background: var(--surface);
  color: var(--text);
}
.phone-promise {
  /* Inherits #auth-overlay p (muted, centered, max-width 400). */
  font-size: 12px;
}
.phone-consent {
  /* Carrier consent fine print: smaller and more muted than the promise. */
  font-size: 11px;
  color: var(--text-muted);
  line-height: 1.4;
}
.phone-consent a {
  color: inherit;
  text-decoration: underline;
}
.phone-back {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 13px;
  cursor: pointer;
  padding: 4px;
}
.phone-back:hover {
  color: var(--text);
}
#auth-error {
  color: var(--accent);
  font-size: 13px;
}
/* Scoped under #auth-overlay so these outweigh the #auth-overlay p base rule
   (1-0-1) — bare #id / .class selectors silently lose to it. */
#auth-overlay .auth-guest-note {
  color: var(--text-muted);
  font-size: 12px;
  text-align: center;
  margin-top: 4px;
}
#auth-overlay .auth-providers-subhead {
  margin: 1.25rem 0 0.25rem;
  font-size: 0.9rem;
  opacity: 0.75;
  /* Two sentences since 2026-08-02 (it absorbed the age requirement), so cap it
     at the auth-button width the deleted #auth-host-note used — otherwise it
     runs the full overlay width and reads as a paragraph, not a caption. */
  max-width: var(--cta-max);
  text-align: center;
}

/* ── Header ──────────────────────────────────────────────────────── */
/* The global chooser chrome (wordmark + account avatar) is the chooser's
   header per spec D4/D7. On every other screen (config, collecting, gameplay)
   the only chrome is the in-display #phone-header, so the global <header> is
   hidden unless the body carries .screen-chooser (set by setScreen). It stays
   hidden behind the full-screen auth overlay during sign-in, which is fine. */
header {
  display: none;
}
body.screen-chooser header {
  background: var(--bg-2);
  padding: 8px 20px;
  display: flex;
  align-items: center;
  gap: 16px;
  border-bottom: 1px solid var(--surface);
  flex-shrink: 0;
}
header h1 {
  font-family: var(--font-display);
  font-size: 16px;
  color: var(--accent);
  white-space: nowrap;
}
/* The zero-width badge slot exists so the pill can't push a CENTERED wordmark
   off center (shared.css). This header brand is corner-anchored — nothing to
   keep centered — so restore normal flow here, or the overflowing pill sits on
   top of #dev-info. */
header h1 .beta-badge-slot {
  width: auto;
}
#action-error {
  position: fixed;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1000;
  padding: 8px 18px;
  background: var(--accent);
  color: var(--bg-2); /* dark text on coral: 7.17:1 */
  font-size: 13px;
  font-weight: 600;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
  pointer-events: none;
}
#action-error.hidden {
  display: none;
}
#dev-info {
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
}

/* ── Account avatar + sheet (header top-right) ─────────────────── */
.account-wrap {
  position: relative;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Persistent reminder that a guest's game data is ephemeral (24h idle purge).
   Sits beside the avatar, not inside the account-sheet dropdown, so it stays
   visible without an extra tap. */
.guest-badge {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}
.guest-badge.hidden {
  display: none;
}
.avatar-btn {
  /* Explicit 44×44 (glyph-family floor) + overflow so an IdP photo cannot
     inflate the control past chrome scale — 2026-08-07 playtest. width/height
     are allowed on members; min-* / border-radius stay with the family. */
  width: 44px;
  height: 44px;
  overflow: hidden;
  border: none;
  cursor: pointer;
  font-size: 13px;
  font-weight: 700;
  color: white;
  background: linear-gradient(135deg, var(--accent), var(--accent-active));
  flex-shrink: 0;
  touch-action: manipulation;
}
.avatar-btn:hover,
.avatar-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* No real display name yet: the button text is a generic person Material Symbol
   (host-identity.js DEFAULT_AVATAR_GLYPH), not an initial, so switch to the icon
   font and bump the size to fill the circle. Toggled by host.js setAvatar(). */
.avatar-btn.avatar-default-glyph {
  font-family: "Material Symbols Rounded";
  font-weight: normal;
  font-size: 20px;
  font-feature-settings: "liga" 0;
}
/* IdP profile photo (data: URL via /auth/account/photo) as the host's face on
   the account avatar AND on every menu control — #header-menu-btn plus the four
   .collecting-menu-btn hamburgers — so who is hosting is legible from any
   screen, not only the chooser. host-auth.js adds the class when the photo
   loads and takes it off at sign-out. The 44×44 + overflow repeats .avatar-btn's
   containment because the hamburgers only ever had a MIN size (the glyph family
   rule): without it a large source inflates them. */
.has-avatar-photo {
  width: 44px;
  height: 44px;
  overflow: hidden;
}
.avatar-btn img,
.has-avatar-photo img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
#account-sheet {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 180px;
  background: var(--surface);
  border-radius: var(--radius-md);
  border: 1px solid rgba(255, 255, 255, 0.08); /* hairline, matches #account-who's divider */
  box-shadow: var(--shadow-lg);
  padding: 8px 0;
  z-index: 300;
  display: flex;
  flex-direction: column;
}
/* A <button> inherits text-align:center from the UA sheet, which is invisible
   while every row fits on one line — the anonymous flex item shrinks to its
   text and space-between parks it at the start, so the row READS left-aligned.
   The moment a row wraps (the sheet is 180px and "Sign in to save your games"
   is not), the item fills the width and the centering appears, leaving one row
   centered among left-aligned siblings. Say what was always meant. */
#account-sheet .menu-item {
  text-align: left;
}
#account-who {
  font-size: 12px;
  color: var(--text-dim);
  padding: 8px 16px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ── Main layout: display (top) + controls (bottom) ──────────── */
main {
  /* Flex child of body: fill remaining height after the header.
     min-height: 0 lets it shrink below its content size so child
     overflow-y: auto containers (e.g. #game-chooser) can scroll. */
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

#app-container {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

/* ── Chooser party-code QR (replaces the old text banner, 2026-08-07) ─── */
/* Same QrOverlay chrome as the lobbies. Chooser-only: hide on every other
   screen via .screen-chooser so a lingering mount cannot float over intro/
   config/gameplay. The shared `.qr-overlay` rule owns fixed positioning. */
body:not(.screen-chooser) #chooser-partycode {
  display: none !important;
}
.code-part {
  letter-spacing: 0.1em;
}
/* .code-part-tail::before (the thin space) lives in shared.css. */

/* ── Game chooser ────────────────────────────────────────────────── */
/* Host chrome rule (2026-08-07 playtest): every setup screen sits on the same
   dark page ground; content objects use --surface. Intro/config/lobby are
   480px phone cards; the chooser is a wider centered column so game tiles
   can breathe in two columns without going full-bleed edge-to-edge. */
#game-chooser {
  padding: 24px;
  flex-direction: column;
  gap: 16px;
  width: 100%;
  max-width: 720px;
  margin-inline: auto;
  box-sizing: border-box;
}
/* .screen-zone gives #collecting-zone its scroll: the join card can exceed a
   short phone viewport (Safari bars shrink it further), and a clipped
   non-scrolling zone left Start/Fill stranded under the toolbar. margin:auto
   on the panel (below) centers it when it fits and lets it scroll when it
   doesn't. Nothing else to add here — the shared recipe is the whole rule. */
#collecting-panel {
  position: relative;
  margin: auto;
  text-align: center;
  padding: 20px 24px;
  /* width:100% is what makes the cap bind — same pairing as #intro-panel and
     #config-step. As a bare flex item in the row-flex zone above, the card
     sized to its own max-content instead: its width tracked whatever text it
     held, so the Start CTA flipping from "Waiting for celebrity names" to
     "Start Game" shrank the card mid-lobby (326px → 272px) and the
     absolutely-positioned .collecting-menu-btn (right:12px) came down on top
     of the QR. A card's width is the layout's business, not its content's. */
  width: 100%;
  max-width: 480px;
  background: var(--surface);
  border-radius: var(--radius-card);
  font-family: var(--font);
}
#collecting-game-name {
  /* Bricolage display face + hero size, matching the intro/config screens so
     the game name reads as this screen's title rather than plain body text. */
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 30px;
  line-height: 1.05;
  font-weight: 700;
  margin-bottom: 6px;
  overflow-wrap: break-word;
}
/* Hamburger in the panel's upper-right — opens the collecting menu (Quit).
   Skin (box, glyph, hover) is the shared .menu-trigger (shared.css); this
   keeps only the positional delta. */
.collecting-menu-btn {
  position: absolute;
  top: 12px;
  right: 12px;
}

/* The tapped player's name inside the Reveal-role confirm — its own
   emphasized line above the static body copy (filled at open time by
   _revealImpostorRole). */
#impostor-reveal-role-confirm-name {
  font-weight: 700;
  margin: 0 0 6px;
}

/* Scroll runway under the viewport-fixed QR overlay: a lobby panel can grow
   (the impostor roster grows with every player; long game-name/next-step copy
   does the same to the collecting panel on a small viewport) until its
   bottom-of-column CTA sits exactly under the expanded card, which intercepts
   every click (found by the 2026-08-05 live verification drive on the
   impostor lobby's Play CTA). .screen-zone scrolls, so padding the ZONE
   reserves space the CTAs can always scroll into, clear of the fixed card
   (~320px expanded). Keyed on the overlay mount being present and visible —
   not on zone ids — so every lobby that mounts one, current or future, gets
   the runway with no wiring (host.js sets `hidden` on the mount when the
   session has no join code, and a hidden mount paints no card to dodge). */
.screen-zone:has(.qr-overlay:not([hidden])) {
  padding-bottom: 340px;
}

/* ── Impostor lobby (PR 10 Task 10.1, D5) ──────────────────────────── */
/* Same panel shape as #impostor-panel above (its own .screen-zone already
   carries the scroll recipe) — a review finding caught this screen shipping
   with zero CSS, and a second (2026-08-05, owner: "the styling is broken on
   the impostor lobby") caught both Impostor panels missing the raised-card
   treatment every other lobby surface (#collecting-panel) has. */
#impostor-lobby-panel {
  position: relative; /* anchors the absolutely-positioned menu trigger */
  margin: auto;
  padding: 20px 24px;
  width: 100%;
  max-width: 480px;
  box-sizing: border-box;
  background: var(--surface);
  border-radius: var(--radius-card);
  /* Shared fade defaults to --bg-2; panel ground is --surface. */
  --impostor-roster-fade-to: var(--surface);
}
#impostor-lobby-game-name {
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 24px;
  line-height: 1.05;
  font-weight: 700;
  margin-bottom: 12px;
  overflow-wrap: break-word;
}
/* Same recipe as .overlay-dialog--account's text inputs (host-auth.js's
   profile/delete-confirm dialogs — #profile-display-name-input,
   #delete-confirm-phrase) — that rule is scoped to dialogs, but the LOOK is
   the codebase's general text-input idiom, not overlay-specific, so it is
   named here as a reusable class rather than re-scoped to this one screen. */
.text-input {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid var(--surface);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
  color: var(--text);
  box-sizing: border-box;
}
/* Plus affordance matches player prompt-entry (play.css .composer-add-btn
   coral fill) — 2026-08-07 playtest. Geometry comes from shared.css's
   glyph-button family; the fixed square keeps the flex row from stretching
   it into an oval the way a bare min-height did on the phone. */
.impostor-lobby-add-row .composer-add-btn {
  flex: none;
  width: 44px;
  height: 44px;
  background: var(--accent);
  color: #14110f;
}
.impostor-lobby-add-row .composer-add-btn svg {
  display: block;
  width: 22px;
  height: 22px;
}
.impostor-lobby-add-row .composer-add-btn:active {
  background: var(--accent-active);
}
#impostor-lobby-error {
  color: var(--accent);
  font-size: 13px;
  margin-top: 4px;
  min-height: 1em;
}
/* The <3-players hint under the roster list (loud UX, not a silent server
   error — see _updateImpostorLobbyPlayCta in host.js). */
#impostor-lobby-hint {
  color: var(--text-muted);
  font-size: 13px;
  margin: 4px 0;
}

/* ── Pre-game intro screen (rules-only beat before the lobby) ─────── */
/* #intro-zone carries no rule of its own — .screen-zone (shared.css) is the
   whole recipe it needs. */
#intro-panel {
  position: relative; /* anchors the pre-play .collecting-menu-btn */
  margin: auto;
  text-align: left;
  padding: 24px;
  max-width: 480px;
  width: 100%;
  background: var(--surface);
  border-radius: var(--radius-card);
  font-family: var(--font);
}
.intro-eyebrow,
.intro-howto-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-muted);
}
/* The Next Step widget reads as ONE line — "Next Step: Configure the game
   settings." (owner, 2026-08-02 playtest) — so the label leaves the eyebrow
   treatment for the bold-label-colon pattern and flows inline with the text,
   wrapping naturally when the sentence is long (the config twin's sentence
   does not fit one physical line at host width). The colon is presentation,
   not content, so it lives here and not in the i18n strings. */
.intro-next-label {
  display: inline;
  font-weight: 700;
  color: var(--text);
}
.intro-next-label::after {
  content: ": ";
}
#intro-game-name {
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 30px;
  margin: 6px 0 2px;
}
#intro-overview {
  color: var(--text-dim);
  margin: 0 0 22px;
}
.intro-howto-label {
  margin-bottom: 12px;
}
#intro-rules {
  display: grid;
  gap: 16px;
  margin-bottom: 22px;
}
#intro-materials-section {
  margin-bottom: 22px;
}
#intro-materials {
  color: var(--text-dim);
  margin: 0;
  line-height: 1.42;
}
.intro-rule-key {
  color: var(--accent);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.06em;
  margin-bottom: 3px;
}
.intro-rule-text {
  line-height: 1.42;
}
#intro-also-known-as {
  color: var(--text-muted);
  font-size: 13px;
  margin: 0 0 10px;
}
/* The commercial-mark relation. Same muted body treatment as the folk aliases
   above by design, NOT a badge/chip/pill: a mark must read as plain running
   text in our own type, never as branded furniture (docs/copy-style.md). */
#intro-plays-like {
  color: var(--text-muted);
  font-size: 13px;
  margin: 0 0 10px;
}
#intro-themes {
  margin-bottom: 22px;
  line-height: 1.5;
}
/* Rounds list: one round per line, numbered. The run-on inline form
   ("1 · Say Anything → 2 · One Word Only") read as a single sentence with the
   round names bleeding together (owner, 2026-07-29 playtest). */
.intro-rounds {
  margin-bottom: 22px;
  line-height: 1.5;
}
.intro-round {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0;
}
/* Fixed-width numeral column so the round names align down the left edge
   regardless of the ordinal's width. */
.intro-round-num {
  flex: none;
  min-width: 1.2em;
  color: var(--accent);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Ways-to-play: names only (notes live in the full-rules view); the
   recommended one gets a small accent badge so a first-time host has a
   default pick. */
.intro-way {
  display: block;
  padding: 2px 0;
}
.intro-way.recommended {
  font-weight: 600;
}
.intro-way-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 8px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, var(--surface));
  border-radius: var(--radius-sm);
  vertical-align: middle;
}
/* Pointer affordance opening the full-rules overlay — a lightweight text
   link (not a CTA), matching the accent color used elsewhere on the panel.
   Shared by the intro/ready screens and the round-transition beat
   (.transition-rules-btn). */
/* Skin + type come from the shared text-link family; what stays here is this
   pair's layout only (the chevron gap and the block's own margin). */
.intro-full-rules-btn,
.transition-rules-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin: 2px 0 22px;
  cursor: pointer;
}
.intro-full-rules-btn:hover,
.intro-full-rules-btn:focus-visible,
.transition-rules-btn:hover,
.transition-rules-btn:focus-visible {
  text-decoration: underline;
}
/* Same affordance, appended to the content mount (#phone-headline) on the
   pre-start screen — override the intro panel's large bottom margin, which
   doesn't fit this smaller context. */
#phone-headline .intro-full-rules-btn {
  margin: 4px 0 0;
}
/* Round-transition beat: a touch more top separation so it reads as distinct
   from the round summary above it. */
#phone-headline .transition-rules-btn {
  margin: 12px 0 0;
}
/* --bg-1 (raised panel), not --bg-2: this widget sits INSIDE a --surface card,
   and painting it the page colour made it the only recessed well in the app —
   elevation running backwards. Tiers go up, never down. */
.intro-next {
  background: var(--bg-1);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
  padding: 13px 15px;
  margin-bottom: 22px;
}
.intro-next-text {
  display: inline;
  color: var(--text-dim);
  margin: 0;
  line-height: 1.42;
}
/* The bottom-anchored QR overlay skin (ui/shared/qr-overlay.js) lives in
   ui/shared/shared.css — the component is mounted by the player app's
   Impostor lobby too, which loads only tokens/shared/play css. The
   host-only half (the scroll runway a .screen-zone reserves under the
   fixed card) stays above/elsewhere. */
/* Done button inside the party-code overlay — closes the modal. */
.partycode-continue-btn {
  margin-top: 16px;
  width: 100%;
}
.collecting-stats {
  font-size: 18px;
  color: var(--text-dim);
  margin-bottom: 12px;
}

/* ── Button system ──────────────────────────────────────────────────────
   `.btn` + role/size variants (the shared base + `--primary` / `--secondary`
   / `--neutral` / `--ghost` / `--surface` / `--danger` / `--sm`; `--lg` and
   `--dialog` were retired by #1740) now live in ui/shared/shared.css
   (host.html + play.html both load it) — moved so a future player-side
   consumer doesn't have to reimplement the skin. Everything below is
   host-only: layout deltas per
   button instance, and the input-affordance families intentionally NOT part
   of `.btn` (gameplay .cta-chip, hardware .hw-btn, gamepad .gp-btn, keycap
   .hint-btn). */
/* Input-aware key-hint glyph on the lobby Start/Cancel buttons. Mirrors
   .confirm-btn-hint, but the glyph is hidden for touch-only operators (no
   keyboard, no gamepad) so a touch user is never shown a ↵/Esc keycap they
   can't press — gamepad → controller glyph, keyboard → ↵/Esc, else nothing. */
.lobby-btn-hint {
  display: inline-flex;
  margin-left: 8px;
  vertical-align: middle;
}
.lobby-btn-hint .hint-btn {
  width: 16px;
  height: 16px;
  font-size: 10px;
}
body.no-controller:not(.has-keyboard) .lobby-btn-hint {
  display: none;
}
/* Chooser RESUME-card delete-shortcut hint (batch-A follow-up #4). Purely a
   keyboard/gamepad discoverability aid — the pointer-only × on each card
   needs no equivalent, so touch-only operators (no keyboard, no gamepad)
   never see this at all, same rule as .lobby-btn-hint above. */
.resume-delete-hint {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 6px;
  font-size: 12px;
  color: var(--text-dim);
}
body.no-controller:not(.has-keyboard) .resume-delete-hint {
  display: none;
}
/* Collecting action buttons stack vertically, full-width, so the primary
   Start CTA anchors the bottom of the panel — via the shared `.btn--stack`
   class (shared.css), which also covers the config launch CTA and the intro
   panel's actions (the three previously re-declared this recipe separately,
   with an 8px/10px margin-top drift between them). */

#game-chooser h2 {
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--accent);
}
/* The heading takes programmatic focus (tabindex="-1") on sign-in as a
   screen-reader screen-change handoff — but it is not an interactive control
   and is never Tab-reachable, so it must never paint a focus ring. Suppress it
   unconditionally: the host drives the chooser entirely by controller keys, and
   the first keypress would otherwise flip the focused heading into
   :focus-visible and surface the default blue ring. The selected game card
   carries the visible selection instead. */
#game-chooser h2:focus,
#game-chooser h2:focus-visible {
  outline: none;
}
/* ── RESUME fast path ────────────────────────────────────────────── */
/* Deliberately not a card: it sits above the game grid as a plain CTA plus a
   quiet link, so it reads as one shortcut rather than a fourth section
   competing with the library and the two session lists below.
   The button's width and centering come from .btn--stack (shared.css), the
   same recipe every other stacked CTA takes; only the link's own placement is
   declared here. */
.chooser-resume-fastpath {
  margin-bottom: 20px;
}
.chooser-resume-see-others {
  display: block;
  margin: 8px auto 0;
  text-align: center;
}
/* ── RESUME section ──────────────────────────────────────────────── */
.chooser-resume-section {
  margin-bottom: 16px;
}
/* Label + divider line come from the .category-header element inside the section
   (same markup as the GUESS/WORDPLAY group headers) — no pseudo-element label. */
/* Session history is a simple list (2026-08-07 playtest) — tiles spend too
   much real estate for resume/recap rows. Game library tiles keep the grid. */
#resume-grid,
#completed-grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.category-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
}
.game-card {
  background: var(--surface);
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-card);
  padding: 20px;
  cursor: pointer;
  transition: border-color 0.15s;
  position: relative;
}
.resume-card,
.completed-card {
  background: var(--surface);
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-card);
  padding: 12px 14px;
  padding-right: 44px; /* room for the dismiss × on resume rows */
  cursor: pointer;
  transition: border-color 0.15s;
  position: relative;
}
.game-card:hover,
.resume-card:hover,
.completed-card:hover {
  border-color: var(--accent);
}
.resume-card-name,
.completed-card-name {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 2px;
}
.resume-card-meta,
.completed-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 12px;
  color: var(--text-dim);
}
.resume-card-time::after,
.resume-card-players::after,
.completed-card-time::after,
.completed-card-players::after {
  content: " ·";
  margin-left: 2px;
}
.resume-card-summary:last-child::after,
.resume-card-players:last-child::after,
.resume-card-time:last-child::after,
.completed-card-winner:last-child::after,
.completed-card-scores:last-child::after,
.completed-card-players:last-child::after,
.completed-card-time:last-child::after {
  content: "";
}
.resume-card-dismiss {
  position: absolute;
  top: 6px;
  right: 6px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 16px;
  cursor: pointer;
}
.resume-card-dismiss:hover {
  color: var(--text);
  background: var(--bg-1);
}
/* ── COMPLETED section ───────────────────────────────────────────── */
.chooser-completed-section {
  margin-bottom: 16px;
}
/* Label + divider line come from the .category-header element inside the section
   (same markup as the GUESS/WORDPLAY group headers) — no pseudo-element label.
   .completed-card-name/-meta and the ::after bullet/reset pairs are grouped
   with their .resume-card-* counterparts above — one JS builder emits both
   card kinds from the same markup shape. */
/* ── Completed recap overlay ─────────────────────────────────────── */
/* `dialog.overlay-backdrop` (ui/shared/shared.css) now supplies the
   position/inset/scrim/[open]-display chrome; `.completed-recap-overlay`
   survives only as a scoping class for the recap-detail rules below (and
   the shared `:is(body.recap-mode, .completed-recap-overlay)` selectors —
   pinned verbatim by stage-bands.test.js) — it carries no layout of its own
   anymore. `#completed-recap-inner` is this dialog's card: wider and taller
   than the base `.overlay-dialog` confirm card (a content viewer, not a
   confirm), so it overrides the shared min/max-width and scrolls internally
   past 90dvh, mirroring `#rules-inner`. */
#completed-recap-inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  min-width: 260px;
  max-width: 520px;
  max-height: 90dvh;
  padding: 0;
}
.completed-recap-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 16px 8px;
  border-bottom: 1px solid var(--bg-1);
  margin-bottom: 16px;
  flex: 0 0 auto;
}
.completed-recap-game-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
/* .overlay-close base + :hover live in shared.css (host + player share them). */
.completed-recap-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 0 16px;
}
/* The body scrolls, so it carries tabindex=0 (keyboard users must be able to
   reach and scroll it); give that focus a visible ring. */
.completed-recap-body:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.completed-recap-footer {
  padding: 16px;
  border-top: 1px solid var(--bg-1);
  display: flex;
  justify-content: center;
  flex: 0 0 auto;
}
/* `.btn.btn--primary` owns the coral skin (and now the shared --radius-btn,
   correcting the former --radius-card mismatch); keep only this button's
   bespoke width/size and its opacity hover. */
.completed-recap-play-again-btn {
  padding: 12px 32px;
  font-size: 15px;
  font-weight: 600;
}
.completed-recap-play-again-btn:hover {
  opacity: 0.85;
}
/* ── Game grid ───────────────────────────────────────────────────── */
#game-grid {
  display: block;
}
.category-section {
  margin-bottom: 16px;
}
.category-header {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  text-transform: uppercase;
  margin-bottom: 8px;
}
.category-header::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--bg-1);
}
.category-subtitle {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.75;
}
.category-expand-btn {
  background: none;
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-xs);
  color: var(--text-dim);
  cursor: pointer;
  font-size: 11px;
  padding: 2px 8px;
  margin-bottom: 8px;
}
.category-expand-btn:hover {
  border-color: var(--accent);
  color: var(--text);
}
/* Gamepad focus glow: cards use alpha 0.3; config rows/choice cards use 0.2
   (below). Intentional pending an owner flatten-or-keep call — do not silently
   unify the two strengths. See docs/todo.md gamepad-focus entry. */
.game-card.gp-focused,
.resume-card.gp-focused,
.completed-card.gp-focused {
  border-color: var(--clr-proceed);
  box-shadow: 0 0 0 2px rgba(var(--clr-proceed-glow-rgb), 0.3);
}
.game-card.selected {
  border-color: var(--accent);
  background: var(--selected-fill);
}
.game-card h3 {
  font-size: 17px;
  margin-bottom: 6px;
}
.game-card p {
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 8px;
}
/* Folk-name aliases, between the description and the fit chips.
   Reads as prose next to the description; .game-fit-chips below is data. */
.game-aka {
  font-size: 11px;
  color: var(--text-dim);
  margin-bottom: 6px;
}
/* Deliberately identical to .game-aka: on the card the mark is one more line
   of muted body text, never emphasized above our own game name. Kept as its
   own class so the two lines stay separately targetable. */
.game-plays-like {
  font-size: 11px;
  color: var(--text-dim);
  margin-bottom: 6px;
}
/* Fit chips (players · minutes · materials) — same muted data voice as the
   retired teams/rounds meta line, laid out as chips so freeform and guided
   games share one chooser grammar. */
.game-fit-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 2px;
}
.game-fit-chip {
  font-size: 11px;
  color: var(--text-dim);
  background: var(--bg-1);
  border-radius: var(--radius-xs);
  padding: 2px 8px;
}

.game-card.more-games-hidden {
  display: none;
}

/* ── Focused game-config step ────────────────────────────────────── */
/* .screen-zone (shared.css) supplies the hide/show + scroll recipe; this
   rule keeps only the config step's own extras. */
#config-zone {
  flex-direction: column;
  padding: 24px;
}
#config-step {
  position: relative; /* anchors the pre-play .collecting-menu-btn */
  width: 100%;
  max-width: 480px;
  margin: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
#config-step-name {
  /* Doubled (F4) so the game name reads as the screen's hero. Bricolage bold at
     this size; tightened line-height and overflow-wrap keep a long name (e.g.
     "Fill in the Blanks") tidy on the phone/host viewport instead of overflowing. */
  font-size: 44px;
  line-height: 1.05;
  font-weight: 700;
  color: var(--accent);
  font-family: var(--font-display);
  margin: 0;
  overflow-wrap: break-word;
}
/* One-line game description under the title (spec §7): a muted subtitle pulled
   up tight against the hero name so the otherwise-bare config screen reads as
   name + context, not two loose rows. Hidden (no reserved space) when the game
   carries no description. */
#config-step-desc {
  margin: -8px 0 0;
  font-size: 17px;
  line-height: 1.3;
  color: var(--text-muted);
}
/* The app's one "go back a step" treatment. These two sit on consecutive
   screens of the pre-game flow, and read as one affordance, so they share a
   size — the intro's used to be 13px in a 28px-tall box (the shortest target in
   the app) against the config's 15px in a 44px one (owner decision 2026-08-01).
   Their POSITIONS still differ by rule: the config Back is a top-left nav chip,
   the intro Back a stacked tertiary under the primary. Everything else
   (`.btn`'s 44px floor, `.btn--stack`'s full-width cap) now applies
   untouched — the intro Back's old `.collecting-btn-tertiary` class was
   re-stating that cap and suppressing that floor, and had exactly one user. */
.config-back-btn,
#intro-back-btn {
  font-size: 15px;
}
.config-back-btn {
  align-self: flex-start;
}
/* `.btn.btn--primary` owns the skin AND the size: this CTA is often literally
   the intro screen's "Continue" one step later, so it must not be a size tier
   taller than the button it continues (owner, 2026-08-01). */
/* The config launch CTA is a full-width stacked action (`.btn--stack`,
   shared.css), so it takes the cap. It measured 480px on staging — exactly
   its panel's width, so it read as attached to the panel while sitting
   outside it. Colour is deliberately coral, not proceed-blue: it's a chrome
   launch button (see the intent-palette note further down).

   Intro screen: Continue + "Back to Games" are a STACKED column too, so both
   take the same full-width treatment (ui-style.md: "auto width in a row,
   full-width in a stacked column"). Previously Continue sat at its 115px
   intrinsic width above a 432px ghost, so the pair had neither shape. */
#game-config-panel {
  display: block;
  background: var(--surface);
  border-radius: var(--radius-card);
  padding: 16px;
}
.config-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 8px 0;
  padding: 4px 8px;
  border-radius: var(--radius-xs);
  border: 1px solid transparent;
}
/* Read-only "presets: 3:00 · 2:30 · 2:00" adornment under the timer row.
   flex-basis:100% (with the row's flex-wrap) drops it onto its own line. */
.config-row .gp-config-preset-hint {
  flex-basis: 100%;
  text-align: right;
  font-size: 11px;
  color: var(--text-dim);
  margin-top: -2px;
}
.config-row label {
  font-size: 13px;
  color: var(--text-dim);
  min-width: 56px;
  flex: 0 0 auto;
}
.config-row input,
.config-row select {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-xs);
  padding: 4px 8px;
  font-size: 13px;
}
/* Alpha 0.2 — softer than the 0.3 card ring above; same pending-owner note. */
.config-row.gp-focused {
  border-color: var(--clr-proceed);
  box-shadow: 0 0 0 2px rgba(var(--clr-proceed-glow-rgb), 0.2);
}
.config-row .config-value {
  font-size: 13px;
  color: var(--text);
  /* Flex-fill the space between the steppers and shrink below content width so
     a long value (e.g. "Celebrity Names (211 items)") ellipsizes instead of
     wrapping the whole row to a second line on a narrow phone. */
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.config-row .config-arrow {
  color: var(--text-dim);
  /* ≥44px tap target for the d-pad-equivalent steppers (touch-primary) */
}
.config-row.gp-focused .config-arrow {
  color: var(--clr-proceed);
}
/* Chooser wrap indicator — host-chooser flashes this on the focused row's
   value chrome when ◀/▶ loops past an end (configNav flags the wrap), so the
   last→first jump reads as a deliberate loop, not a glitch. */
.config-wrap-flash {
  animation: config-wrap-pulse 0.45s ease-out;
}
@keyframes config-wrap-pulse {
  0% {
    color: var(--clr-proceed);
    transform: scale(1);
  }
  35% {
    color: var(--clr-proceed);
    transform: scale(1.12);
  }
  100% {
    transform: scale(1);
  }
}
/* No reduced-motion override: config-wrap-pulse's 100% keyframe (scale(1),
   the un-animated base state) matches what shared.css's universal instant-
   finish rule already lands on — an explicit `animation: none` here would be
   redundant. */
.config-row.action-row {
  justify-content: center;
  margin-top: 12px;
}
/* Skin (fill, radius, size, press) lives on .btn/.btn--primary now (ui-style
   backlog #1; `.btn--sm` retired 2026-08-02 — see the CSS dedup train) —
   only the gamepad focus ring below is layout/state specific to this config
   row. */
.config-row.action-row.gp-focused .config-action-btn {
  outline: 2px solid var(--clr-proceed);
  outline-offset: 2px;
}
.config-arrow {
  cursor: pointer;
}
.config-arrow:hover {
  color: var(--clr-proceed);
}
/* Advanced Configuration expander: a divider-styled header row (⚙ gear + label +
   ▸/▾ disclosure caret) that groups the collapsible advanced config fields.
   Collapsed by default; the advanced rows below it render only when open. */
.config-row.gp-config-expander {
  justify-content: flex-start;
  gap: 10px;
  margin-top: 16px;
  padding: 10px 8px;
  border-top: 1px solid var(--bg-1);
  cursor: pointer;
}
.config-row.gp-config-expander .config-gear {
  font-size: 16px;
  color: var(--text-dim);
  line-height: 1;
}
.config-row.gp-config-expander .config-expander-label {
  flex: 1 1 auto;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.config-row.gp-config-expander .config-expander-caret {
  color: var(--text-dim);
  font-size: 14px;
  min-width: 16px;
  text-align: center;
}
.config-row.gp-config-expander.gp-focused .config-gear,
.config-row.gp-config-expander.gp-focused .config-expander-caret {
  color: var(--clr-proceed);
}
/* Advanced fields render below the expander with a small inset so they read as a
   grouped, secondary section rather than peers of the basic Teams/Timer rows. */
.config-row.gp-config-advanced {
  margin-left: 12px;
}
/* A row whose control is the browser's own, rather than a ◀ ▶ widget we clone
   and drive ourselves — either adopted from the page (the locale <select>) or
   generated by renderConfigFields. Same rhythm as the cloned rows (label left,
   control right), same inset as the advanced group it sits in. Its cursor is
   the control's own focus ring: the row is deliberately never painted with
   .gp-focused (that would be a second cursor), so :focus-within carries the
   identical highlight instead. */
.config-row[data-native="true"] {
  justify-content: space-between;
}
/* The locale row is adopted from the page, so it never carries the generated
   rows' .gp-config-advanced class — but it lives in the same advanced group and
   owes it the same inset. */
#locale-row {
  margin-left: 12px;
}
.config-row[data-native="true"]:focus-within {
  border-color: var(--clr-proceed);
  box-shadow: 0 0 0 2px rgba(var(--clr-proceed-glow-rgb), 0.2);
}
/* A pack name is long ("Celebrity Names (211 items)"); let the control shrink
   and ellipsize inside the row instead of widening the panel. */
.config-row[data-native="true"] select {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
}
/* Quantities are 1-3 digits, so size every number field the same rather than
   letting each one take the width its own value happens to need — a column of
   ragged boxes reads as an accident. */
.config-row[data-native="true"] input[type="number"] {
  flex: 0 0 auto;
  width: 5.5em;
  text-align: center;
}
/* Compact basic-config row: Teams + Timer as side-by-side stepper chips. */
.config-setup-row {
  display: flex;
  gap: 12px;
}
.config-setup-row .gp-config-chip {
  flex: 1;
  /* At phone width each chip is ~160px — too narrow for label + control on one
     line. The label takes its own line so the control below it gets the chip's
     full width instead of being squeezed beside the label. */
  align-content: center;
  justify-content: center;
}
.config-setup-row .gp-config-chip label {
  flex-basis: 100%;
  min-width: 0;
  text-align: center;
}
/* The chip's control is the whole chip's width beneath its label, and centres
   its own text, so Teams and Timer read as one pair of matched controls. */
.config-setup-row .gp-config-chip input,
.config-setup-row .gp-config-chip select {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  text-align: center;
}

/* Content-source choice: a real <fieldset> radio group styled as two cards.
   The <fieldset> is the row's whole content (no cloned .config-row label), so
   it resets the browser's default chrome and lays its parts out itself. */
.gp-config-choice .choice-fieldset {
  flex-basis: 100%;
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.gp-config-choice legend {
  padding: 0;
  font-size: 13px;
  color: var(--text-dim);
}
.gp-config-choice .choice-cards {
  display: flex;
  gap: 12px;
  margin-top: 8px;
}
/* Each card IS the option's <label>, so the whole card is the hit target the
   browser already gives a label — no click handler of ours in between. The
   selector is scoped because `.config-row label` would otherwise outrank a bare
   `.choice-card` and re-impose the narrow field-label metrics. */
.gp-config-choice .choice-card {
  flex: 1;
  padding: 14px 16px;
  text-align: center;
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-card);
  transition: border-color 0.15s;
  cursor: pointer;
}
/* The radio itself is the state and the focus target, but a dot beside a card
   this size reads as clutter — so it is hidden from sight while staying
   focusable and announced (never display:none, which would take it out of the
   tab order and the accessibility tree). The card carries its ring instead. */
.gp-config-choice .choice-card input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.gp-config-choice .choice-card-title {
  font-size: 13px;
  color: var(--text);
}
/* Selection and focus read straight off the control: no state class of ours to
   keep in sync with what the browser already knows. */
.gp-config-choice .choice-card:has(input:checked) {
  border-color: var(--accent);
  background: var(--selected-fill);
}
.gp-config-choice .choice-card:has(input:focus-visible) {
  border-color: var(--clr-proceed);
  box-shadow: 0 0 0 2px rgba(var(--clr-proceed-glow-rgb), 0.2);
}
.gp-config-choice .choice-caption {
  margin-top: 8px;
  font-size: 11px;
  color: var(--text-dim);
  text-align: center;
  white-space: normal;
}
/* The group draws its own focus ring on the focused card, so the row must not
   also light up around the whole thing. */
.gp-config-choice:focus-within {
  border-color: transparent;
  box-shadow: none;
}
#create-error {
  color: var(--accent);
  font-size: 13px;
  margin-top: 8px;
}

/* ── Display zone (top) ──────────────────────────────────────────── */
#display-zone {
  flex: 1;
  display: none;
  overflow: hidden;
  position: relative;
}
#display-zone.visible {
  display: flex;
}

#engine-warming {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  font-size: 0.75rem;
  opacity: 0.6;
  padding: 0.2rem 0.5rem;
  border-radius: 0.4rem;
  background: var(--scrim-subtle);
  z-index: 10;
}
#engine-warming.hidden {
  display: none;
}

/* Production (single display): the grid is just a pass-through so #cell-phone
   and #phone-display fill #display-zone edge-to-edge. No padding/gap/centering
   and no independent scroll container — the gameplay screen must FIT the
   viewport with nothing to scroll. The multi-cell wrap/gap/scroll affordance
   (dev emulation of extra device viewports) is applied ONLY when a
   second .viewport-cell is present (see the :has() rule below), so a single
   production cell is always full-bleed. */
#display-grid {
  display: flex;
  flex: 1;
  /* min-width/height:0 override flexbox's default min-size:auto so the grid
     shrinks to #display-zone's width instead of stretching to its content's
     intrinsic min — without this a single cell overflows the phone viewport
     horizontally (the on-device bug). */
  min-width: 0;
  min-height: 0;
}

.viewport-cell {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

/* Multi-cell emulation (dev): once a second viewport cell exists,
   restore the wrap/gap/padding/centering grid and let it scroll. The cells
   then size to their content instead of stretching full-bleed. */
#display-grid:has(.viewport-cell:nth-child(2)) {
  flex-wrap: wrap;
  gap: 24px;
  align-items: flex-start;
  justify-content: center;
  padding: 16px;
  overflow-y: auto;
}
#display-grid:has(.viewport-cell:nth-child(2)) .viewport-cell {
  flex: 0 0 auto;
  align-items: center;
}
/* Phone display zones */
#phone-display {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* min-width:0 lets the column shrink to the cell width rather than its
     content's intrinsic min — full-bleed without horizontal overflow. */
  min-width: 0;
  overflow: hidden;
}
#phone-header {
  position: relative;
  flex-shrink: 0;
  border-bottom: 1px solid var(--surface);
  overflow: hidden;
}
#phone-header-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  font-size: 12px;
  color: var(--text-dim);
  position: relative;
  z-index: 1;
}
/* Header actions (spec 2026-07-22): the timer's transport + the hamburger ride
   the timer bar — "Game Name  X:XX [⏸] [☰]". Both carry the shared
   .menu-trigger skin (spec 2026-07-30) — identical metrics keep the pair
   aligned; this keeps only the deltas .menu-trigger doesn't cover: the larger
   glyph size and the margin that cancels the 44px box's push against the
   ~44px-tall header row (the row already runs that tall with the 28px title).
   Hidden via the .hidden utility when the intent isn't mapped. */
.header-action-btn {
  font-size: 24px;
  flex-shrink: 0;
  margin: -8px -4px;
}
/* CONTRAST IS THE DELTA, not metrics: the transport is the instrument's own
   control and the hamburger is chrome, so they must not read alike. Seating
   the transport in the header (2026-08-03) left it wearing .menu-trigger's
   --text-dim, pixel-identical to the hamburger beside it — on the one screen
   where starting the clock IS the move, the primary action was dressed as
   chrome. Cream binds it to the clock it drives (#header-timer-value is the
   same --text) so the pair reads as one instrument; the hamburger stays dim.
   Metrics are untouched: the 44px box does not move or resize with state
   (decisions.md § UX & UI, the no-teleport corollary).

   Proximity has to agree with that grouping, and it did the opposite: the row's
   10px gap minus .header-action-btn's -4px inline margin left 6px between the
   clock and its transport but only 2px between the two ICONS (the -4px applies
   on both sides of both boxes), so the transport read as the hamburger's
   sibling. Pull it onto the clock and push the chrome away. */
#header-timer-btn {
  color: var(--text);
  margin-left: -8px; /* 10px row gap − 8 = 2px: the clock's own control */
  margin-right: 8px; /* 8 + 10 − 4 = 14px: chrome sits apart */
}
.header-action-btn.hidden {
  display: none;
}
/* The game name is a proper-noun title, so it takes the Bricolage display face
   (like the intro/collecting/config screens), not the default chrome font. It
   first appears here on the start screen (F13) and persists in-game. */
#header-game-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 28px;
  color: var(--text);
  /* Grow to push the timer + header actions into their right-side cluster
     (the row is flex-start now that it holds 4 children, not space-between's
     2). At hero size a long name ("Categories") would shove the timer; let
     the title shrink+ellipsize instead of pushing its right-aligned
     neighbours. */
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
#header-timer-value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 28px;
  /* Cream by default — a clean readable face (NOT the display font), so the
     countdown stays legible. Amber only signals the final stretch (.warn),
     red at expiry (.expired). */
  color: var(--text);
  min-width: 3em;
  text-align: right;
  flex-shrink: 0;
}
#header-timer-value.warn {
  color: var(--c-timer); /* amber — the last WARN_THRESHOLD seconds */
}
#header-timer-bar {
  position: absolute;
  inset: 0;
  transform-origin: left;
  background-color: var(
    --accent
  ); /* JS overrides per timer state; this is the default */
  transition:
    transform 0.1s linear,
    background-color 0.5s linear;
  opacity: 0.35;
}
#header-timer-bar.hidden {
  display: none;
}
@keyframes timer-warn-blink {
  0% {
    opacity: 0.35;
  }
  25% {
    opacity: 0;
  }
  50% {
    opacity: 0.35;
  }
  75% {
    opacity: 0;
  }
  100% {
    opacity: 0.35;
  }
}
/* No reduced-motion override: timer-warn-blink's 100% keyframe (opacity:0.35)
   matches #header-timer-bar's own resting opacity (above), so shared.css's
   universal instant-finish rule already lands here correctly. */
#header-timer-bar.warn {
  animation: timer-warn-blink 0.8s ease-in-out 1;
}
@keyframes timer-expired-flash {
  0%,
  100% {
    opacity: 0.5;
  }
  50% {
    opacity: 0.15;
  }
}
#header-timer-bar.expired {
  transform: scaleX(1) !important;
  background-color: var(--c-negative) !important;
  animation: timer-expired-flash 0.4s ease-in-out 7;
}
#header-timer-value.expired {
  color: var(--c-negative);
}
#linger-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--clr-proceed);
  opacity: 0.6;
  transform-origin: left;
  transform: scaleX(0);
  pointer-events: none;
  z-index: 1;
}
#linger-bar.hidden {
  display: none;
}
#phone-subheader {
  padding: 6px 12px;
  font-size: 13px;
  color: var(--text-dim);
  text-align: center;
  flex-shrink: 0;
}
/* Subheader band: "Team Red: Say anything except the name." in play, "Round 1
   of 3: Say anything except the name." before the turn starts. The lead is the
   part that changes turn-to-turn / round-to-round, so it pops (bold + full
   text color) against the dim rule text it introduces. */
.team-rule b,
.round-info b {
  color: var(--text);
  font-weight: 700;
}
/* Status vitals ("Score: 3 · 12 left") read as one compact separated line,
   not stacked detail rows (spec 2026-07-22). */
.status-line {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 8px;
  font-size: 14px;
  color: var(--text-dim);
}
.status-item {
  white-space: nowrap;
}
.status-sep {
  opacity: 0.5;
}
#phone-headline {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 12px;
  text-align: center;
  font-weight: 700;
  line-height: 1.3;
  white-space: normal;
  overflow-wrap: normal;
  word-break: keep-all;
  hyphens: none;
  text-wrap: balance;
  /* Stage-title band (type ladder, 2026-07-30): status/general headlines
     (current team, "Turn Over", round title, "Ready", winner) take one FIXED
     title size — peers must not resize with string length or viewport, so a
     screen's title always reads at the same rank. Only the hero prompt
     (#phone-headline.is-prompt below) is fit-to-filled (and gets the glow);
     the non-hero shrink (host.js _shrinkStatusBody) still steps this down
     when a composed body genuinely overflows, scaling the em-based bands
     below with it. */
  font-size: 40px;
}
/* Status (non-prompt) headlines anchor toward the upper third instead of dead
   center, so a short "Ready"/team line doesn't float in a tall void on a tall
   phone.  Data-gated on .is-prompt: the hero prompt keeps justify-content:center
   for fit-to-fill (rule below).  The clamp keeps short phones tight and tall
   phones from over-pushing. */
#phone-headline:not(.is-prompt) {
  justify-content: flex-start;
  padding-top: clamp(48px, 18vh, 160px);
  /* Status/framing headlines ("Ready", "Turn Over", team name, winner) are
     chrome — set in the Bricolage display face like titles + the wordmark. */
  font-family: var(--font-display);
}
/* Adaptive anchor for card beats (owner call 2026-08-02): a card PANEL is a
   content block, not a status line, so it does not ride the upper-third
   anchor. The clamp() padding clipped a tall panel's last rows behind
   overflow:hidden (Categories' 12-row card lost rows 11–12) — release it,
   and let auto block margins center a short panel in the stage / collapse
   to zero for a tall one, which then top-aligns and clips only at the true
   stage bottom. Data-gated on panel presence, not a state name; plain
   headlines keep the anchor above (type ladder, 2026-07-30). */
#phone-headline:not(.is-prompt):has(.card-panel) {
  padding-top: 12px;
}
/* Centre a SHORT non-filling panel; a filling one takes the stage instead. */
#phone-headline:not(.is-prompt)
  > .headline-fit:has(.card-panel):not(:has(.card-panel-fill)) {
  margin-block: auto;
}
/* Give a card-bearing .headline-fit a flex column context so .card-panel-fill's
   flex:1 finally applies. It never has: the wrapper is display:block, so the
   grow was inert and the panel fell back to its 240px min-height "defensive
   floor" — which made Blanks' fill card 240px of a 632px stage, 38% of the
   surface, on the one game whose spec says the phrase IS the gameplay surface
   (measured 2026-08-03). */
#phone-headline > .headline-fit:has(.card-panel) {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}
/* Hero prompt only: the JS sizes .headline-fit to fill the box (FitText), and
   the accent halo applies. glow-blur is 0 under reduced-motion. The prompt is
   *content the room reads* (same role as deck cards), so it takes the card face
   — unifying guided prompts and freeform card text under one treatment. */
#phone-headline.is-prompt {
  text-shadow: 0 0 var(--glow-blur) var(--accent);
  font-family: var(--font-card);
  font-weight: var(--font-card-weight);
}
#phone-headline .headline-fit {
  display: block;
  width: 100%;
  text-align: center;
}
/* Reading-measure cap for the stage + support bands (type ladder): a support
   sentence must not stretch across a desktop (measured ~1,074px pre-fix).
   480px matches the intro screen's proven measure. The hero prompt keeps the
   full stage — it is fit-to-fill display type, not running text. */
#phone-headline:not(.is-prompt) .headline-fit {
  max-width: 480px;
}
/* …but a CARD is not running text, and inherited that cap by accident: both
   card games rendered into 480px of a 1280px display, which split Blanks'
   phrase onto two lines (FitText legitimately prefers a bigger two-line fit
   once a narrow box forces the choice) and wrapped Categories' rows. Owner
   call 2026-08-03: both card shapes take the full stage — an 800px "card
   measure" for the bounded panel was proposed and DECLINED, so don't
   reintroduce a middle number here. This also makes the host match what
   games-primitives/_schema.yaml has always promised for card_fill ("the
   content text grows via FitText to use the full card area"). Data-gated on
   panel presence, exactly like the adaptive anchor above — never on a state
   name. */
#phone-headline:not(.is-prompt) .headline-fit:has(.card-panel) {
  max-width: none;
}
/* A live Impostor roster is scanable content, not centered display prose.
   #phone-headline clips overflow, so the wrapper must form the shrinkable
   column that lets the list itself scroll instead of losing lower rows. */
#phone-headline > .headline-fit:has([data-region-roster]) {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  max-width: none;
  text-align: left;
}
/* The headline inherits 40px display type for short stage titles. Roster
   rows need readable body type, and this scroll container keeps an unusually
   long party reachable before the headline's overflow:hidden can clip it. */
#phone-headline .impostor-roster-list[data-region-roster] {
  width: 100%;
  font-family: var(--font-body);
  font-size: clamp(0.8rem, 3.5vw, 0.95rem);
  font-weight: 400;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
#phone-headline .detail-row {
  font-size: 14px;
  font-weight: 400;
  color: var(--text-dim);
  margin-top: 4px;
}
/* Die readout (Categories' letter) — the hero fact of the round: every one of
   the twelve answers depends on it, so it outranks the status headline rather
   than sitting under it at half its size (measured 2026-08-03: face 20px vs
   "Paused" 40px). It is also its own Roll control (data-intent="dice"), so the
   affordance sits on the instrument it drives instead of being the smallest
   chip at the bottom edge.

   FIXED type steps, not em-relative: the readout rides the same .headline-fit
   wrapper as the card panel, and the card's fit commits the 12px floor (the
   panel carries its own type sizes, so the wrapper always "overflows"), which
   made an em-relative die 6px — illegible (Task 9 walkthrough). */
#phone-headline .dice-readout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-bottom: 8px;
  cursor: pointer;
}
#phone-headline .dice-value {
  font-size: 56px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
}
#phone-headline .dice-label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}
/* Not yet rolled: the readout IS the affordance, so it reads as an empty slot
   asking to be filled rather than rendering as nothing at all. */
#phone-headline .dice-readout--unrolled .dice-value::before {
  content: "?";
  color: var(--text-muted);
}
#phone-headline .prompt-muted {
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 400;
}
/* Draw beat — the "drawing from the bowl" prompt swap. host.js (draw-beat.js)
   gates this on a real hero-prompt change within the same state and skips it
   under reduced motion, so these rules only ever apply during a genuine draw.
   #phone-headline is position:relative + overflow:hidden, so the dropping word
   clips at the bottom edge (falling into the bowl) and the shimmer overlay is
   contained. Durations mirror the draw-beat.js defaults (exit 150 / blank 220 /
   enter 200 ms). */
#phone-headline.db-exit .headline-fit {
  animation: draw-beat-exit 150ms cubic-bezier(0.5, 0, 0.9, 0.3) forwards;
}
/* SCORE exit (Q2): the scored word flies UP and to the RIGHT out of the bowl —
   the celebratory counterpart to the quiet down-drop. A touch longer than the
   down-drop so the upward launch reads, and overflow:hidden on #phone-headline
   clips it as it leaves the slot. */
#phone-headline.db-exit-score .headline-fit {
  animation: draw-beat-exit-score 220ms cubic-bezier(0.3, 0, 0.5, 1) forwards;
}
#phone-headline.db-enter .headline-fit {
  animation: draw-beat-enter 200ms cubic-bezier(0.1, 0.7, 0.3, 1) both;
}
@keyframes draw-beat-exit {
  to {
    opacity: 0;
    transform: translateY(60px);
  }
}
@keyframes draw-beat-exit-score {
  to {
    opacity: 0;
    transform: translate(90px, -120px) scale(1.06);
  }
}
@keyframes draw-beat-enter {
  from {
    opacity: 0;
    transform: translateY(60px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
/* Rainbow shimmer over the blank slot. host.js appends two .draw-beat-band
   children, each a rainbow gradient with shuffled colors + a re-rolled sweep
   angle, so no two draws look the same. The bands sweep across via
   transform: translateX (NOT background-position — WebKit/iOS Safari does not
   animate background-position here, which left the rainbow sitting still). The
   gradient image, sweep angle, and animation-duration are set inline per draw;
   the keyframe owns only the translate so the duration stays a single source of
   truth (host.js DRAW_BEAT_BLANK_MS). transform is GPU-composited everywhere. */
.draw-beat-shimmer {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}
.draw-beat-band {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  /* Wider than the slot (matches the original background-size: 240%) so the full
     rainbow is painted across it and sweeps THROUGH the slot as the band
     translates. Full-height — the gradient's transparent ends feather the
     leading/trailing edge; no mask, so it reads as the original full rainbow. */
  width: 240%;
  mix-blend-mode: screen;
  will-change: transform;
  transform: translateX(-80%);
  animation-name: draw-beat-sweep;
  animation-timing-function: ease-in-out;
  animation-fill-mode: both;
  /* animation-duration is set inline (matches the blank window). */
}
@keyframes draw-beat-sweep {
  from {
    transform: translateX(-80%);
  }
  to {
    transform: translateX(20%);
  }
}
/* Grayscale-rainbow variant (F20). A neutral advance/browse (Fill-in-the-Blanks
   proceed, an undo correction) desaturates the shimmer so it reads as a neutral
   "next" beat, distinct from the colorful SCORE celebration which keeps its full
   rainbow. host.js toggles this class per beat via DrawBeat.shimmerForMode. The
   filter desaturates the composited bands (each still screen-blends over the dark
   slot, so the sweep stays visible as a moving grayscale light). */
.draw-beat-shimmer--gray {
  filter: grayscale(1);
}
/* After the prompt commits, host.js re-attaches the shimmer with this class so it
   fades out over the rise — the new word rises UP THROUGH the fading rainbow
   instead of into an empty slot. Duration is set inline (DRAW_BEAT_ENTER_MS). */
.draw-beat-shimmer.draw-beat-fade {
  animation-name: draw-beat-fade-out;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}
@keyframes draw-beat-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* ── Reserved bands — the action row must not move ──────────────────────────
   #phone-status sits ABOVE #phone-cta (see its comment in host.html):
   anything that appears or disappears BELOW the CTA shifts the bottom-
   anchored chips, and button-strategy.md's Touch CTA band promises "Undo's
   cell NEVER moves or resizes across states." (The old below-CTA
   #phone-detail band was deleted with the regions flip — the engine's
   status region renders above the CTA, and its former secondary content now
   rides the content mount — so #phone-hints is the one collapsible sibling
   left below the CTA.)

   #phone-status/#phone-cta/#phone-hints (host.html's #phone-footer) are laid
   out as an explicit CSS Grid rather than each reserving its own min-height:
   min-height reservations are discovered one sibling at a time (that is how
   #phone-detail, then #phone-hints, both slipped through — see
   partygame-api/tests/test_e2e/test_host_console.py
   ::test_action_band_holds_position_across_every_beat, the behavioural guard
   that caught #phone-hints). grid-template-rows below is the one place that
   lists every reserved row, so a new sibling added to #phone-footer without
   a row here is a visibly incomplete grid, not a silent 43px of drift. Scoped
   to gameplay (:not(.recap-mode)): the recap screen's rows (save/share/
   feedback) render in normal document flow instead, unaffected by this. */
:root {
  /* The Touch CTA band's documented height (button-strategy.md: "Band height
     ~154px"). Reserved even when a beat renders a single chip instead of the
     2x2 grid, so the primary never changes row. */
  --band-action: 154px;
  /* Keyboard-hint chip row. One line of 11px chips plus its 6px padding and
     divider; it renders on some beats and not others. */
  --band-hints: 43px;
}
body:not(.recap-mode) #phone-footer {
  display: grid;
  /* minmax(), not a bare length: a beat may still need MORE than the
     reserved floor (e.g. a long pass-count label wrapping), so the row can
     grow — it just never shrinks below the reserved height. */
  grid-template-rows: auto minmax(var(--band-action), auto) minmax(
      var(--band-hints),
      auto
    );
  flex-shrink: 0;
}
/* Explicit grid-row on each — NOT left to auto-placement. #phone-status has
   its own `:empty{display:none}` (shared.css), and #phone-cta is empty
   before the game's first command lands, so a beat that empties EITHER one
   removes it from the grid item list entirely; auto-placement would then
   compact the remaining items into the first tracks and hand #phone-cta the
   UNRESERVED row-1 slot instead of row-2's var(--band-action) — measured on
   the `created` beat (461 vs every other beat's 523) the first time this
   was tried without explicit rows. The recap-only save/share/feedback rows
   between #phone-cta and #phone-hints stay `hidden` during gameplay, so they
   never become grid items and need no row of their own. */
body:not(.recap-mode) #phone-status {
  grid-row: 1;
}
/* #phone-cta's and #phone-hints's grid-row live with their own box-sizing
   rules, further down (search "The action band's height reservation" and
   "The one collapsible region"), rather than duplicated here. */

/* Team-scores strip in the status band: a horizontal one-line strip, the way
   the settled turn-over draws it (the table treatment emits the same
   stacked .detail-scores block the recap uses; live play flattens it). */
#phone-status .detail-scores {
  display: flex;
  justify-content: center;
  gap: 16px;
  margin: 0;
}
#phone-status .detail-scores .detail-row {
  font-size: 13px;
  margin: 0;
  white-space: nowrap;
}
/* The highlighted row (winner on a concluded game, else the acting team)
   reads like the old strip's current-team emphasis — full text color + bold;
   the recap keeps its accent winner treatment separately. */
#phone-status .detail-winner {
  color: var(--text);
  font-weight: 700;
}
#phone-cta {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  /* iPad/iPhone home-indicator gap: the body.no-controller override below
     already padded for safe-area-inset-bottom on the touch flow, but iPad
     Safari can set body.has-keyboard for a touch session (virtual keyboard,
     paired bluetooth, etc.), so the override never matched and the home bar
     visually overlapped the CTA row (2026-06-26 playtest). The inset is 0
     on devices without a home bar, so this is a no-op there. */
  padding-bottom: max(8px, env(safe-area-inset-bottom));
  flex-shrink: 0;
}
/* The action band's height reservation lives in #phone-footer's
   grid-template-rows (above) so the primary chip occupies the same row slot
   whether or not the 2x2 turn grid is active — bottom-anchoring the band
   alone was not enough: its HEIGHT still changed (154px grid vs a 62px single
   row), so "Start" on the ready beat sat 92px below "Next Team" on the
   turn-over beat — the same role in two places. grid-row is explicit, not
   left to auto-placement (see the #phone-footer comment above: #phone-cta
   renders empty before the game's first command, and an empty grid item is
   removed from placement entirely, which would compact it into the wrong
   row). box-sizing stays here too (the grid stretches the item to the row's
   height, but padding still needs to count INSIDE that height/width rather
   than adding to it). Declared AFTER the base rule it augments so the base
   rule stays the first `#phone-cta {` in the file (see host-css.test.js,
   which extracts it by first match to assert the safe-area inset). */
body:not(.recap-mode) #phone-cta {
  grid-row: 2;
  box-sizing: border-box;
}
/* Top-align ONLY the flat chip row. This must NOT reach the turn-emphasis grid:
   there `align-items` resolves as align-self on the grid items, which stops
   Got It! from stretching across its two-row span and re-breaks PR #1448
   ("stretch the bowl-turn CTA grid so Got It! fills both rows"). Caught by
   real-app verification — a source-regex test cannot see it. */
body:not(.recap-mode) #phone-cta:not(.turn-emphasis) {
  align-items: flex-start;
}
.cta-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 16px;
  font-weight: 600;
  white-space: nowrap;
  position: relative;
}
.cta-chip .hint-btn {
  width: 22px;
  height: 22px;
  font-size: 12px;
  position: relative;
  z-index: 0;
  /* The keycap is metadata (which key fires this), not part of the label —
     it trails the label instead of leading it (owner, 2026-08-02 playtest:
     "center the label and hang the chip to the right"). In the compact
     content-sized row this ordering is the whole job; the wide-chip modes
     (turn-emphasis grid, touch+keyboard band) additionally pin it to the
     chip's right edge below so the label centers alone. */
  order: 1;
}
/* Action icon (Material Symbol) left of the CTA label: ✕ Pass / ✓ Got It! /
   ↩ Undo. Only pure-touch shows it — keyboard/gamepad modes already lead the
   chip with their keycap/controller glyph (.hint-btn), and two leading badges
   would compete. Inherits the chip's intent color. */
.cta-icon {
  display: none;
  font-family: "Material Symbols Rounded";
  font-size: 1.15em;
  line-height: 1;
}
body.no-controller:not(.has-keyboard) .cta-chip .cta-icon {
  display: inline-block;
}
.cta-chip .hint-btn.pill {
  width: auto;
  height: 20px;
  padding: 0 6px;
  font-size: 11px;
}
/* Multi-character keycaps ("Space", "Esc") must size to their label in the
   larger CTA footprint — the base .cta-chip .hint-btn fixes width at 22px, so
   the wider labels overflow the box and crowd the adjacent CTA text. Mirror the
   pill rule: grow to content, keep the square minimum for single-glyph keys
   (↵, −) so they stay keycap-shaped. */
.cta-chip .hint-btn.key {
  width: auto;
  min-width: 22px;
  padding: 0 5px;
}
.cta-proceed {
  color: var(--clr-proceed);
}
.cta-positive {
  color: var(--clr-positive);
}
.cta-negative {
  color: var(--clr-negative);
}
.cta-back {
  color: var(--c-back);
}
/* `secondary` (Impostor's New Word, post-reveal Play Again) shares the PROCEED
   hue — it is a forward action too — and is told apart by weight, not by a new
   colour: outlined where proceed is filled (ui-style.md Principle 4, "solid fill
   is the one primary"). An eighth cap colour would imply an eighth physical
   button, which is precisely what this intent does not have — see
   docs/architecture/button-strategy.md. */
.cta-secondary {
  color: var(--clr-proceed);
}
.cta-disabled {
  opacity: 0.35;
  /* Keep pointer-events ON so a tap on a disabled CTA (e.g. Pass at the limit)
     still reaches the click handler, which plays the Tier-E "blocked" cue.
     Dispatch is separately guarded: _pressableIntent rejects .cta-disabled, so
     the press acknowledges but never fires the underlying command. */
  cursor: not-allowed;
}
.cta-cooldown {
  opacity: 0.35;
  pointer-events: none;
  transition: opacity 0.15s;
}
/* Same visual treatment as .cta-cooldown, but a DISTINCT class: .cta-cooldown
   is exclusively owned by the score-cooldown debounce highlight
   (ctaViz.applyCooldown/reconcileUiEffects), which reconciles it on every
   render from store.ui.cooldownIntents and strips it from any chip not in
   that list — including the Undo chip the instant its own debounce window
   ends, even though Undo has nothing to do with score-cooldown. .cta-inert
   is the Undo-chip's own "nothing to undo yet" state (ui-style.md's
   cooldown-vs-blocked disabled model), untouched by that reconciliation. */
.cta-inert {
  opacity: 0.35;
  pointer-events: none;
  transition: opacity 0.15s;
}

/* Tappable chips — always respond to touch/click */
.cta-chip,
.hint-chip {
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.cta-chip:active:not(.cta-disabled):not(.cta-cooldown):not(.cta-inert) {
  opacity: 0.7;
}
.hint-chip:active {
  opacity: 0.7;
}

/* No-controller mode — style chips as prominent touch buttons */
body.no-controller .cta-chip {
  background: var(--surface);
  border: 1.5px solid var(--bg-1);
  border-radius: var(--radius-btn);
  padding: 10px 18px;
  font-size: 20px;
  transition:
    background 0.1s,
    border-color 0.1s;
}
body.no-controller
  .cta-chip:active:not(.cta-disabled):not(.cta-cooldown):not(.cta-inert) {
  background: var(--bg-1);
  border-color: var(--clr-proceed);
  opacity: 1;
}
/* Pure-touch: the keycap glyph is meaningless without a keyboard, so hide it on
   every CTA chip. */
body.no-controller:not(.has-keyboard) .cta-chip .hint-btn {
  display: none;
}
body.no-controller .hint-chip {
  background: var(--surface);
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-btn);
  padding: 6px 12px;
  font-size: 13px;
  color: var(--text-dim);
  transition:
    background 0.1s,
    border-color 0.1s;
}
body.no-controller .hint-chip:active {
  background: var(--bg-1);
  border-color: var(--clr-proceed);
  opacity: 1;
}
body.no-controller:not(.has-keyboard) .hint-chip .hint-btn {
  display: none;
}
/* Intent text colors on --surface chip background:
   #5596ff (5.40:1) and #f96080 (5.26:1) both meet AA on --surface #262220
   (the charcoal card). */
body.no-controller .cta-proceed {
  color: #5596ff;
}
body.no-controller .cta-negative {
  color: #f96080;
}
/* The primary action carries the proceed fill on EVERY beat, not only inside
   the turn window. The solid fill previously came only from
   #phone-cta.turn-emphasis (gated on turn_active || turn_lingering), so on the
   game-open and team-turn beats "Start" / "Start Turn" rendered as the same
   charcoal --surface pill as the Undo chip beside it — the one action you must
   take was indistinguishable from the lowest-emphasis control on the screen.
   Same fill/text pair the turn-emphasis rule uses, so the two windows agree
   (ui-style.md Principle 4: solid fill is the one primary; Principle 3: intent
   palette, never chrome coral). */
body.no-controller .cta-chip.cta-proceed {
  background: var(--c-proceed);
  border-color: var(--c-proceed);
  color: var(--bg-2);
  font-weight: 700;
}
/* Outlined counterpart to the filled primary above: same hue on the border, no
   fill, so the pair reads as one primary plus one alternative rather than two
   equal buttons. */
body.no-controller .cta-chip.cta-secondary {
  border-color: var(--c-proceed);
}

/* ── Touch-first layout ──────────────────────────────────────────────────── */
/* Primary reference: iPhone Safari 390×844; also applies to any              */
/* coarse-pointer device (iPad in landscape, phones rotated sideways) — the   */
/* ≥44px tap-pill upgrade is about the finger, not the orientation. Keyed on  */
/* modality (pointer: coarse), with orientation: portrait kept as a fallback  */
/* for environments that don't report a coarse pointer.                       */
/* All rules are scoped to body.no-controller (touch-only; ~99% of users)     */
/* so a connected gamepad keeps the compact chip styling.                     */
@media (orientation: portrait), (pointer: coarse) {
  /* Hint bar: promote chips from glyph hints to ≥44px tap pills */
  body.no-controller #phone-hints {
    gap: 6px 8px;
    padding: 8px 12px;
  }

  body.no-controller .hint-chip {
    min-height: 44px;
    padding: 8px 16px;
    font-size: 15px;
    border-radius: var(--radius-btn);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* CTA thumb-band: fill the thumb arc at the bottom of the screen */
  body.no-controller #phone-cta {
    flex-wrap: nowrap;
    padding: 10px 12px;
    padding-bottom: max(10px, env(safe-area-inset-bottom));
    gap: 8px;
    min-height: 80px;
  }

  body.no-controller .cta-chip {
    flex: 1;
    min-width: 0;
    min-height: 60px;
    justify-content: center;
    border-radius: var(--radius-btn);
    font-size: 22px;
  }

  /* Impostor's Reveal Impostors + New Word (and post-reveal Finish + Play Again)
     cannot share one 390px row at 22px with keycap reserve — secondary clipped
     to "New W" in verifier-web. Stack: primary full-width, alternative under. */
  body.no-controller #phone-cta:has(.cta-secondary) {
    flex-wrap: wrap;
  }
  body.no-controller #phone-cta:has(.cta-secondary) .cta-proceed {
    flex: 1 1 100%;
  }
  body.no-controller #phone-cta:has(.cta-secondary) .cta-secondary {
    flex: 1 1 100%;
    min-height: 48px;
    font-size: 18px;
  }

  /* Touch band with a paired keyboard: keycaps are visible on full-width
     chips, so they get the same hang-right treatment as the turn-emphasis
     grid — label centered alone, keycap pinned in a symmetric reserve. */
  body.no-controller.has-keyboard .cta-chip {
    padding-inline: 56px;
  }
  /* `secondary` has no key/gamepad slot (button-strategy.md) — don't reserve
     keycap chrome it will never show. */
  body.no-controller.has-keyboard .cta-chip.cta-secondary {
    padding-inline: 16px;
  }
  body.no-controller.has-keyboard .cta-chip .hint-btn {
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
  }
}

/* Wide viewports (tablet landscape/portrait, desktop): cap the CTA band so a
   single emphasized chip ("Continue Turn", "Next Card") doesn't stretch
   edge-to-edge across the whole display. Keyed on viewport width, not
   orientation — the same full-bleed problem exists on iPad portrait. The
   flat chip row is unaffected (its content was already centered); only the
   full-width band/grid shapes narrow. */
@media (min-width: 700px) {
  #phone-cta {
    width: 100%;
    max-width: 680px;
    margin-inline: auto;
  }
}

/* Generic change-flash — apply to any element after a value change */
@keyframes flash-change {
  0% {
    transform: scale(1);
    filter: brightness(1);
  }
  30% {
    transform: scale(1.08);
    filter: brightness(1.4);
  }
  100% {
    transform: scale(1);
    filter: brightness(1);
  }
}
.flash-change {
  animation: flash-change 0.3s ease-out;
}

/* Coordinated view commit — one transition for the whole game view.
 * renderGameDisplay wraps its DOM writes in document.startViewTransition, so
 * the browser snapshots the old view and cross-fades to the new view on a
 * single shared timeline (the default ::view-transition-group(root) morph).
 * This replaces the old per-headline ghost (prompt-exit) + prompt-enter
 * timelines, which each resolved on their own clock and let the score number
 * land a beat ahead of the new word. Tune the one shared duration here so the
 * whole-view morph keeps the snappy prompt feel. */
::view-transition-group(root) {
  animation-duration: 0.3s;
}
/* The transition overlay is a full-page snapshot layered ON TOP of the live
 * DOM while the ~0.3s morph plays. On a handheld the giver taps the on-screen
 * CTA chip continuously, and an in-flight transition (from a poll or a previous
 * press) must never swallow that input. Let pointer events fall through to the
 * real, interactive DOM underneath — input latency is the one thing this whole
 * change exists to protect. */
::view-transition {
  pointer-events: none;
}

#phone-status {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 2px 16px;
  padding: 4px 12px;
  font-size: 11px;
  color: var(--text-dim);
  border-top: 1px solid var(--surface);
  flex-shrink: 0;
}
#phone-hints {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 4px 10px;
  padding: 6px 12px;
  font-size: 11px;
  color: var(--text-muted);
  border-top: 1px solid var(--surface);
  flex-shrink: 0;
}
/* The one collapsible region left below #phone-cta (the old #phone-detail
   band was deleted with the regions flip), and the third instance of the
   same defect in its day: it renders only on some beats (the turn-concluded
   screen adds hint chips — the mis-tap guard hides the proceed CTA there,
   which drops it out of the CTA-intents exclude set and lets a real "back"/
   Undo mapping leak into the hint bar alongside the CTA's own inert Undo
   placeholder; see handleCtaMistapGuard/ctaIntents in host.js and
   controller-profiles.js — worth a follow-up, see docs/todo.md), so its 43px
   appearing and vanishing translated the whole action band — Undo measured
   463 vs 506 between the scored and concluded beats of a Guess game.
   #phone-status was hoisted above the CTA for this reason, and this one was
   still in flow. A naive `min-height`-on-empty collapse (2026-08-04) was
   tried and reverted — CI proved the "no gameplay beat renders here" premise
   false (test_action_band_holds_position_across_every_beat, run
   30966251837).

   The reservation now lives in #phone-footer's grid-template-rows (above)
   instead of here, so #phone-hints itself no longer needs its own
   min-height — grid-row is explicit (not left to auto-placement, same
   reasoning as #phone-cta's) and box-sizing stays for the same
   padding-must-count-inward reason #phone-cta keeps it. The divider is
   still suppressed while empty so the reservation doesn't draw a stray rule
   across the bottom of the screen.
   partygame-api/tests/test_e2e/test_host_console.py
   ::test_action_band_holds_position_across_every_beat measures the
   band-stability invariant directly. */
body:not(.recap-mode) #phone-hints {
  grid-row: 3;
  box-sizing: border-box;
}
body:not(.recap-mode) #phone-hints:empty {
  display: flex;
  border-top-color: transparent;
}
.hint-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}
.hint-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
  vertical-align: middle;
}
.hint-btn.face {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1.5px solid;
}
.hint-btn.pill {
  height: 14px;
  padding: 0 4px;
  /* 999px, not the box's exact half-height (7px): border-radius clamps to
     half the box on each axis, so this renders byte-identical to 7px today
     but keeps reading as "fully round" if the height ever changes, instead
     of silently reverting to a less-rounded rectangle. */
  border-radius: 999px;
  border: 1.5px solid;
}
.hint-btn.icon {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1.5px solid;
  font-size: 11px;
}
/* Keyboard key chip: square keycap silhouette, neutral/monochrome.
   Visually distinct from controller face/pill/icon buttons so
   iPad + keyboard users recognise the labels as physical keys.
   border-radius:3px is a literal, not a token, on purpose: it has no other
   user in the codebase, and none of the existing radius scale (--radius-xs
   4px through --radius-card 16px) matches it — inventing a single-consumer
   token here would repeat the exact "size tier maintained for one button"
   drift this file's `--btn-lg`/`--btn-ghost`/`--btn-sm` retirements exist to
   avoid (Principle 7, docs/ui-style.md). */
.hint-btn.key {
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 3px;
  border: 1.5px solid;
}
/* TinyConsole controller chip: a Material Symbol glyph on a solid intent-color
   body (white glyph), mirroring the physical cap. The body/glyph colors come
   from the inline style (PROFILES.tinyconsole hintStyle); this rule only sets
   the icon font and chip silhouette. */
.hint-btn.ms {
  width: 16px;
  height: 16px;
  border-radius: var(--radius-xs);
  border: 1.5px solid;
  font-family: "Material Symbols Rounded";
  font-weight: normal;
  font-size: 13px;
  /* Ligatures off — labels are raw codepoints, not icon names. */
  font-feature-settings: "liga" 0;
  -webkit-font-smoothing: antialiased;
}
.detail-feedback {
  color: var(--clr-scored);
}

/* Feedback toast — appears above CTA and fades out */
#phone-feedback {
  text-align: center;
  font-size: 14px;
  font-weight: 600;
  height: 20px;
  overflow: hidden;
}
/* A last-play confirmation can defensively land in the content region (see
   region-renderers.js's _renderDetailLeaf) even though the feedback region is
   its normal home — sized like the in-play feedback toast (#phone-feedback:
   14px/600), keeping the two consistent (playtest 2026-06-29 F1), rather than
   the bounded headline size. FitText never touches it (it only fits the
   .prompt-hero), so this size wins with no inline override. */
#phone-headline .play-confirmation {
  font-size: 14px;
  font-weight: 600;
}
/* Winner row: the focal point of a scores table — accent (coral) and larger
   than the other rows, which stay in the normal cream. The pulse gives the
   "{team} wins!" screen a beat of celebration; the recap variant below drops it
   so the recap reads calm (F19). */
.detail-winner {
  color: var(--accent);
  font-weight: 800;
  font-size: 1.3em;
  animation: winner-pulse 1.2s ease-in-out 3;
}
@keyframes winner-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.8;
    transform: scale(1.04);
  }
}
.detail-header {
  color: var(--text-muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-top: 4px;
}
/* A per-round score table is a reading-width object wherever it appears. The
   cap lives here, on the base rule, because `width: 100%` alone made it as wide
   as whatever contained it — which on the recap was the whole window. */
.round-scores-table {
  width: 100%;
  max-width: var(--recap-col-max);
  margin: 4px auto;
  border-collapse: collapse;
  font-size: 13px;
  display: block;
  overflow-x: auto;
}
.round-scores-table th {
  color: var(--text-muted);
  font-weight: 600;
  text-align: center;
  padding: 2px 6px;
  border-bottom: 1px solid var(--border);
}
.round-scores-table td {
  text-align: center;
  padding: 2px 6px;
}
.round-scores-table td:first-child,
.round-scores-table th:first-child {
  text-align: left;
  color: var(--text-dim);
}
/* ── Recap polish (game-over recap + read-only completed-game viewer) ─────────
   Scoped to recap contexts (body.recap-mode for the live recap, set by host.js;
   .completed-recap-overlay for the viewer) so the shared .detail-* renderers stay
   plain during live play. Data-driven: keys on the recap container, so every
   game's recap and the viewer get the treatment automatically. Styling only —
   the recap markup is unchanged. */
/* Recap rules apply to BOTH the live recap (body.recap-mode .recap-pane — the
   scrollable block host.js writes inside the one content mount) and the
   completed-game viewer (.completed-recap-overlay .phone-detail). Collapsed
   with :is() so a new recap rule can't silently miss one surface (PR #1488
   defect class). */
:is(body.recap-mode, .completed-recap-overlay) :is(.recap-pane, .phone-detail) {
  text-align: center;
  /* The recap is read together, not scanned like a dashboard: one centered
     column instead of the full window width. Without this the per-round table
     ran edge to edge on desktop and iPad while the blocks above it stayed
     narrow. */
  max-width: 560px;
  margin-inline: auto;
}
/* The recap has no running timer — hide the header timer bar so the calm recap
   isn't topped by a leftover progress sliver (F19). */
body.recap-mode #header-timer-bar {
  display: none;
}
/* The recap is a content page, not a stage: recap_regions carries no
   content/title entry (game_name suppressed — display.py F19; the top bar
   shows the name instead), so the pane fills the whole content mount,
   scrolling if a long recap overflows (the stage's fit-to-fill is skipped in
   recap mode — host.js). The pane resets the headline's display type back to
   quiet reading text; the width cap comes from the shared recap rule above. */
body.recap-mode .recap-pane {
  /* Content-sized, shrinkable (adaptive anchor, owner call 2026-08-02): a
     short recap's pane wraps its content so the centered mount (rule below)
     balances it in the stage instead of leaving an empty band; a recap
     taller than the mount shrinks to fit (min-height: 0) and scrolls from
     the top, exactly as the old flex: 1 pane did. */
  flex: 0 1 auto;
  min-height: 0;
  width: 100%;
  overflow-y: auto;
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 400;
  font-family: var(--font);
  color: var(--text-dim);
}
/* The recap is a content page, not a stage: the upper-third clamp() anchor
   has no headline to seat, so the mount centers its (content-sized) pane —
   a full-height pane makes center and top identical, so a long recap still
   reads top-down. Live-stage only, deliberately outside the :is() recap
   pair — the completed-recap overlay's .phone-detail is a document-style
   dialog with its own scroll box, not a stage with a band to balance. */
body.recap-mode #phone-headline:not(.is-prompt) {
  justify-content: center;
  padding-top: 12px;
}
/* Score + breakdown blocks are contained to a reading width.

   This previously selected `> div[data-priority]:has(.detail-header)` and
   therefore matched NOTHING: `data-priority` was never emitted by any HTML or
   JS — it appeared only in this stylesheet — so the card treatment and the
   caption treatment below it had never once applied. That dead selector is why
   the per-round table ran full-bleed.

   The recap detail is a FLAT run of siblings (GC.renderScoreBreakdownHtml
   returns `<div class="detail-header">` and `<table>` as peers, not wrapped),
   so there is no per-block element to turn into a card. Constrain the pieces
   that exist instead; the boxed-card treatment needs wrappers in the renderer
   and is left as a follow-up rather than re-asserted as another rule that
   matches nothing. */
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .detail-scores {
  max-width: var(--recap-col-max);
  margin-inline: auto;
}
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .detail-header {
  color: var(--accent);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  margin: 0 0 10px;
}
/* Final-scores rows: roomier, with the winner as the focal point. Non-winner
   rows are the full cream (not the dimmed caption tone) so only the winner reads
   as accent (F19: winner ORANGE + bigger, everyone else white).

   Each rule lists two selectors sharing one declaration block: the general
   :is() pair, plus a #phone-headline-anchored restatement. The ID beats the
   two class-only selectors (ID outranks any number of classes) now that the
   live recap's pane nests inside #phone-headline (one content mount, Task 6)
   — without the second selector, the winner highlight and final-scores sizing
   would lose the specificity fight and render as plain chrome text. The
   completed-recap-overlay path needs no such restatement: its rows live in
   #completed-recap-detail, never under #phone-headline, so the :is() selector
   already wins there undisputed. */
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .detail-scores
  .detail-row,
body.recap-mode #phone-headline .recap-pane .detail-scores .detail-row {
  color: var(--text);
  font-size: 18px;
  padding: 5px 0;
  animation: none;
}
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .detail-scores
  .detail-winner,
body.recap-mode #phone-headline .recap-pane .detail-scores .detail-winner {
  color: var(--accent);
  font-weight: 800;
  font-size: 26px;
}
/* Scores-by-round table: more breathing room + zebra striping for legibility. */
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .round-scores-table {
  font-size: 14px;
  /* Contained to the same reading width as the scores block above it, and
     scrollable in place so a many-team game widens its own scroll area instead
     of stretching the recap column. */
  max-width: var(--recap-col-max);
  margin-inline: auto;
  display: block;
  overflow-x: auto;
}
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .round-scores-table
  :is(th, td) {
  padding: 5px 10px;
}
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .round-scores-table
  tbody
  tr:nth-child(odd) {
  background: rgba(255, 255, 255, 0.04);
}
/* Bare stat lines (play time, best turn, rounds played) read as quiet caption
   text. Retargeted from the dead `div[data-priority]` selector to the flat
   structure that actually renders: a direct-child div that is neither the
   section header nor the scores block. */
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  > div:not(.detail-header):not(.detail-scores) {
  color: var(--text-dim);
  font-size: 14px;
  margin: 2px 0;
}
/* ── Controls zone (bottom) ──────────────────────────────────────── */
#controls-zone {
  display: none;
  flex-shrink: 0;
  border-top: 2px solid var(--surface);
}
#controls-zone.visible {
  display: flex;
}

#controls-inner {
  display: flex;
  width: 100%;
  min-height: 140px;
}

/* 7-button panel (left) */
#button-panel {
  flex: 1;
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  justify-content: center;
}
.btn-row {
  display: flex;
  gap: 8px;
  justify-content: center;
}
.btn-row.top .hw-btn {
  flex: 1;
  max-width: 160px;
  min-height: 44px;
  font-size: 14px;
}
.btn-row.bottom .hw-btn {
  flex: 1;
  max-width: 110px;
  min-height: 34px;
  font-size: 12px;
}
.hw-btn {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  transition:
    background 0.1s,
    border-color 0.1s,
    box-shadow 0.1s;
  position: relative;
}
.hw-btn:hover:not(:disabled) {
  background: var(--bg-1);
  border-color: var(--accent);
}
.hw-btn:active:not(:disabled) {
  background: var(--accent);
  color: var(--bg-2); /* dark text on coral flash */
}
.hw-btn:disabled {
  opacity: 0.35;
  cursor: default;
  box-shadow: none !important;
}
.hw-btn .btn-label {
  font-weight: 600;
}
.hw-btn .btn-key {
  font-size: 10px;
  color: var(--text-muted);
  font-family: "SF Mono", "Fira Code", monospace;
}
.hw-btn .btn-intent {
  font-size: 9px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* ── Active-turn CTA grid (2026-07-11 CTA weighting design) ─────────────────
   A 2x2 grid replaces the flat chip row for the whole bowl-turn window
   (active play through the post-timer score summary — toggled by
   #phone-cta.turn-emphasis, now driven by turn_active OR turn_lingering, see
   renderState). Got It! is the single largest target (the hot-loop action);
   Pass and Undo stack on the left. Undo's cell (column 1, row 2) NEVER moves
   or resizes across states — that fixed position is the whole point (always
   visible, disabled via .cta-inert when there's nothing to undo, rather
   than appearing/disappearing). See
   docs/architecture/button-strategy.md (Touch CTA band). Higher
   specificity than the base/no-controller chip rules (id + 2 classes), so it
   wins in all input modes. */
#phone-cta.turn-emphasis {
  display: grid;
  grid-template-columns: 1fr 1.3fr;
  grid-template-rows: 1fr 1fr;
  gap: 8px;
  padding: 8px;
  width: 100%;
  height: 154px;
  /* Reset the base #phone-cta flex `align-items: center` (which vertically
     centers chips in the flat row). On a grid container that value becomes the
     default align-self, so each item is sized to its content and centered in
     its cell rather than stretching to fill it — which collapses the
     row-spanning Got It! to content height, floating it in the middle of its
     two-row area. Stretch fills every cell, the whole point of the 2x2
     weighting. */
  align-items: stretch;
}
#phone-cta.turn-emphasis .cta-chip {
  justify-content: center;
  background: var(--surface);
  border: 1.5px solid var(--bg-1);
  border-radius: var(--radius-btn);
  /* Symmetric inline reserve: the keycap hangs in the right band (rule
     below), and the matching left padding keeps the centered label's center
     at the button's true center — the 2026-08-02 playtest push-vs-hang fix.
     56px clears the widest keycap ("Space" pill ≈ 46px) plus its offset. */
  padding: 10px 56px;
  font-size: 17px;
}
/* Wide-chip keycap hang: out of the label's inline run, pinned at the right
   edge, vertically centered. */
#phone-cta.turn-emphasis .cta-chip .hint-btn {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
}
/* Pass: top-left cell. */
#phone-cta.turn-emphasis .cta-negative {
  grid-column: 1;
  grid-row: 1;
}
/* Got It!: right column, spans both rows — the single largest, most-pressed
   target. Fill-as-hierarchy (ui-style.md Principle 4), kept in the intent
   green rather than chrome coral (Principle 3 — never recolor an in-display
   action to match the chrome accent). */
#phone-cta.turn-emphasis .cta-positive {
  grid-column: 2;
  grid-row: 1 / span 2;
  font-size: 22px;
}
/* Next Team (turn-concluded phase): the only primary chip besides Undo, so it
   spans the whole top row in Pass/Got It!'s place. */
#phone-cta.turn-emphasis .cta-proceed {
  grid-column: 1 / span 2;
  grid-row: 1;
}
/* Undo: bottom-left cell, ALWAYS this exact position, enabled or in cooldown,
   across active play and the turn-concluded screen alike. Never moves. */
#phone-cta.turn-emphasis .cta-back {
  grid-column: 1;
  grid-row: 2;
  font-size: 14px;
}

/* Solid fill = the one "primary" of the view (ui-style.md Principle 4,
   translated into the intent palette per Principle 3 — never chrome coral).
   Dark text for contrast against the light fill. Fixes a gap the Task 4 plan
   omitted from the CTA-band design's
   Color & fill section — flagged by code review after bf76d55a landed. */
#phone-cta.turn-emphasis .cta-positive {
  background: var(--c-positive);
  border-color: var(--c-positive);
  color: var(--bg-2);
}
#phone-cta.turn-emphasis .cta-proceed {
  background: var(--c-proceed);
  border-color: var(--c-proceed);
  color: var(--bg-2);
}
/* Pass: outlined "real alternative" — a faint tinted fill, not solid.
   Split into two rule blocks (fill vs. border/text) rather than one: the
   palette-guard bans --c-negative sharing a block with a background fill
   (ui/shared/palette-guard.test.js "one danger fill" — a fill derived from
   --c-negative must go through its own named token, --clr-pass-fill-tint,
   defined above). */
#phone-cta.turn-emphasis .cta-negative {
  background: var(--clr-pass-fill-tint);
}
#phone-cta.turn-emphasis .cta-negative {
  border-color: var(--c-negative);
}
/* Undo: neutral outline, no fill — lowest emphasis at rest. */
#phone-cta.turn-emphasis .cta-back {
  border-color: var(--c-back);
}

/* One-tap iOS audio unlock (Magic Keyboard / HID / numpad-only control).
   Fixed, centered near the top so it's noticed; hides once audio is running.
   Reveals ~100ms after a failed hardware-keydown unlock if still locked (iOS
   often refuses key unlock; resume is async so we wait a beat) and as a 2.5s
   fallback otherwise. */
.audio-unlock-btn {
  position: fixed;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  padding: 10px 18px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow-card);
}
.audio-unlock-btn.hidden {
  display: none;
}

/* CTA color rule (ratified 2026-06-21; post-ship-polish spec retired — see git history):
 * coral (--accent) = app-chrome / account / setup CTAs that live OUTSIDE the live
 *   game display (Sign In, Resume, Start Game on config/collecting);
 * proceed-blue (--clr-proceed) = the in-display proceed CHIP (.cta-proceed), the
 *   on-screen mirror of the physical proceed button — its color must match that
 *   button's identity color.
 * Key CTA color on SCREEN LAYER (chrome vs in-display chip), NOT on data-intent:
 * .config-start-btn is data-intent="proceed" yet correctly coral because it is a
 * chrome launch button. Do not recolor a chrome CTA blue to "match" its intent. */
/* Intent color palette — aliases to canonical --c-* tokens (single source of truth) */
:root {
  --clr-proceed: var(--c-proceed);
  --clr-positive: var(--c-positive);
  --clr-negative: var(--c-negative);
  --clr-back: var(--c-back);
  --clr-timer: var(--c-timer);
  --clr-menu: var(--c-menu);
  --clr-dice: var(--c-timer); /* dice shares amber with timer (equipment pair) */
  /* RGB triples (not colors) so callers can vary alpha via rgba(var(...), a).
     Each mirrors the JS "generic" gamepad profile's per-intent cap color
     (ui/shared/controller-profiles.js PROFILES.generic.buttons.*.color) —
     e.g. --clr-proceed-glow-rgb 66,135,245 = #4287f5 = .generic.proceed.
     Deliberately NOT the on-screen --c-* / --clr-* tier colors above: those
     are a DIFFERENT palette (the proceed comment already called this out
     for blue; positive/negative/timer/dice/back/menu were the same
     situation, just still hand-copied literals until now). */
  --clr-proceed-glow-rgb: 66, 135, 245;
  --clr-positive-glow-rgb: 83, 215, 105;
  --clr-negative-glow-rgb: 233, 69, 96;
  --clr-timer-glow-rgb: 180, 120, 60;
  --clr-dice-glow-rgb: 230, 130, 180;
  --clr-back-glow-rgb: 160, 100, 220;
  --clr-menu-glow-rgb: 220, 200, 60;
}

/* Recurring one-off literals promoted to tokens (docs/ui-style.md backlog #7).
   Host-only — not part of the intent-tier palette above, which mirrors the
   physical controller caps; these are one-shot visual accents that just
   happened to repeat across two-plus rules. */
:root {
  --clr-finale-gold: #ffd166; /* game-over finale-card border + eyebrow accent */
  --clr-undo: #888888; /* undo-sweep neutral gray, distinct from score/pass sweep colors */
  /* Pass chip's faint tinted fill (turn-emphasis CTA grid, "real alternative"
     per docs/architecture/button-strategy.md, Touch CTA band). Kept as
     its own token (not --c-negative doing double duty as fill AND text) per
     the palette-guard "one danger fill" rule below — ui/shared/palette-guard
     .test.js bans --c-negative and a background fill sharing a rule block.
     8%, not the 15% used for hover/active tints elsewhere: this fill sits
     under --c-negative TEXT, and 15% pushed contrast to ~4.34:1, under WCAG
     AA's 4.5:1 (caught by the gameplay a11y e2e test). */
  --clr-pass-fill-tint: color-mix(in srgb, var(--c-negative) 8%, var(--surface));
}

/* Intent colors — 7-button bar (box-shadow glow) */
.hw-btn.intent-proceed {
  box-shadow: 0 2px 8px rgba(var(--clr-proceed-glow-rgb), 0.45);
}
.hw-btn.intent-positive {
  box-shadow: 0 2px 8px rgba(var(--clr-positive-glow-rgb), 0.45);
}
.hw-btn.intent-negative {
  box-shadow: 0 2px 8px rgba(var(--clr-negative-glow-rgb), 0.45);
}
.hw-btn.intent-timer {
  box-shadow: 0 2px 8px rgba(var(--clr-timer-glow-rgb), 0.45);
}
.hw-btn.intent-dice {
  box-shadow: 0 2px 8px rgba(var(--clr-dice-glow-rgb), 0.45);
}
.hw-btn.intent-back {
  box-shadow: 0 2px 8px rgba(var(--clr-back-glow-rgb), 0.45);
}
.hw-btn.intent-menu {
  box-shadow: 0 2px 8px rgba(var(--clr-menu-glow-rgb), 0.45);
}

/* Gamepad panel (right) */
#gamepad-panel {
  padding: 8px 12px;
  border-left: 1px solid var(--surface);
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 8px;
  align-items: flex-start;
  justify-content: center;
}
.controller-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.controller-slot + .controller-slot {
  border-left: 1px solid var(--surface);
  padding-left: 8px;
}
.cs-name {
  font-size: 10px;
  color: var(--text-muted);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Visual controller diagram — 5-column grid */
.controller-diagram {
  display: grid;
  grid-template-columns: auto auto auto auto auto;
  align-items: center;
  justify-items: center;
  gap: 4px 6px;
}
.gp-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.gp-btn {
  border: 2px solid var(--border);
  border-radius: 50%;
  background: var(--bg-2);
  font-size: 8px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  transition:
    background 0.1s,
    border-color 0.1s;
  flex-shrink: 0;
}
.gp-btn.pressed {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-2); /* dark text on coral: 7.17:1 */
}
.gp-btn.face {
  width: 24px;
  height: 24px;
  gap: 1px;
}
.gp-btn.face .xbox {
  font-size: 8px;
}
.gp-btn.face .ps {
  font-size: 7px;
  opacity: 0.5;
}
.gp-btn.bumper {
  width: 40px;
  height: 14px;
  border-radius: var(--radius-xs);
  font-size: 7px;
}
.gp-btn.trigger {
  width: 36px;
  height: 14px;
  border-radius: var(--radius-xs);
  font-size: 7px;
}
.gp-btn.dpad {
  width: 16px;
  height: 16px;
  border-radius: 2px;
  font-size: 7px;
}
.gp-btn.small {
  width: 16px;
  height: 16px;
  font-size: 7px;
}
.gp-cmd-label {
  font-size: 8px;
  color: var(--text-muted);
  white-space: nowrap;
  pointer-events: none;
  text-align: center;
  min-height: 10px;
}
.gp-cmd-label.empty {
  visibility: hidden;
}

/* D-pad cross layout */
.gp-dpad-grid {
  display: grid;
  grid-template: 16px 16px 16px / 16px 16px 16px;
  gap: 1px;
  justify-items: center;
  align-items: center;
}
.gp-dpad-grid .gp-btn.dpad:nth-child(1) {
  grid-area: 1 / 2;
} /* up */
.gp-dpad-grid .gp-btn.dpad:nth-child(2) {
  grid-area: 3 / 2;
} /* down */
.gp-dpad-grid .gp-btn.dpad:nth-child(3) {
  grid-area: 2 / 1;
} /* left */
.gp-dpad-grid .gp-btn.dpad:nth-child(4) {
  grid-area: 2 / 3;
} /* right */

/* Face button diamond layout */
.gp-face-grid {
  display: grid;
  grid-template: auto auto auto / auto auto auto;
  gap: 0px 2px;
  justify-items: center;
  align-items: center;
}
.gp-face-grid .gp-face-slot:nth-child(1) {
  grid-area: 1 / 2;
} /* Y top */
.gp-face-grid .gp-face-slot:nth-child(2) {
  grid-area: 2 / 1;
} /* X left */
.gp-face-grid .gp-face-slot:nth-child(3) {
  grid-area: 2 / 3;
} /* B right */
.gp-face-grid .gp-face-slot:nth-child(4) {
  grid-area: 3 / 2;
} /* A bottom */
.gp-face-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}
/* Top: label above button (outside the diamond) */
.gp-face-grid .gp-face-slot:nth-child(1) {
  flex-direction: column-reverse;
}

/* Intent colors — gamepad diagram (buttons + labels) */
.gp-btn.intent-proceed,
.gp-cmd-label.intent-proceed {
  border-color: var(--clr-proceed);
  color: var(--clr-proceed);
}
.gp-btn.intent-positive,
.gp-cmd-label.intent-positive {
  border-color: var(--clr-positive);
  color: var(--clr-positive);
}
.gp-btn.intent-negative,
.gp-cmd-label.intent-negative {
  border-color: var(--clr-negative);
  color: var(--clr-negative);
}
.gp-btn.intent-timer,
.gp-cmd-label.intent-timer {
  border-color: var(--clr-timer);
  color: var(--clr-timer);
}
.gp-btn.intent-dice,
.gp-cmd-label.intent-dice {
  border-color: var(--clr-dice);
  color: var(--clr-dice);
}
.gp-btn.intent-back,
.gp-cmd-label.intent-back {
  border-color: var(--clr-back);
  color: var(--clr-back);
}
.gp-btn.intent-menu,
.gp-cmd-label.intent-menu {
  border-color: var(--clr-menu);
  color: var(--clr-menu);
}
.gp-btn[class*="intent-"].pressed {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-2); /* dark text on coral: 7.17:1 */
}

/* Input source indicator */
#input-indicator {
  font-size: 11px;
  /* --text-dim meets WCAG AA on the dark controls background;
     plain #555 fails (caught by the axe e2e a11y check). */
  color: var(--text-dim);
  text-align: center;
  padding: 4px;
  border-top: 1px solid var(--surface);
}

/* ── Countdown overlay ──────────────────────────────────────────── */
#phone-display {
  position: relative;
}
#countdown-overlay {
  position: absolute;
  inset: 0;
  z-index: 10;
  background: var(--bg-2);
  display: flex;
  align-items: center;
  justify-content: center;
}
#countdown-overlay.hidden {
  display: none;
}
#countdown-number {
  font-size: 120px;
  font-weight: 900;
  color: var(--accent);
  line-height: 1;
  user-select: none;
  animation: countdown-pop 0.6s ease-out;
}
@keyframes countdown-pop {
  0% {
    transform: scale(2);
    opacity: 0;
  }
  40% {
    transform: scale(0.95);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

/* ── Split-flap dice-roll overlay ────────────────────────────────────
   A departure-board "split-flap" flourish driven by the DiceRoll coordinator
   (ui/shared/dice-roll.js). The overlay is fixed and centered; it is display:none
   until host.js adds .is-active, and it is pointer-events:none the WHOLE time so
   it can never intercept the next controller button press while it settles or
   lingers. z-index 60 sits above game content but below the modal overlays
   (menu/scoreboard at 90+), matching the "flourish, not dialog" layer. The class
   contract host.js toggles is fixed: .is-active shows it, .expanding/.shrinking
   drive the tile grow/shrink, .flip restarts the per-flap keyframe, .landed adds
   the glow. */
.dice-roll-overlay {
  position: fixed;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  pointer-events: none; /* never swallow the next host button press */
  z-index: 60;
}
.dice-roll-overlay.is-active {
  display: flex;
}
/* Backdrop scrim: a subtle dim so the big rolled letter reads cleanly over the
   game card behind it. Painted behind the flap tile (earlier in tree order),
   and fades in/out on the same .expanding/.shrinking classes as the tile so
   the dim tracks the grow and shrink. Under reduced motion the coordinator
   never fires .expanding, so the scrim never appears. */
.dice-roll-overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrim-roll);
  opacity: 0;
  transition: opacity 180ms ease-out;
}
.dice-roll-overlay.expanding::before {
  opacity: 1;
}
.dice-roll-overlay.shrinking::before {
  opacity: 0;
}
.dice-flap {
  font-size: clamp(6rem, 28vw, 22rem);
  font-weight: 800;
  line-height: 1;
  color: var(--clr-dice);
  /* Perspective gives the rotateX flip real depth (a flat rotate looks like a
     squash). The tile grows in on .expanding and shrinks out on .shrinking. */
  perspective: 800px;
  transform: scale(0.4);
  opacity: 0;
  transition:
    transform 180ms ease-out,
    opacity 180ms ease-out;
}
.dice-roll-overlay.expanding .dice-flap {
  transform: scale(1);
  opacity: 1;
}
.dice-roll-overlay.shrinking .dice-flap {
  transform: scale(0.4);
  opacity: 0;
}
.dice-flap-face {
  will-change: transform;
  transform-origin: center center;
}
/* Each flap: host.js removes .flip, forces reflow, re-adds it to restart this. */
.dice-flap.flip .dice-flap-face {
  animation: dice-flip 90ms ease-in;
}
@keyframes dice-flip {
  0% {
    transform: rotateX(0deg);
  }
  50% {
    transform: rotateX(-90deg);
  }
  100% {
    transform: rotateX(0deg);
  }
}
/* Landing glow — held through the linger before the shrink. */
.dice-flap.landed {
  text-shadow: 0 0 30px var(--clr-dice);
}
/* No reduced-motion override: the coordinator already short-circuits to an
   instant land under reduced motion (no expand/flap/shrink callbacks fire),
   and dice-flip's 100% keyframe (rotateX(0deg), the base state) matches
   shared.css's universal instant-finish rule regardless — the old
   belt-and-suspenders block here was redundant with both. */

/* ── Shared modal system ─────────────────────────────────────────────
   The id-based overlays below (menu, undo/action-confirm, timer, resume,
   scoreboard, rules, party-code) all share the same chrome: a scrim backdrop
   that centers a card, the card's surface/border/padding, its accent h3
   title, and the confirm/cancel action buttons. These class families carry
   that chrome; each per-id rule keeps only its deltas (width and
   dialog-specific content).

   Every modal in both apps is now a native <dialog> on `.overlay-backdrop`
   with an `.overlay-dialog` card, shown by `[open]` — including the three
   account dialogs, whose deltas ride the `--account` modifier further down
   rather than a second family. No z-index deltas: showModal() stacks dialogs in the top layer by
   call order. The backdrop centering/scrim, the `[open]` display toggle, and
   the dialog surface/border/radius/padding/title chrome live in
   ui/shared/shared.css. Host keeps just its own min/max-width as a delta. */
.overlay-dialog {
  min-width: 240px;
  max-width: 400px;
}
/* Confirm/cancel dialog buttons now ride the shared `.btn` system: the
   affirmative is `.btn.btn--primary`, the dismiss is `.btn.btn--surface`
   (both the standard-button family, per #1740 — a dialog button is just a
   44px/r14 button now), and the `.focused` selection ring (toggled by
   OverlayFactory) comes from `.btn.focused`. */

/* ── Menu overlay ────────────────────────────────────────────────── */
/* When menu opens mid-turn it acts as an opaque word-cover so a peeking
   player cannot read the secret prompt through the semi-transparent backdrop.
   This replaces the per-game custom paused screen — the menu IS the cover. */
#menu-overlay.menu-turn-cover {
  background: var(--scrim-cover);
}
/* #menu-inner + its h3 inherit the shared .overlay-dialog chrome. */
#menu-items {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* Section break before the irreversible "point of no return" rows (Discard
   Card / End Game) so the destructive block reads as its own group and a host
   doesn't mis-tap End Game (UX inspection 2026-07-06 §P2). A hairline rule with
   a little extra breathing room on top of the flex gap; not a .menu-item, so it
   is never focusable. */
.menu-section-divider {
  height: 1px;
  margin: 4px 4px 0;
  background: var(--surface);
  border: 0;
}
/* .overlay-close + :hover + its roving-.focused ring live in shared.css (host +
   player share them). The menu ✕ carries no host-only gating: it is always
   visible and a roving-focus stop on every input (menu-strategy spec
   2026-07-30, superseding the touch-only gate of playtest 2026-06-29 Q5). */
/* .menu-item base + .menu-btn-hint live in shared.css (host + player share
   them); the collecting-menu-stepper family below layers host-only deltas
   on top of the shared .menu-item base. */
/* Pre-game reconfigure stepper rows in the collecting menu. The row is a
   .menu-item (space-between), so the label sits left and this ◀ value ▶ group
   sits right — reusing the config screen's .config-arrow / .config-value chrome.
   A stepper is not an activate target, so it never flashes the coral :active.
   Hiding a hidden stepper row is handled by shared.css's generic
   `.menu-item[hidden] { display: none; }` — no host-only override needed. */
.collecting-menu-stepper {
  cursor: default;
}
.collecting-menu-stepper:active {
  background: var(--bg-1);
  color: var(--text);
}
.collecting-stepper {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}
.collecting-menu-stepper .config-value {
  min-width: 3.5em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.collecting-menu-stepper .config-arrow {
  color: var(--text-dim);
  font-size: 18px;
  min-width: 36px;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.collecting-menu-stepper.focused .config-arrow {
  color: var(--clr-proceed);
}
.menu-btn-hint .hint-btn {
  width: 18px;
  height: 18px;
  font-size: 10px;
}
/* Touch-only (no controller, no keyboard) hides the key-hint glyph on the menu
   item rows — matching the .cta-chip/.hint-chip .hint-btn hides — since there
   is no physical key to press (a destructive row confirms via the modal, not
   a hold, so nothing depends on a visible glyph here). */
body.no-controller:not(.has-keyboard) .menu-btn-hint .hint-btn {
  display: none;
}

/* ── Undo confirmation overlay ───── */
/* No z-index rule: dialogs opened with showModal() stack in the top layer by
   call order, so a confirm opened over the in-game menu is above it by
   construction. The old .overlay-above-menu bump is gone with the class. */
/* -inner + h3 inherit the shared .overlay-dialog chrome; the body line uses
   .dialog-body and the button row uses .dialog-actions (see the template
   above). */
/* Guest adopt prompt above resume-confirm (96) and feedback (210): both can be
   pending at boot (a restored game arms resume-confirm while the adopt stash
   opens this dialog), and the data-affecting choice must sit on top — an
   EXPLICIT stacking contract, not an accident of open order. dispatchIntent/
   dispatchDpad gate on it before the resume-confirm branch to match. Rides
   above the feedback dialog wherever that lands (it moved 97 → 210 for the
   completed-recap fix), so this tracks it at feedback + 1. */
#adopt-confirm-overlay {
  z-index: 211;
}
/* Inline transient-failure cue — the dialog stays open so Keep can retry. */
#adopt-confirm-error {
  color: var(--c-negative);
  font-size: 13px;
  min-height: 1.2em;
}
/* Buttons carry .btn + .btn--primary/--surface (the standard-button family,
   shared chrome). */
/* Key-hint glyph inside the undo-confirm buttons — always visible (not
   focus-gated like the menu rows) so the P=Undo / U=Cancel affordance reads
   at a glance. The glyph inherits the button's text color for contrast. */
.confirm-btn-hint {
  display: inline-flex;
  margin-left: 6px;
  vertical-align: middle;
}
.confirm-btn-hint .hint-btn {
  width: 16px;
  height: 16px;
  font-size: 10px;
}

/* ── In-game timer-override overlay ──────────────────────────────────
   A ± seconds stepper opened from Menu › Timer (guided turn-timer games).
   Stacks above the still-open menu automatically: showModal() puts each
   dialog in the top layer in call order. Mirrors the confirm card chrome. */
/* #timer-inner + its h3 inherit the shared .overlay-dialog chrome. */
#timer-overlay-stepper {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-bottom: 16px;
}
/* The timer stepper's − / + buttons ARE the scoreboard's stepper — same round
   surface chip, same border, colour and glyph size. They lived here as a second
   copy that differed only in being 40px instead of 44px, which is drift with no
   rule behind it (2026-08-01 metrics sweep), so both now come from one rule
   (`.sb-step`, further down) and land on the app's 44px minimum. */
#timer-overlay-value {
  font-size: 28px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  min-width: 80px;
  text-align: center;
}
/* Set/Cancel: the button row is .dialog-actions; the buttons carry
   .btn + .btn--primary/--surface (the standard-button family, shared
   chrome). */

/* ── Resume-session confirmation overlay ─────────────────────────────
   An interruptible auto-proceed countdown shown before re-entering an
   in-progress game. The progress bar is FUNCTIONAL (it conveys remaining
   time), so reduced-motion drives it in discrete steps rather than killing
   it (see the reduced-motion block). The z-index is part of the explicit
   adopt-confirm > feedback > resume-confirm ordering pin (see
   host-guest-save-cta.test.js); dialog-vs-dialog stacking itself is top-layer
   call order. */
#resume-confirm-overlay {
  z-index: 96;
}
/* Wider, centered card with a larger, tighter title than the base dialog. */
#resume-confirm-inner {
  padding: 20px;
  min-width: 260px;
  max-width: 420px;
  text-align: center;
}
#resume-confirm-inner h3 {
  font-size: 16px;
  margin-bottom: 8px;
}
/* #resume-confirm-message uses the shared .dialog-body spacing. */
#resume-confirm-progress {
  height: 8px;
  border-radius: var(--radius-xs);
  background: var(--surface);
  overflow: hidden;
  margin-bottom: 16px;
}
#resume-confirm-bar {
  height: 100%;
  width: 100%;
  border-radius: var(--radius-xs);
  background: var(--accent);
  /* Start full, animate to empty over the countdown. The host sets
     transform: scaleX(0) + a transition with duration RESUME_CONFIRM_MS so
     the bar drains; transform-origin keeps it shrinking from the right. */
  transform-origin: left center;
  transform: scaleX(1);
}
/* Row is .dialog-actions, but the resume dialog is a centered card so its
   buttons center rather than right-align. */
#resume-confirm-buttons {
  justify-content: center;
}
/* Proceed/Back carry .btn + .btn--primary/--surface (the standard-button
   family); the resume dialog uses slightly taller buttons than the base. */
#resume-confirm-buttons button {
  padding: 8px 16px;
}

/* ── Scoreboard overlay ──────────────────────────────────────────── */
/* Wider min, unbounded max so a many-team board grows to fit its rows. */
#scoreboard-inner {
  min-width: 260px;
  max-width: none;
}
/* Constant padding in both states: the focused highlight must never shift
   the row's − / + tap targets mid-adjust. cursor:pointer advertises the
   tap-to-select affordance on the row itself (the steppers are buttons and
   already get it from the UA/base styles). */
.sb-team {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--surface);
  font-size: 14px;
  cursor: pointer;
}
.sb-team.focused {
  background: var(--surface);
  border-radius: var(--radius-xs);
}
.sb-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The app's one − / + stepper chip: the scoreboard's per-row steppers and the
   timer-override overlay's, which were two identical rules apart from the
   timer's 40px (this rule was written as a copy "sized to the 44px touch-target
   floor" — the original was simply never brought along). One rule now, at the
   floor. */
/* Knob SKIN for the two overlay steppers only. The config screen's ◀ / ▶ take
   the family's geometry (the 44px target) but keep their bare-glyph skin: the
   knob's fill is `--surface`, which is exactly the config panel's own colour,
   so a filled knob is invisible there. Geometry is the family's; skin is the
   member's. */
.sb-step,
#timer-overlay-stepper button {
  border: 1px solid var(--surface);
  background: var(--surface);
  color: var(--text);
}
.sb-step:hover,
#timer-overlay-stepper button:hover {
  background: var(--bg-1);
}
.sb-team.focused .sb-step {
  border-color: var(--bg-1);
}
.sb-score {
  min-width: 2.5ch;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* Single full-width Done row (its .dialog-actions wrapper supplies the flex
   layout; flex:1 stretches the lone button across it). Height comes from the
   standard-button family's own 44px default now (#1740, 2026-08-01) — no
   per-button override needed; every overlay button is a standard button. */
#scoreboard-done {
  flex: 1;
  margin-top: 12px;
}

/* ── Rules overlay — full How-to-Play view (Task 9) ──────────────────
   Reuses the intro's row idiom (.intro-rule-key/.intro-rule-text,
   .intro-way/.intro-way-badge, .intro-howto-label) so the overlay reads as
   the SAME content the intro/ready screens teased, just complete. The full
   view runs longer than the old two-line brief, so the dialog scrolls
   internally instead of overflowing the viewport. */
#rules-inner {
  min-width: 260px;
  max-width: 380px;
  max-height: 90dvh;
  overflow-y: auto;
}
#rules-howto {
  display: grid;
  gap: 14px;
  margin-bottom: 16px;
}
#rules-ways-section {
  margin-bottom: 16px;
}
/* Unlike the intro's name-only teaser, the full view shows each variation's
   note — a short muted line under the name/badge row. */
#rules-ways .intro-way-note {
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}
/* Deck-info renders as a muted footer line, set off from the rows above. */
#rules-deckinfo-section {
  padding-top: 10px;
  border-top: 1px solid var(--surface);
  margin-bottom: 10px;
}
#rules-deckinfo-section .intro-howto-label {
  margin-bottom: 4px;
}
#rules-deckinfo {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}
/* "View online ▸" — a quiet accent link (matching the intro Full-rules
   affordance) that opens the richer web how-to-play page in a new tab.
   Skin (color, weight, size, cursor) comes from the shared .text-link family
   (shared.css) now; this keeps only the layout + the underline reset that
   family doesn't set (it's an <a>, so the browser default needs turning
   off). */
.rules-view-online {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  text-decoration: none;
}
.rules-view-online:hover,
.rules-view-online:focus-visible {
  text-decoration: underline;
}
#rules-dismiss-hint {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--surface);
  font-size: 11px;
  color: var(--text-muted);
}

/* Party-code overlay — mirrors #rules-overlay layout */
#partycode-inner {
  min-width: 260px;
  max-width: 380px;
  text-align: center;
}
/* Title row splits the label and the close X across the width. */
#partycode-inner h3 {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
/* partycode-value is a <button>: tapping SHARES the join link (the appended
   .partycode-share-glyph signals it). Reset button baseline first, then
   restore the original display appearance. */
.partycode-value {
  display: block;
  position: relative;
  width: 100%;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-btn);
  cursor: pointer;
  font-family: var(--font);
  /* 32px → 26px (owner, 2026-08-02 playtest: "the join code can be a bit
     smaller"). Still the loudest text in the block, just not shouting. */
  font-size: 26px;
  font-weight: bold;
  color: var(--text);
  /* Symmetric inline reserve for the hung share glyph (rule below), so the
     code's center stays the box's center instead of being pushed left by an
     inline icon — the 2026-08-02 push-vs-hang fix. */
  padding: 8px 44px;
  margin-bottom: 4px;
  text-align: center;
  transition: border-color 0.15s;
}
.partycode-value:hover,
.partycode-value:focus-visible {
  border-color: var(--accent);
  outline: none;
}
.partycode-value .code-part {
  letter-spacing: 0.15em;
}
/* The share glyph rides the code box, visually quieter than the code itself
   so it reads as "this box does something", not part of the code. Hung at
   the right edge and vertically centered rather than sharing the code's
   inline run — inline it sat on the code's baseline (optically low against
   the tall bold digits) and pushed the code off the box's center. */
.partycode-share-glyph {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 20px;
  color: var(--accent);
}
.partycode-hint {
  font-size: 13px;
  color: var(--text-dim);
  text-align: center;
  margin: 0 0 10px;
  /* Honors the authored "\n" in chrome_partycode_hint so the two ways in sit on
     their own lines (still wrapping normally beyond that). `pretty` is the
     backstop for narrow viewports where the second line wraps again: it avoids
     leaving a one-word last line, which is how "code:" got orphaned. */
  white-space: pre-line;
  text-wrap: pretty;
}
.partycode-copy-hint {
  font-size: 11px;
  color: var(--accent);
  min-height: 1.4em;
  margin-bottom: 6px;
  text-align: center;
  /* Visibility toggled by JS; aria-live announces to screen readers. */
}
/* Sits directly under the QR it spells out (owner, 2026-07-29). */
.partycode-url-link {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
  margin: 8px 0 12px;
  word-break: break-all;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.partycode-url-link:hover,
.partycode-url-link:focus-visible {
  color: var(--accent);
  outline: none;
}
.partycode-qr {
  display: flex;
  justify-content: center;
}
/* Join QR — brand-TINTED, not brand-inverted (see --qr-dark/--qr-light in
   shared.css): plum ink on cream, which stays dark-on-light the way phone
   cameras expect. Brand accent must never touch a module — at 2.8:1 on cream it
   would not decode — so the brand reads here through the cream paper itself.
   No accent ring: at 2px on a charcoal card it read as an error/focus state
   (owner, 2026-07-30), and the cream against --surface is already a 14.4:1
   boundary, so the outline was decoration doing no work. */
.partycode-qr .qr-code {
  background: var(--qr-light);
  border-radius: var(--radius-card);
  padding: 6px; /* card breathing room; the quiet zone is inside the svg */
  line-height: 0; /* drop the inline-svg baseline gap */
}
/* The generator hardcodes fill="white"/fill="black" as PRESENTATION attributes,
   which any CSS rule outranks — so the recolor needs no change to qr-code.js. */
.partycode-qr svg rect {
  fill: var(--qr-light);
}
.partycode-qr svg path {
  fill: var(--qr-dark);
}
/* Spans modules + the 4-module quiet zone, so it is wider than the old
   modules-only 180px at the same ~6px module pitch. Physical size, not color, is
   the binding scannability constraint for a phone across a room. */
.partycode-qr svg {
  width: 224px;
  height: 224px;
  display: block;
}
/* Menu feedback thumbs — the shared .fb-row chrome, centred under the menu
   rows so it reads as a quiet footer affordance rather than a menu action. */
#feedback-menu-row {
  justify-content: center;
  margin-top: 2px;
}
/* Session id (short hash) footer — small light print for troubleshooting. Not
   an action row, so no hover/focus/hold affordances. Uses --text-muted (AA
   >=4.5:1 on --bg-2) at full opacity: a dimming `opacity` blends toward the
   plum bg and drops small text below the WCAG AA contrast floor (axe caught
   4.42:1 — see test_host_a11y menu-overlay check). */
.menu-session-id {
  margin-top: 6px;
  text-align: center;
  font-size: 10px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* ── Reduced-motion accessibility ─────────────────────────────────────
   shared.css's universal `*, *::before, *::after` rule (animation-duration:
   0.01ms + iteration-count:1 !important) already suppresses every animation
   whose own keyframe 100% state matches its un-animated base — which is most
   of them (see the sweep of host.css/play.css reduced-motion blocks removed
   alongside this comment). What survives here are the genuine exceptions:
   a keyframe that ends mid-effect rather than at rest, or a target the `*`
   selector can't reach at all. #countdown-number, .flash-change and
   .detail-winner were dropped from this block for exactly that reason —
   their keyframes already end at the base state. */
@media (prefers-reduced-motion: reduce) {
  /* timer-expired-flash's 0%/100% keyframe is 0.5 opacity — the bar's OWN
     resting opacity (#header-timer-bar, above) is 0.35, so an instant single
     iteration would leave it dimmer than intended. (timer-warn-blink has no
     such gap — its 0%/50%/100% keyframe is already 0.35, so .warn needs no
     override; verified by computed style in the running app.) */
  #header-timer-bar.expired {
    animation: none !important;
    transition: none !important;
  }
  /* Resume-confirm progress bar is functional (it conveys remaining time), so
     it is NOT suppressed — instead the host drains it in discrete per-second
     steps. Kill the continuous transition so each stepped width change snaps
     rather than sweeps; the numeric "Resuming in N…" in the message is the
     primary cue. */
  #resume-confirm-bar {
    transition: none !important;
  }
  /* Belt-and-suspenders for the coordinated view commit: the host already
   * skips document.startViewTransition under reduced-motion (instant atomic
   * write), but if a transition ever runs, kill the cross-fade so the new view
   * appears at once instead of morphing. The universal `*` rule above can't
   * reach these — browser-generated view-transition pseudo-elements live
   * outside the normal DOM tree `*` selects. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
  /* `.region-toast`/`.region-toast--linger`'s own reduced-motion
     force-visible exception moved to shared.css with the rules themselves
     (docs/todo.md "One feedback dedup key" — both apps mount through them
     now). Same reasoning applies here: fb-thanks's keyframe also fades back
     to opacity:0, so force the "thanks" confirmation visible instead of an
     imperceptible flash. */
  .fb-thanks.fb-flash {
    animation: none;
    opacity: 1;
  }
}

/* ==========================================================================
   Directional feedback sweep — full-viewport overlay for score/pass/undo.
   Locked design (docs/plans/2026-04-19-playtest-fixes.md): a low-alpha
   gradient vignette that sweeps in a direction + color keyed to the feedback
   type, so it reads from across the room without obscuring the prompt text.
     score → bottom-right → upper-left, var(--clr-scored) blue
     pass  → bottom-left  → upper-right, var(--accent) coral
     undo  → retrograde upper-left → bottom-right, var(--clr-undo) neutral gray
   The .sweep-* class is toggled by ui/shared/feedback-sweep.js. The three
   directions used to be three near-identical @keyframes (differing only in
   the start/end background-position corner); collapsed to the one `sweep`
   keyframe below, parameterized per modifier via `--sweep-from`/`--sweep-to`.
   `--sweep-color` names each modifier's gradient color for the same reason;
   the gradient's origin corner and its 0-alpha fade-out stop stay inline
   per modifier (each is a one-off value, not shared) — the fade-out
   literal deliberately matches the start color's own hue (not the generic
   `transparent` keyword, which is 0-alpha BLACK) so the fade doesn't dip
   through gray; `--sweep-passed` is the one exception, since var(--accent)
   has no rgb-triple form to zero the alpha of, so it uses `transparent`. */
.feedback-sweep {
  position: fixed;
  inset: 0;
  pointer-events: none;
  /* Above gameplay content (z 1-10), below all overlays (menu/scoreboard 90+,
     auth 200, connection banner 100, action-error toast 1000). */
  z-index: 50;
  opacity: 0;
  background-size: 220% 220%;
  background-repeat: no-repeat;
}

@keyframes sweep {
  0% {
    background-position: var(--sweep-from);
    opacity: 0;
  }
  30% {
    opacity: 0.3;
  }
  100% {
    background-position: var(--sweep-to);
    opacity: 0;
  }
}

.feedback-sweep.sweep-scored {
  --sweep-color: var(--clr-scored);
  --sweep-from: 100% 100%;
  --sweep-to: 0% 0%;
  background-image: radial-gradient(
    circle at 90% 90%,
    var(--sweep-color) 0%,
    rgba(92, 157, 242, 0) 55%
  );
  animation: sweep 450ms ease-out forwards;
}
.feedback-sweep.sweep-passed {
  --sweep-color: var(--accent);
  --sweep-from: 0% 100%;
  --sweep-to: 100% 0%;
  background-image: radial-gradient(
    circle at 10% 90%,
    var(--sweep-color) 0%,
    transparent 55%
  );
  animation: sweep 450ms ease-out forwards;
}
.feedback-sweep.sweep-undone {
  --sweep-color: var(--clr-undo);
  --sweep-from: 0% 0%;
  --sweep-to: 100% 100%;
  background-image: radial-gradient(
    circle at 10% 10%,
    var(--sweep-color) 0%,
    rgba(136, 136, 136, 0) 55%
  );
  animation: sweep 450ms ease-out forwards;
}

/* No reduced-motion override: the JS controller already skips the sweep, and
   the sweep keyframe's 100% state (opacity:0) matches shared.css's universal
   instant-finish rule regardless. */

#auth-overlay site-footer {
  margin-top: 2rem;
}
/* Chooser footer sits below the game grid, centered like the rest. */
#game-chooser site-footer {
  margin-top: 2rem;
}
/* Inside the account dropdown the footer reads as a quiet menu coda:
   just the legal links (no brand line — hiding the brand span also drops
   its trailing separator), left-aligned to match the menu items and set
   off by a divider. */
#account-sheet site-footer {
  text-align: left;
  font-size: 11px;
  line-height: 1.5;
  padding: 10px 16px 4px;
  margin-top: 4px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
#account-sheet site-footer .site-footer__copy {
  display: none;
}
.auth-btn.loading {
  opacity: 0.6;
  cursor: progress;
}

/* ── Generic overlay (profile, delete-confirm, frozen) ───────────────────── */
/* ── Account dialogs: the overlay-dialog family plus their own deltas ─────────
   Profile / delete-confirm / frozen used to be a second overlay family
   (`class="overlay"` + `.overlay-card`) that re-implemented the scrim,
   centering, [open] toggle and card chrome the shared family already carries.
   They now ride .overlay-backdrop/.overlay-dialog like every other dialog;
   only the deltas below are account-specific. They are form-like account
   surfaces rather than two-button confirms, hence the roomier padding, the
   scroll-in-card, the <h2> headings and the text inputs.

   The auth overlay predates all of this and keeps its own #auth-overlay rules. */
.overlay-dialog--account {
  padding: 24px;
  min-width: 280px;
  max-width: 440px;
  width: 90%;
  /* Tall content (parties list + danger zone) scrolls WITHIN the card instead of
     being clipped unreachably by the backdrop. */
  max-height: 90dvh;
  overflow-y: auto;
  /* Long unbroken values (display names, party codes) wrap instead of forcing
     the card wider than the viewport. */
  overflow-wrap: anywhere;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* The shared family styles h3 (shared.css); these dialogs head with h2, which
   would otherwise render completely unstyled. */
.overlay-dialog--account h2 {
  margin: 0;
  font-size: 20px;
  color: var(--accent);
}
/* Style every text-like overlay input, not just type=email — the delete-confirm
   input is type=text, so an email-only selector left it as a raw white box. */
.overlay-dialog--account input[type="email"],
.overlay-dialog--account input[type="text"] {
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid var(--surface);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
  color: var(--text);
  width: 100%;
  box-sizing: border-box;
}

/* ── Contact line ("email ✦ Signed in with google") ──────────────────────── */
/* The ✦ separator is drawn only when the email half is non-empty, so a phone
   host ("Signed in by phone" + blank meta) or a guest (blank email + "Guest
   account") never shows a dangling glyph. Both halves are set via textContent,
   so :empty is reliable. */
.profile-contact-line {
  margin: 0;
}
.profile-contact-line #profile-email:not(:empty) + #profile-meta:not(:empty)::before {
  content: " ✦ ";
  color: var(--text-muted);
}

/* ── Editable display-name override (inside profile overlay) ─────────────── */
/* Effective name (override or IdP) plus an always-visible override field —
   no read/edit mode toggle, so backdrop dismiss never traps half-typed text. */
.profile-name-row {
  margin: 0;
}
.profile-display-name {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.profile-display-name label {
  font-size: 13px;
  color: var(--text-muted);
}
/* The name input inherits the shared .overlay-dialog--account input baseline
   above; the Update-name button carries its own .btn.btn--primary role
   (standard-button family — app-unification Phase 2A Task 11 retired the
   generic overlay-button safety net). */
.profile-display-name #profile-display-name-status {
  font-size: 12px;
  color: var(--text-muted);
  min-height: 1em;
}

/* ── Contact preference (opt-out checkbox inside profile overlay) ────────── */
.profile-contact {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.profile-contact-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: var(--text);
  cursor: pointer;
}
.profile-contact-row input[type="checkbox"] {
  cursor: pointer;
}
.profile-contact-note {
  margin: 0;
  font-size: 13px;
  color: var(--text-muted);
}
.profile-contact #profile-contact-status {
  font-size: 12px;
  color: var(--text-muted);
  min-height: 1em;
}

/* ── Danger zone (delete account section inside profile overlay) ─────────── */
.danger-zone {
  border-top: 1px solid rgba(255, 80, 80, 0.3);
  margin-top: 8px;
  padding-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.danger-zone h3 {
  margin: 0;
  font-size: 14px;
  color: var(--clr-negative);
}
/* At rest the description is calm, muted body copy — the alarm lives in the
   confirmation dialog, not on the profile page. */
.danger-zone p {
  margin: 0;
  font-size: 13px;
  font-weight: 400;
  color: var(--text-muted);
}
/* #profile-delete-btn carries .btn.btn--neutral (standard-button family) —
   the delete-account at-rest entry point is intentionally a neutral button,
   not a huge red one (ui-style.md Principle 5). The red is reserved for the
   confirmation dialog's submit, which carries `.btn.btn--danger`. */

/* ── Delete-confirmation dialog: this is where the red/scary lives ────────── */
#delete-confirm-overlay .overlay-dialog--account {
  border-color: rgba(220, 38, 38, 0.6);
}
#delete-confirm-overlay h2 {
  color: var(--clr-negative);
}
/* The reassurance copy under the prompt (we'll message you / you can cancel). */
.delete-confirm-note {
  margin: 0;
  font-size: 13px;
  color: var(--text-muted);
}
/* "Keep my account" is the safe escape hatch — #delete-confirm-cancel carries
   .btn.btn--surface (the safe-choice role every other confirm pair uses),
   beside the red destructive action (only the submit carries .danger). */

/* Winner Finale celebration layer (see ui/shared/celebration-finale.js).
   A passive decoration — pointer-events:none so it never blocks the host's
   proceed/act input while it plays.

   isolation:isolate makes #phone-display its own stacking context, which is
   what lets the canvas below sit at a NEGATIVE z-index: it then paints above
   #phone-display's own background but beneath every band inside it, instead of
   dropping behind an ancestor's background and vanishing. */
#phone-display {
  isolation: isolate;
}
/* BEHIND the content, not over it. The renderer lays down
   rgba(12,12,20,0.30) every frame onto its own bitmap, so the canvas converges
   on opaque within ~20 frames — full-bleed above the bands it did not tint the
   winner headline and the final score line, it buried them (2026-08-02
   four-game pass; measured in the running app, #phone-feedback's play-toast
   band was under it too). Lifting bands out one id at a time only moved the
   seam to whichever band was added next, so the decoration goes under the
   whole content stack and the sparks show through the gaps. */
.finale-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: -1;
}
.finale-card-layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2;
}
/* Keep the end-of-game chrome legible ON TOP of the finale's result card.
   The card layer (z-2) is centred full-bleed over #phone-display, so on a short
   display it can reach the header slot — which on the winner screen carries the
   Spraoi wordmark (host.js, WORDMARK_HTML), the frame people photograph — or
   the bottom nav band: the proceed CTA ("Continue"/"Play Again") and the "Back
   to Games"/"Menu" hint chips. A host who cannot see the exit feels stranded on
   the celebration screen even though the keys still work. Lift the three bands
   above the card so the brand and the way out stay discoverable. Data-driven:
   this rides the concluded screen for EVERY game type (the finale is not
   game-type-gated), so a freeform quit (Blanks/Categories) and a scored
   game-over (Guess) are both covered. The card stays pointer-events:none.

   The canvas needs no lift over it: it sits at z-index:-1, under the whole
   content stack (see the .finale-canvas block above). Until 2026-08-03 it was
   above, and every band that had not been enumerated here — the winner
   headline, the final score line, the play-toast band — was buried by it. */
#phone-header,
#phone-cta,
#phone-hints {
  position: relative;
  z-index: 3;
}
.finale-card {
  text-align: center;
  padding: 1.2rem 1.8rem;
  border-radius: 14px;
  /* Was a literal rgba(20, 20, 30, 0.72) — an off-palette cool blue-black
     (B > R/G) rather than the app's warm charcoal (--bg-2, R > G > B). Same
     72% opacity, derived from the real token so a future palette change
     propagates here too (owner call, 2026-08-02). */
  background: color-mix(in srgb, var(--bg-2) 72%, transparent);
  border: 2px solid var(--clr-finale-gold);
}
/* The game names the moment ("Celebrity / Winner / Team A", owner 2026-08-02).
   Quieter than the gold eyebrow below it: this line says WHICH game, the
   eyebrow says WHAT happened, and the name says WHO — three tiers, loudest
   last. */
.finale-game {
  color: rgba(255, 255, 255, 0.72);
  font-size: 0.85rem;
  letter-spacing: 0.06em;
  margin-bottom: 0.15rem;
}
.finale-eyebrow {
  color: var(--clr-finale-gold);
  font-weight: 700;
  letter-spacing: 0.04em;
}
/* The wordmark takes the header's game-name slot on the winner screen. The
   display face at the header's own size — brand, not a second headline. */
.finale-wordmark {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: 0.01em;
}
.finale-name {
  color: #fff;
  font-size: 1.6rem;
  font-weight: 700;
  margin-top: 0.25rem;
}
/* ── Guest "save your data" recap card — shown only to guests on the
   game-over recap (visibility owned by _applyGameView). A real .cta-chip-
   adjacent surface, NOT a #button-panel .hw-btn (two-control-surfaces
   gotcha). ─────────────────────────────────────────────────────────────── */
.save-cta-row {
  display: flex;
  justify-content: center;
  padding: 6px 10px;
}
/* Share-link feedback ("Link copied", or the URL itself where there is neither
   a share sheet nor a clipboard). Sits beside the button and occupies no space
   until it has something to say, so the row doesn't jump when it fires. */
#share-recap-row {
  align-items: center;
  gap: 10px;
}
.share-hint {
  font-size: 0.8rem;
  color: var(--text-dim);
  /* A link is long: wrap it rather than push the button off the row. */
  overflow-wrap: anywhere;
}

/* ── User feedback ghost row (spec: quietness level 1 — icons only, no
   prompt). Shared across the chooser footer, live recap, and completed-recap
   overlay. ────────────────────────────────────────────────────────────── */
.fb-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 18px;
  padding: 6px 10px;
}
/* The thumbs are a real tap target, not decoration — a mis-tap writes a wrong
   vote into the feedback digest we actually read. They measured 35x35 with NO
   corner radius at all, the only such control in the app (2026-08-01 sweep);
   the glyph is unchanged, the target reaches the 44px floor, and the corner
   takes the compact row/chip token. */
.fb-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  opacity: 0.7;
}
.fb-btn:hover,
.fb-btn:focus-visible {
  opacity: 1;
  color: var(--text);
}
.fb-glyph {
  font-family: "Material Symbols Rounded Outline";
  font-weight: normal;
  font-size: 22px;
  font-feature-settings: "liga" 0;
  -webkit-font-smoothing: antialiased;
}
.fb-btn.fb-voted .fb-glyph {
  font-family: "Material Symbols Rounded";
}
.fb-btn.fb-voted[data-fb-vote="up"] {
  color: var(--c-positive);
  opacity: 1;
}
.fb-btn.fb-voted[data-fb-vote="down"] {
  color: var(--c-negative);
  opacity: 1;
}
/* The flag escalation is a stronger "down" — same red family when recorded. */
.fb-btn.fb-voted[data-fb-vote="flag"] {
  color: var(--c-negative);
  opacity: 1;
}
.fb-btn.fb-pop .fb-glyph {
  animation: fb-pop 0.35s ease-out;
}
@keyframes fb-pop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.35);
  }
  70% {
    transform: scale(0.92);
  }
  100% {
    transform: scale(1);
  }
}
.fb-thanks {
  font-size: 12px;
  color: var(--text-muted);
  opacity: 0;
  min-width: 48px; /* reserved space: no layout shift */
  text-align: right;
}
.fb-thanks.fb-flash {
  animation: fb-thanks 1.6s ease-out;
}
@keyframes fb-thanks {
  0% {
    opacity: 0;
    transform: translateX(4px);
  }
  12% {
    opacity: 1;
    transform: none;
  }
  75% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
/* fb-pop has no reduced-motion override: its 100% keyframe (scale(1), the
   base state) matches shared.css's universal instant-finish rule regardless.
   .fb-thanks.fb-flash's override lives in the consolidated
   "── Reduced-motion accessibility ──" block above — its keyframe fades back
   to opacity:0 like .region-toast's, so it needs the same force-visible
   treatment. */

/* Per-card content rating (feedback Phase 2): the ghost thumbs live in the
   card's OWN footer (.card-foot, shared.css) — host.js moves #card-feedback-row
   into the panel's .card-foot-fb mount every commit. No positioning of its own:
   the footer is a normal flex row, so nothing here needs to measure the panel,
   reserve a band for the thumbs, or hide the panel's number. */

/* Feedback dialog (Task 12) — reachable from the account sheet, the chooser/
   recap/completed-recap bubbles, AND the in-game menu (which stays open
   underneath it, rules-precedent). completed-recap-overlay became a native
   <dialog> (Task 5): showModal() now stacks the feedback dialog opened from
   inside it above by call order alone, so this no longer needs to out-rank
   an opaque completed-recap-overlay div (the old 97 → 210 bump for that is
   retired — host-css.test.js pins .completed-recap-overlay's z-index gone).
   The z-index stays, though: #adopt-confirm-overlay (211) still needs to
   explicitly out-rank this regardless of open order (both can be pending at
   boot) — see that rule's comment and host-guest-save-cta.test.js's
   "stacks explicitly above resume-confirm AND the feedback dialog" pin. */
#feedback-overlay {
  z-index: 210;
}
.fb-dialog-toprow {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 10px 0;
}
.fb-dialog-thumbs {
  display: flex;
  gap: 14px;
}
.fb-consent {
  display: flex;
  gap: 7px;
  align-items: center;
  font-size: 13px;
  color: var(--text-dim);
  cursor: pointer;
}
#feedback-text {
  width: 100%;
  min-height: 96px;
  resize: vertical;
  box-sizing: border-box;
  /* Match the themed .overlay-dialog--account input baseline — this dialog uses the
     .overlay-dialog pattern, so it doesn't inherit that rule. */
  padding: 8px 12px;
  font-size: 14px;
  font-family: inherit;
  border: 1px solid var(--surface);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
  color: var(--text);
}
#feedback-counter {
  text-align: right;
  font-size: 11px;
  color: var(--text-muted);
  margin: 3px 0 10px;
}
/* #feedback-buttons uses the shared .dialog-actions row. */
