/* 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). */

/* `--cta-max` (the one full-width CTA cap) MOVED to shared.css's :root — see
   the note there. It is consumed by `.btn--stack`, which lives in shared.css
   and which the player app now uses too; defined only here it resolved to
   nothing on /play, silently dropping the cap (review, 2026-08-16). */
:root {
  /* 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;
  /* px, against the rem rule the rest of the families now follow, and the
     FIXED box above is why: this control cannot grow, so its contents must
     not either. Measured on the rendered chooser, the rem form already
     overflowed at root 32px (scrollHeight 46 in a clientHeight of 44) and by
     8×14px at 48px — and `overflow: hidden` clips that silently, so the
     initials would lose their tops with nothing to see it happen. */
  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;
  /* px for the same reason as the initials above — the fixed 44px box. */
  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 three
   .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;
}
/* Skin (fill/border/radius/shadow/padding) is the shared `.menu-panel`
   recipe in shared.css — this sheet is its reference look (owner,
   2026-08-18: every menu standardizes on the chooser menu's appearance).
   Row left-alignment lives on the shared .menu-item. Only the anchoring
   is host-specific and lives here. */
#account-sheet {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 180px;
  z-index: 300;
  display: flex;
  flex-direction: column;
}
#account-who {
  font-size: 12px;
  color: var(--text-dim);
  padding: 8px 16px 10px;
  /* --border, matching the .menu-panel border this divider was always meant
     to pair with (the old white-alpha hairline paired with the retired
     hand-rolled sheet border — review 2026-08-18). */
  border-bottom: 1px solid var(--border);
  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-bottom is deliberately OMITTED, not merely set to 0: the
     .screen-zone:has(.qr-overlay:not([hidden])) runway rule below needs sole
     ownership of padding-bottom to reserve scroll space under the fixed QR
     card, and #game-chooser (an ID selector) always beats that class+:has()
     selector on specificity REGARDLESS of what value it assigns — even
     `padding: 24px 24px 0` (which still declares padding-bottom:0 via the
     shorthand) keeps outranking the runway rule's 340px. Longhand top/inline
     properties are the only way to leave padding-bottom undeclared here so
     the runway rule is the sole declaration and wins by default. A
     QR-mounted chooser measured 71% Impostor-tile coverage before this
     split (2026-08-08 playtest). */
  padding-top: 24px;
  padding-inline: 24px;
  flex-direction: column;
  gap: 16px;
  width: 100%;
  max-width: 720px;
  margin-inline: auto;
  box-sizing: border-box;
}
/* .screen-zone gives #gather-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. */
#gather-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);
}
#gather-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 gather 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
   (349px expanded, measured 2026-08-09 at 1280x800 after the collapse button's
   56px top reserve — the pre-2026-08-09 "~320px" estimate is why the old 340px
   reservation would have gone 9px short). Keyed on the mount being 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).

   Caveat that "no wiring" promise depends on: these rules only win for a
   CLASS-selected zone. An ID-selected zone (e.g. #game-chooser) outranks them
   on specificity regardless of source order, so that zone must declare its
   own padding-bottom via longhand only — never the padding SHORTHAND, which
   sets padding-bottom too even at 0 — or omit it entirely, the same
   discipline #game-chooser's own rule follows (see its comment) and
   host-css.test.js pins as a regression test. A future ID-selected zone that
   mounts an overlay and reaches for `padding: Npx` will silently defeat these
   rules exactly the way an earlier fix attempt here did.

   ONE reserve since 2026-08-18: the pill is the overlay's only state (the
   expanded card and its 370px runway retired with it — the expanded
   presentation is the party-code dialog now, which floats in the top layer
   and needs no runway). Sized to the pill: 56px QR thumbnail + 4px card
   padding + the wrapper's max(16px, safe-area) bottom inset, rounded up
   for the shadow. */
.screen-zone:has(.qr-overlay:not([hidden])) {
  padding-bottom: 96px;
}

/* 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;
  /* 16px, not 14px: iOS Safari zooms the viewport on focus for any text
     input under 16px — the player app's inputs already pin 16px for this
     reason (audit §6, 2026-08-08). */
  font-size: 16px;
  border: 1px solid var(--surface);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
  color: var(--text);
  box-sizing: border-box;
}
/* The shared inline-rename input (`ui/shared/lobby-row-edit.js`) is built in
   JS, so it has no markup class to hang this on and shipped as a raw UA input
   at the browser-default 13.3px — under the 16px threshold the rule above
   exists for, on the one control a host retypes their own name into. The pin
   is UNCONDITIONAL, not part of the touch-first block: focus-zoom is a
   font-size behaviour, not a pointer one. The 44px floor for it IS
   pointer-gated and lives with the other promotions. Only the type is set
   here — the rest of the row's look is the caller's. */
.row-edit-input {
  font-size: 16px;
}
/* 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: var(--clr-on-accent);
}
.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);
}
/* The unified gather screen's roster-shape add-error slot — vocabulary
   decision: error text is --c-negative everywhere. */
#gather-roster-error {
  color: var(--c-negative);
  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 _updateGatherStartCta in host.js). */
