/* ═══════════════════════════════════════════════════════════════════
   TropeRPGAI — Visual Overhaul Layer (theme.css)
   Loaded AFTER the inline <style> block in index.html, so equal-
   specificity rules here win. Purely additive polish: no display/
   layout-mode changes, no JS hooks required.
   ═══════════════════════════════════════════════════════════════════ */

/* ── 0. Tokens: three-tier token layer ──────────────────────────────
   Tier 1 primitives → Tier 2 semantics → Tier 3 back-compat aliases.
   Every alias resolves to the exact color the inline sheet ships today,
   so this changes ZERO visuals by default. It exists so a per-world
   reskin can override one semantic var and cascade everywhere.
   Aliases are var() chains on purpose — never flatten them to hex. ── */
:root {
  /* — Tier 1: primitives — */
  --pink-500: #ff2d7a;
  --gold-500: #ffd700;
  --cyan-500: #00d4ff;
  --green-500: #39ff9f;
  --purple-500: #bf5fff;
  --azure-400: #7aa2ff;            /* info-blue primitive */
  /* Neutral ramp — sampled from the inline sheet's live surfaces */
  --ink-900: #08060d;              /* inline --bg */
  --ink-800: #0f0b18;              /* inline --bg2 */
  --ink-700: #16112a;              /* inline --bg3 */
  --ink-600: #2a1f44;              /* inline --border */
  --ink-500: #3d2c63;              /* strong strokes / scrollbar thumb */
  --ink-400: #9386ab;              /* inline's original text-dim */
  --ink-300: #a396c2;              /* muted text (+contrast, live value) */
  --ink-200: #c9bfdb;
  --ink-100: #e8daf0;              /* inline --text */

  /* — Tier 2: semantics — */
  --fg: var(--ink-100);
  --fg-muted: var(--ink-300);
  --surface-1: var(--ink-900);
  --surface-2: var(--ink-800);
  --surface-3: var(--ink-700);
  --stroke-subtle: var(--ink-600);
  --stroke-strong: var(--ink-500);
  --brand: var(--pink-500);
  --highlight: var(--gold-500);
  --accent-cool: var(--cyan-500);
  --arcane: var(--purple-500);
  --info: var(--azure-400);
  --success: var(--green-500);
  --danger: var(--pink-500);

  /* — Tier 3: back-compat aliases (every name the inline :root defines,
       re-routed through the semantic tier at identical resolved values) — */
  --bg: var(--surface-1);
  --bg2: var(--surface-2);
  --bg3: var(--surface-3);
  --border: var(--stroke-subtle);
  --text: var(--fg);
  --text-dim: var(--fg-muted);
  --accent: var(--brand);
  --accent2: var(--highlight);
  --blue: var(--accent-cool);
  --green: var(--success);
  --yellow: var(--highlight);
  --purple: var(--arcane);
  --hb-full: var(--success);
  --hb-mid: var(--highlight);
  --hb-low: var(--danger);
  --mana: var(--accent-cool);
  --neon-pink: var(--brand);
  --neon-pink-dim: color-mix(in srgb, var(--brand) 15%, transparent);
  --neon-gold: var(--highlight);
  --neon-gold-dim: color-mix(in srgb, var(--highlight) 10%, transparent);
  --neon-cyan: var(--accent-cool);
  --neon-purple: var(--arcane);
  --glow-pink: 0 0 20px color-mix(in srgb, var(--brand) 30%, transparent);
  --glow-gold: 0 0 20px color-mix(in srgb, var(--highlight) 20%, transparent);
  --glow-blue: 0 0 15px color-mix(in srgb, var(--accent-cool) 20%, transparent);

  /* Overlay-native tokens (pre-existing, unchanged values) */
  --glass: rgba(15, 11, 26, 0.72);
  --glass-border: rgba(191, 95, 255, 0.14);
  --edge-glow: rgba(255, 45, 122, 0.35);
  --gold-soft: rgba(255, 215, 0, 0.55);
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  /* — Missing axes: spacing / radii / elevation / motion — */
  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
  --space-5: 20px; --space-6: 24px; --space-7: 32px; --space-8: 40px;
  --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-pill: 999px;
  --z-1: 10; --z-2: 100; --z-3: 500; --z-4: 1000; --z-5: 10000;
  --dur-fast: 0.15s; --dur-base: 0.25s; --dur-slow: 0.6s;

  /* — Type scale — */
  --fs-display: 56px;
  --fs-h1: 26px;
  --fs-h2: 18px;
  --fs-h3: 16px;
  --fs-body: 14px;
  --fs-sm: 12px;
  --fs-label: 10px;
}

html { text-rendering: optimizeLegibility; }
body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection { background: rgba(255, 45, 122, 0.35); color: #fff; }

/* ── 1. Background: fix the 404'd dungeon art + living atmosphere ── */
/* The inline sheet points at img/dungeon_bg.png which resolves to
   /img/… (not served). /static/img/… is the mounted path — and the
   pre-converted webp (71 KB vs the png's 1.78 MB) is the one to ship. */
body {
  background-image:
    linear-gradient(rgba(8, 6, 13, 0.82) 0%, rgba(8, 6, 13, 0.92) 100%),
    radial-gradient(ellipse at 20% 50%, rgba(255, 45, 122, 0.05) 0%, transparent 60%),
    radial-gradient(ellipse at 80% 20%, rgba(191, 95, 255, 0.04) 0%, transparent 50%),
    radial-gradient(ellipse at 50% 90%, rgba(255, 215, 0, 0.03) 0%, transparent 40%),
    url('/static/img/dungeon_bg.webp');
}

/* Slow aurora drift behind everything (GPU transform only) */
body::before {
  content: '';
  position: fixed;
  left: -25vw; top: -25vh;
  width: 150vw; height: 150vh;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(ellipse 42% 30% at 30% 35%, rgba(255, 45, 122, 0.055), transparent 70%),
    radial-gradient(ellipse 38% 28% at 72% 60%, rgba(0, 212, 255, 0.045), transparent 70%),
    radial-gradient(ellipse 30% 24% at 55% 25%, rgba(191, 95, 255, 0.05), transparent 70%);
  animation: auroraDrift 46s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes auroraDrift {
  from { transform: translate3d(-3%, -2%, 0) rotate(-1.5deg) scale(1); }
  to   { transform: translate3d(3%, 2%, 0) rotate(1.5deg) scale(1.06); }
}

/* Cinematic vignette + whisper-subtle scanlines over the scene */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg,
      rgba(255, 255, 255, 0.012) 0px, rgba(255, 255, 255, 0.012) 1px,
      transparent 1px, transparent 3px),
    radial-gradient(ellipse 120% 100% at 50% 45%, transparent 62%, rgba(4, 2, 8, 0.42) 100%);
}

/* ── 2. Scrollbars: thin neon everywhere ────────────────────────── */
* { scrollbar-width: thin; scrollbar-color: #3d2c63 rgba(8, 6, 13, 0.4); }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: rgba(8, 6, 13, 0.4); }
*::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, #3d2c63, #2a1f44);
  border-radius: 8px;
  border: 2px solid rgba(8, 6, 13, 0.6);
}
*::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, var(--neon-pink), #7a2450);
}
*::-webkit-scrollbar-corner { background: transparent; }

/* ── 3. Buttons: tactile, lit, alive ────────────────────────────── */
button {
  transition: background 0.18s var(--ease-out), border-color 0.18s var(--ease-out),
              box-shadow 0.18s var(--ease-out), transform 0.12s var(--ease-out),
              color 0.18s var(--ease-out);
}
button:hover { transform: translateY(-1px); }
button:active { transform: translateY(0) scale(0.985); transition-duration: 0.05s; }
button:disabled { transform: none; }

button.primary {
  position: relative;
  overflow: hidden;
  background: linear-gradient(135deg, #ff3d86 0%, var(--neon-pink) 45%, #b3124e 100%);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
button.primary::before {
  content: '';
  position: absolute;
  top: 0; bottom: 0; left: -80%;
  width: 50%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.28), transparent);
  transform: skewX(-20deg);
  transition: left 0.55s var(--ease-out);
  pointer-events: none;
}
button.primary:hover::before { left: 130%; }
button.primary:hover {
  box-shadow: 0 0 25px rgba(255, 45, 122, 0.55), 0 4px 18px rgba(0, 0, 0, 0.4);
}
button.green {
  background: linear-gradient(135deg, #4bffab, #22cc7a);
  box-shadow: 0 0 12px rgba(57, 255, 159, 0.25);
  font-weight: 700;
}
button.blue {
  background: linear-gradient(135deg, #2fdcff, #009ec4);
  box-shadow: 0 0 12px rgba(0, 212, 255, 0.25);
  font-weight: 700;
}

/* ── 4. Inputs: smooth focus glow ───────────────────────────────── */
input, textarea, select {
  transition: border-color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out),
              background 0.2s var(--ease-out);
}
.auth-box input:focus, .lobby-card input:focus, .setup-step input[type="text"]:focus,
.setup-step textarea:focus, .modal input:focus, .modal textarea:focus,
.modal select:focus, .input-bar input:focus {
  border-color: var(--neon-pink);
  box-shadow: 0 0 0 1px rgba(255, 45, 122, 0.35), 0 0 18px rgba(255, 45, 122, 0.18);
  background: rgba(8, 6, 13, 0.95);
}

/* ── 5. Auth view: marquee moment ───────────────────────────────── */
.auth-logo {
  background: linear-gradient(180deg, #ff7ab0 0%, var(--neon-pink) 55%, #c2185b 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 0 22px rgba(255, 45, 122, 0.55))
          drop-shadow(0 0 70px rgba(255, 45, 122, 0.22));
  text-shadow: none;
}
.auth-box {
  border-color: rgba(255, 45, 122, 0.55);
  box-shadow:
    0 0 0 1px rgba(255, 45, 122, 0.12),
    0 0 34px rgba(255, 45, 122, 0.16),
    0 24px 60px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(14px) saturate(1.2);
  animation: panelRise 0.5s var(--ease-out) both;
}
@keyframes panelRise {
  from { opacity: 0; transform: translateY(16px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* ── 6. Panels: glass depth + hover lift ────────────────────────── */
.lobby-card, .setup-step, .admin-card {
  background: linear-gradient(160deg, rgba(22, 17, 42, 0.82) 0%, rgba(13, 9, 24, 0.88) 100%);
  border-color: var(--glass-border);
  backdrop-filter: blur(8px);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.04);
  transition: border-color 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out);
}
.lobby-card:hover, .setup-step:hover {
  border-color: rgba(255, 45, 122, 0.35);
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45), 0 0 24px rgba(255, 45, 122, 0.07),
              inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

.room-item {
  background: linear-gradient(150deg, rgba(22, 17, 42, 0.85) 0%, rgba(13, 9, 24, 0.9) 100%);
  backdrop-filter: blur(6px);
  transition: border-color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out),
              transform 0.2s var(--ease-out);
}

/* Selectable cards: springy select + glow ring */
.pick-card, .trope-chip, .death-card, .cadence-card {
  transition: border-color 0.18s var(--ease-out), background 0.18s var(--ease-out),
              box-shadow 0.18s var(--ease-out), transform 0.18s var(--ease-out);
}
.pick-card:hover, .trope-chip:hover, .death-card:hover, .cadence-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35), 0 0 14px rgba(255, 45, 122, 0.10);
}
.pick-card.selected, .trope-chip.selected {
  background: linear-gradient(160deg, rgba(255, 215, 0, 0.10), rgba(255, 215, 0, 0.04));
  box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.35), 0 0 20px rgba(255, 215, 0, 0.16),
              inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

