@import url('/framework/ui-scale.css');   /* game pages scale with the screen (AGENTS.md hard fact 17); must stay the first rule */
/* TwizGame shared theme.
 * Ported from twizGame/TwitchExtension/css/styles.css so every host, TV view,
 * and player UI shares one design system. Edit tokens here, not per page. */

:root {
  --bg-primary: #0a0a0a;
  --bg-secondary: #1a1a1a;
  --bg-hover: #2a2a2a;
  --bg-input: #333333;

  --text-primary: #efeff1;
  --text-secondary: #adadb8;
  --text-muted: #848494;

  --accent: #FFD700;
  --accent-hover: #FFED4E;

  --primary: #FFA500;
  --secondary: #333333;
  --success: #00e676;
  --danger: #ff4444;
  --warning: #ffab00;
  --gold: #ffd700;
  --purple: #9966ff;

  /* Height of the bottom letterbox bar of the 16:9 .tv-canvas on screens
     taller than 16:9 (0 at 16:9-or-wider). The TV lobby's background art lives
     in that letterboxed canvas, but the title + card strip are anchored to the
     raw viewport — so on tall screens they'd drift DOWN, away from the art.
     .hero-content and .strip-wrap add this to their `bottom` to track the art
     instead. Defined here so both stay in sync. */
  --tv-letterbox-bottom: max(0px, (100vh - 100vw * 9 / 16) / 2);

  --border-radius: 6px;
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 12px;
  --spacing-lg: 16px;
  --spacing-xl: 24px;

  /* Comic-book black stroke around big titles. The 8-direction shadow paints a
     uniform 2px outline; the soft drop adds depth. Use --text-outline-md for
     mid-size headings (16-24px) and --text-outline-lg for display text. */
  --text-outline-lg:
    -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000,
    -2px 0   0 #000, 2px  0   0 #000,  0   -2px 0 #000, 0   2px 0 #000,
    0    4px 0 rgba(0, 0, 0, 0.45);
  --text-outline-md:
    -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000,
    0    2px 0 rgba(0, 0, 0, 0.4);
}

/* Self-hosted display + body type for the settings panel ("Playfield" theme).
   Variable woff2s served by the host at /fonts/* (see routes/assets.ts), so
   there's no runtime Google Fonts dependency on a LAN/offline host. Only the
   settings panel opts into these; the rest of the app keeps its own fonts. */
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/bricolage-grotesque.woff2') format('woff2');
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/hanken-grotesk.woff2') format('woff2');
}
/* Bluff Dictionary reveal type: EB Garamond (book body) + Uncial Antiqua
   (display — the WORD, headers, and result labels). Self-hosted woff2 so the
   parchment reveal keeps its look on a LAN/offline/Steam host. */
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/eb-garamond.woff2') format('woff2');
}
@font-face {
  font-family: 'Uncial Antiqua';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/uncial-antiqua.woff2') format('woff2');
}
/* Baloo 2 (rounded, poppy) — the player join screen. Self-hosted variable woff2
   so there's no third-party Google Fonts round-trip on join. latin loads for
   normal names; latin-ext lazy-loads only when an Eastern-European glyph appears. */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/baloo-2-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/baloo-2-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Bagel Fat One (brand title) + Manrope (UI text) — the browser host's connect/shell pages and
   the join page. Self-hosted so the host works offline / inside an Electron build; the pages
   must never link the Google Fonts CDN. Manrope's variable range is 200–800: a request for 900
   renders as 800. */
@font-face {
  font-family: 'Bagel Fat One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/bagel-fat-one-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url('/fonts/manrope-latin.woff2') format('woff2');
}

/* Reset + base */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
* { touch-action: manipulation; }

html, body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
  font-size: 15px;
  line-height: 1.4;
  color: var(--text-primary);
  background: var(--bg-primary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }

/* Page layout */
.page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--spacing-lg);
}
.page-center {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-lg);
}

/* ─── Phone shell ─────────────────────────────────────────────────────────
   Locks player UIs to a vertical-phone container even on desktop. On a
   real phone, viewport is narrow enough that the clamp bottoms out and the
   shell fills the screen. On desktop, sits centered as a phone-shaped pane
   so dev/testers see what mobile users see. Pattern lifted from the old
   project's BrowserGameWindow .game-active sizing.

   Use:
     <body class="phone-page">
       <div class="phone-shell">…game UI…</div>
     </body>
*/
.phone-page {
  margin: 0;
  min-height: 100vh;
  background: var(--bg-primary);
  display: flex;
  align-items: stretch;
  justify-content: center;
  padding: 0;
}
.phone-shell {
  position: relative;
  width: 100%;
  max-width: 460px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
  overflow: hidden;
}
/* Desktop: lift off the page edges, lock to phone proportions, draw a
   frame + notch so dev/testers see what mobile users see. The 9/16
   aspect-ratio works here because exactly one of width/height is auto. */
@media (min-width: 600px) {
  .phone-page { align-items: center; padding: 24px; }
  .phone-shell {
    width: auto;
    height: 90vh;
    min-height: 640px;
    max-height: 920px;
    aspect-ratio: 9 / 16;
    border: 2px solid var(--bg-hover);
    border-radius: 32px;
    box-shadow:
      0 0 0 6px #000,
      0 24px 64px rgba(0, 0, 0, 0.55),
      0 4px 12px rgba(0, 0, 0, 0.4);
  }
  .phone-shell::before {
    /* Tiny notch hint — pure decoration. */
    content: "";
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translateX(-50%);
    width: 90px;
    height: 18px;
    background: #000;
    border-radius: 12px;
    z-index: 5;
  }
}

/* ─── Fixed one-viewport shell (opt-in `pp-fixed`) ────────────────────────
   The go-forward standard for phone player UIs (AGENTS.md §0.1.1): the
   screen is ALWAYS exactly one viewport — pinned header and action buttons
   can never end up below the fold — and anything of unbounded length
   (chat logs, long lists, rules) lives in a .phone-pane scroll window
   INSIDE the shell.

   Use:
     <body class="phone-page pp-fixed">
       <div class="phone-shell">
         …pinned header (status strip / timer)…
         <div class="phone-pane-wrap">
           <div class="phone-pane">…scrollable content…</div>
           <div class="pane-fade top"></div><div class="pane-fade bot"></div>
         </div>
         <div class="phone-actions">…pinned buttons / chat input…</div>
       </div>
     </body>
   plus mountPhoneShell() from /framework/phone-shell.js (wires the
   more-to-scroll fades + keyboard scroll-into-view); paneHtml() from the
   same module emits the wrap+fades block for innerHTML-style renders.
   `pp-fixed` is opt-in so legacy scrolling player UIs keep working. */
html { height: 100%; }
body.phone-page.pp-fixed {
  /* NEVER plain 100vh here (AGENTS.md §0.1.1): 100% tracks the real
     visible viewport on old browsers; dvh refines it where supported. */
  height: 100%; height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
.pp-fixed .phone-shell { min-height: 0; height: 100%; }
@media (min-width: 600px) {
  .pp-fixed .phone-shell { height: 90vh; min-height: 0; max-height: min(920px, 100%); }
}
.phone-pane-wrap {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
}
.phone-pane {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column;
}
/* Centre short content vertically (status/win screens). */
.phone-pane > .pane-center { margin-top: auto; margin-bottom: auto; }
/* "More to scroll" fades — real elements, NOT ::before/::after, so game
   pseudo-element effects (e.g. Bluff's ink-wipe) can never collide. The
   classes are toggled by mountPhoneShell; override --pane-fade per game. */
.pane-fade {
  position: absolute; left: 0; right: 0; height: 30px;
  pointer-events: none; opacity: 0; transition: opacity .18s ease; z-index: 6;
}
.pane-fade.top { top: 0; background: linear-gradient(to bottom, var(--pane-fade, rgba(0, 0, 0, .55)), transparent); }
.pane-fade.bot { bottom: 0; background: linear-gradient(to top, var(--pane-fade, rgba(0, 0, 0, .55)), transparent); }
.phone-pane-wrap.pane-more-above .pane-fade.top { opacity: 1; }
.phone-pane-wrap.pane-more-below .pane-fade.bot { opacity: 1; }
.phone-actions { flex: 0 0 auto; }

/* Card */
.card {
  background: var(--bg-secondary);
  border-radius: var(--border-radius);
  padding: var(--spacing-xl);
  width: 100%;
  max-width: 420px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.card h1 {
  color: var(--accent);
  font-size: 1.8rem;
  font-weight: 800;
  text-align: center;
  margin-bottom: var(--spacing-lg);
  text-shadow: var(--text-outline-md);
  letter-spacing: 0.5px;
}
.card .subtitle {
  color: var(--text-secondary);
  text-align: center;
  margin-bottom: var(--spacing-lg);
  font-size: 14px;
}

/* Headings */
.h-display {
  font-size: 2.5rem;
  font-weight: 800;
  color: var(--accent);
  text-align: center;
  text-shadow: var(--text-outline-lg);
  letter-spacing: 0.5px;
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
  padding: var(--spacing-md) var(--spacing-lg);
  border: none;
  border-radius: var(--border-radius);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s ease;
  min-height: 44px;
  user-select: none;
  text-decoration: none;
  font-family: inherit;
}
.btn:active { transform: scale(0.97); }
.btn:disabled, .btn.disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-full { width: 100%; }
.btn-lg { padding: var(--spacing-lg) var(--spacing-xl); font-size: 17px; min-height: 52px; font-weight: 700; }
.btn-sm { padding: var(--spacing-xs) var(--spacing-sm); font-size: 13px; min-height: 32px; font-weight: 700; }

.btn-primary { background: var(--primary); color: #000; }
.btn-primary:hover:not(:disabled) { background: #e69500; }

.btn-secondary { background: var(--secondary); color: var(--text-primary); }
.btn-secondary:hover:not(:disabled) { background: var(--bg-hover); }

.btn-accent { background: var(--accent); color: #000; }
.btn-accent:hover:not(:disabled) { background: var(--accent-hover); }

.btn-success { background: var(--success); color: #000; }
.btn-success:hover:not(:disabled) { background: #00c464; }

.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { background: #cc3333; }

/* Form fields */
.field { margin-bottom: var(--spacing-md); }
.field label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: var(--spacing-xs);
  letter-spacing: 0.3px;
}

.input, input[type="text"], input[type="password"] {
  width: 100%;
  padding: var(--spacing-md);
  background: var(--bg-input);
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  color: var(--text-primary);
  font-size: 14px;
  outline: none;
  font-family: inherit;
}
.input:focus, input[type="text"]:focus, input[type="password"]:focus {
  border-color: var(--accent);
}
.input::placeholder, input::placeholder { color: var(--text-muted); }

input[type="color"] {
  flex: 1;
  height: 48px;
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  background: var(--bg-input);
  padding: 4px;
  cursor: pointer;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 4px; }

/* Text helpers */
.txt-heading { font-size: 19px; font-weight: 800; text-shadow: var(--text-outline-md); }
.txt-body { font-size: 15px; color: var(--text-primary); }
.txt-caption { font-size: 13px; color: var(--text-muted); font-weight: 500; }
.txt-error { color: var(--danger); font-size: 14px; min-height: 1rem; text-align: center; font-weight: 600; }
.txt-success { color: var(--success); font-weight: 600; }
.txt-muted { color: var(--text-muted); }
.txt-accent { color: var(--accent); }
.txt-gold { color: var(--gold); }

/* Spinner (used during connect) */
.spinner {
  width: 48px;
  height: 48px;
  border: 4px solid var(--bg-secondary);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  margin: 0 auto var(--spacing-lg);
}
@keyframes spin { to { transform: rotate(360deg); } }

/* QR display frame (white bg keeps the QR readable) */
.qr-frame {
  background: #fff;
  padding: var(--spacing-md);
  border-radius: var(--border-radius);
  display: inline-block;
}
.qr-frame img { display: block; width: 200px; height: 200px; }

/* Status pip */
.status-pip {
  position: fixed;
  bottom: 8px;
  right: 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  opacity: 0.75;
}

/* Animations for newly added components */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.fade-in { animation: fadeIn 0.2s ease; }

/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-hover); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }

/* ============================================
   SETTINGS GEAR + SLIDE-OUT PANEL
   ============================================ */

.settings-gear {
  position: fixed;
  top: var(--spacing-md);
  left: var(--spacing-md);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--bg-secondary);
  border: 2px solid var(--accent);
  color: var(--accent);
  font-size: 22px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 1100;
  transition: transform 0.15s ease, background 0.15s ease;
  padding: 0;
}
.settings-gear:hover { background: var(--bg-hover); transform: rotate(30deg); }
.settings-gear:active { transform: rotate(30deg) scale(0.95); }

.settings-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.settings-backdrop.open {
  opacity: 1;
  pointer-events: auto;
}

.settings-panel {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 720px;
  max-width: 90vw;
  background: var(--bg-secondary);
  border-right: 2px solid var(--accent);
  box-shadow: 4px 0 24px rgba(0, 0, 0, 0.5);
  z-index: 1050;
  display: flex;
  flex-direction: column;
  transform: translateX(-100%);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.settings-panel.open { transform: translateX(0); }

.settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-md) var(--spacing-lg);
  background: var(--bg-primary);
  border-bottom: 1px solid var(--bg-hover);
  flex-shrink: 0;
}
.settings-title { font-size: 18px; font-weight: 800; color: var(--accent); text-shadow: var(--text-outline-md); letter-spacing: 0.3px; }
.settings-close {
  background: transparent;
  border: none;
  color: var(--text-secondary);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  padding: 0 var(--spacing-xs);
}
.settings-close:hover { color: var(--text-primary); }

.settings-tabs {
  display: flex;
  background: var(--bg-primary);
  border-bottom: 1px solid var(--bg-hover);
  flex-shrink: 0;
}
.settings-tab {
  flex: 1;
  padding: var(--spacing-md);
  background: transparent;
  border: none;
  color: var(--text-secondary);
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: all 0.15s ease;
  font-family: inherit;
  letter-spacing: 0.2px;
}
.settings-tab:hover { background: var(--bg-hover); color: var(--text-primary); }
.settings-tab.active { color: var(--accent); border-bottom-color: var(--accent); }

.settings-content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--spacing-md);
  /* Extra room at the bottom so the last setting isn't hidden under the
     absolutely-positioned footer (Exit / Disconnect). Matches the footer's
     content height (~36px button) + its top/bottom padding (~16px). */
  padding-bottom: 64px;
  /* IMPORTANT: keep this as block layout. Earlier this was `display: flex;
     flex-direction: column` so the chat playground could `flex: 1` to fill
     remaining space, but that caused flex-shrink to squash sibling children
     (e.g. the SVG section's textareas + canvas) and killed scrolling. */
}
.settings-content > * + * { margin-top: var(--spacing-sm); }
.settings-content[hidden] { display: none; }
/* The drawer's tabs toggle visibility via the `hidden` property, but much of
   their markup carries inline `display:flex`, which outranks the UA's
   [hidden] rule. Without this override the 📡 tab's credential reveal-warning
   cards (position:absolute) render permanently and cover the Connect buttons.
   Scoped to the drawer so game UIs that (deliberately or not) pair hidden
   with inline display are untouched. */
.settings-panel [hidden] { display: none !important; }

/* Scoped readability bump inside the settings panel — phones-on-LAN testers
   sit a couch-distance from the TV, so the default control sizes used
   elsewhere are too small here. Stays scoped so /join, /player, /tv lobby
   buttons are unaffected. */
.settings-panel .btn-sm { font-size: 14px; min-height: 36px; padding: var(--spacing-xs) var(--spacing-md); }
.settings-panel .btn    { font-size: 16px; }

/* Footer is decoupled from the panel's flex stack and pinned to the bottom
   so .settings-content can grow + scroll freely without the footer eating
   vertical space or moving with the content. The content area adds
   padding-bottom so the last visible setting doesn't slide under it. */
.settings-footer {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--bg-primary);
  border-top: 1px solid var(--bg-hover);
  display: flex;
  gap: var(--spacing-sm);
  z-index: 5;
}
.settings-footer .btn { flex: 1; }
.settings-footer .btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* ============================================
   PLAYERS TAB — color-stripe cards
   ============================================
   Each connected player renders as a distinct card (see renderRow in
   twiz-shell.js). The player's color arrives as the --player-color CSS var
   set inline on the row, and feeds BOTH the left stripe and the slot-number
   chip from one source. Visual theming (radius, surface, shadow) comes from
   the PLAYFIELD THEME LAYER further down via scoped vars; this block owns the
   structure + the per-player color wiring. Player colors are user-chosen and
   may be dark, so the chip keeps a guaranteed-contrast white numeral rather
   than dark-on-color. */