#gather-roster-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);
}
/* Below-the-fold affordance for a long intro (Blanks/Categories overflow a
   1280x720 host — measured scrollHeight 854/882 against a 720 clientHeight;
   "Back to Games" landed off-screen with no scroll indicator, 2026-08-08
   playtest). Mirrors shared.css's `.roster-scroll-fade` recipe — position:
   sticky pinned inside the SAME scrolling content, height cancelled by an
   equal negative margin-top so it never inflates scrollHeight itself (no
   false "more below" once actually scrolled to the end) — but for a
   different scroll shape: the roster list is its own overflow:auto
   container, while here #intro-zone (shared.css .screen-zone) is the
   scrolling ancestor and #intro-panel is the tall content, so this fade is
   #intro-panel's own last child rather than the scrolling element's.
   Toggled by HostIntro.show (ui/web/host/host-chooser.js) from a real
   #intro-zone.scrollHeight > clientHeight read — data-driven, never a fixed
   always-on band, so a short game's intro (Guess fits at 720) shows nothing.
   Does not move #intro-back-btn — purely an added-on-top affordance. */
.intro-scroll-fade {
  display: none;
  position: sticky;
  bottom: 0;
  height: 1.5rem;
  margin-top: -1.5rem;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none;
}
#intro-panel.has-scroll-more .intro-scroll-fade {
  display: block;
}
.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.

   "A little more space" (owner 2026-08-16, in the same breath as moving this
   above the CTA): both halves of it. The padding grows so the sentence is not
   pressed against its own accent rule, and the margins now separate the widget
   from BOTH neighbours — above the CTA it has content on one side and a button
   on the other, where below it the CTA's own margin had been doing that job on
   its behalf. The 18px bottom collapses with .btn--stack's 10px top rather
   than adding to it, so the gap under the widget is 18px, not 28px. */
.intro-next {
  background: var(--bg-1);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
  padding: 15px 17px;
  margin: 24px 0 18px;
}
.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. */
.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 {
  /* Centred in its own row, so it needs no negative pull: the padding that
     lifts it to the 44px floor (above) is symmetric and the label stays
     centred. */
  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: 1rem;
  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 width cap and scrolls internally.
   max-height 100% (of the backdrop's padded content box), not 90dvh: with
   the backdrop's 1.5rem inset a 90dvh card overflows a short viewport and
   flex-centering clips the title + ✕ unreachably (review 2026-08-18). */
#completed-recap-inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 520px;
  max-height: 100%;
  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 the type (survey §5 / owner call
   1: a role owns one size). This button's own 15px/600 was the third of the
   three sizes `.btn` shipped, and it sat under an 18px `#recap-back-btn` on the
   sibling recap; only the bespoke width and the opacity hover are left. */
.completed-recap-play-again-btn {
  padding: 12px 32px;
}
.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: alpha 0.3 everywhere (cards, config rows, choice cards).
   Owner flattened the former 0.2/0.3 split on 2026-08-10 — stronger ring for
   controller visibility at distance. See docs/decisions.md. */
.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-top: 24px;
  padding-inline: 24px;
  /* Unlike #game-chooser, this zone has no QR-overlay mount today — the only
     two are #chooser-partycode and #gather-partycode — so the
     .screen-zone:has(.qr-overlay:not([hidden])) runway rule can never match
     it and there is nothing else to supply a bottom pad. Omitting
     padding-bottom here (the #game-chooser fix, applied blind) left config
     content scrolling flush to the bottom edge instead of keeping its prior
     24px. This zone keeps OWNING its own bottom padding until the day a QR
     overlay actually mounts inside it — at which point drop this line the
     same way #game-chooser's was dropped above, not before. */
  padding-bottom: 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 shared a
   15px size (owner decision 2026-08-01, replacing the intro's 13px in a 28px
   box). That size is gone: "Back to Games" was 15px here and 18px on the
   recap — same label, same classes, one flow — and the answer to owner call 1
   is that the ROLE owns the size, so both take `.btn`'s 18px. What still
   differs is POSITION, 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) applies untouched. */
.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;
  /* 16px, not 13px: iOS Safari zooms the viewport on focus for any text
     input under 16px (audit §6, 2026-08-08 sweep — this row renders real
     type=text/type=number fields, e.g. Impostor's custom word). <select>
     never triggers the zoom, but stays paired at 16px for a consistent row
     — .config-row is flex-wrap, so the extra width wraps rather than
     overflows. */
  font-size: 16px;
}
/* Alpha 0.3 — same strength as the card ring above (owner flatten 2026-08-10). */
.config-row.gp-focused {
  border-color: var(--clr-proceed);
  box-shadow: 0 0 0 2px rgba(var(--clr-proceed-glow-rgb), 0.3);
}
.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;
}
/* Advanced Configuration expander: a native <details>/<summary> disclosure —
   the browser owns open/closed state (reflected on the "open" attribute) and
   hides its content (the advanced rows nested inside, see game-core.js's
   renderConfigFields) when closed, so no JS display bookkeeping is needed.
   .config-row's flat single-row flex layout is overridden to block here
   since this row WRAPS its (stacked) advanced children rather than being
   one; the header line's own layout (⚙ gear + label + ▸/▾ caret) moves onto
   <summary>. Its cursor is the browser's own focus ring — same
   :focus-within rule every other native row gets (`.config-row[data-native]`
   below) — never .gp-focused (a second cursor on the same row). */