/* ── 7. Game header: command-deck chrome ────────────────────────── */
.header {
  backdrop-filter: blur(12px) saturate(1.25);
  background: linear-gradient(90deg, rgba(15, 11, 24, 0.9) 0%, rgba(22, 17, 42, 0.88) 50%, rgba(15, 11, 24, 0.9) 100%);
}
.header h1 {
  background: linear-gradient(180deg, #ff8ab8, var(--neon-pink));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 0 12px rgba(255, 45, 122, 0.5));
  text-shadow: none;
}
.header .join-code-display {
  transition: box-shadow 0.2s var(--ease-out), transform 0.15s var(--ease-out);
}
.header .join-code-display:hover { transform: translateY(-1px); }

.sidebar-left, .sidebar-right {
  background: linear-gradient(180deg, rgba(15, 11, 24, 0.92) 0%, rgba(8, 6, 13, 0.95) 100%);
  box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3);
}

/* ── 8. Chat: readable, characterful ────────────────────────────── */
.chat-area { padding: 20px 24px; }
.chat-message .bubble {
  font-size: 14.5px;
  line-height: 1.65;
  border-radius: 4px 14px 14px 14px;
  box-shadow: 0 3px 14px rgba(0, 0, 0, 0.25);
  backdrop-filter: blur(4px);
}
.chat-message.player .bubble { border-radius: 14px 4px 14px 14px; }
.chat-message .speaker { display: flex; align-items: center; gap: 6px; }
.chat-message.player .speaker { justify-content: flex-end; }
.chat-message .speaker::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 8px currentColor;
  flex-shrink: 0;
}
.chat-message.system .speaker::before { box-shadow: none; }
.chat-message.dungeon_ai .bubble {
  background: linear-gradient(150deg, rgba(255, 45, 122, 0.09) 0%, rgba(22, 17, 42, 0.55) 60%);
  border-color: rgba(255, 45, 122, 0.55);
}
.chat-message.gm_ai .bubble {
  background: linear-gradient(150deg, rgba(0, 212, 255, 0.07) 0%, rgba(22, 17, 42, 0.55) 60%);
  border-color: rgba(0, 212, 255, 0.5);
}
.chat-message.companion_ai .bubble {
  border-color: rgba(255, 215, 0, 0.45);
  background: linear-gradient(150deg, rgba(255, 215, 0, 0.06) 0%, rgba(22, 17, 42, 0.55) 60%);
}
.chat-message.player .bubble {
  background: linear-gradient(150deg, rgba(57, 255, 159, 0.05) 0%, rgba(8, 6, 13, 0.85) 60%);
  border-color: rgba(57, 255, 159, 0.45);
}

.input-bar {
  backdrop-filter: blur(10px);
  background: rgba(15, 11, 24, 0.88);
  box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.3);
}
.input-bar input { border-radius: 10px; }

.chat-tab { position: relative; }
.chat-tab.active::after {
  content: '';
  position: absolute;
  left: 20%; right: 20%; bottom: -1px;
  height: 2px;
  background: var(--neon-pink);
  box-shadow: 0 0 10px var(--neon-pink);
  border-radius: 2px;
}

/* ── 9. Vital bars: liquid shimmer ──────────────────────────────── */
.bar {
  height: 15px;
  background: rgba(4, 3, 8, 0.9);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.6);
}
.bar-fill {
  position: relative;
  overflow: hidden;
  transition: width 0.6s var(--ease-out);
}
.bar-fill::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0; left: -60%;
  width: 45%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.32), transparent);
  transform: skewX(-18deg);
  animation: barShimmer 2.8s var(--ease-out) infinite;
}
@keyframes barShimmer {
  0%   { left: -60%; }
  55%  { left: 130%; }
  100% { left: 130%; }
}
.bar-fill.hb.low { animation: lowPulse 1.1s ease-in-out infinite; }
@keyframes lowPulse {
  0%, 100% { filter: brightness(1); }
  50%      { filter: brightness(1.45); }
}

/* ── 10. Party & combat panels ──────────────────────────────────── */
.character-card {
  backdrop-filter: blur(4px);
  transition: border-color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out);
}
.character-card:hover {
  border-color: rgba(255, 215, 0, 0.35);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35), 0 0 16px rgba(255, 215, 0, 0.06);
}

.combat-panel {
  animation: combatBreath 2.6s ease-in-out infinite;
}
@keyframes combatBreath {
  0%, 100% { box-shadow: inset 0 0 30px rgba(255, 45, 122, 0.05), 0 0 16px rgba(255, 45, 122, 0.18); }
  50%      { box-shadow: inset 0 0 30px rgba(255, 45, 122, 0.09), 0 0 30px rgba(255, 45, 122, 0.34); }
}