.players-empty {
  text-align: center;
  color: var(--text-muted);
  padding: var(--spacing-xl) var(--spacing-md);
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
}

/* Vertical stack — tight gap so a full 12-player roster fits without
   scrolling, while cards stay visually distinct. */
.players-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.player-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  padding: 6px 12px;
  background: var(--pf-surface, var(--bg-hover));
  border: 2px solid var(--pf-border, rgba(255, 255, 255, 0.07));
  /* Left stripe in the player's own color — the at-a-glance color code. */
  border-left: 5px solid var(--player-color, var(--text-muted));
  border-radius: 12px;
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.22);
  transition: opacity 0.2s ease, transform 0.12s ease;
}
/* Offline players recede: card dims and stripe/chip desaturate to gray. */
.player-row.disconnected { opacity: 0.4; }
.player-row.disconnected { border-left-color: var(--text-muted); }

/* Slot-number chip — server-assigned slot number, tinted with the player's
   color (faint fill + colored ring) so the number and the color read as one
   unit. White numeral keeps it legible on any player color. */
.player-num {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 26px;
  padding: 0 6px;
  border-radius: 9px;
  font-family: 'Hanken Grotesk', monospace;
  font-size: 14px;
  font-weight: 800;
  line-height: 1;
  color: #fff;
  background: color-mix(in srgb, var(--player-color, var(--text-muted)) 28%, transparent);
  border: 2px solid var(--player-color, var(--text-muted));
}
.player-row.disconnected .player-num {
  background: transparent;
  border-color: var(--text-muted);
  color: var(--text-muted);
}
/* Identity bead desaturates alongside the stripe/chip when offline. */
.player-row.disconnected .twiz-bead { filter: grayscale(1); }

.player-name {
  flex: 1;
  font-size: 15px;
  color: var(--text-primary);
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.player-status {
  font-size: 11px;
  padding: 3px 10px;
  border-radius: 20px;
  background: var(--success);
  color: #06351d;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.6px;
}
.player-row.disconnected .player-status {
  background: var(--pf-border, var(--text-muted));
  color: var(--text-muted);
}

/* ============================================
   AI SETTINGS TAB (LLM config + testing chat)
   ============================================ */

/* Collapsible section wrapper inside the AI tab. Each <details> groups a
   related cluster of controls so the tab doesn't feel like a wall of inputs. */
.ai-section {
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  background: var(--bg-secondary);
  margin-bottom: var(--spacing-sm);
  overflow: hidden;
}
.ai-section-summary {
  list-style: none;
  cursor: pointer;
  padding: var(--spacing-sm) var(--spacing-md);
  font-weight: 700;
  font-size: 14px;
  color: var(--text-primary);
  letter-spacing: 0.3px;
  user-select: none;
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  background: var(--bg-hover);
}
.ai-section-summary::-webkit-details-marker { display: none; }
.ai-section-summary::before {
  content: '▸';
  display: inline-block;
  transition: transform 0.15s ease;
  color: var(--text-muted);
  width: 0.9em;
}
.ai-section[open] > .ai-section-summary::before { transform: rotate(90deg); }
.ai-section > section { padding: var(--spacing-md); }
.ai-section:not([open]) > section { display: none; }
/* Remove the bottom border on the inner .ai-config since the section wrapper
   already provides visual separation. */
.ai-section .ai-config { border-bottom: 0; padding-bottom: 0; }

.ai-config {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  padding-bottom: var(--spacing-md);
  border-bottom: 1px solid var(--bg-hover);
}
.ai-config label {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  font-size: 14px;
  font-weight: 600;
  color: var(--text-secondary);
  letter-spacing: 0.2px;
}
.ai-config-hint {
  font-weight: 400;
  color: var(--text-muted);
  font-size: 12px;
}
.ai-config input[type="text"],
.ai-config input[type="password"],
.ai-config select {
  background: var(--bg-hover);
  color: var(--text-primary);
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  padding: var(--spacing-sm) var(--spacing-sm);
  font-family: inherit;
  font-size: 15px;
  width: 100%;
  box-sizing: border-box;
}
.ai-config input[type="range"] { width: 100%; accent-color: var(--accent); }
.ai-config-checkbox {
  flex-direction: row;
  align-items: center;
  gap: var(--spacing-sm);
  cursor: pointer;
  user-select: none;
}
.ai-config-checkbox input[type="checkbox"] {
  width: auto;
  margin: 0;
  accent-color: var(--accent);
  flex-shrink: 0;
}
.ai-config-checkbox > span { font-size: 14px; color: var(--text-secondary); font-weight: 600; }
.ai-config-checkbox input[type="checkbox"] { width: 18px; height: 18px; }
.ai-config-row { display: flex; gap: var(--spacing-xs); align-items: center; }
.ai-config-row select { flex: 1; }
.ai-config-actions { display: flex; justify-content: flex-end; }
.ai-status { font-size: 13px; color: var(--text-muted); min-height: 18px; }
.ai-status.error { color: var(--danger); }
.ai-status.success { color: var(--success); }

/* Capability probe results — one row per probe (basic / json / tools /
   vision). Each row shows an icon, label, latency, and short summary. */
.ai-test-results {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  margin-top: var(--spacing-sm);
  padding: var(--spacing-sm);
  background: var(--bg-primary);
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
}
.ai-test-row {
  display: grid;
  grid-template-columns: 20px auto 70px 1fr;
  align-items: baseline;
  gap: var(--spacing-sm);
  font-size: 13px;
  line-height: 1.35;
}
.ai-test-row.pending .ai-test-icon { color: var(--text-muted); animation: ai-streaming-dots 1.4s ease-in-out infinite; }
.ai-test-row.pass    .ai-test-icon { color: var(--success); }
.ai-test-row.fail    .ai-test-icon { color: var(--danger); }
.ai-test-icon    { font-weight: 900; text-align: center; }
.ai-test-label   { color: var(--text-primary); font-weight: 600; }
.ai-test-ms      { color: var(--text-muted); font-variant-numeric: tabular-nums; text-align: right; }
.ai-test-summary { color: var(--text-secondary); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-test-row.fail .ai-test-summary { color: var(--danger); }

.ai-chat {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  margin-top: var(--spacing-md);
  flex: 1;
  min-height: 0;
}
.ai-chat-log {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  flex: 1;
  min-height: 120px;
  max-height: 280px;
  overflow-y: auto;
  padding: var(--spacing-xs);
  background: var(--bg-primary);
  border-radius: var(--border-radius);
  border: 1px solid var(--bg-hover);
}
.ai-chat-log .bubble {
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--border-radius);
  font-size: 15px;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
}
.ai-chat-log .bubble.user { background: var(--bg-hover); color: var(--text-primary); align-self: flex-end; max-width: 85%; }
.ai-chat-log .bubble.assistant { background: var(--bg-secondary); border: 1px solid var(--bg-hover); color: var(--text-primary); align-self: flex-start; max-width: 95%; }
.ai-chat-log .bubble.error { background: rgba(255, 68, 68, 0.12); border: 1px solid var(--danger); color: var(--danger); align-self: flex-start; max-width: 95%; }

/* Streaming indicator — pulsing accent dot at the start of the assistant
   bubble while a response is in flight. Three-dot "typing" placeholder
   appears only when the bubble is still empty (waiting on first token). */
.ai-chat-log .bubble.streaming { position: relative; }
.ai-chat-log .bubble.streaming::before {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 8px;
  vertical-align: middle;
  animation: ai-streaming-pulse 0.9s ease-in-out infinite;
  box-shadow: 0 0 6px rgba(255, 215, 0, 0.6);
}
@keyframes ai-streaming-pulse {
  0%, 100% { opacity: 0.25; transform: scale(0.9); }
  50%      { opacity: 1;    transform: scale(1.15); }
}
/* When the bubble has produced nothing yet (no reasoning visible AND no
   content text), show a three-dot typing placeholder inside the content
   area so it's obvious the model is still warming up / processing the
   prompt. As soon as the first chunk lands, the content fills with text
   and this placeholder is naturally replaced. */
.ai-chat-log .bubble.streaming .bubble-content:empty::before {
  content: '⋯';
  display: inline-block;
  letter-spacing: 4px;
  color: var(--text-muted);
  animation: ai-streaming-dots 1.4s ease-in-out infinite;
}
@keyframes ai-streaming-dots {
  0%, 100% { opacity: 0.3; }
  50%      { opacity: 1; }
}
/* Reasoning models (Qwen3 + vLLM, DeepSeek-R1, …) stream a "thinking" prelude
   before the final answer. Render it faded + italic so the actual answer
   stands out. .visible is added the moment the first reasoning chunk lands. */
.ai-chat-log .bubble .bubble-reasoning {
  display: none;
  font-style: italic;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.4;
  border-left: 2px solid var(--bg-hover);
  padding: 2px var(--spacing-xs);
  margin-bottom: var(--spacing-xs);
  max-height: 120px;
  overflow-y: auto;
  white-space: pre-wrap;
}
.ai-chat-log .bubble .bubble-reasoning.visible { display: block; }
.ai-chat-log .bubble .bubble-content { white-space: pre-wrap; }
.ai-chat-stats {
  display: flex;
  gap: var(--spacing-md);
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.2px;
}
.ai-chat-input { display: flex; gap: var(--spacing-xs); }
.ai-chat-input input {
  flex: 1;
  background: var(--bg-hover);
  color: var(--text-primary);
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  padding: var(--spacing-sm) var(--spacing-md);
  font-family: inherit;
  font-size: 15px;
}
.ai-chat-input input:focus { outline: none; border-color: var(--accent); }

/* SVG illustrator playground (inside the AI tab) — shares the .ai-config
   label styling but adds textareas, a 16:9 canvas, and a knob row. */
.ai-svg {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
}
.ai-svg-intro {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0;
  line-height: 1.4;
}
.ai-svg-intro code {
  background: var(--bg-hover);
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 11px;
}
.ai-svg label {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  font-size: 14px;
  font-weight: 600;
  color: var(--text-secondary);
}
.ai-svg textarea {
  background: var(--bg-hover);
  color: var(--text-primary);
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  padding: var(--spacing-sm);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.4;
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
}
.ai-svg textarea:focus,
.ai-svg input[type="text"]:focus,
.ai-svg input[type="number"]:focus,
.ai-svg select:focus { outline: none; border-color: var(--accent); }
.ai-svg input[type="text"],
.ai-svg input[type="number"],
.ai-svg select {
  background: var(--bg-hover);
  color: var(--text-primary);
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  padding: var(--spacing-sm);
  font-family: inherit;
  font-size: 14px;
  width: 100%;
  box-sizing: border-box;
}
.ai-svg-knobs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-sm);
}
.ai-svg-actions { justify-content: space-between; }
.ai-svg-test {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  margin-top: var(--spacing-sm);
  padding-top: var(--spacing-sm);
  border-top: 1px dashed var(--bg-hover);
}
/* The preview frame — fixed 16:9 ratio matching the SVG viewBox so the
   model's coordinate space maps 1:1. Empty state shows a hint string. */
.ai-svg-canvas {
  aspect-ratio: 16 / 9;
  width: 100%;
  background: #0b0b10;
  border: 1px solid var(--bg-hover);
  border-radius: var(--border-radius);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.ai-svg-canvas:empty::before {
  content: 'Type a subject above and click Draw to test the prompt.';
  color: var(--text-muted);
  font-size: 12px;
  padding: var(--spacing-md);
  text-align: center;
}
.ai-svg-canvas svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* ============================================================================
   PLAYFIELD THEME LAYER — settings panel
   ============================================================================
   A cohesive visual re-skin scoped entirely under `.settings-panel`. Nothing
   here changes behaviour or markup — it only repaints. It works in two parts:

   1. Scoped CSS-variable overrides on `.settings-panel`. The generated Game /
      Audio / Assets tabs and the AI tab all build their controls out of the
      same tokens (--bg-hover, --bg-input, --border-radius, …), so overriding
      the tokens here re-themes every tab at once — no per-tab CSS needed.
   2. Targeted chrome overrides (header, tabs, buttons, slider, sections,
      footer) for the pieces that need shape/gradient/typography beyond tokens.

   Because this block comes after the base rules, it wins the cascade at equal
   specificity. Scope keeps games, the TV view, and player phones untouched.

   Identity: warm near-black with a gold↔purple glow, chunky rounded surfaces,
   Bricolage Grotesque display type, Hanken Grotesk body. ("Playfield" / C.) */

.settings-panel {
  /* Re-themed token palette (purple-tinted charcoals, chunkier radius). */
  --bg-primary: #100c17;
  --bg-secondary: #1b1622;
  --bg-hover: #271f33;
  --bg-input: #221d2b;
  --text-primary: #efeaf6;
  --text-secondary: #c9bfe0;
  --text-muted: #8a8195;
  --border-radius: 12px;
  /* Theme-local accents (not global tokens). */
  --pf-gold: #ffd700;
  --pf-gold-hi: #ffed4e;
  --pf-purple: #9d7bff;
  --pf-border: #322a3f;
  --pf-surface: #221d2b;

  font-family: 'Hanken Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  /* Dual corner glow over a deep aubergine base. */
  background:
    radial-gradient(90% 60% at 100% 0%, rgba(157, 123, 255, 0.13), transparent 60%),
    radial-gradient(90% 55% at 0% 100%, rgba(255, 215, 0, 0.10), transparent 55%),
    #141118;
  border-right: 2px solid #2a2433;
}

/* ── Header ───────────────────────────────────────────────────────────── */
.settings-panel .settings-header {
  background: transparent;
  border-bottom: 1px solid #2a2433;
}
.settings-panel .settings-title {
  font-family: 'Bricolage Grotesque', 'Hanken Grotesk', sans-serif;
  font-weight: 800;
  font-size: 22px;
  letter-spacing: -0.01em;
  color: #fff;
  text-shadow: none;
}
.settings-panel .settings-close {
  background: var(--pf-surface);
  border: 1px solid var(--pf-border);
  border-radius: 9px;
  width: 34px;
  height: 34px;
  color: var(--text-secondary);
}
.settings-panel .settings-close:hover { color: #fff; background: var(--bg-hover); }

/* ── Tabs as pills ────────────────────────────────────────────────────── */
.settings-panel .settings-tabs {
  background: transparent;
  border-bottom: 1px solid #2a2433;
  gap: 6px;
  padding: 10px 14px;
}
.settings-panel .settings-tab {
  flex: 1;
  padding: 9px 6px;
  border-radius: 10px;
  background: var(--pf-surface);
  color: #9a90a8;
  font-weight: 700;
  border: none;
  transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.settings-panel .settings-tab:hover { background: var(--bg-hover); color: var(--text-primary); }
.settings-panel .settings-tab.active {
  background: linear-gradient(180deg, var(--pf-gold-hi), var(--pf-gold));
  color: #2a1d00;
  border: none;
  box-shadow: 0 3px 10px rgba(255, 215, 0, 0.28);
}
.settings-panel .settings-tab:active { transform: translateY(1px); }

/* ── Buttons ──────────────────────────────────────────────────────────── */
.settings-panel .btn {
  border-radius: 12px;
  font-weight: 800;
  font-family: 'Hanken Grotesk', sans-serif;
}
/* Plain + secondary buttons → ghost (surface + border). */
.settings-panel .btn:not(.btn-primary):not(.btn-accent):not(.btn-success):not(.btn-danger) {
  background: var(--pf-surface);
  border: 2px solid var(--pf-border);
  color: var(--text-secondary);
}
.settings-panel .btn:not(.btn-primary):not(.btn-accent):not(.btn-success):not(.btn-danger):hover:not(:disabled) {
  border-color: var(--pf-purple);
  color: #fff;
}
/* Primary / accent → gold gradient (Save, confirm). */
.settings-panel .btn-primary,
.settings-panel .btn-accent {
  background: linear-gradient(180deg, var(--pf-gold-hi), var(--pf-gold));
  color: #2a1d00;
  box-shadow: 0 4px 12px rgba(255, 215, 0, 0.25);
}
.settings-panel .btn-primary:hover:not(:disabled),
.settings-panel .btn-accent:hover:not(:disabled) {
  background: linear-gradient(180deg, #fff3a0, var(--pf-gold-hi));
}
.settings-panel .btn-success {
  background: linear-gradient(180deg, #34f08e, var(--success));
  color: #06351d;
}
/* Danger → outlined, not a solid red slab (Exit / Disconnect / Kick). */
.settings-panel .btn-danger {
  background: #2e1f28;
  border: 2px solid rgba(255, 90, 90, 0.4);
  color: #ff8a8a;
}
.settings-panel .btn-danger:hover:not(:disabled) {
  background: rgba(255, 90, 90, 0.16);
  border-color: var(--danger);
  color: #ffb3b3;
}

/* ── Inputs / selects / textareas ─────────────────────────────────────── */
.settings-panel input[type="text"],
.settings-panel input[type="password"],
.settings-panel input[type="number"],
.settings-panel select,
.settings-panel textarea,
.settings-panel .ai-config input[type="text"],
.settings-panel .ai-config input[type="password"],
.settings-panel .ai-config select,
.settings-panel .ai-chat-input input {
  background: var(--bg-input);
  border: 2px solid var(--pf-border);
  border-radius: 12px;
  color: var(--text-primary);
}
.settings-panel input:focus,
.settings-panel select:focus,
.settings-panel textarea:focus,
.settings-panel .ai-config input:focus,
.settings-panel .ai-config select:focus,
.settings-panel .ai-chat-input input:focus {
  outline: none;
  border-color: var(--pf-purple);
  box-shadow: 0 0 0 3px rgba(157, 123, 255, 0.18);
}
.settings-panel input[type="checkbox"] { accent-color: var(--pf-purple); }

/* ── Range sliders → gradient track + white knob (Chromium/Electron) ──── */
.settings-panel input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 22px;
  background: transparent;
  cursor: pointer;
}
.settings-panel input[type="range"]::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 5px;
  background: linear-gradient(90deg, var(--pf-purple), var(--pf-gold));
}
.settings-panel input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.settings-panel input[type="range"]::-moz-range-track {
  height: 8px;
  border-radius: 5px;
  background: linear-gradient(90deg, var(--pf-purple), var(--pf-gold));
}
.settings-panel input[type="range"]::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}

/* ── Labels ───────────────────────────────────────────────────────────── */
.settings-panel .ai-config label,
.settings-panel .ai-svg label {
  color: var(--text-secondary);
  font-weight: 700;
}

/* ── Collapsible sections (AI tab) → rounded cards, Bricolage summaries ── */
.settings-panel .ai-section {
  border: 2px solid var(--pf-border);
  border-radius: 14px;
  background: var(--bg-secondary);
}
.settings-panel .ai-section-summary {
  background: transparent;
  font-family: 'Bricolage Grotesque', 'Hanken Grotesk', sans-serif;
  font-weight: 700;
  font-size: 15px;
  color: var(--text-primary);
}
.settings-panel .ai-section-summary::before { color: var(--pf-purple); }

/* ── Footer ───────────────────────────────────────────────────────────── */
.settings-panel .settings-footer {
  background: transparent;
  border-top: 1px solid #2a2433;
}

/* ============================================
   QR DRAWER (right-side slide-out)
   ============================================ */

.qr-toggle {
  position: fixed;
  top: var(--spacing-md);
  right: var(--spacing-md);
  height: 44px;
  min-width: 44px;
  padding: 0 var(--spacing-md);
  border-radius: 22px;
  background: var(--bg-secondary);
  border: 2px solid var(--accent);
  color: var(--accent);
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.3px;
  cursor: pointer;
  z-index: 1100;
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  transition: background 0.15s ease, transform 0.15s ease;
  font-family: inherit;
}
.qr-toggle:hover { background: var(--bg-hover); transform: translateY(-1px); }
.qr-toggle:active { transform: translateY(0) scale(0.97); }
.qr-toggle .qr-toggle-icon { font-size: 19px; line-height: 1; }

.qr-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 360px;
  max-width: 90vw;
  background: var(--bg-secondary);
  border-left: 2px solid var(--accent);
  box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
  z-index: 1050;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.qr-drawer.open { transform: translateX(0); }

.qr-drawer-content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--spacing-lg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-md);
}