.config-row.gp-config-expander {
  display: block;
  margin-top: 16px;
  padding: 0;
  border-top: 1px solid var(--bg-1);
}
.config-row.gp-config-expander summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 8px;
  cursor: pointer;
  list-style: none;
}
/* Suppress the browser's own disclosure triangle — the label already draws
   its ▸/▾ caret below, matching the pre-native visual. */
.config-row.gp-config-expander summary::-webkit-details-marker {
  display: none;
}
.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 .config-expander-caret::before {
  content: "▸";
}
.config-row.gp-config-expander[open] .config-expander-caret::before {
  content: "▾";
}
/* 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.3);
}
/* 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.3);
}
.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 {
  /* px, and the `margin` below is why: it is calibrated to cancel a 44px
     box against a header row whose height comes from a static 28px title.
     Scaling the glyph alone desynchronises the pair — the box grows, the
     compensation does not, and the row grows with it. Scaling the row too
     means scaling the in-game header, which is host DISPLAY, sized to the
     room. Named in both guards' px-by-design lists; the follow-up to make
     the whole in-game header scale together is in docs/todo.md. */
  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 inherit the stage's centering — a short
   cluster ("Turn Over", ready briefing) plus a bottom-pinned CTA reads as a
   title card when the space around it is equal, and as a hole when the
   headline is shoved into the upper third (owner call 2026-08-14). Hero
   prompts keep fit-to-fill via .is-prompt (rule below). Auth overlay is a
   different surface and keeps its own clamp. */
#phone-headline:not(.is-prompt) {
  /* 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 layout for card beats (owner call 2026-08-02): a card PANEL is a
   content block. Auto block margins center a short panel; a filling one
   takes the stage and clips only at the true stage bottom. Data-gated on
   panel presence, not a state name. */
/* 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 (rendered as a real
   `<button>` — see renderDiceValueHtml), 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).

   The soft pill background is a permanent (not hover-only) affordance: a
   bare cursor:pointer div gave a touch user — who never sees :hover — no
   visual cue that the readout was tappable at all (2026-08-08 playtest: "no
   cue that it is tappable"). Token language matches the rest of the app's
   "background reveal on an interactive surface" idiom (.menu-trigger family,
   shared.css) rather than inventing a new one. */
#phone-headline .dice-readout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-bottom: 8px;
  padding: 10px 22px;
  border: none;
  border-radius: var(--radius-btn);
  background: var(--bg-2);
  font: inherit;
  color: inherit;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-appearance: none;
  appearance: none;
}
#phone-headline .dice-readout:hover {
  background: var(--bg-1);
}
#phone-headline .dice-readout:active {
  background: var(--bg-1);
}
#phone-headline .dice-readout:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
#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);
}
/* Rolling: hold the letter back until the animation stops (owner, 2026-08-16 —
   the render commits the rolled face ~1.9s before the flaps land, so the answer
   was on the page the whole time the tile was still spinning for it). host.js
   sets/clears body.dice-rolling from the coordinator's own onExpand/onLand, so
   the reveal is the animation's, not a timer's. `visibility`, not `display`, so
   nothing reflows underneath the overlay. The label stays — "Letter" is the
   caption, and hiding it would make the readout look broken rather than
   pending. */
body.dice-rolling #phone-headline .dice-value {
  visibility: hidden;
}
#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;
  /* Half-height variant for the card-browse shape (owner, 2026-08-16: "The next
     and previous buttons can be half their current height"). Next/Previous are
     navigation, not the scoring decisions the 154px band was sized for, and
     Categories in particular is fighting for vertical room. 77px leaves ~61px
     chips after the band's 8px padding — comfortably over the 44px touch floor.
     Applied wherever #phone-cta carries .card-emphasis — the band's own shape
     class — and to the grid row reserving space for it, via :has(). NOT gated
     on body.hint-band-shape-collapsed: that class is `cardNavEmphasis AND an
     empty hint bar`, a strict subset, and using it here left the two out of
     step on any card beat that rendered a chip. */
  --band-action-nav: 77px;
  /* 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;
  /* The two lower rows are reserved through their own custom properties, and
     the overrides below each set ONE of them. That is deliberate: the action
     row's height and the hint row's presence are independent facts (the CTA
     shape vs. whether updateHintBar emitted a chip), and writing both into a
     single `grid-template-rows` override made them look like one condition.
     They were then keyed to one class that is only a SUBSET of the other —
     see the two rules below for what that cost. Composing custom properties
     also sidesteps the specificity race a second full-shorthand override
     would have to win. */
  --row-action: var(--band-action);
  --row-hints: minmax(var(--band-hints), auto);
  /* 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(--row-action), auto) var(--row-hints);
  flex-shrink: 0;
}
/* The action row's reserve follows the band that actually occupies it, via
   :has() on the CTA's own shape class — NOT the hint-band class next to it.
   Those are different conditions: host.js sets .card-emphasis from
   `cardNavEmphasisActive(...)` alone, while body.hint-band-shape-collapsed is
   `cardNavEmphasis && !newHintHtml`. Keying the half-height reserve on the
   latter meant a card-browse beat that DID render a hint chip (updateHintBar
   falls back to a chip for a state whose layout omits the die entry) reserved
   the full 154px for a 77px band — leaving Next/Previous at the top of a row
   with 77px of dead space under them, and jumping down when the next beat
   emptied the bar. That is exactly the "the action row must not move"
   guarantee this grid exists to hold. */