.turn-entry { transition: background 0.2s, opacity 0.2s; }
.turn-entry.current-turn {
  background: linear-gradient(100deg, var(--neon-pink) 0%, #d81b60 55%, var(--neon-pink) 100%);
  background-size: 200% 100%;
  animation: turnSweep 2.4s linear infinite;
}
@keyframes turnSweep {
  from { background-position: 0% 0; }
  to   { background-position: 200% 0; }
}

.mob-card {
  transition: border-color 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.mob-card:hover { border-color: rgba(191, 95, 255, 0.5); transform: translateX(2px); }

/* ── 11. Modals: cinematic entrance ─────────────────────────────── */
.modal-overlay { backdrop-filter: blur(7px) saturate(1.1); }
.modal-overlay.active .modal { animation: modalPop 0.28s var(--ease-out) both; }
@keyframes modalPop {
  from { opacity: 0; transform: translateY(18px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
.modal {
  box-shadow:
    0 0 0 1px rgba(255, 45, 122, 0.15),
    0 0 46px rgba(255, 45, 122, 0.14),
    0 30px 80px rgba(0, 0, 0, 0.65);
}
.world-info-backdrop, .detail-backdrop { backdrop-filter: blur(6px); }
.world-info-backdrop.open .world-info-modal,
.detail-backdrop.open .detail-modal { animation: modalPop 0.26s var(--ease-out) both; }

/* ── 12. Toasts: glass slivers with accent edge ─────────────────── */
.toast {
  backdrop-filter: blur(12px);
  border-left-width: 3px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(255, 45, 122, 0.14);
}
.toast.success { box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(57, 255, 159, 0.16); }
.toast.warning { box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(255, 215, 0, 0.16); }

/* ── 13. Battle map: tactical glass grid ────────────────────────── */
.battle-grid {
  background: rgba(61, 44, 99, 0.55);
  border-color: rgba(191, 95, 255, 0.3);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 18px rgba(191, 95, 255, 0.08);
}
.battle-cell {
  background: rgba(10, 7, 18, 0.92);
  transition: background 0.15s, box-shadow 0.15s;
}
.battle-cell:hover {
  background: rgba(34, 24, 58, 0.95);
  box-shadow: inset 0 0 0 1px rgba(0, 212, 255, 0.45), inset 0 0 10px rgba(0, 212, 255, 0.15);
}
.battle-cell.has-token {
  text-shadow: 0 0 10px rgba(255, 215, 0, 0.6);
}
.battle-cell.terrain-difficult {
  background:
    repeating-linear-gradient(45deg, rgba(255, 215, 0, 0.08) 0 5px, transparent 5px 10px),
    rgba(10, 7, 18, 0.92);
}

/* ── 14. Scene stage ("the doors"): brushed neon ────────────────── */
.scene-stage {
  border-color: rgba(255, 45, 122, 0.28);
  box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.6), 0 6px 24px rgba(0, 0, 0, 0.4);
}
.scene-door.left  { box-shadow: inset -10px 0 26px rgba(255, 45, 122, 0.16); }
.scene-door.right { box-shadow: inset 10px 0 26px rgba(255, 45, 122, 0.16); }
.scene-placeholder .door-glyph { animation: doorGlyphPulse 3s ease-in-out infinite; }
@keyframes doorGlyphPulse {
  0%, 100% { filter: drop-shadow(0 0 8px rgba(255, 45, 122, 0.4)); }
  50%      { filter: drop-shadow(0 0 16px rgba(255, 45, 122, 0.75)); }
}
.scene-stage .scene-img { transition: opacity 1.1s ease, transform 6s var(--ease-out); }
.scene-stage.has-image.doors-open .scene-img { transform: scale(1.04); }

/* ── 15. Music bar / footer chrome ──────────────────────────────── */
.music-bar {
  backdrop-filter: blur(10px);
  background: rgba(15, 11, 24, 0.85);
  box-shadow: 0 -4px 18px rgba(0, 0, 0, 0.35);
}

/* ── 16. Section titles: etched circuitry line ──────────────────── */
.section-title, .cs-section-title {
  border-image: linear-gradient(90deg, rgba(255, 45, 122, 0.6), rgba(255, 45, 122, 0.05)) 1;
}

/* ── 17. Empty states ───────────────────────────────────────────── */
.no-rooms, .inv-empty {
  border: 1px dashed rgba(147, 134, 171, 0.3);
  border-radius: 10px;
  background: rgba(15, 11, 24, 0.35);
}
.no-rooms { padding: 28px 20px; }

/* ── 18. Onboarding + achievements ──────────────────────────────── */
.onboarding-box {
  backdrop-filter: blur(14px);
  background: linear-gradient(165deg, rgba(22, 17, 42, 0.92), rgba(13, 9, 24, 0.95));
  box-shadow: 0 0 60px rgba(255, 45, 122, 0.18), 0 30px 80px rgba(0, 0, 0, 0.6);
  animation: modalPop 0.3s var(--ease-out) both;
}
.achievement-popup { backdrop-filter: blur(10px); }

/* ── 19. Command bar chips ──────────────────────────────────────── */
#cmd-bar button {
  border-radius: 6px;
  transition: all 0.15s var(--ease-out);
}
#cmd-bar button:hover {
  border-color: var(--neon-pink);
  color: #fff;
  box-shadow: 0 0 10px rgba(255, 45, 122, 0.2);
  transform: translateY(-1px);
}
.prompt-chip { transition: background 0.15s, color 0.15s, box-shadow 0.15s, transform 0.12s; }
.prompt-chip:hover { box-shadow: 0 0 12px rgba(255, 45, 122, 0.35); transform: translateY(-1px); }

/* ── 20. Capability switches: juicier toggle ────────────────────── */
.cap-switch { transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out); }
.cap-switch.on { box-shadow: 0 0 10px rgba(78, 226, 154, 0.35); }
.cap-switch::after { transition: transform 0.22s var(--ease-out), background 0.2s; }

/* ── 21. Portrait ring glow ─────────────────────────────────────── */
.portrait-circle {
  box-shadow: 0 0 12px rgba(255, 45, 122, 0.3), inset 0 0 8px rgba(0, 0, 0, 0.5);
}

/* ── 22. Setup wizard: numbered steps light up ──────────────────── */
.setup-step > .step-title .step-num {
  background: rgba(255, 45, 122, 0.08);
  box-shadow: 0 0 10px rgba(255, 45, 122, 0.22);
}
.seg-btn { border-radius: 7px; }
.axis-seg .seg-btn.active {
  box-shadow: 0 0 12px rgba(255, 215, 0, 0.22), inset 0 0 8px rgba(255, 215, 0, 0.06);
}

/* ── 22a. Chat area: the System's eye ghosted into the empty table ── */
.chat-area {
  background-image: url('/static/img/watermark.png');
  background-repeat: no-repeat;
  background-position: center 42%;
  background-size: 150px;
}

/* Setup wizard: a lone card in a category row spans the full width and
   lays out horizontally, so single-option groups stop looking sparse */
.pick-grid .pick-card:only-child {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(180px, 260px) 1fr;
  gap: 4px 22px;
  align-items: start;
}
.pick-grid .pick-card:only-child .pick-desc {
  grid-column: 2;
  grid-row: 1 / span 2;
  -webkit-line-clamp: 3;
  padding-right: 30px;
}
.pick-grid .pick-card:only-child .pick-more { grid-column: 2; }
@media (max-width: 640px) {
  .pick-grid .pick-card:only-child { display: flex; flex-direction: column; }
}

/* ── 22b. Legal disclaimer: true watermark, never fights the UI ──── */
#disclaimer-footer {
  font-size: 10px !important;   /* was 9px — legibility floor */
  opacity: 0.6 !important;      /* was 0.3 — contrast fix */
  padding: 2px 12px !important;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

/* ── 23. Mobile performance + polish guards ─────────────────────── */
@media (max-width: 768px) {
  body::before { display: none; }                 /* skip aurora on phones */
  .auth-box, .lobby-card, .setup-step, .modal-overlay,
  .toast, .onboarding-box, .header, .input-bar, .music-bar,
  .chat-message .bubble { backdrop-filter: none; }
  .bar-fill::after { animation-duration: 4s; }
}

/* ── 25. Baked art: mob cards, battle tokens, class portraits ───── */
.mob-card.has-art { display: flex; gap: 9px; align-items: flex-start; }
.mob-card.has-art .mob-art {
  width: 46px; height: 46px; border-radius: 7px; object-fit: cover;
  flex-shrink: 0; border: 1px solid var(--neon-purple, #bf5fff);
  box-shadow: 0 0 10px rgba(191, 95, 255, 0.28);
}
.mob-card.has-art .mob-card-body { flex: 1; min-width: 0; }
.battle-cell .token-art {
  position: absolute; inset: 2px; width: calc(100% - 4px); height: calc(100% - 4px);
  object-fit: cover; border-radius: 4px; box-shadow: 0 0 6px rgba(255, 45, 122, 0.4);
}
.battle-cell .token-label {
  z-index: 1; background: rgba(8, 6, 13, 0.7); padding: 0 2px; border-radius: 2px;
}
/* Class-portrait thumbnails get a subtle frame consistent with the neon UI. */
.portrait-circle img { transition: transform 0.2s var(--ease-out, ease); }
.pick-card:hover .portrait-circle img { transform: scale(1.05); }

/* ── 25b. Combat + party chrome polish (complements the baked art) ── */
/* Boss mob-cards feel dangerous: gold frame + deeper glow. */
.mob-card:has(.mob-name):hover { border-color: var(--neon-purple, #bf5fff); }
.combat-panel .mob-card { transition: border-color 0.2s ease, box-shadow 0.2s ease; }
.mob-card.has-art .mob-art { animation: artIn 0.35s var(--ease-out, ease) both; }
@keyframes artIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }
/* The System's sprite gets a faint arcane pedestal glow + lift on hover. */
#dm-panel { position: relative; }
.dm-sprite { filter: drop-shadow(0 6px 14px rgba(191, 95, 255, 0.25)); transition: transform 0.25s var(--ease-out, ease); }
#dm-panel:hover .dm-sprite { transform: translateY(-2px) scale(1.03); }
.dice-tower-wrap { filter: drop-shadow(0 0 18px rgba(255, 45, 122, 0.18)); }
/* Party roster card: portrait + name/class in a header row. */
.character-card .cc-head { display: flex; gap: 10px; align-items: center; margin-bottom: 6px; }
.character-card .cc-head .portrait-circle { width: 52px; height: 52px; font-size: 24px; }
.character-card .cc-head-text { flex: 1; min-width: 0; }
.character-card .cc-head-text .name { margin: 0; }
/* Portrait state rings: current turn glows gold, downed/dead goes red-dim. */
.portrait-circle { transition: box-shadow 0.25s ease, filter 0.25s ease; }
.character-card.is-down .portrait-circle,
.character-card.is-dead .portrait-circle { filter: grayscale(0.85) brightness(0.6); border-color: #ff5555 !important; }
.character-card.current-turn .portrait-circle {
  border-color: var(--neon-gold) !important;
  box-shadow: 0 0 12px rgba(255, 215, 0, 0.55), 0 0 22px rgba(255, 215, 0, 0.25);
}

/* ── 26. Reduced motion: honor it fully ─────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }
  .bar-fill::after, .bar-fill.hb.low,
  .combat-panel, .turn-entry.current-turn,
  .scene-placeholder .door-glyph,
  .auth-logo,                                    /* title neon-flicker */
  .mic-btn.recording, #live-listen-btn.live-on,  /* mic push-to-talk / live-listen pulse */
  .mob-card.has-art .mob-art,                    /* monster-art pop-in */
  button.primary::before { animation: none; }
  #dm-panel:hover .dm-sprite, .character-card .portrait-circle { transition: none; }
  button:hover, .pick-card:hover, .trope-chip:hover,
  .death-card:hover, .cadence-card:hover { transform: none; }
  .auth-box, .modal-overlay.active .modal, .onboarding-box,
  .world-info-backdrop.open .world-info-modal,
  .detail-backdrop.open .detail-modal { animation: none; }
  /* Title flicker, mic pulses, and the sliding door reveal were left out of
     the first pass — a flickering logo, pulsing mic ring, or gliding door
     leaf all run on a fixed timer/transition regardless of this setting. */
  .auth-logo,
  .mic-btn.recording,
  #live-listen-btn.live-on { animation: none; }
  .scene-door, .scene-placeholder { transition: none; }
}

/* ── 27. Native control skins: selects, ranges, details, lists, ticks ── */
:root {
  --ctl-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.25l3.5 3.5 3.5-3.5' fill='none' stroke='%23bf5fff' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* Selects — dark glass + custom purple chevron. The two GM selects with
   inline background styles (#gm-mode-select, #gm-fog-target) are excluded:
   their style attribute would erase the chevron image, which would leave
   them arrow-less if we stripped native appearance. :where() keeps the
   exclusion at zero specificity so styled contexts still win their ties. */
select:where(:not(#gm-mode-select, #gm-fog-target)) {
  appearance: none;
  -webkit-appearance: none;
  background-color: rgba(8, 6, 13, 0.75);
  background-image: var(--ctl-chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 12px 12px;
  border: 1px solid var(--stroke-subtle);
  border-radius: var(--r-sm);
  color: var(--fg);
  font-family: 'Rajdhani', sans-serif;
  font-size: 13px;
  padding: 6px 30px 6px 10px;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
select:where(:not(#gm-mode-select, #gm-fog-target)):hover {
  border-color: rgba(191, 95, 255, 0.55);
}
select:where(:not(#gm-mode-select, #gm-fog-target)):focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 1px rgba(255, 45, 122, 0.25), 0 0 12px rgba(255, 45, 122, 0.15);
}
select option { background-color: #16112a; color: #e8daf0; }
/* Styled contexts set a `background` shorthand (which erases the chevron
   image at their higher/equal specificity) — re-assert image + arrow gutter. */
.modal select, .pack-picker select, .encounter-controls select, .faction-slot select {
  background-image: var(--ctl-chevron);
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 12px 12px;
  padding-right: 30px;
}

/* Ranges — neon track + glowing thumb (personality sliders, encounter
   difficulty, music volume). accent-color kept as baseline fallback. */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 16px;
  background: transparent;
  cursor: pointer;
  accent-color: var(--brand);
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, rgba(255, 45, 122, 0.5), rgba(191, 95, 255, 0.45));
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px; height: 14px;
  margin-top: -4.5px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #ff7ab0, var(--brand) 70%);
  border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: 0 0 8px rgba(255, 45, 122, 0.55);
}
input[type="range"]::-moz-range-track {
  height: 5px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(255, 45, 122, 0.5), rgba(191, 95, 255, 0.45));
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
}
input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #ff7ab0, var(--brand) 70%);
  border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: 0 0 8px rgba(255, 45, 122, 0.55);
}
/* Music-bar volume keeps its established cyan identity */
.music-bar input[type="range"]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg, rgba(0, 212, 255, 0.45), rgba(0, 212, 255, 0.18));
}
.music-bar input[type="range"]::-webkit-slider-thumb {
  background: radial-gradient(circle at 35% 35%, #9fe9ff, var(--accent-cool) 70%);
  box-shadow: 0 0 8px rgba(0, 212, 255, 0.5);
}
.music-bar input[type="range"]::-moz-range-track {
  background: linear-gradient(90deg, rgba(0, 212, 255, 0.45), rgba(0, 212, 255, 0.18));
}
.music-bar input[type="range"]::-moz-range-thumb {
  background: radial-gradient(circle at 35% 35%, #9fe9ff, var(--accent-cool) 70%);
  box-shadow: 0 0 8px rgba(0, 212, 255, 0.5);
}

/* Details/summary — custom rotating marker. .dnd-sheet already ships its
   own marker treatment (list-style:none + emoji), so it is excluded. */
summary { cursor: pointer; }
details:not(.dnd-sheet) > summary { list-style: none; }
details:not(.dnd-sheet) > summary::-webkit-details-marker { display: none; }
details:not(.dnd-sheet) > summary::before {
  content: '\25B8';
  display: inline-block;
  margin-right: 7px;
  color: var(--brand);
  text-shadow: 0 0 6px rgba(255, 45, 122, 0.5);
  transition: transform var(--dur-fast) var(--ease-out);
}
details:not(.dnd-sheet)[open] > summary::before { transform: rotate(90deg); }

/* List bullets — raw browser dots/numbers pick up the neon language.
   Color-only: no spacing changes, so no layout risk. */
ul li::marker { color: var(--brand); }
ol li::marker { color: var(--highlight); font-weight: 700; }

/* Radios + checkboxes — custom neon ticks; accent-color kept as the
   baseline fallback for engines without appearance:none support. */
input[type="radio"], input[type="checkbox"] {
  accent-color: var(--brand);
  appearance: none;
  -webkit-appearance: none;
  width: 15px; height: 15px;
  flex-shrink: 0;
  border: 1px solid var(--stroke-strong);
  background: rgba(8, 6, 13, 0.85);
  cursor: pointer;
  vertical-align: middle;
  position: relative;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"] { border-radius: 4px; }
input[type="radio"]:hover, input[type="checkbox"]:hover { border-color: var(--arcane); }
input[type="radio"]:checked {
  border-color: var(--brand);
  background: radial-gradient(circle, var(--brand) 0 4px, transparent 5px), rgba(8, 6, 13, 0.85);
  box-shadow: 0 0 8px rgba(255, 45, 122, 0.4);
}
input[type="checkbox"]:checked {
  border-color: var(--brand);
  background: linear-gradient(135deg, #ff5c96, var(--brand));
  box-shadow: 0 0 8px rgba(255, 45, 122, 0.4);
}
input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  left: 4px; top: 1px;
  width: 4px; height: 8px;
  border: solid #12030c;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* Reduced motion: kill the new control transitions (nothing here
   auto-dismisses on animationend, so plain none is safe). */
@media (prefers-reduced-motion: reduce) {
  select, input[type="range"], input[type="radio"], input[type="checkbox"],
  details:not(.dnd-sheet) > summary::before { transition: none; }
}

/* ── 28. Info-hue correction: --accent2 was gold, but these three
   components use it in an info-blue role — border, background and glow
   now all agree on --info (#7aa2ff). --accent2 itself stays gold. ── */
.qs-card.qs-ai {
  border-color: var(--info);
  background: color-mix(in srgb, var(--info) 5%, transparent);
  box-shadow: 0 0 16px color-mix(in srgb, var(--info) 12%, transparent);
}
.qs-ai-summary {
  border-color: var(--info);
  background: color-mix(in srgb, var(--info) 8%, transparent);
}
.detail-modal {
  border-color: var(--info);
  background:
    linear-gradient(color-mix(in srgb, var(--info) 4%, transparent),
                    color-mix(in srgb, var(--info) 4%, transparent)),
    var(--bg2);
  box-shadow: 0 0 36px color-mix(in srgb, var(--info) 22%, transparent);
}

/* ── 29. Type scale: collapse the six competing page-title treatments
   onto two — display (big pink marquee) and chrome (gold panel header).
   Sizes ride the --fs-* tokens and stay within ~2px of today. ── */
/* Display treatment */
.auth-logo { font-size: var(--fs-display); }
.lobby-header h1, .setup-head h1 {
  font-family: 'Orbitron', monospace;
  font-size: var(--fs-h1);
  font-weight: 700;
  color: var(--neon-pink);
  letter-spacing: 3px;
  text-transform: uppercase;
  text-shadow: 0 0 20px rgba(255, 45, 122, 0.5);
}
.header h1 { font-size: var(--fs-h3); }
/* Chrome treatment */
.modal h2, .world-info-modal h2, .auth-box h2 {
  font-family: 'Orbitron', monospace;
  font-size: var(--fs-h2);
  color: var(--neon-gold);
  letter-spacing: 2px;
}
/* Retire the off-scale 13.5px / 11.5px one-offs onto tokens */
.world-info-modal .wi-sec p { font-size: var(--fs-body); }
.detail-modal .dm-lead { font-size: var(--fs-body); }
.axis-row .axis-help { font-size: var(--fs-sm); }
/* Restate the inline sheet's responsive title overrides, which the
   unqualified token rules above would otherwise defeat (cascade tie). */
@media (min-width: 769px) and (max-width: 900px) {
  .header h1 { font-size: 14px; }
}
@media (max-width: 768px) {
  .auth-logo { font-size: 36px; }
  .lobby-header h1 { font-size: 22px; letter-spacing: 2px; }
  .header h1 { font-size: 13px; }
  .modal h2 { font-size: 16px; }
}
@media (max-width: 380px) {
  .auth-logo { font-size: 28px; }
  .header h1 { font-size: 11px; }
}

/* ── 30. Mobile cascade guards: unqualified overlay rules above (chat
   padding, bubble size) were defeating the inline sheet's ≤768px branch
   because equal specificity + later cascade wins. Restate the mobile
   values here so phones get the compact layout back. ── */
@media (max-width: 768px) {
  .chat-area { padding: 10px 12px; }
  .chat-message .bubble { font-size: 14px; }
}

/* ── 31. Contrast fix: button.blue was white-on-cyan (~1.7:1). Dark ink
   like button.green uses, tuned to the cyan family. ── */
button.blue { color: #04202a; }

/* ── 32. Character-card state chrome: replaces the old inline card styles
   that updateHUD used to stamp on every rebuild (they defeated the §10
   hover/transition rules). Order matters — later rules win ties, so the
   priority matches the old inline ternary chain: owner < active turn <
   down < dead. No animation added here, so nothing to neutralize. ── */
.character-card.is-owner { border-color: var(--accent); }
.character-card.current-turn {
  border-color: #4ee29a;
  box-shadow: 0 0 14px rgba(0, 200, 120, 0.55);
}
.character-card.is-down {
  border-color: #a31515;
  box-shadow: 0 0 12px rgba(255, 45, 45, 0.45);
}
.character-card.is-dead {
  border-color: #666;
  box-shadow: none;
  opacity: 0.5;
}

/* ── 33. Battle-map token overlay: tokens now live on a layer above the
   grid (renderBattleMap no longer rebuilds cells per tick) and glide
   between cells via a transform transition. Mirrors the old in-cell
   token styling (§13 + the inline sheet's .battle-cell rules). ── */
.battle-grid { position: relative; }
.battle-grid .bm-token-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;   /* clicks fall through to the cells beneath */
}
.bm-token {
  position: absolute;
  top: 0; left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 700;
  text-shadow: 0 0 10px rgba(255, 215, 0, 0.6);
  transition: transform 0.35s var(--ease-out, ease);
  will-change: transform;
}
.bm-token .token-art {
  position: absolute; inset: 2px;
  width: calc(100% - 4px); height: calc(100% - 4px);
  object-fit: cover; border-radius: 4px;
  box-shadow: 0 0 6px rgba(255, 45, 122, 0.4);
}
.bm-token .token-label {
  position: absolute; bottom: 1px;
  font-size: 8px; color: var(--text-dim);
  z-index: 1; background: rgba(8, 6, 13, 0.7);
  padding: 0 2px; border-radius: 2px;
}
/* Reduced motion: tokens snap between cells instead of gliding. */
@media (prefers-reduced-motion: reduce) {
  .bm-token { transition: none; }
}

/* ── 34. World picker art: 16:9 painted banner + info-modal band ──
   worldCardHTML injects .pick-art (scenes/<world_id>.webp) at the top of
   each card; onerror removes the whole banner, and every rule below keys
   off its presence, so art-less worlds keep the exact old text card. ── */
.pick-card .pick-art {
  position: relative;
  aspect-ratio: 16 / 9;
  margin: -12px -14px 2px;          /* bleed across the card's 12px/14px padding */
  border-radius: 10px 10px 0 0;     /* sit inside the card's 10px frame */
  overflow: hidden;
}
.pick-card .pick-art img {
  display: block;
  width: 100%; height: 100%; max-width: 100%;
  object-fit: cover;
  transition: transform 0.45s var(--ease-out, ease);
}
.pick-card:hover .pick-art img { transform: scale(1.05); }
.pick-card .pick-art-scrim {
  position: absolute; inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(8, 6, 13, 0) 30%,
    rgba(8, 6, 13, 0.55) 76%,
    rgba(8, 6, 13, 0.94) 100%);
}
/* When the banner survives, the world name rides up onto its scrim.
   (Art and info button are emitted together, hence the two chains; if
   onerror drops the banner neither matches and the name reflows.) */
.pick-card .pick-art + .pick-info-btn + .pick-name,
.pick-card .pick-art + .pick-name {
  position: relative; z-index: 1;
  margin-top: -26px;
  text-shadow: 0 1px 6px rgba(8, 6, 13, 0.95), 0 0 14px rgba(8, 6, 13, 0.7);
}
/* World-info modal: same art as a blurred header band above the title. */
.world-info-modal .wi-art {
  position: relative;
  margin: -22px -24px 16px;         /* bleed across the modal's 22px/24px padding */
  height: 130px;
  overflow: hidden;
  border-radius: 13px 13px 0 0;
}
.world-info-modal .wi-art img {
  display: block;
  width: 100%; height: 100%; max-width: 100%;
  object-fit: cover;
  filter: blur(3px) saturate(1.15) brightness(0.85);
  transform: scale(1.08);           /* hide the blur's edge fringing */
}
.world-info-modal .wi-art::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(15, 11, 26, 0.12) 0%,
    rgba(15, 11, 26, 0.55) 70%,
    var(--bg2, #14101f) 100%);
}
@media (prefers-reduced-motion: reduce) {
  .pick-card .pick-art img { transition: none; }
  .pick-card:hover .pick-art img { transform: none; }
}

/* ── 35. Class picker: 3:4 painted hero panels ─────────────────────
   makeCard emits .pick-portrait + a .pick-body wrapper and stamps
   has-art on the card; onerror strips both img and class, reverting to
   the plain text card (the old dead .portrait-circle hover hook is
   superseded by the .pick-portrait rules here). ── */
.cs-class-card .pick-body {          /* layout-neutral wrapper on text cards */
  display: flex; flex-direction: column; gap: 4px; min-width: 0;
}
.cs-class-card.has-art {
  position: relative;
  aspect-ratio: 3 / 4;
  padding: 0;
  overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
}
.cs-class-card.has-art .pick-portrait {
  position: absolute; inset: 0;
  width: 100%; height: 100%; max-width: 100%;
  object-fit: cover; object-position: center top;
  transition: transform 0.45s var(--ease-out, ease);
}
.cs-class-card.has-art:hover .pick-portrait { transform: scale(1.05); }
.cs-class-card.has-art .pick-body {
  position: relative;                /* paints above the absolute portrait */
  padding: 34px 12px 11px;
  background: linear-gradient(180deg,
    rgba(8, 6, 13, 0) 0%,
    rgba(8, 6, 13, 0.72) 38%,
    rgba(8, 6, 13, 0.95) 100%);
}
.cs-class-card.has-art .pick-name {
  text-shadow: 0 1px 8px rgba(8, 6, 13, 0.95);
}
.cs-class-card.has-art.selected {
  box-shadow: 0 0 14px rgba(255, 215, 0, 0.22),
              inset 0 0 0 1px var(--neon-gold);
}
@media (prefers-reduced-motion: reduce) {
  .cs-class-card.has-art .pick-portrait { transition: none; }
  .cs-class-card.has-art:hover .pick-portrait { transform: none; }
}

/* ── 36. Character-sheet portrait: hide the empty grey box ─────────
   The portrait <img> now lives outside the room-gated generator block;
   with no portrait at all (creation state) it vanishes instead of
   showing a blank frame. !important is needed to beat the inline
   display:block in its style attribute. No motion added. ── */
#cs-portrait-img:not([src]) { display: none !important; }

/* ── 37. Ambient whole-app world backdrop ──────────────────────────
   Two fixed layers under everything, driven by setWorldBackdrop().
   z-index -3 keeps the painted world BEHIND the aurora glow (body::before
   is -2) but above the body's own dungeon texture. ── */
.world-bg {
  position: fixed; inset: 0;
  z-index: -3;
  pointer-events: none;
  overflow: hidden;
}
.world-bg-layer {
  position: absolute; inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity 1.6s ease;
}
.world-bg-layer.on { opacity: 0.16; }
.world-bg::after {                   /* settle the art into the room's dark */
  content: '';
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 50% 38%, rgba(8, 6, 13, 0) 0%, rgba(8, 6, 13, 0.5) 100%),
    linear-gradient(180deg, rgba(8, 6, 13, 0.22) 0%, rgba(8, 6, 13, 0.6) 100%);
}
@media (max-width: 768px) {
  .world-bg { display: none; }       /* phones skip the extra fixed layer */
}
@media (prefers-reduced-motion: reduce) {
  .world-bg-layer { transition: none; }   /* world art appears, no cross-fade */
}

/* ── 38. Boss presentation: gold-framed banner card ────────────────
   renderCombatPanel stamps is-boss on boss mob-cards (kept alongside the
   " ⚔ BOSS" name suffix). With art the card becomes a full-bleed banner
   with the HP bar riding on the painting; the art-less GM encounter
   preview just gets the gold frame + title treatment. ── */
.mob-card.is-boss {
  border-color: var(--neon-gold);
  box-shadow: 0 0 16px rgba(255, 215, 0, 0.22),
              inset 0 0 30px rgba(255, 215, 0, 0.05);
}
.mob-card.is-boss .mob-name {
  font-family: 'Orbitron', monospace;
  color: var(--neon-gold);
  letter-spacing: 1px;
  text-shadow: 0 0 10px rgba(255, 215, 0, 0.45);
}
.mob-card.is-boss.has-art {
  display: block;                    /* override §25's thumbnail flex row */
  position: relative;
  overflow: hidden;
  padding: 0;
  border-radius: 8px;
}
.mob-card.is-boss.has-art .mob-art {
  position: absolute; inset: 0;
  width: 100%; height: 100%; max-width: 100%;
  border: none; border-radius: 0;
  object-fit: cover; object-position: center 22%;
  box-shadow: none;
}
.mob-card.is-boss.has-art .mob-card-body {
  position: relative;                /* stacks above the full-bleed art */
  display: flex; flex-direction: column; justify-content: flex-end;
  min-height: 112px;
  min-width: 0;
  padding: 30px 10px 8px;
  background: linear-gradient(180deg,
    rgba(8, 6, 13, 0.05) 0%,
    rgba(8, 6, 13, 0.55) 55%,
    rgba(8, 6, 13, 0.92) 100%);
}
/* No new animation here (§25b's artIn pop-in is already neutralized in §26). */

/* ── 39. Clickable art: combat + battle-map imagery zooms ──────────
   openLightbox is wired onto .mob-art / .token-art; the battle-map token
   layer is pointer-events:none, so the img itself re-enables hit-testing
   (onTokenArtClick re-drives the cell's select/move flow first). ── */
.mob-art, .token-art { cursor: zoom-in; }
.bm-token .token-art { pointer-events: auto; }

/* ── 40. Chat avatars: party portraits beside the speaker label ────
   addMessage prepends .chat-avatar when the speaker is in the party
   map; flex keeps label alignment identical for avatar-less speakers
   (player rows re-anchor right via justify-content). No motion. ── */
.chat-message .speaker {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.chat-message.player .speaker { justify-content: flex-end; }
.chat-avatar {
  width: 26px; height: 26px;
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--stroke-strong, #3d2c63);
  box-shadow: 0 0 8px rgba(255, 45, 122, 0.25);
  background: var(--bg2, #0f0b18);
}

/* ── 41. Pending send: composing bubble + thinking System ──────────
   sendGameMessage sets a _sending guard, disables #send-btn, appends the
   ephemeral #composing-msg bubble (animated dots), and stamps .thinking
   on the DM sprite; its finally block tears all of it down, so errors
   surface exactly as before. ── */
#composing-msg .bubble {
  font-family: 'Orbitron', monospace;
  font-size: 11px;
  letter-spacing: 2px;
  color: var(--text-dim);
  border-style: dashed;
}
.composing-dots span {
  display: inline-block;
  animation: composeDot 1.2s ease-in-out infinite;
}
.composing-dots span:nth-child(2) { animation-delay: 0.2s; }
.composing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes composeDot {
  0%, 60%, 100% { opacity: 0.2; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-2px); }
}
#send-btn:disabled { opacity: 0.55; cursor: wait; filter: saturate(0.5); }
.dm-sprite.thinking .dm-eye {
  fill: var(--mana);
  filter: drop-shadow(0 0 3px var(--mana));
  animation: dmThink 1.1s ease-in-out infinite;
}
#dm-panel.thinking .dm-caption { color: var(--mana); }
@keyframes dmThink { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  /* Static variant — the bubble/sprite are removed by JS, nothing waits on
     animationend, so plain animation:none is safe here. */
  .composing-dots span { animation: none; opacity: 0.7; }
  .dm-sprite.thinking .dm-eye { animation: none; opacity: 1; }
}

/* ── 42. Floating combat text + hit shake ──────────────────────────
   floatNumber() spawns .float-num at the struck card's viewport rect;
   removal is a 950ms setTimeout, never animationend. Damage/crit also
   stamp .hit-shake on the card for one 0.4s wobble. ── */
.float-num {
  position: fixed;
  z-index: 9998;
  pointer-events: none;
  transform: translate(-50%, 0);
  font-family: 'Orbitron', monospace;
  font-weight: 700;
  font-size: 18px;
  text-shadow: 0 0 8px currentColor;
  animation: floatNumUp 0.9s ease-out forwards;
}
.float-num.fn-damage { color: var(--neon-pink); }
.float-num.fn-crit   { color: var(--neon-gold); font-size: 26px; letter-spacing: 1px; }
.float-num.fn-heal   { color: var(--green); }
.float-num.fn-miss   { color: var(--text-dim); font-size: 13px; letter-spacing: 2px; }
/* translate (not transform) for the drift so it composes with the centering */
@keyframes floatNumUp {
  0%   { opacity: 0; translate: 0 6px; }
  12%  { opacity: 1; translate: 0 0; }
  100% { opacity: 0; translate: 0 -44px; }
}
@keyframes floatNumFade {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  100% { opacity: 0; }
}
.hit-shake { animation: hitShake 0.4s ease-in-out; }
@keyframes hitShake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-5px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}
@media (prefers-reduced-motion: reduce) {
  /* Numbers appear/fade with opacity only — same 0.9s life, no drift. */
  .float-num { animation-name: floatNumFade; }
  .hit-shake { animation: none; }
}

/* ── 43. Message tags drive appearance ─────────────────────────────
   addMessage stamps data-tag on the message root. Styled tags are the
   real backend vocabulary: error/engine_error, the roll family, loot +
   shop_purchase, and scene narration. Static styles — no motion. ── */
.chat-message[data-tag="error"] .bubble,
.chat-message[data-tag="engine_error"] .bubble {
  border-color: var(--hb-low);
  background: rgba(255, 45, 122, 0.07);
}
.chat-message[data-tag="roll"] .bubble,
.chat-message[data-tag="dice_roll"] .bubble,
.chat-message[data-tag="secret_roll"] .bubble,
.chat-message[data-tag="initiative"] .bubble,
.chat-message[data-tag="check_result"] .bubble,
.chat-message[data-tag="check_nat20"] .bubble,
.chat-message[data-tag="check_nat1"] .bubble {
  font-variant-numeric: tabular-nums;
  border-left: 3px solid var(--neon-gold);
}
.chat-message[data-tag="loot"] .bubble::before,
.chat-message[data-tag="shop_purchase"] .bubble::before {
  content: '◈ ';
  color: var(--neon-gold);
}
.chat-message[data-tag="scene_change"] .bubble,
.chat-message[data-tag="scene_narration"] .bubble {
  font-style: italic;
  border-left: 3px solid var(--purple);
}

/* ── 44. "N new ↓" pill: un-jacked chat scroll ─────────────────────
   addMessage only autoscrolls when the reader is already near the bottom
   (own messages always follow). Otherwise the count accumulates in this
   pill, absolutely parked above the game input bar (which is already
   position:relative for #cmd-suggest — z-index stays below its 50). ── */
#new-msgs-pill {
  position: absolute;
  bottom: 100%;
  right: 14px;
  margin-bottom: 10px;
  z-index: 40;
  display: none;
  align-items: center;
  gap: 4px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--neon-pink);
  background: rgba(15, 11, 24, 0.92);
  color: var(--neon-pink);
  font-family: 'Rajdhani', sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1px;
  cursor: pointer;
  box-shadow: 0 0 12px rgba(255, 45, 122, 0.35);
}
#new-msgs-pill.show { display: inline-flex; animation: pillIn 0.25s ease; }
#new-msgs-pill:hover { background: rgba(255, 45, 122, 0.15); }
@keyframes pillIn {
  from { opacity: 0; translate: 0 6px; }
  to   { opacity: 1; translate: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  #new-msgs-pill.show { animation: none; }
}

/* ── 45. Layout dead zone + wide reading measure ───────────────────
   The inline tablet rule stops at 900px, so 901-1180px viewports ran the
   desktop 300px/1fr/320px grid and squeezed the story column to ~281px —
   narrower than a phone. Clamp the sidebars through that band. Above
   1600px, cap the chat column's reading measure (~78ch, centered). ── */
@media (min-width: 901px) and (max-width: 1180px) {
  .game-view.active {
    grid-template-columns: minmax(200px, 240px) minmax(0, 1fr) minmax(210px, 250px);
  }
}
@media (min-width: 1601px) {
  .chat-area,
  #game-chat-panel .input-bar,
  #private-chat-panel .input-bar,
  #cmd-bar,
  #prompt-chips,
  #roll-request-bar {
    width: 100%;
    max-width: 78ch;
    margin-left: auto;
    margin-right: auto;
  }
}

/* ── 46. Mobile tab badges ─────────────────────────────────────────
   switchMobilePanel/_setTabBadge stamp .tab-badge on the bottom tabs:
   unread count on Chat, "!" on Party when combat starts, pulsing "▶"
   when it's the local player's turn. Cleared on tab visit. ── */
.mobile-nav-tab { position: relative; }
.tab-badge {
  position: absolute;
  top: 3px;
  left: 50%;
  margin-left: 8px;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--neon-pink);
  color: #fff;
  font-family: 'Rajdhani', sans-serif;
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  box-shadow: 0 0 8px rgba(255, 45, 122, 0.6);
  pointer-events: none;
}
.tab-badge.combat {
  background: var(--neon-gold);
  color: #1a1206;
  box-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
}
.tab-badge.your-turn {
  background: var(--green);
  color: #06130a;
  animation: badgePulse 1.1s ease-in-out infinite;
}
@keyframes badgePulse {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 6px rgba(57, 255, 159, 0.5); }
  50%      { transform: scale(1.25); box-shadow: 0 0 14px rgba(57, 255, 159, 0.9); }
}
@media (prefers-reduced-motion: reduce) {
  .tab-badge.your-turn { animation: none; }
}

/* ── 47. Per-world skins: 31 worlds stop dressing identically ──────
   updateHUD stamps data-world on BOTH <html> and <body>; palettes live on
   :root[data-world=…] because the §0 alias chain is *defined* on :root —
   custom-property var()s resolve on the element that defines them, so a
   semantic override must land on :root for every Tier-3 alias to follow.
   Unlisted worlds (classic_fantasy, the_tuesday_that_won_t_end, …) keep the
   default dungeon-pink. Light text (--fg) is never overridden, and every
   brand below was spot-checked against the near-black surfaces.
   First: route the aurora + primary button through the semantic tier so the
   reskin reaches them too (defaults resolve to the exact shipped colors). */
:root {
  --aurora-a: color-mix(in srgb, var(--brand) 5.5%, transparent);
  --aurora-b: color-mix(in srgb, var(--accent-cool) 4.5%, transparent);
  --aurora-c: color-mix(in srgb, var(--arcane) 5%, transparent);
  /* Primary-button gradient endpoints. Defaults are the EXACT shipped
     hexes (§2 line ~183); per-world palettes below override them with
     brand-derived mixes so skins keep their tinted buttons. */
  --btn-primary-hi: #ff3d86;
  --btn-primary-lo: #b3124e;
}
body::before {
  background:
    radial-gradient(ellipse 42% 30% at 30% 35%, var(--aurora-a), transparent 70%),
    radial-gradient(ellipse 38% 28% at 72% 60%, var(--aurora-b), transparent 70%),
    radial-gradient(ellipse 30% 24% at 55% 25%, var(--aurora-c), transparent 70%);
}
button.primary {
  background: linear-gradient(135deg,
    var(--btn-primary-hi) 0%,
    var(--brand) 45%,
    var(--btn-primary-lo) 100%);
}
button.primary:hover {
  box-shadow: 0 0 25px color-mix(in srgb, var(--brand) 55%, transparent),
              0 4px 18px rgba(0, 0, 0, 0.4);
}

/* Cultivation jade — sect robes and immortal-spring green */
:root[data-world="ninefold_cloud_sect"] {
  --brand: #17a06d; --highlight: #ffd700;
  --accent-cool: #4be8c9; --arcane: #8f7bff; --info: #7ac9ff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* VRMMO glitch — patch-notes cyan + toxic-lime UI */
:root[data-world="closed_beta_build_0_9"],
:root[data-world="logout_disabled"],
:root[data-world="the_bracket"],
:root[data-world="first_to_a_million"],
:root[data-world="player_two_never_existed"],
:root[data-world="logged_in_forever"] {
  --brand: #00d9f2; --highlight: #c8ff4d;
  --accent-cool: #4d7bff; --arcane: #ff4de3;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* System-apocalypse ember — the sky is on fire and so are the quests */
:root[data-world="hometown_instance"],
:root[data-world="integration_day"],
:root[data-world="protocol_earthfall"],
:root[data-world="the_overnight_spire"],
:root[data-world="the_gate_brokers"] {
  --brand: #ff5c33; --highlight: #ffc247;
  --accent-cool: #58c8ff; --arcane: #d96bff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Noir brass — smoke, lamplight, and a ledger nobody audits */
:root[data-world="larcency_and_co"] {
  --brand: #cf9b2e; --highlight: #ffe08a;
  --accent-cool: #6bd6ff; --arcane: #a06bff; --info: #86a6ff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Academy / spectacle crimson — house colors drawn in blood-red ink */
:root[data-world="rankfall_academy"],
:root[data-world="refund_denied"],
:root[data-world="the_undercard"],
:root[data-world="prime_time_descent"],
:root[data-world="sponsored_descent"] {
  --brand: #e63757; --highlight: #ffd75e;
  --accent-cool: #7ab8ff; --arcane: #a64dff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Tower ice — thin air, glass floors, checkpoint blue */
:root[data-world="the_hundredth_rung"],
:root[data-world="the_glass_ascendant"],
:root[data-world="respawn_purgatory"],
:root[data-world="checkpoint_mercy"] {
  --brand: #2e7fe6; --highlight: #cfe8ff;
  --accent-cool: #9ff2ff; --arcane: #8f9bff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Carnival candy — wholesome pastels over a deadly cellar */
:root[data-world="family_fun_dungeon"],
:root[data-world="creature_capture_classic"],
:root[data-world="creature_capture_with_guns"] {
  --brand: #f052ae; --highlight: #ffe066;
  --accent-cool: #64e3c6; --arcane: #b28cff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Hearth amber — market smoke, copper pans, warm coin */
:root[data-world="the_simmering_frontier"],
:root[data-world="summoning_clause"] {
  --brand: #e0801f; --highlight: #ffd98a;
  --accent-cool: #6bd0c8; --arcane: #c98cff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Spawn-pool venom — you ARE the dungeon-side now */
:root[data-world="bottom_of_the_spawn_pool"],
:root[data-world="keepers_of_the_deep"] {
  --brand: #49a82a; --highlight: #d6ff4d;
  --accent-cool: #4de1ff; --arcane: #a64dff;
  --btn-primary-hi: color-mix(in srgb, var(--brand) 86%, #fff);
  --btn-primary-lo: color-mix(in srgb, var(--brand) 62%, #000);
}
/* Bright-brand worlds: white-on-brand fails contrast, so their primary
   buttons take dark ink (same treatment §31 gave button.blue). */
:root[data-world="closed_beta_build_0_9"] button.primary,
:root[data-world="logout_disabled"] button.primary,
:root[data-world="the_bracket"] button.primary,
:root[data-world="first_to_a_million"] button.primary,
:root[data-world="player_two_never_existed"] button.primary,
:root[data-world="logged_in_forever"] button.primary,
:root[data-world="larcency_and_co"] button.primary,
:root[data-world="the_simmering_frontier"] button.primary,
:root[data-world="summoning_clause"] button.primary,
:root[data-world="bottom_of_the_spawn_pool"] button.primary,
:root[data-world="keepers_of_the_deep"] button.primary {
  color: #071016;
  text-shadow: none;
}
/* No new animation in this section — nothing to neutralize. */

/* ── 48. Reactive atmosphere: the room feels the danger state ──────
   updateHUD stamps body[data-danger= calm|combat|boss|critical ]. Combat
   quickens the aurora; a boss swaps its cyan lobe for a red one; boss and
   critical tighten the §1 vignette from 62% to 48%; critical adds a slow
   red heartbeat at the screen edges. Pure CSS from one line of JS state. */
body[data-danger="combat"]::before { animation-duration: 24s; }
body[data-danger="boss"]::before,
body[data-danger="critical"]::before { animation-duration: 14s; }
body[data-danger="boss"],
body[data-danger="critical"] {
  --aurora-b: color-mix(in srgb, #ff2437 7%, transparent);
}
body[data-danger="boss"]::after,
body[data-danger="critical"]::after {
  background:
    repeating-linear-gradient(0deg,
      rgba(255, 255, 255, 0.012) 0px, rgba(255, 255, 255, 0.012) 1px,
      transparent 1px, transparent 3px),
    radial-gradient(ellipse 120% 100% at 50% 45%, transparent 48%, rgba(4, 2, 8, 0.5) 100%);
}
body[data-danger="critical"]::after {
  animation: crimsonPulse 2.4s ease-in-out infinite;
}
@keyframes crimsonPulse {
  0%, 100% { box-shadow: inset 0 0 90px 20px rgba(255, 32, 64, 0.10); }
  50%      { box-shadow: inset 0 0 160px 45px rgba(255, 32, 64, 0.30); }
}
@media (prefers-reduced-motion: reduce) {
  /* Heartbeat off (nothing dismisses on it); §26 already stops the aurora,
     so the duration overrides above are inert there. */
  body[data-danger="critical"]::after { animation: none; }
}

/* ── 49. THE BLUE WINDOW: System stat readouts, machine-cold ───────
   addMessage routes multi-line System readouts (help/log/achievements/
   conditions/initiative/town + `=== … ===` blocks) through
   renderSystemWindow(): monospace tabular rows, hairline cyan frame,
   corner brackets, a 1px scanline wash, and a 220ms boot snap. */
.chat-message.has-sys-window .bubble {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
  backdrop-filter: none;
}
.sys-window {
  position: relative;
  max-width: 100%;
  overflow-x: auto;
  padding: 10px 14px 12px;
  border: 1px solid rgba(0, 212, 255, 0.55);
  border-radius: 4px;
  background:
    repeating-linear-gradient(0deg,
      rgba(0, 212, 255, 0.028) 0px, rgba(0, 212, 255, 0.028) 1px,
      transparent 1px, transparent 3px),
    linear-gradient(165deg, rgba(0, 40, 56, 0.5), rgba(8, 6, 13, 0.92));
  box-shadow: 0 0 22px rgba(0, 212, 255, 0.12),
              inset 0 0 30px rgba(0, 212, 255, 0.05);
  transform-origin: 50% 0;
  animation: swBoot 0.22s var(--ease-out, ease) both;
}
/* Corner brackets: eight 2px strips, two per corner, one pseudo. */
.sys-window::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) left 0 top 0 / 12px 2px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) left 0 top 0 / 2px 12px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) right 0 top 0 / 12px 2px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) right 0 top 0 / 2px 12px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) left 0 bottom 0 / 12px 2px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) left 0 bottom 0 / 2px 12px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) right 0 bottom 0 / 12px 2px,
    linear-gradient(var(--accent-cool, #00d4ff), var(--accent-cool, #00d4ff)) right 0 bottom 0 / 2px 12px;
  background-repeat: no-repeat;
}
.sw-title {
  font-family: 'Orbitron', monospace;
  font-size: 11px;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--accent-cool, #00d4ff);
  text-shadow: 0 0 10px rgba(0, 212, 255, 0.45);
  padding-bottom: 5px;
  margin-bottom: 7px;
  border-bottom: 1px solid rgba(0, 212, 255, 0.25);
}
.sw-body {
  font-family: Consolas, 'Cascadia Mono', 'SF Mono', 'Roboto Mono', monospace;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--fg, #e8daf0);
  font-variant-numeric: tabular-nums;
}
.sw-row {
  white-space: pre-wrap;      /* keep the readout's own indentation */
  word-break: break-word;
  min-width: 0;
}
.sw-row strong { color: var(--accent-cool, #00d4ff); font-weight: 700; }
.sw-row.sw-gap { height: 8px; }
@keyframes swBoot {
  from { opacity: 0; transform: scaleY(0.55); }
  to   { opacity: 1; transform: scaleY(1); }
}
/* History replay renders instantly (JS stamps .sw-still when quiet). */
.chat-message.sw-still .sys-window { animation: none; }
@media (prefers-reduced-motion: reduce) {
  /* Boot snap becomes an opacity-only fade — same duration + fill. The
     window persists (never dismissed by animationend), so this is purely
     a motion swap. */
  .sys-window { animation: swBootFade 0.22s ease both; }
}
@keyframes swBootFade { from { opacity: 0; } to { opacity: 1; } }

/* ── 50. Death ceremony: a delver's end is a moment ────────────────
   deathCeremony() mirrors levelUpCeremony's mechanics exactly: fixed
   overlay elements appended to body, removed by setTimeout (5s, 7s on
   permadeath) — no animationend dependency anywhere. Portrait desaturates
   over 900ms; permadeath gets the darker, harder variant. */
.death-shroud {
  position: fixed;
  inset: 0;
  z-index: 9995;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 42%,
    rgba(80, 0, 14, 0.30) 0%, rgba(4, 1, 3, 0.78) 100%);
  animation: dbFadeIn 0.7s ease both;
}
.death-shroud.permadeath {
  background: radial-gradient(ellipse at 50% 42%,
    rgba(52, 0, 8, 0.55) 0%, rgba(2, 0, 1, 0.9) 100%);
}
.death-banner {
  position: fixed;
  top: 40%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9996;
  text-align: center;
  padding: 26px 42px;
  max-width: min(92vw, 440px);
  border: 2px solid #a31515;
  border-radius: 14px;
  background: linear-gradient(155deg, rgba(255, 45, 60, 0.10), var(--bg2, #0f0b18));
  box-shadow: 0 0 60px rgba(255, 32, 64, 0.35), 0 30px 80px rgba(0, 0, 0, 0.6);
  animation: deathIn 0.6s var(--ease-out, ease) both;
}
.death-banner.permadeath {
  border-color: #5c0810;
  background: linear-gradient(155deg, rgba(120, 0, 16, 0.16), #070408);
  box-shadow: 0 0 90px rgba(120, 0, 16, 0.5), 0 30px 80px rgba(0, 0, 0, 0.75);
}
.db-portrait {
  width: 84px;
  height: 84px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid #a31515;
  margin-bottom: 10px;
  filter: grayscale(1) brightness(0.72);     /* rest state after the fade */
  animation: dbDesat 0.9s ease both;
}
.db-label {
  font-family: 'Orbitron', monospace;
  font-size: 13px;
  letter-spacing: 6px;
  color: #ff3b5c;
  text-shadow: 0 0 18px rgba(255, 45, 80, 0.6);
}
.death-banner.permadeath .db-label { color: #c9182f; text-shadow: none; }
.db-name { font-size: 21px; font-weight: 800; color: var(--fg, #e8daf0); margin-top: 5px; }
.db-aside {
  font-size: 12px;
  font-style: italic;
  color: var(--fg-muted, #a396c2);
  max-width: 340px;
  margin: 9px auto 0;
}
.db-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  justify-content: center;
  margin-top: 13px;
  font-family: 'Orbitron', monospace;
  font-size: 12px;
  letter-spacing: 1px;
  color: var(--highlight, #ffd700);
}
.db-share { margin-top: 15px; font-size: 11px; padding: 5px 14px; }
@keyframes deathIn {
  from { opacity: 0; transform: translate(-50%, -50%) scale(1.07); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes dbDesat {
  from { filter: grayscale(0) brightness(1); }
  to   { filter: grayscale(1) brightness(0.72); }
}
@keyframes dbFadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (max-width: 520px) {
  .death-banner { padding: 18px 16px; max-width: calc(100vw - 24px); }
}
@media (prefers-reduced-motion: reduce) {
  /* Static card: opacity-only entrance; the portrait rests desaturated.
     Auto-dismiss is a JS setTimeout, so removal never depended on these. */
  .death-banner { animation: dbFadeIn 0.6s ease both; }
  .db-portrait { animation: none; }
}

/* ── 51. The System has moods: eye flares + dread blink ────────────
   pulseDM('crit'|'dread') stamps class-driven states beside the existing
   .talking treatment; at body[data-danger=critical] the eyes dim to a slow
   blink on their own. Captions rotate through original bark pools in JS. */
.dm-sprite.crit .dm-eye {
  fill: var(--brand, #ff2d7a);
  animation: none;
  filter: drop-shadow(0 0 4px var(--brand, #ff2d7a))
          drop-shadow(0 0 11px color-mix(in srgb, var(--brand, #ff2d7a) 55%, transparent));
}
#dm-panel.crit .dm-caption { color: var(--brand, #ff2d7a); }
.dm-sprite.dread .dm-eye {
  fill: #ff3b5c;
  animation: dmDreadBlink 2.6s ease-in-out infinite;
}
#dm-panel.dread .dm-caption { color: #ff3b5c; }
body[data-danger="critical"] .dm-eye {
  fill: #ff3b5c;
  animation: dmDreadBlink 3.4s ease-in-out infinite;
}
@keyframes dmDreadBlink {
  0%, 100% { opacity: 0.22; }
  50%      { opacity: 0.85; }
}
@media (prefers-reduced-motion: reduce) {
  /* These selectors out-rank §26's bare .dm-eye reset, so they need their
     own neutralizer. Nothing dismisses on the blink — plain none is safe. */
  .dm-sprite.dread .dm-eye,
  body[data-danger="critical"] .dm-eye { animation: none; opacity: 0.55; }
}

/* ── 52. Reduced motion: inline-sheet stragglers ───────────────────
   Five keyframes in the inline sheet ignore the setting: fadeIn (chat),
   toastIn, achieveIn, rollCardIn, and §14's slow scene zoom. Everything
   below dismisses on a JS setTimeout or a re-render — never animationend
   (verified: no animationend listeners exist in the app) — so opacity-only
   swaps and plain none are both safe. Chat keeps a soft fade so messages
   don't pop harshly; timed popups keep their exact in/out timings. */
@keyframes rmSoftFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes rmSoftOut  { from { opacity: 1; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .chat-message       { animation: rmSoftFade 0.3s ease; }
  .world-shifted-beat { animation: rmSoftFade 0.4s ease; }
  .toast              { animation: rmSoftFade 0.3s ease,  rmSoftOut 0.3s ease 3.7s forwards; }
  .levelup-banner     { animation: rmSoftFade 0.45s ease, rmSoftOut 0.5s ease 3.5s forwards; }
  .achievement-popup  { animation: rmSoftFade 0.5s ease,  rmSoftOut 0.5s ease 4s forwards; }
  /* Icon spring (achieveIconPop, inline sheet ~1331): scale+rotate — drop it. */
  .achievement-popup .achieve-icon { animation: none; }
  .roll-request-card  { animation: none; }
  /* §14's 6s zoom: keep the 1.1s art crossfade, drop the movement. */
  .scene-stage .scene-img { transition: opacity 1.1s ease; }
  .scene-stage.has-image.doors-open .scene-img { transform: none; }
}

/* ── 53. Compositor-friendly loops: bar shimmer + combat glow ──────
   barShimmer (§9) animated `left` — a 5-delver party in combat runs ~19
   of those layout/paint loops at once. Same sweep, transform-only: the
   strip is 45% of the bar wide and starts at left:-60%, so the old
   -60% → 130% run of the PARENT's width equals 190/45 ≈ 422% of the
   strip's OWN width (translateX %% resolves against the element). */
.bar-fill::after {
  animation: barShimmerX 2.8s var(--ease-out) infinite;
}
@keyframes barShimmerX {
  0%        { transform: skewX(-18deg) translateX(0); }
  55%, 100% { transform: skewX(-18deg) translateX(422.3%); }
}
@media (max-width: 768px) {
  /* keep §23's slower-shimmer phone guard, which this shorthand outranks */
  .bar-fill::after { animation-duration: 4s; }
}
/* combatBreath (§10) animated box-shadow (paint). Pin the panel at its
   rest shadow and breathe an overlay's opacity instead (composited).
   Peak = rest + overlay ≈ the old 50% frame. .combat-panel had no
   ::after before this and no absolutely-positioned descendants that
   anchor past it, so position:relative is safe. */
.combat-panel {
  position: relative;
  animation: none;
  box-shadow: inset 0 0 30px rgba(255, 45, 122, 0.05), 0 0 16px rgba(255, 45, 122, 0.18);
}
.combat-panel::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 0 30px rgba(255, 45, 122, 0.04), 0 0 26px rgba(255, 45, 122, 0.26);
  opacity: 0;
  animation: combatBreathX 2.6s ease-in-out infinite;
}
@keyframes combatBreathX { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  /* These rules out-cascade §26's earlier resets — re-neutralize here.
     Neither loop gates any dismissal; plain none is safe. */
  .bar-fill::after { animation: none; }
  .combat-panel::after { animation: none; }  /* rests invisible at opacity 0 */
}

/* ── 54. Touch targets: ≥44px effective hit areas, unchanged visuals ─
   No new animations or transitions in this section. */
/* Row delete buttons (~20×24 visual): grow padding, pull margins back in,
   so the glyph and row layout don't move. The left reach stops inside the
   row's 6px flex gap so taps meant for the neighboring input can't land
   on a destructive delete. */
.remove-btn {
  padding: 13px 14px 13px 8px;
  margin: -11px -8px -11px -2px;
}
/* Health-bead slots (28×22, clickable mid-combat) sit in a 3px-gap row:
   overlay expands the tap zone to 44px tall but only ±1px sideways, so
   neighboring beads never overlap. */
.hb-slot { position: relative; }
.hb-slot::after {
  content: '';
  position: absolute;
  left: -1px; right: -1px;
  top: -11px; bottom: -11px;
}
/* Command chips shrink to ~24px tall on phones: extend reach vertically
   only — the scrollable row's 3-4px gaps stay distinct sideways. */
#cmd-bar button { position: relative; }
#cmd-bar button::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 0; bottom: -10px;
}

/* ═══════════════════════════════════════════════════════════════════
   §55. BRAND 2026 — "Clean Modern Game UI"  (character creation)
   ───────────────────────────────────────────────────────────────────
   Round-1 scope: the character-creation / character-sheet modal ONLY.
   Every rule here is scoped under #char-modal so the 9 per-world skins,
   the lobby, the landing page and the in-game HUD are untouched. When
   this look is approved, lift the token block to :root and drop the
   #char-modal prefixes to roll it app-wide.

   Direction: near-black charcoal ground, soft slate panels with real
   edges, ONE accent (amber), near-white text, clean system-UI type.
   Replaces the competing neon-pink/gold/cyan/green palette that made
   the creation screen read as four screens shouting at once.
   ═══════════════════════════════════════════════════════════════════ */

#char-modal {
  /* — Brand primitives — */
  --b2-ink:        #0e1116;   /* page ground */
  --b2-surface:    #151a21;   /* panel */
  --b2-surface-2:  #1c222b;   /* raised panel / input */
  --b2-surface-3:  #232a35;   /* hover */
  --b2-line:       #2c343f;   /* hairline */
  --b2-line-strong:#3a4553;   /* stronger stroke */
  --b2-text:       #eef2f7;   /* near-white */
  --b2-text-2:     #aeb8c6;   /* secondary */
  --b2-text-3:     #7d8797;   /* tertiary / hints */
  --b2-accent:     #f5a524;   /* THE accent — amber */
  --b2-accent-ink: #1a1206;   /* text on accent */
  --b2-accent-dim: rgba(245,165,36,0.12);
  --b2-danger:     #f0526a;
  --b2-danger-dim: rgba(240,82,106,0.12);
  --b2-ok:         #38c793;
  --b2-ok-dim:     rgba(56,199,147,0.12);

  --b2-font: 'Inter var', Inter, -apple-system, BlinkMacSystemFont,
             'Segoe UI Variable Text', 'Segoe UI', Roboto,
             'Helvetica Neue', Arial, sans-serif;
  --b2-r:    10px;
  --b2-r-sm: 7px;
}

/* ── 55a. Modal shell + type ─────────────────────────────────────── */
#char-modal .modal {
  background: var(--b2-ink);
  border: 1px solid var(--b2-line);
  border-radius: 14px;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  font-family: var(--b2-font);
  color: var(--b2-text);
}
#char-modal, #char-modal * { font-family: var(--b2-font); }
#char-modal h2 {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;   /* modern UI type tightens, not tracks out */
  color: var(--b2-text);
  text-shadow: none;
}

/* Labels: the old style was 11px uppercase in a dim purple-grey — the
   single biggest readability loss on this screen. Sentence case, larger,
   higher contrast. */
#char-modal .modal label,
#char-modal label {
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--b2-text-2);
  margin-bottom: 5px;
}

/* ── 55b. Inputs ─────────────────────────────────────────────────── */
#char-modal input[type=text],
#char-modal input[type=number],
#char-modal textarea,
#char-modal select {
  background: var(--b2-surface-2);
  border: 1px solid var(--b2-line);
  color: var(--b2-text);
  border-radius: var(--b2-r-sm);
  padding: 9px 11px;
  font-size: 14px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
#char-modal input::placeholder,
#char-modal textarea::placeholder { color: var(--b2-text-3); }
#char-modal input:focus,
#char-modal textarea:focus,
#char-modal select:focus {
  outline: none;
  border-color: var(--b2-accent);
  box-shadow: 0 0 0 3px var(--b2-accent-dim);
}

/* ── 55c. REQUIRED / OPTIONAL markers ────────────────────────────── */
.req-badge {
  display: inline-block;
  margin-left: 7px;
  padding: 2px 7px;
  border-radius: var(--r-pill, 999px);
  background: var(--b2-accent);
  color: var(--b2-accent-ink);
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  vertical-align: middle;
}
.opt-badge {
  display: inline-block;
  margin-left: 7px;
  padding: 2px 7px;
  border-radius: var(--r-pill, 999px);
  background: transparent;
  border: 1px solid var(--b2-line-strong);
  color: var(--b2-text-3);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  vertical-align: middle;
}

/* ── 55d. The REQUIRED bar — live checklist at the top of creation ── */
#cs-required-bar {
  background: var(--b2-surface);
  border: 1px solid var(--b2-line);
  border-left: 3px solid var(--b2-accent);
  border-radius: var(--b2-r);
  padding: 13px 15px;
  margin-bottom: 14px;
}
#cs-required-bar .rq-head {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--b2-accent);
  margin-bottom: 7px;
}
#cs-required-bar .rq-item {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 14px;
  font-weight: 600;
  color: var(--b2-text);
  padding: 5px 0;
  cursor: pointer;
  background: none;
  border: 0;
  text-align: left;
  width: 100%;
  font-family: inherit;
}
#cs-required-bar .rq-item:hover { color: var(--b2-accent); }
#cs-required-bar .rq-dot {
  flex: 0 0 auto;
  width: 17px; height: 17px;
  border-radius: 50%;
  border: 2px solid var(--b2-accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 800;
  color: var(--b2-accent-ink);
}
#cs-required-bar.done .rq-dot {
  background: var(--b2-ok);
  border-color: var(--b2-ok);
}
#cs-required-bar.done { border-left-color: var(--b2-ok); }
#cs-required-bar.done .rq-head { color: var(--b2-ok); }
#cs-required-bar .rq-note {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--b2-text-3);
  margin-top: 6px;
}

/* ── 55e. Invalid-field state ────────────────────────────────────────
   Replaces "a dim grey sentence in the footer you never scrolled to". */
#char-modal .field-invalid input,
#char-modal input.field-invalid,
#char-modal select.field-invalid {
  border-color: var(--b2-danger) !important;
  box-shadow: 0 0 0 3px var(--b2-danger-dim) !important;
}
.field-error {
  display: none;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--b2-danger);
  margin-top: 5px;
}
.field-error.show { display: block; }
@keyframes b2-shake {
  0%,100% { transform: translateX(0); }
  25%     { transform: translateX(-5px); }
  75%     { transform: translateX(5px); }
}
.b2-shake { animation: b2-shake .28s ease; }
@media (prefers-reduced-motion: reduce) {
  .b2-shake { animation: none; }
}

/* ── 55f. Numbered steps ─────────────────────────────────────────── */
#char-modal .cs-step {
  background: var(--b2-surface);
  border: 1px solid var(--b2-line);
  border-radius: var(--b2-r);
  padding: 15px;
  margin-bottom: 14px;
}
#char-modal .cs-step-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 4px;
}
#char-modal .cs-step-num {
  flex: 0 0 auto;
  width: 23px; height: 23px;
  border-radius: 50%;
  background: var(--b2-accent);
  color: var(--b2-accent-ink);
  font-size: 12px;
  font-weight: 800;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
#char-modal .cs-step-title {
  font-size: 15.5px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--b2-text);
}
#char-modal .cs-step-sub {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--b2-text-3);
  margin: 0 0 11px 33px;
}

/* ── 55g. Build-path cards (replaces 3 competing neon panels) ────── */
.cs-paths {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px;
}
.cs-path {
  position: relative;
  text-align: left;
  background: var(--b2-surface-2);
  border: 1px solid var(--b2-line);
  border-radius: var(--b2-r);
  padding: 13px;
  cursor: pointer;
  font-family: inherit;
  transition: border-color .15s ease, background .15s ease, transform .15s ease;
}
.cs-path:hover {
  background: var(--b2-surface-3);
  border-color: var(--b2-line-strong);
  transform: translateY(-1px);
}
.cs-path.selected {
  border-color: var(--b2-accent);
  background: var(--b2-accent-dim);
}
.cs-path-name {
  display: block;              /* spans inside a <button> stay inline otherwise */
  font-size: 14.5px;
  font-weight: 700;
  color: var(--b2-text);
  margin-bottom: 4px;
}
.cs-path-desc {
  display: block;
  font-size: 12px;
  line-height: 1.5;
  color: var(--b2-text-3);
}
.cs-path-tag {
  position: absolute;
  top: -8px; right: 11px;
  padding: 2px 8px;
  border-radius: var(--r-pill, 999px);
  background: var(--b2-accent);
  color: var(--b2-accent-ink);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

/* ── 55h. Advanced disclosure ────────────────────────────────────── */
#cs-advanced-toggle {
  width: 100%;
  background: transparent;
  border: 1px dashed var(--b2-line-strong);
  border-radius: var(--b2-r);
  color: var(--b2-text-3);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 11px;
  cursor: pointer;
  margin-bottom: 12px;
  transition: color .15s ease, border-color .15s ease;
}
#cs-advanced-toggle:hover {
  color: var(--b2-accent);
  border-color: var(--b2-accent);
}

/* ── 55i. Tabs + buttons in the new palette ──────────────────────── */
#char-modal .cs-tab {
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--b2-text-3);
}
#char-modal .cs-tab.active {
  color: var(--b2-accent);
  border-bottom-color: var(--b2-accent);
}
#char-modal .cs-section {
  background: var(--b2-surface);
  border: 1px solid var(--b2-line);
  border-radius: var(--b2-r);
}
#char-modal .cs-section-title {
  font-family: inherit;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--b2-text-3);
}
#char-modal button.green,
#char-modal #cs-guided-build-btn {
  background: var(--b2-accent);
  border: 1px solid var(--b2-accent);
  color: var(--b2-accent-ink);
  font-family: inherit;
  font-weight: 700;
  font-size: 14px;
  border-radius: var(--b2-r-sm);
  text-shadow: none;
  box-shadow: none;
}
#char-modal button.green:hover,
#char-modal #cs-guided-build-btn:hover { filter: brightness(1.08); }
#char-modal #cs-guided-build-btn:disabled { opacity: .55; cursor: not-allowed; }

/* Creation mode: collapse the full sheet until the player asks for it.
   #cs-advanced-toggle flips this class. Existing characters never get it. */
#char-modal.creating-simple .cs-tabs,
#char-modal.creating-simple .cs-page { display: none !important; }
/* Marking the Identity name field on the sheet itself, for Full Sheet mode */
#char-modal .cs-name-label .req-badge { vertical-align: baseline; }