.qr-info {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  margin-top: var(--spacing-sm);
}
.qr-info-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--bg-hover);
  border-radius: var(--border-radius);
  gap: var(--spacing-sm);
}
.qr-info-label {
  font-size: 13px;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  font-weight: 700;
}
.qr-info-row code {
  font-family: monospace;
  font-size: 14px;
  color: var(--accent);
  word-break: break-all;
  text-align: right;
  font-weight: 700;
}

/* ============================================
   GENERIC OVERLAY BACKDROP
   Shared by settings-panel and qr-drawer.
   ============================================ */

.overlay-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.overlay-backdrop.open {
  opacity: 1;
  pointer-events: auto;
}

/* ============================================
   TOAST NOTIFICATIONS
   ============================================ */

.toast-container {
  position: fixed;
  top: var(--spacing-md);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  z-index: 1200;
  pointer-events: none;
  width: max-content;
  max-width: 90vw;
}

/* Bottom-left anchor — used on the TV/host page only (the class is added by
   twiz-fx.js, not the phone player UI). Tucks notifications into the corner
   along the settings panel's edge so they don't cover the center action or
   the top chips. Stacks upward (newest nearest the corner). The lift clears
   the settings footer's Exit/Disconnect buttons when the panel is open. */
.toast-container.toast-anchor-bl {
  top: auto;
  left: var(--spacing-lg);
  bottom: var(--spacing-lg);
  transform: none;
  flex-direction: column-reverse;
  align-items: flex-start;
  max-width: min(420px, 46vw);
  transition: bottom 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
/* When the settings panel is open, lift the toasts above its Exit/Disconnect
   footer so they never cover those buttons. Drops back to the corner when the
   panel closes (which is when most joins happen anyway). */
body:has(.settings-panel.open) .toast-container.toast-anchor-bl {
  bottom: 76px;
}

.toast {
  background: var(--bg-secondary);
  color: var(--text-primary);
  padding: var(--spacing-md) var(--spacing-lg);
  border-radius: var(--border-radius);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.2px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  border: 1px solid var(--bg-hover);
  opacity: 0;
  transform: translateY(-12px);
  transition: opacity 0.25s ease, transform 0.25s ease;
  pointer-events: auto;
  text-align: center;
}
.toast.show {
  opacity: 1;
  transform: translateY(0);
}
.toast-success { background: var(--success); color: #000; border-color: transparent; }
.toast-error   { background: var(--danger);  color: #fff; border-color: transparent; }
.toast-info    { background: var(--primary); color: #000; border-color: transparent; }

/* ─── Operator notice cards (TV shell, six per-message anchors) ─────── */
/* Broadcast messages v3: each notice/flash picks one of six screen anchors
   (top/bottom × left/center/right). Shared box model lives here; each
   anchor only differs in which edges it pins to and how its stack grows. */
.twiz-notice-anchor-tl,
.twiz-notice-anchor-tc,
.twiz-notice-anchor-tr,
.twiz-notice-anchor-bl,
.twiz-notice-anchor-bc,
.twiz-notice-anchor-br {
  position: fixed;
  z-index: 1200;
  display: flex;
  gap: var(--spacing-sm);
  max-width: min(380px, 30vw);
  pointer-events: none;
  transition: bottom 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.twiz-notice-anchor-tl { top: var(--spacing-xl); left: var(--spacing-xl); flex-direction: column; align-items: flex-start; }
.twiz-notice-anchor-tc { top: var(--spacing-xl); left: 50%; transform: translateX(-50%); flex-direction: column; align-items: center; }
.twiz-notice-anchor-tr { top: var(--spacing-xl); right: var(--spacing-xl); flex-direction: column; align-items: flex-end; }
.twiz-notice-anchor-bl { bottom: var(--spacing-xl); left: var(--spacing-xl); flex-direction: column-reverse; align-items: flex-start; }
.twiz-notice-anchor-bc { bottom: var(--spacing-xl); left: 50%; transform: translateX(-50%); flex-direction: column-reverse; align-items: center; }
.twiz-notice-anchor-br { bottom: var(--spacing-xl); right: var(--spacing-xl); flex-direction: column-reverse; align-items: flex-end; }
body:has(.settings-panel.open) .twiz-notice-anchor-bl,
body:has(.settings-panel.open) .twiz-notice-anchor-bc,
body:has(.settings-panel.open) .twiz-notice-anchor-br { bottom: 76px; }
.twiz-notice-card {
  pointer-events: auto;
  background: var(--bg-secondary);
  color: var(--text-primary);
  border: 1px solid rgba(255, 171, 0, 0.55);
  border-radius: 12px;
  padding: 12px 14px;
  width: 100%;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
  font-size: 15px;
}
.twiz-notice-card.promo { border-color: rgba(255, 215, 0, 0.55); }
.twiz-notice-card img { max-width: 100%; border-radius: 8px; display: block; margin-bottom: 8px; }
.twiz-notice-title { font-weight: 800; margin-bottom: 2px; }
.twiz-notice-text { opacity: 0.92; }
.twiz-notice-link { font-size: 12px; opacity: 0.65; margin-top: 6px; word-break: break-all; }
.twiz-notice-x {
  float: right;
  background: none;
  border: none;
  color: inherit;
  opacity: 0.6;
  cursor: pointer;
  font-size: 14px;
  padding: 0 0 4px 8px;
}
.twiz-notice-x:hover { opacity: 1; }

/* ============================================
   AI ACTIVITY DRAWER (right-side slide-out)
   ============================================ */

/* Toggle button — sits to the LEFT of the QR toggle so it's right next to it
   in the top-right corner. Uses muted styling so it doesn't compete visually
   with the gold QR "Join" CTA (which is the primary action for players). */
.activity-toggle {
  position: fixed;
  top: var(--spacing-md);
  right: calc(var(--spacing-md) + 110px); /* leave room for the QR toggle */
  height: 44px;
  min-width: 44px;
  padding: 0 var(--spacing-md);
  border-radius: 22px;
  background: var(--bg-secondary);
  border: 2px solid var(--text-secondary);
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.3px;
  cursor: pointer;
  z-index: 1100;
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  transition: background 0.15s ease, transform 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  font-family: inherit;
}
.activity-toggle:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--text-primary); transform: translateY(-1px); }
.activity-toggle:active { transform: translateY(0) scale(0.97); }
.activity-toggle .activity-toggle-icon { font-size: 18px; line-height: 1; }

/* Free-floating, draggable, resizable window. Default geometry is set
   here in CSS; JS overrides via inline style once a saved position
   exists. `display:none` keeps it fully out of layout when hidden;
   .open flips it to flex so the header/list/footer stack works. */
.activity-panel {
  position: fixed;
  top: 80px;
  left: calc(100vw - 540px);   /* default: snug against the right edge */
  width: 520px;
  height: 600px;
  min-width: 320px;
  min-height: 200px;
  background: var(--bg-secondary);
  border: 2px solid var(--accent);
  border-radius: var(--border-radius);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
  z-index: 1050;
  display: none;
  flex-direction: column;
  resize: both;        /* native bottom-right corner resize */
  overflow: hidden;    /* clip rounded corners over inner content */
}
.activity-panel.open { display: flex; }

/* Header doubles as the drag handle. JS toggles .dragging while a drag
   is in flight so we can disable text-select on the body during drag. */
.activity-panel .activity-drag-handle {
  cursor: move;
  user-select: none;
}
.activity-panel.dragging { box-shadow: 0 16px 48px rgba(0, 0, 0, 0.7); }
body.activity-dragging { user-select: none; cursor: move; }

.activity-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--spacing-md);
  display: flex;
  flex-direction: column;     /* oldest at top, newest at bottom — chat style */
  gap: var(--spacing-sm);
}
/* Anchor entries to the bottom while the list has free space, so a short
   log doesn't float at the top with a gap below. The pseudo grows to
   absorb the slack; when the list overflows it collapses to zero and
   normal scrolling kicks in (no scroll-jam quirk like with
   justify-content: flex-end). */
.activity-list::before {
  content: '';
  flex: 1 0 auto;
}
.activity-empty {
  text-align: center;
  color: var(--text-muted);
  padding: var(--spacing-xl) var(--spacing-md);
  font-size: 14px;
  line-height: 1.5;
}

/* Each activity entry: header row with badges, then optional prompt/response
   bodies. Color of the left border encodes the entry's type/outcome. */
.activity-entry {
  background: var(--bg-primary);
  border: 1px solid var(--bg-hover);
  border-left: 4px solid var(--text-muted);
  border-radius: var(--border-radius);
  padding: var(--spacing-sm) var(--spacing-md);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
  font-size: 13px;
  line-height: 1.4;
}
.activity-entry.kind-chat       { border-left-color: var(--accent); }
.activity-entry.kind-probe.pass { border-left-color: var(--success); }
.activity-entry.kind-probe.fail { border-left-color: var(--danger); }
.activity-entry.status-error    { border-left-color: var(--danger); }

/* Processing state — pulsing accent border + subtle shimmer in the
   background so a still-in-flight entry is impossible to miss in a long
   activity log. The bordered-left edge actually animates between accent
   and a muted shade, giving a clear "heartbeat" without being noisy. */
.activity-entry.status-pending {
  border-left-color: var(--accent);
  animation: activity-pending-border 1.4s ease-in-out infinite;
  background: linear-gradient(90deg, rgba(255,215,0,0.05), var(--bg-primary) 40%);
}
@keyframes activity-pending-border {
  0%, 100% { border-left-color: var(--accent);     box-shadow: inset 0 0 0 0 rgba(255,215,0,0); }
  50%      { border-left-color: var(--text-muted); box-shadow: inset 4px 0 12px -6px var(--accent); }
}

.activity-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}
.activity-head .activity-time {
  margin-left: auto;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
}