body:not(.recap-mode) #phone-footer:has(#phone-cta.card-emphasis) {
  --row-action: var(--band-action-nav);
}
/* The terminal beat ("{team} wins!" / "Game over!", set by host.js from
   recap_regions presence). The action row's fixed reserve buys ONE thing —
   the band landing on the same pixel from beat to beat — and there is no
   next beat here, so all it renders is 100px of measured emptiness under a
   lone Continue chip, with the hint band's divider drawn across the screen
   above "Back to Games". Both were owner findings (button survey 2026-08-19,
   §5: "remove the dead space beneath it and the horizontal rule above Back to
   Games"). Only the RESERVE goes: the rows still size to their content, so
   nothing about the in-play stability guarantee
   (test_action_band_holds_position_across_every_beat) changes — that suite
   never reaches this beat. */
body.game-over:not(.recap-mode) #phone-footer {
  --row-action: auto;
  --row-hints: auto;
}
body.game-over:not(.recap-mode) #phone-hints {
  border-top-color: transparent;
}
/* Shape-scoped hint-band collapse (host.js updateHintBar, cardNavEmphasis):
   a freeform DeckReveal-browse game (Categories/Blanks) promotes timer/menu
   to the header and dice/card-nav into the CTA band, so #phone-hints is
   empty for the WHOLE session, not just some beats — unlike Guess, whose
   band content genuinely varies beat to beat (the naive `:empty` collapse
   below was tried and reverted for exactly that game; see its comment). Drop
   the reserved row entirely rather than leaving --band-hints worth of dead
   space under the CTA for every freeform round. */
body.hint-band-shape-collapsed:not(.recap-mode) #phone-footer {
  /* This rule owns the HINT row only. The action row's reserve is the :has()
     rule's above — this class implies card-emphasis but is not implied BY it,
     so it is the wrong gate for anything about the CTA's own height. */
  --row-hints: 0;
}
/* The display:none half lives further down, AFTER #phone-hints:empty (same
   specificity — ID + two class/pseudo-class selectors each — so source
   order decides): #phone-hints is normally :empty at the exact moment this
   class is set, and :empty's own `display: flex` must not win the cascade
   and silently defeat this collapse. Search "body.hint-band-shape-collapsed"
   again near #phone-hints:empty. */
/* 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). */
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 or
   card-emphasis grids: there `align-items` resolves as align-self on the grid
   items, which stops chips stretching to fill their cells (turn-emphasis:
   Got It! two-row span — PR #1448; card-emphasis: Hybrid 1×2 freeform band). */
body:not(.recap-mode) #phone-cta:not(.turn-emphasis):not(.card-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;
  /* A disabled chip is a status readout, not an action, and its label is a
     phrase rather than a verb — "No more passes" in a ~160px cell overflowed
     against the base .cta-chip `nowrap` (owner, 2026-08-16). Safe to relax
     here and nowhere else: every ENABLED chip's label is one or two words that
     never reach the wrap point, so this only ever fires on the long
     status labels it was added for. */
  white-space: normal;
}
.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: the intent-tier colors
   already clear AA on --surface (proceed 5.68:1, negative 5.43:1), so the
   brighter private hint twins were dropped — same verb, same color
   (2026-08-10 color review). */
body.no-controller .cta-proceed {
  color: var(--c-proceed);
}
body.no-controller .cta-negative {
  color: var(--c-negative);
}
/* 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%);
  }

  /* SETUP AND ACCOUNT FIELDS JOIN THE FLOOR TOO. Everything above promotes a
     control a finger drives during PLAY; these are the ones it drives during
     SETUP and account admin, and they were the last ones the promotion
     skipped. Measured 2026-08-20 on a coarse-pointer 390x844 host whose
     `.cta-chip` this very block had already grown to 60px — so the promotion
     was demonstrably live on the same page: Teams and Target Score at 30px,
     the timer/content-pack selects at 32px, the display-name, delete-confirm
     and Impostor roster fields at 38px (that last one beside its own 44px add
     button), and the feedback consent row at 16px — the smallest target on
     the host — while `#config-start-btn` and `#header-menu-btn` beside them
     were 44px. Teams and Timer are the two most-touched controls on the
     configure screen and were the smallest targets on it.
     Coarse-gated like `.hint-chip`'s promotion above, deliberately: a
     mouse-driven host keeps the compact fields it was tuned with.
     Keyed on the FAMILIES rather than ids — `.text-input` is this codebase's
     general text-field idiom and `.overlay-dialog--account input[type=...]`
     is the account dialogs' — so a field added to either recipe inherits the
     floor instead of needing a new id here.
     Checkbox and radio are excluded because the floor does not belong on the
     BOX: a 44px checkbox is a stretched glyph, not a bigger target. Its LABEL
     is the target — but "the label" is two different shapes here, and only
     one of them is a wrapper. `.profile-contact-row` and `.fb-consent` wrap
     their box, so naming them is enough. A `.config-row` checkbox does NOT:
     game-config-fields.js emits `<label for=ID>…</label><input id=ID>` as
     SIBLINGS, so the row rule below is what gives those labels the floor.
     Without it the content filters — 11 rows for Guess (Difficulty x3,
     Category x8), rendered whenever the host picks Content Library and opens
     Advanced — stay 26px rows with a 16px label and a 13px box, and a thumb
     aimed at "Difficulty: Medium" toggles "Easy". The content-source radios
     keep their 1px visually-hidden size for the same label-is-the-target
     reason (the card is what a finger hits), and `#feedback-text` already
     clears the floor at 118px.
     `input[type="email"]` matches no element today; it is named because the
     adjacent `.overlay-dialog--account` styling rule names it, so a future
     email field inherits the floor with the look. */
  body.no-controller .config-row input:not([type="checkbox"]):not([type="radio"]),
  body.no-controller .config-row select,
  body.no-controller .text-input,
  body.no-controller .phone-input,
  body.no-controller .overlay-dialog--account input[type="email"],
  body.no-controller .overlay-dialog--account input[type="text"],
  body.no-controller .profile-contact-row,
  body.no-controller .fb-consent {
    min-height: 44px;
  }
  /* The sibling-label half of the rule above. `:has()` is already the
     codebase's idiom for keying a row off its own control (see
     `.gp-config-choice .choice-card:has(input:checked)`). The label carries
     the floor rather than the row, because the label is the `for=`-linked
     target a finger actually hits; the row would grow without widening
     anything tappable. */
  body.no-controller .config-row:has(input[type="checkbox"]) label {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
  /* The shared inline-rename input (`.row-edit-input`, built in JS by
     `ui/shared/lobby-row-edit.js`). Keyed on the CONTROL, not on a container:
     the host has two call sites for it — the Impostor roster and, since
     2026-08-20, the profile display name — and an id-scoped rule would have
     covered only whichever one it named. */
  body.no-controller .row-edit-input {
    min-height: 44px;
  }
}

/* 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;
  }
}

/* Categories/Blanks card-browse list (.card-list, shared.css): the clamp
   there — font-size: clamp(0.8rem, 3.5vw, 0.95rem) — is phone-primary by
   design (the 3.5vw preferred value is what actually drives sizing on a
   phone viewport). On a 1280x800 HOST the 3.5vw term (44.8px) is already far
   past the 0.95rem/15.2px cap, so the list sits pinned at its phone-sized
   ceiling despite ~104px of unused vertical room in the panel. Scoped here
   rather than in shared.css: .card-list ships in both the host and player
   manifests (region-renderers.js is shared), so a shared.css override at
   this breakpoint would silently reach the player app too — kept out of
   that file entirely so "host-only" is a structural fact, not just what
   happened to be measured. Raises the ceiling only where the vw term never
   reaches it; the phone-facing clamp itself is untouched. Measured:
   15.2px -> 18.4px at 1280x800 host, 13.65px unchanged at 390w. */
@media (min-width: 700px) {
  .card-list {
    font-size: 1.15rem;
  }
}

/* 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;
}
/* Wins the cascade over :empty above by source order (equal specificity —
   see the comment near body.hint-band-shape-collapsed's grid-template-rows
   half, up with #phone-footer): the shape-collapsed row must fully
   disappear, not just lose its border, or its own padding still consumes
   space inside the now-zero-height grid track. */
body.hint-band-shape-collapsed:not(.recap-mode) #phone-hints {
  display: none;
}
.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;
  /* The shared pill value (see --radius-pill's note in shared.css for why it
     is 999px rather than this box's exact half-height). */
  border-radius: var(--radius-pill);
  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-recycle-toast
   (host.js _showDeckRecycledToast) shares this same visual treatment but is
   a SEPARATE mount, not a shared container — see the host.html comment on
   it for why (its content would otherwise get wiped by #phone-feedback's
   own per-render engine-driven clear). */
#phone-feedback,
#phone-recycle-toast {
  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 shared centering stage
     mount balances it 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);
  /* A real edge (owner asked, 2026-08-16: "The recap scroll area should have a
     border around it?"). Answered yes because this pane is already a distinct
     object — the one independently scrolling region on the screen — and it had
     no visual identity at all, so the share/rate controls now seated inside it
     would otherwise read as loose chrome floating above the nav stack. The
     pane is content-sized when short (flex: 0 1 auto above), so the border
     hugs the recap rather than framing empty space; when a long recap does
     overflow, it is also what makes the scroll boundary legible.

     Scoped to body.recap-mode, NOT the shared :is() rule above: the
     completed-recap viewer renders inside a <dialog> that already draws its
     own frame, and a second border inside it would read as a box in a box. */
  border: 1px solid var(--bg-1);
  border-radius: var(--radius-card);
}
/* "About this recap" — the share button and the rating thumbs, seated INSIDE
   the pane by host.js's _seatRecapActions.

   They sit here rather than in the nav stack because they act on the recap
   you are reading (share this result, rate this game), while Play Again /
   Back to Games / Sign in answer "what next". Splitting the recap's controls
   along that line is what took the screen from four control rows in three
   idioms down to two coherent groups (owner, 2026-08-16: "screen is too
   busy"; "Share the recap should just be a share button at the bottom of the
   recap scroll area").

   Sticky-free and in normal flow: it scrolls away with the content it belongs
   to, which is the point of putting it inside the pane. */