/* Pill-shaped badges used in the entry header. Reusable per-type. */
.activity-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  line-height: 1.4;
}
.activity-badge.kind-chat        { background: var(--accent);    color: #000; }
.activity-badge.kind-probe       { background: var(--bg-hover);  color: var(--text-primary); }
/* Pending pill — bright accent with an animated dot prefix so it reads
   "actively working" not just "informational". Padding makes room for
   the absolutely-positioned dot. */
.activity-badge.status-pending {
  background: var(--accent);
  color: #000;
  position: relative;
  padding-left: 20px;
  animation: activity-pending-pill 1.4s ease-in-out infinite;
}
.activity-badge.status-pending::before {
  content: '';
  position: absolute;
  left: 7px;
  top: 50%;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #000;
  transform: translateY(-50%);
  animation: activity-pending-dot 0.9s ease-in-out infinite;
}
@keyframes activity-pending-pill {
  0%, 100% { opacity: 1;   }
  50%      { opacity: 0.7; }
}
@keyframes activity-pending-dot {
  0%, 100% { opacity: 0.35; transform: translateY(-50%) scale(0.75); }
  50%      { opacity: 1;    transform: translateY(-50%) scale(1.15); }
}
.activity-badge.status-success,
.activity-badge.status-pass      { background: var(--success);   color: #000; }
.activity-badge.status-fail,
.activity-badge.status-error,
.activity-badge.status-aborted   { background: var(--danger);    color: #fff; }
.activity-badge.provider         { background: transparent;      color: var(--text-secondary); border: 1px solid var(--bg-hover); }
.activity-badge.model            { background: transparent;      color: var(--text-muted);     border: 1px solid var(--bg-hover); text-transform: none; font-weight: 600; letter-spacing: 0.2px; }

.activity-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-md);
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.activity-stats span { white-space: nowrap; }
/* Context-utilization color cues. Threshold pick is arbitrary but useful:
   green = plenty of headroom; yellow = getting tight; red = close to cap. */
.activity-stats .ctx-low  { color: var(--success); font-weight: 700; }
.activity-stats .ctx-mid  { color: var(--accent);  font-weight: 700; }
.activity-stats .ctx-high { color: var(--danger);  font-weight: 800; }

.activity-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 2px;
}
.activity-row {
  display: flex;
  gap: var(--spacing-xs);
  align-items: baseline;
}
.activity-row .activity-label {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-muted);
  min-width: 56px;
}
.activity-row .activity-text {
  flex: 1;
  font-size: 13px;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 4.5em;     /* ~3 lines; click row to expand */
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.activity-row.expanded .activity-text { max-height: none; }
.activity-row.error .activity-text { color: var(--danger); }

/* Inline disclosure row used by AI activity entries to hide the bot's
   system prompt behind a click. Collapsed by default — the toggle line
   draws a triangle via ::before so JS doesn't have to touch the glyph. */
.activity-row-disclosure {
  display: block;
  margin-top: 4px;
}
.activity-disclosure-toggle {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
}
.activity-disclosure-toggle::before {
  content: '▸ ';
  display: inline-block;
  width: 1em;
}
.activity-row-disclosure.expanded .activity-disclosure-toggle::before {
  content: '▾ ';
}
.activity-disclosure-body {
  display: none;
  margin-top: 4px;
  padding: 8px 10px;
  background: var(--surface-2, rgba(0, 0, 0, 0.15));
  border-radius: 4px;
  max-width: 100%;
  overflow: hidden;
}
.activity-row-disclosure.expanded .activity-disclosure-body {
  display: block;
}
.activity-disclosure-body pre {
  margin: 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ============================================
   TV LOAD BAR (twiz-tv-loadbar.js) — per-game asset warm-up indicator
   ============================================ */

/* Slim, bottom-center, informational only (aria-hidden, click-through).
   Fades in late (0.4s delay) so a fast warm-up never even shows it. */
.tv-loadbar {
  position: absolute;
  left: 50%;
  bottom: 56px;
  transform: translateX(-50%);
  width: min(420px, 40%);
  z-index: 30;
  pointer-events: none;
  text-align: center;
  opacity: 0;
  animation: tv-loadbar-in 300ms ease 400ms forwards;
  transition: opacity 500ms ease;
}
@keyframes tv-loadbar-in { to { opacity: 1; } }
.tv-loadbar.done { opacity: 0 !important; }
.tv-loadbar-label {
  font-size: 0.95rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.75;
  margin-bottom: 6px;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.85);
}
.tv-loadbar-track {
  height: 8px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.tv-loadbar-fill {
  height: 100%;
  width: 0;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--accent, #ffd700), #5cd2ff);
  transition: width 250ms ease;
}

/* ============================================
   TV LOADING SCREEN (twiz-tv-loading.js) — full-screen game-boot title card
   ============================================ */

/* Theatrical loader: logo over a layered glow atmosphere, marquee progress
   bar, huge %, rotating flavor line, READY! stamp, curtain-lift exit.
   Animations are transform/opacity only (phone-budget). Themed per game
   via --ld-accent / --ld-accent2 / --ld-font set inline by the module. */
.tv-loading {
  position: absolute;
  inset: 0;
  z-index: 70;                    /* over gameplay (fx 50, stamps 60) */
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background:
    radial-gradient(1200px 700px at 50% 38%, color-mix(in srgb, var(--ld-accent) 16%, transparent), transparent 70%),
    radial-gradient(900px 600px at 18% 85%, color-mix(in srgb, var(--ld-accent2) 12%, transparent), transparent 70%),
    linear-gradient(165deg, #15131c 0%, #0c0b12 55%, #080710 100%);
  transition: transform 700ms cubic-bezier(.65, 0, .35, 1), opacity 700ms ease;
}
.tv-loading.exit { transform: translateY(-104%); opacity: .9; pointer-events: none; }

/* Drifting grain + vignette atmosphere (one cheap composited layer). */
.tv-loading-atmo {
  position: absolute;
  inset: -60px;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
  box-shadow: inset 0 0 320px 80px rgba(0, 0, 0, 0.78);
  animation: tv-loading-drift 9s linear infinite;
}
@keyframes tv-loading-drift {
  from { transform: translate3d(0, 0, 0); }
  50%  { transform: translate3d(24px, -18px, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

.tv-loading-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 44px;
  width: min(1100px, 80%);
  animation: tv-loading-card-in 650ms cubic-bezier(.2, .9, .25, 1.15) both;
}
@keyframes tv-loading-card-in {
  from { transform: translateY(36px) scale(.94); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

.tv-loading-logo {
  max-width: 760px;
  max-height: 380px;
  filter: drop-shadow(0 18px 44px rgba(0, 0, 0, .65));
  animation: tv-loading-bob 4.2s ease-in-out infinite;
}
.tv-loading-title {
  font-family: var(--ld-font);
  font-size: 124px;
  line-height: 1;
  text-align: center;
  color: #fffdf5;
  text-shadow:
    0 4px 0 color-mix(in srgb, var(--ld-accent2) 75%, #000),
    0 14px 38px rgba(0, 0, 0, .7);
  animation: tv-loading-bob 4.2s ease-in-out infinite;
}
@keyframes tv-loading-bob {
  0%, 100% { transform: translateY(0) rotate(-.4deg); }
  50%      { transform: translateY(-12px) rotate(.4deg); }
}

/* ── Marquee bar ───────────────────────────────────────────────────── */
.tv-loading-marquee {
  display: flex;
  align-items: center;
  gap: 34px;
  width: 100%;
}
.tv-loading-track {
  position: relative;
  flex: 1;
  height: 58px;
  border-radius: 16px;
  background: rgba(255, 253, 245, .07);
  border: 3px solid color-mix(in srgb, var(--ld-accent) 55%, #2a2533);
  box-shadow:
    inset 0 3px 12px rgba(0, 0, 0, .65),
    0 0 0 6px rgba(0, 0, 0, .35),
    0 0 44px color-mix(in srgb, var(--ld-accent) 28%, transparent);
  overflow: hidden;
}
.tv-loading-fill {
  position: absolute;
  inset: 0;
  transform: translateX(-100%);   /* slides right as progress lands */
  transition: transform 280ms cubic-bezier(.3, .8, .4, 1);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--ld-accent) 92%, #fff) 0%,
    var(--ld-accent) 45%,
    color-mix(in srgb, var(--ld-accent) 70%, #000) 100%);
  box-shadow: 0 0 26px var(--ld-accent);
}
.tv-loading-stripes {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(-55deg,
    rgba(255, 255, 255, .22) 0 22px, transparent 22px 44px);
  animation: tv-loading-stripes 900ms linear infinite;
}
@keyframes tv-loading-stripes {
  to { transform: translateX(62px); }   /* one stripe period: seamless loop */
}
.tv-loading-ready {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ld-font);
  font-size: 38px;
  letter-spacing: .22em;
  color: #15131c;
  text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
  opacity: 0;
  transform: scale(1.7) rotate(-5deg);
}
.tv-loading.done .tv-loading-ready {
  animation: tv-loading-stamp 360ms cubic-bezier(.2, 1.4, .4, 1) forwards;
}
@keyframes tv-loading-stamp {
  from { opacity: 0; transform: scale(1.7) rotate(-5deg); }
  to   { opacity: 1; transform: scale(1) rotate(-1.5deg); }
}
.tv-loading.done .tv-loading-stripes { animation-duration: 240ms; }
.tv-loading.done .tv-loading-track {
  box-shadow:
    inset 0 3px 12px rgba(0, 0, 0, .4),
    0 0 0 6px rgba(0, 0, 0, .35),
    0 0 80px var(--ld-accent);
}

.tv-loading-pct {
  font-family: var(--ld-font);
  font-size: 96px;
  line-height: 1;
  min-width: 210px;
  text-align: right;
  color: var(--ld-accent);
  text-shadow: 0 6px 26px color-mix(in srgb, var(--ld-accent) 55%, transparent);
  font-variant-numeric: tabular-nums;
}
.tv-loading-pct span { font-size: .45em; opacity: .75; margin-left: 4px; }

/* ── Flavor line ───────────────────────────────────────────────────── */
.tv-loading-flavor {
  font-family: 'EB Garamond', serif;
  font-style: italic;
  font-size: 34px;
  letter-spacing: .04em;
  color: rgba(255, 253, 245, .82);
  text-shadow: 0 2px 10px rgba(0, 0, 0, .8);
  min-height: 1.4em;
}
.tv-loading-flavor.swap { animation: tv-loading-flavor-in 460ms ease both; }
@keyframes tv-loading-flavor-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .tv-loading-atmo, .tv-loading-logo, .tv-loading-title,
  .tv-loading-stripes { animation: none; }
}

/* ============================================
   PLAYER FX (twiz-fx.js) — connect / disconnect feedback
   ============================================ */

/* Particle-burst layer: fixed, full-viewport, click-through. Particles
   inside it are also pointer-events:none so they never block UI. */
.twiz-fx-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
}

.twiz-fx-particle {
  position: fixed;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  pointer-events: none;
  will-change: transform, opacity;
  background: var(--p-color, var(--accent));
  box-shadow: 0 0 8px var(--p-color, var(--accent));
  animation: twiz-fx-burst var(--p-dur, 700ms) cubic-bezier(.2, .6, .3, 1) forwards;
}

@keyframes twiz-fx-burst {
  0%   { transform: translate(0, 0) scale(0.4); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: translate(var(--tx, 0px), var(--ty, 0px)) scale(0.9); opacity: 0; }
}

/* Themed pieces (Bluff "the works" burst) — letters, sprites, paper scraps. */
.twiz-fx-piece {
  width: auto; height: auto;
  border-radius: 0; background: none; box-shadow: none;
  animation: twiz-fx-tumble var(--p-dur, 1100ms) cubic-bezier(.2, .6, .3, 1) forwards;
}
.twiz-fx-piece.is-glyph {
  font-family: Georgia, 'Times New Roman', serif;
  font-weight: 800; font-size: 28px; line-height: 1;
  text-shadow: 0 2px 4px rgba(0,0,0,0.45);
}
.twiz-fx-piece.is-sprite { width: 30px; height: 30px; object-fit: contain;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.4)); }
.twiz-fx-piece.is-scrap { width: 18px; height: 13px; border-radius: 2px;
  background: #f2e8cf; box-shadow: 0 1px 3px rgba(0,0,0,0.35); }
@keyframes twiz-fx-tumble {
  0%   { transform: translate(0,0) rotate(0deg) scale(.5); opacity: 0; }
  18%  { opacity: 1; }
  100% { transform: translate(var(--tx,0px), var(--ty,0px)) rotate(var(--p-rot,180deg)) scale(1); opacity: 0; }
}

/* Leading color dot inside player toasts so the room can identify whose
   event fired even if the name is small or far away. */
.toast-player-dot {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: var(--spacing-xs);
  vertical-align: -1px;
  border: 1px solid rgba(0, 0, 0, 0.25);
  box-shadow: 0 0 6px currentColor;
}

/* Player-row entrance/exit/reconnect animations — applied transiently
   by twiz-fx.js helpers (flashRow / flashRowThen). */
@keyframes player-row-join {
  0%   { opacity: 0; transform: translateX(-12px) scale(0.96); }
  60%  { opacity: 1; transform: translateX(0)    scale(1.04); }
  100% { opacity: 1; transform: translateX(0)    scale(1); }
}
@keyframes player-row-leave {
  to { opacity: 0; transform: translateX(8px) scale(0.95); }
}
@keyframes player-row-reconnect-glow {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50%      { box-shadow: 0 0 14px 2px var(--p-color, var(--success)); }
}

.player-row.fx-joining     { animation: player-row-join 0.45s cubic-bezier(.2, .6, .3, 1); }
.player-row.fx-leaving     { animation: player-row-leave 0.35s ease forwards; }
.player-row.fx-reconnected { animation: player-row-reconnect-glow 0.9s ease; }

/* Subtler grayscale on disconnected rows so they read as "muted" rather
   than just "faded". Stacks with the existing opacity rule above. */
.player-row.disconnected { filter: grayscale(0.6); }

/* Player-personalized toast: background + text color are set inline from
   JS based on the player's color (and luminance for contrast). This class
   only contributes the richer drop shadow + matching text-shadow. The
   .text-light / .text-dark sub-class is also set by JS so the text-shadow
   direction reads correctly against the chosen color. */
.toast.toast-player {
  background: var(--bg-secondary);   /* fallback if no inline color */
  color: var(--text-primary);
  border-color: transparent;
  box-shadow:
    0 8px 28px rgba(0, 0, 0, 0.55),
    0 2px 8px  rgba(0, 0, 0, 0.40),
    inset 0 0 0 1px rgba(0, 0, 0, 0.18);
}
/* Light text on a dark player color → use a dark drop shadow for depth. */
.toast.toast-player.text-light {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.40);
}
/* Dark text on a light player color → a faint light "outline" reads as a
   subtle pop without muddying the text. */
.toast.toast-player.text-dark {
  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.55), 0 2px 4px rgba(255, 255, 255, 0.30);
}
/* Slightly stronger dot border for legibility against the colored background. */
.toast.toast-player .toast-player-dot {
  border-color: rgba(0, 0, 0, 0.35);
}

/* ── Player lifecycle toast cards (join / leave / disconnect / reconnect) ──
   A compact card matching the Playfield settings theme instead of a flat
   color-flooded bar: a slot-number chip in the player's color, the name, and
   a variant tag. Pops in with a spring + a one-shot glow in the player's
   color, then settles. The player color arrives as --pc (set inline by
   twiz-fx.js); sentiment lives in the tag, so the same card serves all four
   events without taking over the screen. Generic (colorless / no-variant)
   toasts keep the plain .toast styling above. */
.toast.toast-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px 12px 12px;
  background: linear-gradient(180deg, #241d30, #18121f);
  color: #f1ecf8;
  border: 2px solid color-mix(in srgb, var(--pc, var(--accent)) 55%, #2a2433);
  border-radius: 16px;
  text-align: left;
  box-shadow:
    0 12px 30px rgba(0, 0, 0, 0.55),
    0 0 22px -6px var(--pc, var(--accent));
}
.toast.toast-card .toast-card-chip {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 34px;
  padding: 0 8px;
  border-radius: 11px;
  font-size: 17px;
  font-weight: 800;
  color: #fff;
  background: color-mix(in srgb, var(--pc, var(--accent)) 30%, transparent);
  border: 2px solid var(--pc, var(--accent));
  box-shadow: 0 0 14px -2px var(--pc, var(--accent));
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
.toast.toast-card .toast-card-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  line-height: 1.1;
}
.toast.toast-card .toast-card-name {
  font-size: 16px;
  font-weight: 800;
  color: #fff;
  max-width: 52vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.toast.toast-card .toast-card-tag {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.1);
  color: var(--text-secondary, #c9bfe0);
}
/* Sentiment per event — only the tag (and a desaturate for offline) changes. */
.toast.toast-card[data-variant="join"] .toast-card-tag {
  background: linear-gradient(180deg, #ffed4e, #ffd700);
  color: #2a1d00;
}
.toast.toast-card[data-variant="reconnect"] .toast-card-tag {
  background: #00e676;
  color: #06351d;
}
.toast.toast-card[data-variant="leave"] .toast-card-tag {
  background: rgba(255, 90, 90, 0.2);
  color: #ff9a9a;
}
.toast.toast-card[data-variant="disconnect"] { filter: saturate(0.5); }
.toast.toast-card[data-variant="disconnect"] .toast-card-tag {
  background: rgba(255, 255, 255, 0.08);
  color: #9a90a8;
}

/* Spring pop entrance — the keyframe owns the motion (base .toast transition
   is overridden); exit falls back to a plain opacity fade (base .toast is
   opacity:0 at rest). */
.toast.toast-card { transition: opacity 0.28s ease; opacity: 0; transform: none; }
.toast.toast-card.show { animation: toast-card-pop 0.46s cubic-bezier(.34, 1.56, .64, 1) both; }
/* Rises up into place — reads right for the bottom-left anchor (see
   .toast-anchor-bl). The overshoot gives it the "pop". */
@keyframes toast-card-pop {
  0%   { opacity: 0; transform: translateY(16px) scale(0.82); }
  55%  { opacity: 1; transform: translateY(-3px) scale(1.05); }
  100% { opacity: 1; transform: translateY(0)    scale(1); }
}
/* One-shot color halo as the card lands. */
.toast.toast-card::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 16px;
  pointer-events: none;
  opacity: 0;
}
.toast.toast-card.show::after { animation: toast-card-glow 0.7s ease-out; }
@keyframes toast-card-glow {
  0%   { opacity: 0.85; box-shadow: 0 0 0 0 var(--pc, var(--accent)); }
  100% { opacity: 0;    box-shadow: 0 0 0 12px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .toast.toast-card.show { animation: none; opacity: 1; }
  .toast.toast-card.show::after { animation: none; }
}

/* ============================================
   LOBBY REDESIGN (2026-05-24) — Brand chassis
   Used by /connect, /join, /player lobby, and as the framework wordmark
   on any "TwizGame is talking" surface. Per-game theming (Task 7) layers
   on top via --game-color CSS vars; this chassis is gold-only.
   ============================================ */

:root {
  /* Active game's brand. Default to TwizGame-gold; setActiveHeroGame()
     overwrites these when a game is the hero. */
  --game-color:      #FFD700;
  --game-color-soft: rgba(255, 215, 0, 0.22);
  --game-color-glow: rgba(255, 215, 0, 0.5);
}

.brand-card {
  position: relative;
  background: linear-gradient(180deg, rgba(20,20,20,.92) 0%, rgba(14,14,14,.96) 100%);
  border: 1px solid rgba(255, 215, 0, 0.22);
  border-radius: 18px;
  padding: 28px 26px 24px;
  box-shadow:
    0 30px 80px rgba(0,0,0,.65),
    0 0 0 1px rgba(0,0,0,.4),
    inset 0 1px 0 rgba(255,255,255,.04);
  width: 100%;
  max-width: 440px;
  backdrop-filter: blur(12px);
}
.brand-card::before {
  /* Decorative top accent line — a thin gold marquee bar */
  content: "";
  position: absolute;
  top: -1px;
  left: 18%;
  right: 18%;
  height: 2px;
  background: linear-gradient(90deg, transparent, #FFD700, transparent);
  border-radius: 2px;
  opacity: 0.7;
}

.wordmark {
  text-align: center;
  font-family: 'Bagel Fat One', cursive;
  font-size: 56px;
  line-height: 0.95;
  letter-spacing: 1px;
  color: #FFD700;
  text-shadow:
    -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000,
    -2px 0 0 #000, 2px 0 0 #000, 0 -2px 0 #000, 0 2px 0 #000,
    0 6px 0 rgba(0,0,0,.5), 0 0 42px rgba(255,215,0,.22);
  margin-bottom: 6px;
}

.tagline {
  text-align: center;
  font: 800 11.5px/1 'Manrope', sans-serif;
  letter-spacing: 3.5px;
  text-transform: uppercase;
  color: #FFA500;
  margin-bottom: 18px;
  text-shadow: 0 1px 3px #000;
}

.subtitle-pill {
  display: block;
  margin: 0 auto 18px;
  padding: 10px 18px;
  background: rgba(0,0,0,.55);
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 12px;
  color: #dcdce0;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  text-align: center;
  backdrop-filter: blur(4px);
  max-width: 380px;
}

/* Form-field refresh — tracked-out gold uppercase labels, deeper input chrome */
.brand-card .field          { margin-bottom: 12px; }
.brand-card .field label    { display: block; font: 800 11px/1 'Manrope', sans-serif; letter-spacing: 1.8px; text-transform: uppercase; color: #FFD700; margin-bottom: 6px; text-shadow: 0 1px 2px #000; }
.brand-card .input          { width: 100%; padding: 13px 14px; background: #0d0d0d; border: 1.5px solid #2a2a2a; border-radius: 10px; color: #efeff1; font: 600 15px/1.2 'Manrope', sans-serif; outline: none; transition: border-color .15s ease, box-shadow .15s ease; }
.brand-card .input:focus    { border-color: #FFD700; box-shadow: 0 0 0 3px rgba(255,215,0,.18); }
.brand-card .input::placeholder { color: #666; font-weight: 500; }
.brand-card .field-hint     { margin-top: 5px; font-size: 11.5px; color: #7a7a7a; line-height: 1.4; }
.brand-card .field-hint b   { color: #adadb8; font-weight: 700; }

/* Framework CTA — gold ▶ pill in Bagel Fat One. Different from .hero-cta
   (which uses per-game font + per-game color). */
.cta {
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  height: 56px;
  margin-top: 6px;
  border-radius: 28px;
  background: #FFD700;
  color: #0a0a0a;
  border: none;
  font-family: 'Bagel Fat One', cursive;
  font-size: 22px;
  letter-spacing: 1.4px;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(255,215,0,.32), 0 2px 6px rgba(0,0,0,.6);
  transition: transform .15s ease, box-shadow .15s ease;
}
.cta:hover:not(:disabled)  { transform: translateY(-2px); box-shadow: 0 10px 28px rgba(255,215,0,.5), 0 4px 10px rgba(0,0,0,.7); }
.cta:active { transform: translateY(0) scale(.98); }
.cta:disabled { opacity: .5; cursor: not-allowed; transform: none; box-shadow: 0 2px 6px rgba(0,0,0,.6); }

/* Blurred backdrop layer (used on /connect + /join over the bluff lobby art) */
.brand-backdrop {
  position: absolute;
  inset: 0;
  background-image: url('/tv-assets/bluff-dictionary/images/lobby_bg.webp');
  background-size: cover;
  background-position: center;
  filter: blur(14px) brightness(.45) saturate(1.1);
  transform: scale(1.1);  /* hide the blurred edges */
  z-index: 0;
}
.brand-backdrop::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at center, rgba(255,165,0,.12) 0%, transparent 55%),
    linear-gradient(180deg, rgba(0,0,0,.5), rgba(0,0,0,.85));
}

/* ============================================
   PLAYER JOIN SCREEN — Noodle Dog redesign (2026-06-14)
   Scoped under .join-stage / .join-bg so it overrides the generic brand-card /
   field / input / cta chrome on /join ONLY. Art served at /host-assets/* (bun
   host route at runtime + copied into the Firebase build by build-firebase.ts).
   ============================================ */
.join-bg {
  position: fixed; inset: 0; z-index: 0; background-color: #1b140e;
  background-image: url('/host-assets/connect_scene_sm.webp');
  background-size: cover; background-position: center;
}
.join-bg::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(120% 80% at 50% 30%, rgba(0,0,0,0) 38%, rgba(0,0,0,.5) 100%),
    linear-gradient(180deg, rgba(0,0,0,.16), rgba(0,0,0,.42));
}

.join-anchor {
  width: 100%; max-width: 374px;
  display: flex; flex-direction: column; align-items: center;
}
.join-logo {
  width: 75%; max-width: 268px; height: auto; display: block;
  margin: 16px 0 -99px; position: relative; top: -24px; z-index: 3;
  pointer-events: none; filter: drop-shadow(0 10px 18px rgba(0,0,0,.55));
}

/* The form panel — uploaded portrait with the baked "JOIN A ROOM" tag */
.join-stage .brand-card {
  width: 100%; max-width: none; aspect-ratio: 895 / 1618;
  background: url('/host-assets/join_panel.webp') center / 100% 100% no-repeat;
  border: 0; border-radius: 0; backdrop-filter: none; box-shadow: none;
  filter: drop-shadow(0 16px 26px rgba(0,0,0,.55));
  padding: 33% 12% 6%;
  display: flex; flex-direction: column; align-items: center;
}
.join-stage .brand-card::before { content: none; }   /* kill the gold marquee bar */
.join-form { width: 100%; display: flex; flex-direction: column; }

/* Fields — cream baked-art inputs, big poppy Baloo 2 text */
.join-stage .brand-card .field { width: 100%; margin-bottom: 2.6%; }
.join-stage .brand-card .field label {
  display: block; font: 800 18px/1 'Baloo 2', sans-serif; letter-spacing: .3px;
  text-transform: uppercase; color: #2a1d0e; margin: 0 0 5px 5px;
  text-shadow: 0 1px 0 rgba(255,255,255,.45);
}
.join-stage .brand-card .field label span {
  font-weight: 700; font-size: 14px; opacity: .6; letter-spacing: .2px;
  text-transform: none; color: #6b5436;
}
.join-stage .brand-card .input {
  width: 100%; height: 54px; border: 0; border-radius: 0; outline: 0;
  background: url('/host-assets/input_field.webp') center / 100% 100% no-repeat;
  padding: 0 18px; font: 700 21px/1 'Baloo 2', sans-serif; color: #3b2a16;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.2)); transition: none;
}
.join-stage .brand-card .input:focus { border: 0; box-shadow: none; }
.join-stage .brand-card .input::placeholder { color: rgba(90,68,38,.5); font-weight: 600; }
.join-stage .brand-card .field-hint {
  font: 600 13px/1.35 'Baloo 2', sans-serif; color: rgba(59,42,22,.7); margin: 5px 0 0 5px;
}
.join-stage .brand-card .field-hint b { color: #b3402f; font-weight: 800; }

/* Stamp picker — live bead preview (left) + 3 labelled colour swatches
   (fill / ring / letter, right). Replaces the old single-swatch color-row. */
.join-stage .stamp-pick-row { display: flex; align-items: center; gap: 14px; width: 100%; }
.join-stage .stamp-preview {
  flex: 0 0 96px; width: 96px; height: 96px;
  display: flex; align-items: center; justify-content: center;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.25));
}
.join-stage .stamp-pickers { flex: 1; display: flex; flex-direction: column; gap: 9px; }
.join-stage .stamp-pick {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  background: rgba(59,42,22,.05); border: 1px solid rgba(59,42,22,.18);
  border-radius: 9px; padding: 5px 8px;
}
.join-stage .stamp-pick .lbl {
  flex: 1; font: 800 13px/1 'Baloo 2', sans-serif; color: #3b2a16;
  letter-spacing: .3px; text-transform: uppercase;
}
.join-stage .stamp-pick .hex {
  font: 700 12px/1 ui-monospace, 'Cascadia Mono', 'DejaVu Sans Mono', monospace;
  color: #7a6a52; letter-spacing: .5px; min-width: 66px; text-align: right;
  font-variant-numeric: tabular-nums;
}
.join-stage .stamp-pick input[type="color"] {
  width: 46px; height: 30px; border-radius: 7px; border: 2px solid rgba(59,42,22,.4);
  background: none; padding: 0; cursor: pointer; flex: none;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.2));
}
.join-stage .stamp-pick input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.join-stage .stamp-pick input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 5px; }

/* JOIN button — pink baked-art pill with the host's gentle breath */
.join-stage #join-btn.cta {
  align-self: center; width: 112%; max-width: 500px; height: 104px; margin-top: 7%;
  border: 0; border-radius: 0; background: url('/host-assets/btn_join.webp') center / 100% 100% no-repeat;
  color: transparent; font-size: 0; gap: 0; box-shadow: none; cursor: pointer;
  filter: drop-shadow(0 7px 12px rgba(0,0,0,.4));
  animation: ctaBreath 2.6s ease-in-out infinite;
}
.join-stage #join-btn.cta:hover:not(:disabled) { transform: none; box-shadow: none; }
.join-stage #join-btn.cta:active { animation: none; transform: scale(.97); }
.join-stage #join-btn.cta:disabled { opacity: .55; filter: drop-shadow(0 4px 8px rgba(0,0,0,.3)) grayscale(.3); }
@keyframes ctaBreath { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.03); } }

.join-stage .brand-card .notice { width: 100%; }

/* ============================================
   LOBBY REDESIGN (2026-05-24) — Hero + Strip
   Used by the TV lobby (/tv with no active game) ONLY. Per-game theming
   is applied at runtime by setActiveHeroGame() via the --game-color CSS
   vars defined in the brand-chassis section above.
   ============================================ */

.hero-stack {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: #0a0a0a;
}
.hero-layer {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity .5s ease;
}
.hero-layer.active { opacity: 1; }

/* Editable per-game hero scene (twiz-scene mounts here). Sits above the flat
   .hero-layer image, below the .hero-vignette, inside the scaled .tv-canvas so
   1920×1080-authored scene coords land correctly. Fades in like the image
   crossfade; empty + transparent when the active game has no hero scene. */
.hero-scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity .5s ease;
}
.hero-scene.active { opacity: 1; }

.hero-vignette {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(0,0,0,.15) 0%,
    rgba(0,0,0,0) 22%,
    rgba(0,0,0,0) 62%,
    rgba(0,0,0,.45) 88%,
    rgba(0,0,0,.72) 100%);
}

.hero-content {
  position: fixed;
  left: 0; right: 0;
  /* Anchor 200px above the BACKGROUND's bottom edge, not the raw viewport, so
     the title tracks the art on tall (non-16:9) screens. See
     --tv-letterbox-bottom. No-op at 16:9-or-wider. */
  bottom: calc(200px + var(--tv-letterbox-bottom));
  z-index: 10;
  text-align: center;
  padding: 0 32px;
  pointer-events: none;
}
.hero-content > * { pointer-events: auto; }
/* Branded hero (game supplies its own title art via heroBg): hide the centered
   title/desc/meta text so the artwork reads clean. The Start CTA stays. */
.hero-content.branded .hero-title,
.hero-content.branded .hero-desc,
.hero-content.branded .hero-meta { display: none; }

.hero-title {
  font-size: 104px;
  line-height: 1;
  letter-spacing: 1px;
  margin-bottom: 18px;
  animation: lobby-fade-up .4s ease;
  transition: color .35s ease;
  /* font-family, color, text-shadow set inline by setActiveHeroGame() */
}

/* A game with a `logo` in games.json shows that picture instead of the text title: big, it is the game's main mark.
   The text, the Start button and the game strip under it take about 520 px, so the logo gets the rest, up to 500 px. */
.hero-title .hero-logo {
  display: block;
  height: clamp(160px, calc(100vh - 530px), 500px);
  max-width: min(90vw, 900px);
  margin: 0 auto;
  object-fit: contain;
  filter: drop-shadow(0 8px 24px rgba(0,0,0,.6));
}

.hero-desc {
  font-size: 22px;
  font-weight: 600;
  color: #f4f4f5;
  line-height: 1.4;
  max-width: 760px;
  margin: 0 auto 18px;
  display: inline-block;
  padding: 14px 26px;
  background: rgba(0,0,0,.6);
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 14px;
  backdrop-filter: blur(6px);
  box-shadow: 0 8px 24px rgba(0,0,0,.45);
  animation: lobby-fade-up .4s ease .05s backwards;
}

.hero-meta {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  margin-bottom: 22px;
  text-shadow: 0 2px 6px rgba(0,0,0,.7);
  color: var(--game-color);
  animation: lobby-fade-up .4s ease .1s backwards;
  transition: color .35s ease;
}

.hero-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 60px;
  padding: 0 36px;
  border-radius: 30px;
  background: var(--game-color);
  color: #0a0a0a;
  border: none;
  font-size: 24px;
  letter-spacing: 1.5px;
  cursor: pointer;
  box-shadow: 0 6px 24px var(--game-color-soft), 0 2px 8px rgba(0,0,0,.6);
  transition: transform .15s ease, box-shadow .15s ease, background .35s ease;
  animation: lobby-fade-up .4s ease .15s backwards;
  font-weight: 900;
  /* font-family set inline by setActiveHeroGame() to the game's theme.font */
}
.hero-cta:hover  { transform: translateY(-2px); box-shadow: 0 10px 32px var(--game-color-glow), 0 4px 12px rgba(0,0,0,.7); }
.hero-cta:active { transform: translateY(0) scale(.98); }