.recap-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--bg-1);
}
/* 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;
}
/* Impostor's public word-reveal row (host.js's _highlightImpostorRecapPayload
   tags it .detail-highlight, keyed on state.impostor_word data presence) —
   "it is the one fact the room wants" (2026-08-08 playtest), so it gets the
   SAME biggest-in-scale treatment .detail-winner uses above for a
   final-scores winner, not a new size. Unlike .detail-winner this row sits
   directly under .recap-pane (no .detail-scores ancestor — Impostor has no
   team-score table), so the selector doesn't nest under .detail-scores. */
:is(body.recap-mode, .completed-recap-overlay)
  :is(.recap-pane, .phone-detail)
  .detail-highlight,
body.recap-mode #phone-headline .recap-pane .detail-highlight {
  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,
#phone-cta.card-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. TURN-emphasis only — see the card-nav override
   below for why its half-height chips cannot use this idiom. */
#phone-cta.turn-emphasis .cta-chip .hint-btn {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
}
/* Card-nav chips opt OUT of the hang-and-reserve idiom and take the compact
   trailing keycap the base .cta-chip already defines (inline-flex, gap 6px,
   `order: 1`).
   The hang idiom needs a 56px reserve on each side. A two-column card-nav band
   is ~164px per chip at 360px wide, so that reserve leaves 52px of content box
   for a "Next Card" label measuring 106px at this size — it overflowed, which
   is why a rule here used to lift the keycap out of the label's band with
   `top: 12px`. That dodge spent vertical space the cell no longer has: halving
   the band to --band-action-nav (owner, 2026-08-16) took the cell from 134px to
   60px, and the keycap went straight back to overlapping its own label —
   measured 36x12px at 360px before this rule (code review, 2026-08-16).
   Flowing the keycap with the label instead makes the collision unreachable at
   any width rather than tuned out at three of them. The cost is that the
   centered thing is now "label + keycap" rather than the label alone; on a
   62px-wide-per-side reserve that was never going to hold here anyway. */
#phone-cta.card-emphasis .cta-chip .hint-btn {
  position: static;
  transform: none;
  /* In the inline run it is a flex ITEM, so the default flex-shrink: 1 lets a
     long label squeeze it — "Space" collapsed from 44px to 26px and clipped.
     The keycap is fixed-content metadata; it never gives up width. */
  flex-shrink: 0;
}
#phone-cta.card-emphasis .cta-chip {
  padding: 10px 12px;
}
/* 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);
}

/* ── Freeform card-nav CTA band (Hybrid, 2026-08-08) ───────────────────────
   1×2 emphasis for Blanks/Categories card browse: same fill/outline tokens and
   reserved height as turn-emphasis, without the Undo cell or 2-row Got It!
   span. Bindings stay positive/negative (Space/−); chrome only. Gated by
   #phone-cta.card-emphasis (card_nav_buttons ∧ ¬bowl-turn). Mutually exclusive
   with .turn-emphasis. Always both cells (inert Previous on first card). */
#phone-cta.card-emphasis {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr;
  gap: 8px;
  padding: 8px;
  width: 100%;
  /* Half the turn-emphasis band (owner, 2026-08-16). Next/Previous navigate a
     deck; they are not the scoring decisions the full band was sized for. */
  height: var(--band-action-nav);
  align-items: stretch;
}
#phone-cta.card-emphasis .cta-negative {
  grid-column: 1;
  grid-row: 1;
  background: var(--clr-pass-fill-tint);
}
#phone-cta.card-emphasis .cta-negative {
  border-color: var(--c-negative);
}
#phone-cta.card-emphasis .cta-positive {
  grid-column: 2;
  grid-row: 1;
  background: var(--c-positive);
  border-color: var(--c-positive);
  color: var(--bg-2);
  /* Was 22px, sized for the 134px cell this chip used to live in. In the
     half-height nav band (owner, 2026-08-16) the cell is 60px and the chip
     ~164px wide at 360px, where a 22px "Next Card" measures 106px and leaves
     nothing for the keycap beside it — it got flex-shrunk to an illegible
     26px. The fill and the full-width cell carry the emphasis here; the type
     does not have to. Inherits the shared .cta-chip 17px. */
}
/* 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;
  /* The floor comes from the floating-overlay family rule (shared.css); the
     button's own UA layout centres the label in the taller box, so no flex
     context is needed here — and must not be added: this button's markup is
     `<span class="ms-icon">…</span> Tap to enable sound`, and a flex container
     would swallow that one space between glyph and label AND make .ms-icon's
     `vertical-align: -0.15em` nudge inert (both measured, 2026-08-20). */
  /* The LOUDER border is this member's own, and deliberate: the family's
     --border hairline is 1.25:1 on its fill, and this chip is the last-resort
     one-tap iOS audio unlock — revealed only after the keydown unlock has
     already failed, and if the host never notices it, nothing audible plays
     for the whole game. Being seen across a room is its entire job, which the
     party-code chip (66px, a white QR tile, 22px code) does not need its
     border for. The role owns geometry; emphasis stays with the member. */
  border-color: var(--accent);
  color: var(--text);
  font-size: 0.875rem;
  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 accents as tokens (docs/ui-style.md backlog #7; open-decisions §6
   promote + 2026-08-10 color review). Host-only — not part of the intent-tier
   palette above. Finale aliases brand marigold; undo is a warm mid-gray kept
   distinct from score/pass sweeps and from --c-back nav gray. */