@keyframes lobby-fade-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Bottom strip */
.strip-wrap {
  position: fixed;
  left: 0; right: 0;
  /* Sit at the BACKGROUND's bottom edge, not the raw viewport, so the card
     strip hugs the art instead of dropping into the letterbox bar on tall
     screens. See --tv-letterbox-bottom. No-op at 16:9-or-wider. */
  bottom: var(--tv-letterbox-bottom);
  z-index: 15;
  padding: 18px 24px 22px;
  background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,.85) 60%, rgba(0,0,0,.95) 100%);
  pointer-events: none;
}
.strip-label {
  font: 800 11px/1 'Manrope', sans-serif;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: #adadb8;
  margin-bottom: 12px;
  text-align: center;
  text-shadow: 0 1px 4px rgba(0,0,0,.7);
}
.strip-label .ready { color: var(--game-color); transition: color .35s ease; }

.strip {
  display: flex;
  gap: 14px;
  justify-content: center;
  align-items: center;
  overflow-x: auto;
  padding: 4px 8px 8px;
  scroll-snap-type: x mandatory;
  pointer-events: auto;
  scrollbar-width: none;
}
.strip::-webkit-scrollbar { display: none; }

.thumb {
  flex-shrink: 0;
  width: 148px;
  height: 84px;
  border-radius: 10px;
  border: 3px solid transparent;
  background: #1a1a1a;
  cursor: pointer;
  position: relative;
  overflow: visible;   /* allow the game name to float ABOVE the thumbnail */
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
  scroll-snap-align: center;
}
.thumb:hover { transform: translateY(-3px); border-color: rgba(255,255,255,.35); }
.thumb.active {
  transform: scale(1.08);
  border-color: var(--game-color);
  box-shadow: 0 0 0 4px var(--game-color-soft), 0 8px 20px rgba(0,0,0,.6);
}
.thumb-img    { position: absolute; inset: 0; background-size: cover; background-position: center; border-radius: 7px; overflow: hidden; }
/* Label sits centered directly OVER the tile. Only the "Coming Soon"
   placeholders use it now; real game tiles show just the art. */
.thumb-label  { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 0 4px; background: none; color: #fff; font: 800 9px/1.15 'Manrope', sans-serif; letter-spacing: .3px; text-shadow: 0 1px 3px #000, 0 0 2px #000; text-align: center; text-transform: uppercase; white-space: normal; overflow-wrap: break-word; }
/* Thumbnail SCENE tile: a 1920×1080 stage scaled to fit the tile (148px wide →
   ~0.077). twiz-shell.js sets --thumb-scale from the tile's real width and
   mounts thumbnail.scene.json into .thumb-stage. */
.thumb-scene  { background: #000; }
.thumb-stage  { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px;
                transform: scale(var(--thumb-scale, 0.077)); transform-origin: top left;
                pointer-events: none; }

.thumb.offline .thumb-img { filter: grayscale(1) brightness(.55); }
/* the "Offline" tag on a game switched off in the admin panel (relay/site.js) */
.thumb-tag    { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) rotate(-4deg); padding: 3px 8px; border-radius: 5px; background: #3a3a3a; color: #fff; border: 2px solid #000; box-shadow: 2px 2px 0 #000; font: 800 11px/1 'Manrope', sans-serif; letter-spacing: .5px; text-transform: uppercase; white-space: nowrap; }
.hero-cta:disabled { cursor: not-allowed; filter: grayscale(1); opacity: .75; box-shadow: none; transform: none; }
.thumb.coming {
  background: repeating-linear-gradient(45deg, #1a1a1a 0 8px, #222 8px 16px);
  border: 3px dashed #333;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: default;
}
.thumb.coming:hover { transform: none; border-color: #333; }
.thumb.coming::after {
  content: "?";
  font-family: 'Bagel Fat One', cursive;
  font-size: 32px;
  color: #444;
  text-shadow: 0 2px 0 #000;
}
.thumb.coming .thumb-label { background: transparent; color: #555; font-size: 9px; }

.strip-edge { flex-shrink: 0; width: 44px; display: flex; align-items: center; justify-content: center; color: #666; font-size: 22px; font-weight: 800; pointer-events: none; }

/* Floating-chrome refresh: existing .settings-gear / .qr-toggle / .activity-toggle
   already provide TwizGame-gold pills with backdrop-blur. No changes needed here —
   the existing styles in theme.css (around the SETTINGS GEAR section) read cleanly
   over the new hero art because they already use rgba bg + blur. Verified in
   hero-layout.html mockup. */


/* ════════════════════════════════════════════════════════════════════════
   📖 Question Studio — Bluffictionary pack-authoring tab (host settings panel)
   Refined "library workshop": gold-on-dark, Uncial Antiqua title accent,
   EB Garamond question text (bookish), Hanken Grotesk controls. All q-* card
   classes are emitted by twiz-shell-questions-tab.js; qs-* are static shell.
   ════════════════════════════════════════════════════════════════════════ */
.qstudio { --qs-gold: var(--accent); --qs-line: rgba(255,215,0,.14); }
.qstudio, .qstudio button, .qstudio input, .qstudio select { font-family: 'Hanken Grotesk', -apple-system, sans-serif; }

/* — Header — */
.qs-header { display:flex; align-items:center; justify-content:space-between; gap:var(--spacing-md);
  padding-bottom:var(--spacing-sm); border-bottom:1px solid var(--qs-line); }
.qs-title { font-family:'Uncial Antiqua', serif; font-weight:400; font-size:22px; letter-spacing:.5px;
  color:var(--qs-gold); margin:0; text-shadow:0 1px 0 #000, 0 0 18px rgba(255,215,0,.18); }
.qs-title-mark { filter:drop-shadow(0 2px 4px rgba(0,0,0,.6)); }
.qs-conn { display:inline-flex; align-items:center; gap:7px; font-size:12.5px; max-width:55%;
  padding:4px 11px; border-radius:999px; border:1px solid var(--bg-hover); background:var(--bg-secondary);
  color:var(--text-secondary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.qs-conn::before { content:''; width:8px; height:8px; border-radius:50%; flex:0 0 auto; background:var(--text-muted); }
.qs-conn[data-state="on"]  { color:var(--success); border-color:color-mix(in srgb,var(--success) 40%,transparent); }
.qs-conn[data-state="on"]::before  { background:var(--success); box-shadow:0 0 8px var(--success); }
.qs-conn[data-state="off"]::before { background:var(--text-muted); }

/* — Path bar — */
.qs-pathbar { display:flex; align-items:center; gap:var(--spacing-sm); margin-top:var(--spacing-sm);
  padding:6px 10px; background:#000; border:1px solid var(--bg-hover); border-radius:var(--border-radius); }
.qs-path-key { font-size:10px; letter-spacing:1.5px; color:var(--text-muted); flex:0 0 auto; }
.qs-path { flex:1; min-width:0; font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12px;
  color:var(--text-secondary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; direction:rtl; text-align:left; }

/* — Mode segmented control — */
.qs-modes { display:flex; gap:4px; margin-top:var(--spacing-md); padding:4px;
  background:var(--bg-primary); border:1px solid var(--bg-hover); border-radius:10px; }
.qs-mode { flex:1; padding:9px 12px; border:0; border-radius:7px; cursor:pointer; font-size:14px; font-weight:600;
  background:transparent; color:var(--text-secondary); transition:background .18s,color .18s; }
.qs-mode:hover { color:var(--text-primary); }
.qs-mode.active { background:linear-gradient(180deg,var(--qs-gold),#e6a800); color:#000;
  box-shadow:0 2px 8px rgba(255,215,0,.25); }

/* — Toolbar (controls) — */
.qs-pane { margin-top:var(--spacing-md); }
.qs-toolbar { display:flex; flex-wrap:wrap; align-items:flex-end; gap:var(--spacing-sm);
  padding:var(--spacing-md); background:var(--bg-secondary); border:1px solid var(--bg-hover); border-radius:10px; }
.qs-field { display:flex; flex-direction:column; gap:4px; }
.qs-field-sm { width:74px; } .qs-field-grow { flex:1; min-width:140px; }
.qs-field-label { font-size:10px; letter-spacing:1.2px; text-transform:uppercase; color:var(--text-muted); }
.qstudio select, .qstudio input[type="number"] { background:var(--bg-input); color:var(--text-primary);
  border:1px solid var(--bg-hover); border-radius:var(--border-radius); padding:7px 9px; font-size:14px; min-width:120px; }
.qstudio .qs-field-sm input { min-width:0; width:100%; }
.qstudio select:focus, .qstudio input:focus, .qstudio textarea:focus {
  outline:none; border-color:var(--qs-gold); box-shadow:0 0 0 2px rgba(255,215,0,.18); }

/* — Buttons — */
.qs-btn { border:1px solid var(--bg-hover); border-radius:var(--border-radius); padding:8px 16px; cursor:pointer;
  font-size:14px; font-weight:600; background:var(--secondary); color:var(--text-primary); transition:all .15s; white-space:nowrap; }
.qs-btn:hover:not(:disabled) { background:var(--bg-hover); transform:translateY(-1px); }
.qs-btn:active:not(:disabled) { transform:translateY(0); }
.qs-btn:disabled { opacity:.45; cursor:not-allowed; }
.qs-btn.primary { background:linear-gradient(180deg,var(--qs-gold),#e6a800); color:#000; border-color:#e6a800;
  box-shadow:0 2px 10px rgba(255,215,0,.22); }
.qs-btn.primary:hover:not(:disabled) { background:linear-gradient(180deg,#ffe24e,var(--qs-gold)); }
.qs-btn.ghost { background:transparent; border-color:color-mix(in srgb,var(--qs-gold) 35%,transparent); color:var(--qs-gold); }
.qs-btn.ghost:hover:not(:disabled) { background:rgba(255,215,0,.10); }
.qs-btn.danger { background:transparent; border-color:color-mix(in srgb,var(--danger) 55%,transparent); color:var(--danger); }
.qs-btn.danger:hover:not(:disabled) { background:rgba(255,68,68,.12); }
.qs-btn.subtle { background:transparent; border-color:transparent; color:var(--text-muted); }
.qs-btn.subtle:hover:not(:disabled) { color:var(--danger); background:rgba(255,68,68,.08); }
.qs-btn.sm { padding:5px 11px; font-size:12.5px; } .qs-btn.xs { padding:4px 9px; font-size:12px; font-weight:500; }

/* — Status line (hidden when empty) — */
.qs-status { margin:var(--spacing-md) 2px 0; font-size:13px; color:var(--text-secondary); display:flex; align-items:center; gap:8px; }
.qs-status:empty { display:none; }
.qs-status::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--qs-gold);
  box-shadow:0 0 8px var(--qs-gold); animation:qs-pulse 1s ease-in-out infinite; }
@keyframes qs-pulse { 0%,100%{opacity:.35;transform:scale(.8)} 50%{opacity:1;transform:scale(1.15)} }

/* — Empty state — */
.qs-empty { margin:var(--spacing-md) 0; padding:26px 20px; text-align:center; font-size:13.5px; line-height:1.6;
  color:var(--text-muted); border:1px dashed var(--bg-hover); border-radius:10px; }
.qs-empty b { color:var(--qs-gold); }
.qs-cards:not(:empty) + .qs-empty { display:none; }

/* — Cards — */
.qs-cards { display:flex; flex-direction:column; gap:var(--spacing-md); margin-top:var(--spacing-md); }
.q-card { position:relative; background:var(--bg-secondary); border:1px solid var(--bg-hover); border-left:3px solid var(--qs-gold);
  border-radius:10px; padding:var(--spacing-md) var(--spacing-md) var(--spacing-md) calc(var(--spacing-md) + 2px);
  display:flex; flex-direction:column; gap:9px; animation:qs-rise .3s cubic-bezier(.2,.8,.2,1) both; }
.q-card:hover { border-color:color-mix(in srgb,var(--qs-gold) 30%,var(--bg-hover)); }
.q-card.saved { border-left-color:var(--success); opacity:.92; }
@keyframes qs-rise { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
.q-label { font-size:10px; letter-spacing:1.4px; text-transform:uppercase; color:var(--text-muted); }
.q-prompt { width:100%; box-sizing:border-box; min-height:2.6em; resize:vertical; overflow:hidden;
  font-family:'EB Garamond', Georgia, serif; font-size:17px; line-height:1.4; color:var(--text-primary);
  background:var(--bg-input); border:1px solid var(--bg-hover); border-radius:var(--border-radius); padding:9px 11px; }
.q-answer-row { display:flex; align-items:center; gap:8px; }
.q-fakes { display:flex; flex-direction:column; gap:6px; }
.q-fakerow { display:flex; align-items:center; gap:8px; }
.q-mark { flex:0 0 auto; width:18px; text-align:center; font-weight:700; font-size:13px; }
.q-mark-check { color:var(--success); } .q-mark-x { color:var(--text-muted); }
.q-answer, .q-fake { flex:1; box-sizing:border-box; background:var(--bg-input); color:var(--text-primary);
  border:1px solid var(--bg-hover); border-radius:var(--border-radius); padding:8px 10px; font-size:14px; }
.q-answer { font-weight:600; border-color:color-mix(in srgb,var(--success) 45%,var(--bg-hover)); }
.q-fake.secretly-true { border-color:var(--danger); background:color-mix(in srgb,var(--danger) 14%,var(--bg-input)); }
.q-fakerow:has(.secretly-true) .q-mark-x { color:var(--danger); }
.q-badge { flex:0 0 auto; font-size:11.5px; font-weight:600; white-space:nowrap; padding:3px 10px; border-radius:999px;
  background:var(--bg-hover); color:var(--text-muted); }
.q-badge.supported   { background:color-mix(in srgb,var(--success) 22%,transparent); color:var(--success); }
.q-badge.contradicted{ background:color-mix(in srgb,var(--danger) 22%,transparent); color:var(--danger); }
.q-badge.unclear     { background:color-mix(in srgb,var(--warning) 22%,transparent); color:var(--warning); }
.q-src { font-size:12px; line-height:1.5; color:var(--text-muted); padding:6px 9px; border-radius:6px;
  background:var(--bg-primary); border:1px solid var(--bg-hover); }
.q-src a { color:var(--qs-gold); text-decoration:none; } .q-src a:hover { text-decoration:underline; }
.q-card-actions { display:flex; gap:var(--spacing-sm); justify-content:flex-end; flex-wrap:wrap; margin-top:2px; }

/* — Action bar (footer of the generate pane) — */
.qs-actionbar { display:flex; align-items:center; gap:var(--spacing-sm); flex-wrap:wrap;
  margin-top:var(--spacing-md); padding:var(--spacing-md);
  background:var(--bg-secondary); border:1px solid var(--bg-hover); border-radius:10px; }
.qs-spacer { flex:1; } .qs-kept { font-size:12.5px; color:var(--text-muted); }

/* — Toggle switch (deep fake-check) — */
.qs-switch { display:inline-flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
.qs-switch input { position:absolute; opacity:0; width:0; height:0; }
.qs-switch-track { position:relative; width:36px; height:20px; border-radius:999px; background:var(--bg-hover); transition:background .2s; }
.qs-switch-track::after { content:''; position:absolute; top:3px; left:3px; width:14px; height:14px; border-radius:50%;
  background:var(--text-muted); transition:transform .2s,background .2s; }
.qs-switch input:checked + .qs-switch-track { background:color-mix(in srgb,var(--qs-gold) 55%,transparent); }
.qs-switch input:checked + .qs-switch-track::after { transform:translateX(16px); background:var(--qs-gold); }
.qs-switch input:focus-visible + .qs-switch-track { box-shadow:0 0 0 2px rgba(255,215,0,.35); }
.qs-switch-text { font-size:12.5px; color:var(--text-secondary); }

/* ════════════════════════════════════════════════════════════════════════
   📣 On-stream join overlay — persistent Room/Password card + top-bar toggle
   ════════════════════════════════════════════════════════════════════════ */
/* Top-bar toggle. The right-side pill cluster is a set of `position:fixed`
   buttons stacked via explicit `right` offsets (not a flex row):
     .qr-toggle       → right: var(--spacing-md)                   (rightmost)
     .activity-toggle → right: calc(var(--spacing-md) + 110px)     (next in)
   This is the third pill, one more 110px slot further left so it doesn't
   overlap either. Matches their box/typography/z-index band exactly. */
.join-overlay-toggle {
  position: fixed;
  top: var(--spacing-md);
  right: calc(var(--spacing-md) + 220px); /* leave room for QR + activity toggles */
  height: 44px;
  min-width: 44px;
  padding: 0 var(--spacing-md);
  border-radius: 22px;
  background: var(--bg-secondary);
  border: 2px solid var(--text-secondary);
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.3px;
  cursor: pointer;
  z-index: 1100;
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  transition: background 0.15s ease, transform 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  font-family: inherit;
}
.join-overlay-toggle:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--text-primary); transform: translateY(-1px); }
.join-overlay-toggle:active { transform: translateY(0) scale(0.97); }
.join-overlay-toggle .join-overlay-toggle-icon { font-size: 18px; line-height: 1; }
.join-overlay-toggle.active {
  outline: 2px solid var(--twiz-gold, #ffcc33);
  outline-offset: 1px;
}

/* Floating card. `.game-layer` (the iframe layer) sits at z-index: 0;
   `.overlay-backdrop`/`.settings-panel`/`.qr-drawer`/`.activity-panel` sit at
   1000–1050 so host drawers always win over the game. This card must clear
   the game layer but stay under every drawer, so it sits at 500 — squarely
   in the open 0↔1000 gap with headroom on both sides. */
.join-overlay {
  position: fixed;
  left: 24px;
  top: 24px;
  width: 340px;                /* JS overrides from the stored viewport fraction */
  z-index: 500;                /* above .game-layer (0), below drawers/backdrop (1000-1050) */
  display: flex;
  flex-direction: column;
  gap: 1cqw;
  /* Tight: the black panel hugs the banner — the info rows carry their own
     side inset so text doesn't touch the edge. */
  padding: 1cqw;
  background: rgba(8, 8, 10, 0.82);
  border: 2px solid rgba(255, 255, 255, 0.14);
  border-radius: 16px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  /* Container query root: every inner font/spacing is in cqw so the WHOLE
     card (text included) scales proportionally with the S/M/L width. */
  container-type: inline-size;
}
.join-overlay.dragging { cursor: grabbing; }
.join-overlay[hidden] { display: none; }

.join-overlay-banner {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  pointer-events: none;        /* clicks fall through to the card for dragging */
}
.join-overlay-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 1.6cqw 0.8cqw;
}
.join-overlay-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
}
.join-overlay-row[hidden] { display: none; }
/* Fonts are in cqw (1% of card width) so text scales 1:1 with the S/M/L
   width. 8.4cqw = 1.5× the classic look (~28px at M) — readable on stream. */
.join-overlay-label {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.75;
  color: #fff;
  font-size: 8.4cqw;
}
.join-overlay-value {
  font-weight: 800;
  color: var(--twiz-gold, #ffcc33);
  -webkit-text-stroke: 1px rgba(0, 0, 0, 0.65);
  word-break: break-all;
  text-align: right;
  margin-left: auto;           /* hug the right so value + pass-toggle group together */
  font-size: 8.4cqw;
}
/* Room row gets a phantom eye-button so its value right-aligns with the
   password value below it (server hides it when there's no password row). */
.join-overlay-value-spacer {
  flex: 0 0 auto;
  width: 26px;
}
.join-overlay-value-spacer[hidden] { display: none; }
/* Masked password ("—hidden—") reads as a placeholder, not a real value. */
.join-overlay-value.is-masked {
  color: rgba(255, 255, 255, 0.55);
  -webkit-text-stroke: 0;
  font-style: italic;
  font-weight: 700;
}
/* Small eye button in the Password row: toggles the global on-stream mask. */
.join-overlay-pass-toggle {
  flex: 0 0 auto;
  align-self: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.06);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.join-overlay-pass-toggle.active {
  background: var(--twiz-gold, #ffcc33);
}
/* Control strip: S/M/L size presets + reset-to-default. Floats over the
   card's top-right and only appears on hover, so it adds NO height and the
   on-stream footprint stays a clean banner+info rectangle. */
.join-overlay-ctl {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}
.join-overlay:hover .join-overlay-ctl {
  opacity: 1;
  pointer-events: auto;
}
.join-overlay-ctl button {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
  font-weight: 700;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.join-overlay-ctl button:hover {
  background: rgba(255, 255, 255, 0.16);
}
.join-overlay-size.active {
  background: var(--twiz-gold, #ffcc33);   /* classic gold active preset */
  border-color: var(--twiz-gold, #ffcc33);
  color: #111;
}

/* On-stream storefront strip (spec 2026-07-25 §6.1). Horizontal trading
   cards, sized so a viewer on a phone can read them. Same stacking rationale
   as .join-overlay: clear the game layer (0), stay under every drawer
   (1000-1050) — sits at the same 500. Everything inside is em-based off a
   font-size the JS sets from the card width, so one assignment scales it all. */
.tv-shop-overlay {
  position: fixed;
  z-index: 500;
  background: linear-gradient(180deg, #1d1330, #0c0716);
  border: 0.04em solid rgba(255, 255, 255, 0.16);
  border-radius: 0.4em;
  padding: 0.34em;
  color: #fff;
  font-family: inherit;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  box-shadow: 0 0.3em 0.9em rgba(0, 0, 0, 0.55);
  /* Slide-in (spec §6.3). The card keeps its translate while out; show()
     animates it to 0,0. `hidden` is only applied AFTER the out-transition,
     because an element cannot transition out of display:none. Only transform
     and opacity are listed — `all` would also animate the left/top that
     applyPlacement() and the drag handler write, making the card swim after
     the cursor. Keep the 0.42s in step with SLIDE_MS in tv-shop-overlay.js. */
  transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.42s ease;
  will-change: transform, opacity;
}
/* pointer-events:none matters as much as the fade: a fully transparent card
   still swallows clicks over the whole strip, and while it is on its way out
   it must not eat a grab meant for whatever is underneath. It also means a
   mousedown can never land mid-out-slide, so a drag only ever has to cancel a
   slide-IN. */
.tv-shop-overlay.tv-shop-out { opacity: 0; pointer-events: none; }
/* Held for the length of one synchronous style write so the browser ADOPTS the
   result instead of animating toward it — see commitNoAnim() in
   tv-shop-overlay.js. Beats the rule above on specificity, no !important. */
.tv-shop-overlay.tv-shop-noanim { transition: none; }
.tv-shop-overlay[hidden] { display: none; }
.tv-shop-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.4em; padding: 0.1em 0.25em 0.3em;
}
.tv-shop-title { font-weight: 900; font-size: 1.2em; letter-spacing: 0.04em; color: var(--twiz-gold, #ffcc33); }
.tv-shop-title { font-family: 'Bricolage Grotesque', sans-serif; letter-spacing: 0.14em; text-transform: uppercase; }
.tv-shop-title span { color: #fff; }
.tv-shop-rule { flex: 1; height: 0.06em; background: linear-gradient(90deg, rgba(255,204,51,0.4), transparent); }
.tv-shop-hint { font-size: 0.7em; color: rgba(255, 255, 255, 0.55); }
/* auto-fit here, overridden below by a fixed --tsh-cols track count: with
   fewer than 3 slots filled (a small catalogue) the fixed grid left dead
   columns and a half-empty strip, so this stays the fallback and the
   explicit rule further down (which renderCards() actually drives) wins the
   cascade once it applies. */
/* Horizontal trading cards. Everything is em-based off the font-size the JS
   sets from the card width, so one assignment scales the whole strip.
   Rarity (host-resolved, see tierByKey) is the ONLY colour axis: it drives
   the ribbon, the frame stroke, the bloom behind the art and the command
   tint. Item KIND used to own three gradient washes and a pill badge; both
   are gone — kind is now a plain word at the end of the ribbon. */
.tv-shop-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); gap: 0.28em;
  opacity: 1; transition: opacity 0.24s ease; }
.tv-shop-cards.tsh-in { animation: tsh-fade 0.24s ease; }
@keyframes tsh-fade { from { opacity: 0 } to { opacity: 1 } }

.tsh-card {
  position: relative; display: flex; flex-direction: column;
  background: #150e26; border-radius: 0.22em; overflow: hidden;
  /* Notched top-left / bottom-right: reads as a collectible plate rather than
     a web card, which is the whole point of the redesign. */
  clip-path: polygon(0.7em 0, 100% 0, 100% calc(100% - 0.7em), calc(100% - 0.7em) 100%, 0 100%, 0 0.7em);
  box-shadow: inset 0 0 0 0.055em var(--tier);
}
.tsh-tier {
  display: flex; align-items: center; gap: 0.3em; padding: 0.16em 0.3em 0.16em 0.8em;
  background: linear-gradient(90deg, var(--tier), color-mix(in srgb, var(--tier) 35%, #150e26));
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 0.6em;
  letter-spacing: 0.16em; text-transform: uppercase; color: #12091f;
  /* One line, always. The bar carries tier + kind (+ fling cost for emoji),
     and a wrap turned it into a two-line block that read as broken. The tier
     label is the only part allowed to give up room, since it is also the
     most guessable from colour. */
  flex-wrap: nowrap; white-space: nowrap; overflow: hidden;
}
/* Tier label sits on the LEFT; kind follows it directly with no
   `margin-left:auto` — it reads as a qualifier right after "LEGENDARY". The
   price no longer rides this bar at all (see .tsh-price beside the command);
   only .tsh-fling still claims the right end, and only for emoji rows. */
.tsh-kind { opacity: 0.72; letter-spacing: 0.1em; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
/* The designated yielder. A bare text node is an anonymous flex item that will
   not shrink under `white-space: nowrap`, so the price — which must stay whole,
   it is the number people are deciding on — was the thing getting clipped
   instead. Wrapping the label makes the give explicit. */
.tsh-tier-label { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Name and price READ FIRST, above the rarity bar, and nothing overlaps the
   art (owner call, 2026-07-26 — the scrim that used to sit over the bead was
   fighting it). The art panel gives back the height the header now costs:
   0.76 -> 0.72, so the card lands slightly SHORTER than the scrim version
   while the bead is unobstructed.
   Extra left/top padding clears the card's top-left clip-path notch (0.7em),
   which would otherwise bite the first letter. */
.tsh-head { padding: 0.34em 0.42em 0.26em 0.58em; }

/* The art is the pitch: it was 2.2em tall inside a ~380px track (19% of the
   card's width) and is now the card's whole middle.
   Height in em, NOT an aspect-ratio: the bead inside is sized in em too, so an
   aspect-ratio panel left ~57px of dead space below it at M and grew the card
   for nothing. 7.2em hugs the 6.4em bead with an even margin, and the panel no
   longer changes height when the strip width or the card count changes. */
.tsh-art { position: relative; height: 7.2em; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.tsh-art::before { content: ''; position: absolute; inset: 0;
  background: radial-gradient(58% 52% at 50% 46%, color-mix(in srgb, var(--tier) 62%, transparent) 0%, transparent 68%); }
.tsh-glyph { position: relative; font-size: 5.2em; line-height: 1; font-family: 'Segoe UI Emoji', sans-serif; }
/* stampHtml() can only be handed a PIXEL size, which it inlines on the bead —
   left alone the stamp would be the one thing in the card that ignores the
   font-size knob. Overriding here needs !important to beat that inline style.
   The bead's font-size IS its diameter.
   6.4em of an 8.5em panel (~75%): big enough to be the pitch, and short of
   the panel height so the scrim clips the chin rather than half the face.
   Skins render as a bead too now, so this one number sizes every previewable
   item — it used to be 3.4em, which left finishes at ~41% of the panel while
   skin art had 88%. */
.tsh-art .twiz-bead { font-size: 6.4em !important; position: relative;
  filter: drop-shadow(0 0.04em 0.06em rgba(0, 0, 0, 0.55)); }

/* No text-shadow any more: the name sits on the card's flat background rather
   than over art, so the shadow was only muddying it. Still clamped to two
   lines — a long name wraps once and then ellipses instead of pushing every
   card in the row taller. */
.tsh-name {
  font-family: 'Baloo 2', sans-serif; font-weight: 800; font-size: 1.42em; line-height: 1.02;
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.tsh-coin { width: 0.8em; height: 0.8em; border-radius: 999px;
  background: radial-gradient(circle at 34% 30%, #ffe9a8, #e0a21c); box-shadow: 0 0 0 0.06em rgba(0, 0, 0, 0.33); }

/* The command is the call to action, styled as something you TYPE — mono,
   tier-tinted, with a caret. Never clamp it: a half-eaten command is worse
   than a two-line one. */
.tsh-cmd {
  margin-top: auto; display: flex; align-items: center; gap: 0.3em;
  padding: 0.3em 0.42em; background: #0d0818;
  box-shadow: inset 0 0.06em 0 var(--tier), inset 0 0 2.2em color-mix(in srgb, var(--tier) 16%, transparent);
  font-family: ui-monospace, monospace; font-weight: 800; font-size: 1.12em;
  line-height: 1.1; overflow-wrap: anywhere;
}
.tsh-caret { width: 0.09em; height: 1em; flex: none; background: var(--tier);
  animation: tsh-blink 1.1s steps(1, end) infinite; }
@keyframes tsh-blink { 50% { opacity: 0 } }
/* The price moved OUT of the rarity bar and down beside the command: the bar
   was carrying tier + kind + price on one nowrap line, and now that the
   command is only "!buy 1" the bottom row has room for the number people are
   actually deciding on. Tier-coloured on the dark command plate, not the dark
   ink it used when it rode the bar's light fill. */
.tsh-price { display: inline-flex; align-items: center; gap: 0.22em; margin-left: auto;
  flex: 0 0 auto; white-space: nowrap;
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800;
  font-size: 0.94em; letter-spacing: 0.02em; color: var(--tier); text-transform: none; }
.tsh-cmd-txt { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
/* Light fill again — the fling cost still rides the tier bar. */
.tsh-fling { margin-left: auto; color: rgba(18, 9, 31, 0.68); font-weight: 700; }

/* Fixed track count: auto-fit collapsed a 4-card strip into uneven columns
   once the pinned card joined. --tsh-cols is set by renderCards(). */
.tv-shop-cards { grid-template-columns: repeat(var(--tsh-cols, 3), minmax(0, 1fr)); }

/* The pinned promo is deliberately NOT part of the trio: extra gap, a green
   accent no rarity uses, and a FEATURED ribbon instead of a tier label. It
   does not rotate, so it must not read as one of the three that do. */
/* No margin-left: the grid tracks are equal 1fr, so a margin would make the
   pinned card visibly NARROWER than the other three — the opposite of "each
   item as big as they used to be" (owner, 2026-07-26). The separation is
   carried by the green accent, the heavier ring and the 📌 badge instead,
   none of which cost width. */
.tsh-pinned { box-shadow: inset 0 0 0 0.085em var(--tier); }
.tsh-pinned .tsh-tier { letter-spacing: 0.2em; }
.tsh-pinned::before { content: '📌'; position: absolute; top: 0.3em; right: 0.35em;
  font-size: 0.8em; line-height: 1; z-index: 2; }

/* Legendary only. Three simultaneous sweeps would be noise; one says "this is
   the expensive one" without a word. */
.tsh-legendary::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 38%, rgba(255,255,255,0.18) 46%,
    rgba(255,246,217,0.3) 50%, rgba(255,255,255,0.18) 54%, transparent 62%);
  background-size: 260% 100%; animation: tsh-foil 4.6s ease-in-out infinite; }
@keyframes tsh-foil { 0%, 62% { background-position: 150% 0 } 100% { background-position: -60% 0 } }

@media (prefers-reduced-motion: reduce) {
  .tsh-caret, .tsh-legendary::after, .tv-shop-cards.tsh-in { animation: none; }
  .tsh-legendary::after { opacity: 0; }
}
.tv-shop-ctl { display: flex; gap: 0.1em; justify-content: flex-end; margin-top: 0.24em; }
.tv-shop-ctl button {
  background: rgba(255, 255, 255, 0.1); color: #fff; border: 0;
  border-radius: 0.2em; padding: 0.06em 0.3em; font-size: 0.4em;
  font-family: inherit; cursor: pointer;
}
.tv-shop-ctl button:hover { background: rgba(255, 255, 255, 0.2); }
.tv-shop-size.active { background: var(--twiz-gold, #ffcc33); color: #111; }

/* Emoji fling layer (spec 2026-07-24-chat-shop-and-emoji-fling): DOM sprites
   spawned from a player's stamp position (or a screen edge for non-seated
   chatters) that pop/arc across the stream. Sits above the shop card (500)
   but below every drawer/backdrop (1000-1050). */
.tv-fling {
  position: fixed;
  z-index: 510;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  will-change: transform, left, top, opacity;
}
.tv-fling-glyph { font-size: 56px; font-family: 'Segoe UI Emoji', sans-serif; text-shadow: 0 2px 8px rgba(0,0,0,.5); }
.tv-fling-name { font-size: 13px; font-weight: 700; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.8); }
.tv-fling-simple { transition: transform 1.2s ease-out, opacity 1.2s ease-out; }

/* Shared ⓘ setting explainer (spec 2026-07-25 §4). Replaces the 💬 Bot tab's
   native-title tooltips: themed, wraps long text, no 500ms browser delay. */
.twiz-info {
  cursor: help;
  opacity: 0.55;
  font-size: 11px;
  margin-left: 4px;
  user-select: none;
}
.twiz-info:hover, .twiz-info:focus { opacity: 1; color: var(--accent, #ffcc33); outline: none; }
.twiz-info-pop {
  position: fixed;
  z-index: 1100;                 /* above the drawer (1050), below nothing else */
  max-width: 320px;
  background: var(--bg-elevated, #1e1e1e);
  border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.14));
  border-radius: 8px;
  padding: var(--spacing-sm, 8px) 10px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-primary, #eee);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

/* 📡 Stream tab subtab nav (spec 2026-07-25 §3). Visually subordinate to the
   top-level .settings-tabs — smaller, pill-shaped, no bottom rule. */
.stream-subtabs {
  display: flex;
  gap: 4px;
  margin-bottom: var(--spacing-md, 12px);
  flex-wrap: wrap;
}
.stream-subtab {
  padding: 5px 14px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: var(--bg-hover, rgba(255, 255, 255, 0.07));
  color: var(--text-secondary, #9a9aa6);
  font-size: 13px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.stream-subtab:hover { color: var(--text-primary, #eee); }
.stream-subtab.active {
  background: var(--accent, #ffcc33);
  color: #16171d;
}
.stream-subpanel[hidden] { display: none; }

/* 🎒 Shop subtab cosmetics grid (spec 2026-07-25 §5.1). Art-forward tiles with
   filter chips, search, sorting and on-tile quick edits. */
/* Create actions, above the catalogue. These were previously a plain button row
   BELOW the grid, which on a 1080p screen sat just past the fold behind 21
   tiles — effectively invisible. They are the entry point to the whole panel,
   so they lead, and they are styled to look like it. */
.cg-actions {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  margin-bottom: 10px; padding-bottom: 10px;
  border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
}
.cg-make {
  background: var(--twiz-gold, #ffcc33); color: #14141c; border: 0;
}
.cg-make:hover { filter: brightness(1.08); }
.cg-actions-hint { font-size: 11px; color: var(--text-secondary, #c9c9d3); flex: 1 1 180px; }

.cg-toolbar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.cg-chip {
  font-size: 12px; padding: 4px 10px; border-radius: 999px; cursor: pointer;
  background: var(--bg-hover, rgba(255, 255, 255, 0.08));
  color: var(--text-secondary, #c9c9d3); border: 1px solid transparent;
  font-family: inherit;
}
.cg-chip b { opacity: 0.65; font-weight: 700; }
.cg-chip.active { background: var(--accent, #ffcc33); color: #16171d; font-weight: 700; }
.cg-chip.active b { opacity: 0.75; }
/* input[type="search"] is not covered by the panel's field styling, so this
   carries its own box or it renders as a raw white UA input. */
.cg-search {
  font-size: 12px; padding: 3px 8px; width: 150px; font-family: inherit;
  background: var(--bg-input, rgba(0, 0, 0, 0.35));
  border: 1px solid var(--bg-hover, rgba(255, 255, 255, 0.16));
  border-radius: 8px; color: var(--text-primary, #eee); outline: none;
}
.cg-search:focus { border-color: var(--accent, #ffcc33); }
.cg-sort { font-size: 12px; padding: 3px 6px; max-width: 150px; }
/* auto-fill rather than a hard 4 columns: the drawer is 720px on a TV (→ 4 up)
   but max-width:90vw on anything smaller, where 4 columns are unreadable. */
.cg-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }
.cg-empty { grid-column: 1 / -1; font-size: 12px; opacity: 0.6; padding: 8px 2px; }
.cg-tile {
  position: relative; display: flex; flex-direction: column; gap: 5px; padding: 8px;
  background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 8px; cursor: pointer;
}
.cg-tile:hover { border-color: rgba(255, 255, 255, 0.22); }
.cg-tile.cg-open { border-color: var(--accent, #ffcc33); box-shadow: 0 0 0 1px var(--accent, #ffcc33); }
/* The pinned item has to be findable AT A GLANCE in a full grid — before this
   the only way to know which item held shop slot 4 was to open every editor in
   turn (owner, 2026-07-26). Green, not the gold `--accent`, so "pinned" never
   reads as "currently open for editing"; both can be true at once and
   cg-open's gold border still wins on the border while this keeps the ring. */
.cg-tile.cg-pinned { border-color: #39d98a; box-shadow: 0 0 0 1px #39d98a; }
.cg-pin-badge {
  position: absolute; top: 4px; right: 4px; z-index: 2;
  padding: 1px 4px; line-height: 1.1; font-size: 13px;
  background: #39d98a; color: #08301d; border: 0; border-radius: 6px;
  cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.cg-pin-badge:hover { filter: brightness(1.12); }
.cg-off { opacity: 0.38; }
.cg-art { height: 68px; display: flex; align-items: center; justify-content: center; }
.cg-img { max-width: 100%; max-height: 100%; object-fit: contain; }
.cg-glyph { font-size: 42px; font-family: 'Segoe UI Emoji', sans-serif; }
.cg-stamp { display: inline-flex; align-items: center; justify-content: center; }
.cg-name { font-size: 12.5px; font-weight: 700; line-height: 1.15; }
.cg-name em { opacity: 0.7; font-size: 11px; }
/* padding-right clears the absolutely-positioned .cg-grip, which otherwise
   sits on top of the price input / unlock label. */
.cg-foot { display: flex; align-items: center; justify-content: space-between; gap: 4px; padding-right: 13px; }
.cg-code {
  font-family: ui-monospace, monospace; font-size: 11px; color: var(--text-secondary, #8f8fa0);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cg-nolock { font-size: 11px; opacity: 0.7; white-space: nowrap; }
/* .settings-panel-scoped so the drawer's chunky 12px-radius/2px-border number
   input styling doesn't win over the compact in-tile look. */
.settings-panel input.cg-price, .cg-price {
  width: 62px; font-size: 12px; padding: 2px 6px; text-align: right;
  background: rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 5px; color: var(--twiz-gold, #ffd36b); font-weight: 700;
}
.cg-kind {
  position: absolute; top: 6px; left: 6px; font-size: 9px; font-weight: 900;
  padding: 1px 5px; border-radius: 999px; letter-spacing: 0.05em;
}
.cg-kind-skin { background: #3b82f6; color: #fff; }
.cg-kind-finish { background: #a855f7; color: #fff; }
.cg-kind-emoji { background: #eab308; color: #1a1a1a; }
.cg-tog { position: absolute; top: 4px; right: 4px; }
.cg-grip { position: absolute; bottom: 6px; right: 7px; color: #6a6a78; font-size: 12px; cursor: grab; letter-spacing: -2px; }
/* Reordering only takes effect while sorted by shop order — tileMarkup only
   marks the tile draggable="true" in that mode, so this greys the grip out
   in every other sort rather than leaving it looking actionable. */
.cg-tile:not([draggable="true"]) .cg-grip { opacity: .25; cursor: default; }
.cg-tile.cg-drop { border-color: var(--accent, #ffcc33); border-style: dashed; }

/* Inline tile editor (spec §5.2) — a grid ITEM spanning the full row below
   its tile, so opening one pushes the rest of the grid down instead of
   reflowing the tile it belongs to. */
.cg-editor {
  grid-column: 1 / -1; display: flex; gap: 14px; padding: 10px 12px;
  background: rgba(255, 204, 51, 0.06);
  border: 1px solid rgba(255, 204, 51, 0.35); border-radius: 8px;
}
.cg-ed-art { width: 92px; flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px; align-items: center; }
/* The framing panel (a row of a 132px card box + a 96px bead box + gap ≈
   242px) is wider than the 92px this column is sized for by default — flexbox
   does not clip an overflowing child, so without this override the panel
   would spill into `.cg-ed-fields`. Scoped to :has(.cg-frame) so the plain
   92×92 `.cg-ed-box` fallback (non-skin items, or a skin with no art) keeps
   its original narrow column. */
.cg-ed-art:has(.cg-frame) { width: auto; min-width: 242px; }
.cg-ed-box {
  width: 92px; height: 92px; background: rgba(0, 0, 0, 0.3); border-radius: 8px;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
/* Per-item art framing (spec 2026-07-26) — drag-to-pan + zoom previews for the
   shop card and the player bead, one box per surface. */
.cg-frame-row { display: flex; gap: 14px; }
.cg-frame-one { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.cg-frame-cap { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; opacity: .7; }
.cg-frame-box { position: relative; overflow: hidden; cursor: grab; background: rgba(0,0,0,.35); }
.cg-frame-box:active { cursor: grabbing; }
.cg-frame-box img { display: block; }
/* Same proportions the surfaces actually use: 1/0.76 for the card, a circle
   for the bead. Framing against a differently-shaped preview is worthless. */
/* Flex-centred like the real card's `.tsh-art`: the image inside is sized to
   88%/contain (see paint()'s FIT.card), not a 100% cover-fill, so it needs the
   same centring container the real card gives it or it would hug a corner
   instead of sitting where the operator actually sees it land on stream. */
/* Both previews are circles now that the shop card renders a bead rather than
   raw art. They differ in SIZE on purpose — that is the whole reason two
   framings still exist: the shop bead is big enough for a whole character,
   the in-game bead is small enough that you usually want the face. */
.cg-frame-card { width: 132px; height: 132px; border-radius: 50%; }
.cg-frame-bead { width: 78px; height: 78px; border-radius: 50%; }
.cg-frame-actions { display: flex; gap: 8px; margin-top: 8px; }
/* min-width:0 — without it the fields column refuses to shrink below its
   content and the editor overflows the drawer on a narrow phone/laptop. */
.cg-ed-fields {
  flex: 1; min-width: 0; display: grid; grid-template-columns: auto 1fr;
  gap: 6px 10px; align-items: center; font-size: 12px;
}
.cg-ed-fields > label { color: var(--text-secondary, #9a9aa6); }
.cg-ed-head { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; gap: 8px; font-weight: 800; font-size: 13px; }
.cg-ed-meta { color: var(--text-secondary, #9a9aa6); font-weight: 400; display: flex; gap: 6px; align-items: center; white-space: nowrap; }
.settings-panel input.cg-ed-in, .cg-ed-in {
  font-size: 12px; padding: 3px 7px; background: rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 5px;
  color: var(--text-primary, #e8e8ec); min-width: 0; font-family: inherit;
}
.settings-panel input.cg-ed-code, .cg-ed-code { width: 140px; font-family: ui-monospace, monospace; }
.settings-panel input.cg-ed-glyph, .cg-ed-glyph { width: 70px; text-align: center; font-size: 18px; }
.cg-ed-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; min-width: 0; }
/* Showing the WHOLE command is the point of this field, so unlike the tile's
   code chip the preview wraps instead of ellipsing when the drawer is narrow. */
.cg-editor [data-ed-preview] { white-space: normal; overflow: visible; text-overflow: clip; }
.cg-rad { display: flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; }
.cg-ed-err { grid-column: 1 / -1; color: var(--danger, #e66); font-size: 11px; min-height: 14px; }
.cg-ed-actions { grid-column: 1 / -1; display: flex; gap: 6px; flex-wrap: wrap; }

/* 🛒 On-stream storefront placement block (spec §6.3). Same label|controls
   two-column grid as the tile editor, so the two read as one panel. */
.cg-section { font-weight: 800; font-size: 13px; margin: 18px 0 8px; color: var(--text-primary, #e8e8ec); }
.cg-place { display: grid; grid-template-columns: auto 1fr; gap: 8px 12px; align-items: center; font-size: 12px; }
.cg-place > span { color: var(--text-secondary, #9a9aa6); }
/* The quadrant glyphs are tiny at chip size and have to read as a corner. */
.cg-place [data-corner] { font-size: 15px; line-height: 1; padding: 3px 9px; }

/* 🎲 Rotation odds panel (spec 2026-07-26-shop-rotation-windows §4/§10).
   Sits ABOVE the grid it feeds — the operator-editable draw config that
   decides what the grid's own tiles end up competing to appear as. Boxed
   like the tile editor (.cg-editor) rather than a bare .cg-section so it
   reads as its own control surface, not another row of the catalogue. */
.cg-tiers:empty { display: none; }
.cg-tiers {
  display: flex; flex-direction: column; gap: 2px; margin-bottom: 10px;
  padding: 8px 10px; background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.09); border-radius: 8px; font-size: 12px;
}
.cg-tiers-head { font-weight: 800; font-size: 13px; margin-bottom: 4px; color: var(--text-primary, #e8e8ec); }
.cg-tier-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 5px 0; border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.cg-tiers-head + .cg-tier-row { border-top: 0; }
.cg-tier-row label { display: flex; align-items: center; gap: 6px; color: var(--text-secondary, #9a9aa6); white-space: nowrap; }
.cg-tier-row input[type="number"] {
  width: 64px; font-size: 12px; padding: 2px 6px; text-align: right;
  background: rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 5px; color: var(--text-primary, #e8e8ec);
}
.cg-tier-row select {
  font-size: 12px; padding: 2px 6px; background: rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 5px; color: var(--text-primary, #e8e8ec);
}
/* Rarity colour is the SAME palette the live TV card uses (SHOP_TIERS in
   tv-shop-overlay-state.js) — the card sets it inline via --tier, this panel
   has no per-card element to hang a var on, so the four values are repeated
   here as the one place that still needs them as static classes. */
.cg-tier-name { font-weight: 900; min-width: 76px; text-transform: uppercase; font-size: 11px; letter-spacing: 0.06em; }
.cg-tier-name.tsh-common { color: #8ea3b8; }
.cg-tier-name.tsh-rare { color: #3fa9ff; }
.cg-tier-name.tsh-epic { color: #c05cff; }
.cg-tier-name.tsh-legendary { color: #ffb020; }
.cg-tier-rate { margin-left: auto; font-size: 11px; color: var(--text-secondary, #9a9aa6); white-space: nowrap; }
.cg-tier-note { font-size: 11px; color: var(--text-secondary, #9a9aa6); flex: 1 1 220px; }

/* 📌 Pin button in the tile editor — same "toggled on" look the filter chips
   use (.cg-chip.active), so a pinned item reads consistently with the rest
   of the panel's on/off language. */
.cg-editor [data-ed-pin][data-pinned] { background: var(--accent, #ffcc33); color: #16171d; font-weight: 700; }

/* ============================================
   GAME PAGES SCALE WITH THE SCREEN (spec docs/superpowers/specs/2026-10-21-ui-scale-design.md, AGENTS.md hard fact 17)
   On pages with <html class="ui-host"> or <html class="ui-phone"> (the root size: ui-scale.css) this file's own px sizes become rem
   too (1 rem = 15 px at the reference, so nothing moves at 1080p or on a phone). Pages without the class keep their sizes.
   ============================================ */
/* the root font size itself: /framework/ui-scale.css (imported at the top of this file) */
html.ui-host, html.ui-phone {
  --border-radius: 0.4rem;
  --spacing-xs: 0.26667rem; --spacing-sm: 0.53333rem; --spacing-md: 0.8rem; --spacing-lg: 1.06667rem; --spacing-xl: 1.6rem;
}
:is(html.ui-host, html.ui-phone) .card { max-width: 28rem; }
:is(html.ui-host, html.ui-phone) .card .subtitle { font-size: 0.93333rem; }
:is(html.ui-host, html.ui-phone) .btn { font-size: 0.93333rem; min-height: 2.93333rem; }
:is(html.ui-host, html.ui-phone) .btn-lg { font-size: 1.13333rem; min-height: 3.46667rem; }
:is(html.ui-host, html.ui-phone) .btn-sm { font-size: 0.86667rem; min-height: 2.13333rem; }
:is(html.ui-host, html.ui-phone) .toast { font-size: 1rem; }
:is(html.ui-host, html.ui-phone) .toast-container.toast-anchor-bl { max-width: min(28rem, 46vw); }
@media (min-width: 600px) {
  html.ui-phone .phone-page { padding: 1.6rem; }
  html.ui-phone .phone-shell { min-height: 0; max-height: 61.33333rem; border-radius: 2.13333rem; }
  html.ui-phone .phone-shell::before { top: 0.53333rem; width: 6rem; height: 1.2rem; border-radius: 0.8rem; }
  html.ui-phone .pp-fixed .phone-shell { max-height: min(61.33333rem, 100%); }
}