:root {
  --clr-finale-gold: var(--color-accent-2); /* brand marigold — game-over is brand-allowed */
  --clr-undo: #908880; /* warm undo-sweep gray; ≠ score/pass, ≠ --c-back */
  /* 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/web/host/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 {
  /* The page's display face, not whatever the overlay inherited (owner,
     2026-08-16: "The animation should use the same font as the display on the
     page (the title one)"). The flap is the biggest glyph the host ever draws,
     so a body face there was the most conspicuous place for the type to
     disagree with itself. */
  font-family: var(--font-display);
  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, the
   dialog surface/border/radius/padding/title chrome, AND the card width
   (100% capped at 22rem, 2026-08-18 standardization — the old host-only
   min 240 / max 400 range is retired) all live in ui/shared/shared.css.
   Content viewers (recap/scoreboard/rules) and the --account form dialogs
   keep per-id/per-modifier width deltas below. */
/* Confirm/cancel dialog buttons now ride the shared `.btn` system: the
   affirmative is `.btn.btn--primary`, the dismiss is `.btn.btn--secondary`
   (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 + .menu-panel
   chrome (full-bleed rows; the h3's 16px inline inset is shared too). */
#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;
  /* --border, not --surface: the panel fill IS --surface now (.menu-panel,
     2026-08-18), so a --surface hairline would vanish into it. */
  background: var(--border);
  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. The ◀/▶ are the shared stepper family (shared.css names this
   selector); `.config-value` and `.config-arrow` are named after the config
   screen they were copied from, but this menu is now their ONLY consumer —
   the config screen's own `.config-row .config-*` rules were deleted
   2026-08-19 once its rows became native fields and nothing rendered them.
   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;
}
/* Skin only: the box (44px round knob, glyph size, cursor) comes from the
   stepper family rule in shared.css, which names this selector. Re-declaring
   any of that here is what the family guard in host-css.test.js refuses. */
.collecting-menu-stepper .config-arrow {
  color: var(--text-dim);
}
/* Coral, not proceed-blue: this is chrome inside a menu whose own roving
   cursor is coral — blue is reserved for the in-display proceed CHIP (see
   the intent-palette comment above `:root`'s --clr-* aliases). */
.collecting-menu-stepper.focused .config-arrow,
.collecting-menu-stepper .config-arrow:hover {
  color: var(--accent);
}
.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;
}
/* Same gate, same reason, for the roving cursor itself (button survey §5
   finding e): shared.css parks `.focused` on row 0 the moment a menu opens,
   and its accent ring + tint is the CONTROLLER/KEYBOARD cursor the
   menu-strategy spec asks for — legible across a room, meaningless to a
   finger. With neither input attached it is not a cursor at all: it reads as
   a pre-selected radio button on the first row ("Unpause", "Party code"),
   which is exactly what the owner's Paused-menu screenshot flagged. The
   moment a key or a pad shows up the class flips and the cursor returns.
   `:not(:hover)` so a mouse-only host still gets the hover tint under the
   pointer — the one place the fill means what it looks like. */
body.no-controller:not(.has-keyboard) .menu-item.focused,
body.no-controller:not(.has-keyboard) .btn.focused,
body.no-controller:not(.has-keyboard) .collecting-menu-stepper.focused .config-arrow {
  outline: none;
}
/* Every surface the cursor parks on, not just menu rows: OverlayFactory
   stamps `.btn.focused` on a confirm dialog's affirmative button as it opens,
   and the pre-play stepper row tints its own ◀/▶. A white ring around "Quit
   Game" reads as armed to someone who never touched a key. The ring goes
   under the pointer too — a hovered row that still wears it looks unlike
   every other hovered row — while the TINT below stays hover-scoped, because
   there it means what it looks like. */
body.no-controller:not(.has-keyboard) .menu-item.focused:not(:hover) {
  background: transparent;
}
body.no-controller:not(.has-keyboard)
  .collecting-menu-stepper.focused
  .config-arrow:not(:hover) {
  color: var(--text-dim);
}

/* ── 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.
   Renders AT 420px (base width:100% + this cap) — pinned width is the
   2026-08-18 system: cards don't grow-jump with content. */
#resume-confirm-inner {
  padding: 20px;
  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.
   width: auto (content-sized flex item), not the base card's width:100% —
   with max-width unset that would full-bleed a 1920px projector (review
   2026-08-18). */
#scoreboard-inner {
  width: auto;
  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 {
  max-width: 380px;
  /* 100% of the backdrop's padded content box, not 90dvh — see
     #completed-recap-inner's comment (short-viewport clipping). */
  max-height: 100%;
  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);
}

/* 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 and --surface, per its token comment) at full opacity: a
   dimming `opacity` blends toward the 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/web/host/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. */
/* Two classes, not one: the shared base `.overlay-dialog` declares
   width/max-width at the same (0,1,0) specificity, so a bare
   `.overlay-dialog--account` rule would win on link order alone — the exact
   trap shared.css's hold-reveal comment documents (review 2026-08-18). */
.overlay-dialog.overlay-dialog--account {
  /* padding comes from the shared base card (24px) since 2026-08-18. */
  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. 100% of the
     backdrop's padded content box, not 90dvh — see #completed-recap-inner. */
  max-height: 100%;
  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;
  /* 16px — see .text-input's comment above (iOS focus-zoom, audit §6). */
  font-size: 16px;
  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. */
/* The name row IS the display-name field (edit-in-place, 2026-08-20). In read
   mode it is the name plus its ✎; in edit mode LobbyRowEdit has replaced both
   with an input and a ✓, so the same flex row has to hold either pair. The
   input takes the free space so it does not collapse to its intrinsic width. */
.profile-name-row {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0;
}
.profile-name-row input {
  flex: 1;
  min-width: 0;
}
/* The pencil is the glyph family's 44x44 box, so it would otherwise push the
   row 44px tall on its own; the negative margin keeps the row the height of
   the name it annotates while the TARGET stays the full 44px. */
.profile-name-row .icon-btn {
  margin-block: -10px;
}
/* No `min-height` reserve: this line used to sit inside the display-name
   section's own column, where holding a row kept the section from jumping.
   It is a direct child of the dialog's flex column now, so an empty reserve is
   ~16px plus the column's 12px gap of permanent blank between the name and
   the contact line — and empty is its state every time the dialog opens.
   Collapsed when empty instead, the same `:empty` idiom shared.css uses for
   every other region that has nothing to say. */
#profile-display-name-status {
  font-size: 12px;
  color: var(--text-muted);
}
#profile-display-name-status:empty {
  display: none;
}

/* ── 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) ─────────── */
/* A text link that STANDS ALONE is a target and takes the 44px floor; one
   sitting inline in a sentence is prose and does not (padding it would break
   the line). `#profile-export-btn` and `#resume-see-others` are the two
   standalone members — the latter is 8px above the 44px Resume button, so an
   ~18px target there is a mis-tap that resumes the wrong game.
   `--link-hit-pad` is the padding each needs to clear the floor; only the
   START edge is pulled back, so the LABEL stays flush with the copy beside it
   while the END padding keeps its distance from whatever follows (measured
   2026-08-20: pulling both edges left 5px between "Download my data" and its
   status message, which read as one run-on line). */
.profile-export-btn,
.chooser-resume-see-others {
  --link-hit-pad: 8px;
  min-height: 44px;
  padding: 0 var(--link-hit-pad);
}
.profile-export-btn {
  display: inline-flex;
  align-items: center;
  margin-inline-start: calc(-1 * var(--link-hit-pad));
}
.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--secondary (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--secondary (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/web/host/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,
/* #recap-nav takes the same lift as the two bands it replaces on the recap.
   It is latent today — the finale tears down when recapMode opens, so the two
   never coexist — but that teardown is a separate mechanism in host.js, and
   this list exists precisely because bands left off it got buried once the
   stacking changed underneath them. Cheaper to be on the list than to rely on
   an ordering guarantee living in another file. */
#recap-nav {
  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;
}
/* The recap's navigation stack: Play Again / Back to Games / Sign in to Save
   Results, in that order, all the same width and height (owner, 2026-08-16 —
   who also spotted the inequality: Play Again was a 60px CTA-band chip against
   44px for everything else). .btn--stack gives all three the shared
   full-width-capped treatment, so "equal" is inherited rather than restated. */
#recap-nav {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px 10px calc(10px + env(safe-area-inset-bottom));
}
/* The save row is a flex wrapper around its own .btn, so it has to carry the
   stack's width itself or its button would size to content while its two
   siblings are capped. */
#recap-nav .save-cta-row {
  width: 100%;
  max-width: var(--cta-max);
  padding: 0;
}
/* The CTA band and the keyboard-hint bar are live-play chrome and are gone on
   the recap (owner, 2026-08-16: "Let's review the smaller button CTA area to
   see where it's really needed. It's not needed on recap."). #recap-nav now
   carries both of the intents they used to: proceed as a real Play Again
   button, and back as a real Back to Games button rather than the Esc HINT
   CHIP that was previously the recap's only tappable exit. Keyboard and
   controller presses are unaffected — those dispatch through the global
   keydown map, never through these two elements. */
body.recap-mode #phone-cta,
body.recap-mode #phone-hints {
  display: none;
}
/* 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;
  /* The rendered glyph lives on this CHILD, so `.fb-btn`'s own rem type never
     reached it — the family conversion was inert for the feedback thumbs
     until this moved too. */
  font-size: 1.375rem;
  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. 16px, not 14px:
     iOS Safari zooms the viewport on focus for any text input under 16px
     (audit §6, 2026-08-08 — same fix as .text-input and the account-dialog
     inputs). */
  padding: 8px 12px;
  font-size: 16px;
  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. */

/* Boot-failure panel (boot-guard.js). Deliberately self-contained: it is the
   one screen that must render when the host's own scripts did not arrive, so
   it leans on nothing but tokens.css and the shared .btn. `hidden` does the
   hiding — no .hidden class — because the guard sets `panel.hidden = false`
   without host.js's class helpers being available. */
.boot-error {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 32px;
  text-align: center;
  background: var(--bg-1);
  color: var(--text);
}
.boot-error[hidden] {
  display: none;
}
.boot-error h1 {
  font-size: clamp(24px, 5vw, 40px);
  margin: 0;
}
.boot-error p {
  max-width: 42ch;
  margin: 0;
  color: var(--text-muted);
}
