/* ─────────────────────────────────────────────────────────────────
   Scenario Mode CSS — SP Vertical Slice ("Sigillite's Edict" + future scenarios)
   Only renders visible UI when body.scenario-mode is set.
   UX1 placeholder: static score strip + dialogue overlay + faction tint.
   L-series replaces with live logic (dynamic score, timed dialogue, etc).
   ─────────────────────────────────────────────────────────────── */

/* Hide by default — only visible in scenario mode. The later
   `#scenario-dialogue { display: flex; ... }` block at line ~632 used
   to override this via equal-specificity cascade, leaking a ghost
   dialogue into non-scenario fixtures (#369 human-test bug). Use a
   more-specific `body:not(.scenario-mode)` hide so the default rule
   wins regardless of how other `#scenario-dialogue` blocks are
   ordered. */
#scenario-score-strip,
#scenario-dialogue {
  display: none;
}
body:not(.scenario-mode) #scenario-dialogue {
  display: none !important;
}

/* ── UX4 End-of-scenario screen ────────────────────────────── */
#scenario-end {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: radial-gradient(ellipse at center, #05080d 0%, #020304 70%, #000 100%), #000;
  color: #f4ecd0;
  align-items: center;
  justify-content: center;
  overflow-y: auto;
  padding: 40px 20px;
  opacity: 0;
  transition: opacity 600ms ease-out;
}
body.scenario-end-visible #scenario-end {
  display: flex;
  opacity: 1;
}

.scenario-end-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  max-width: 740px;
  width: 100%;
  text-align: center;
}

.scenario-end-tagline {
  font: 400 14px/1.55 'Rajdhani', sans-serif;
  font-style: italic;
  color: rgba(244, 236, 208, 0.7);
  opacity: 0;
  animation: scenario-fade-in 700ms ease-out 400ms forwards;
}
/* Collapse the tagline slot when end-screen has no narrator line — Voryn
   typewriter is the in-character text now. `:empty` matches when
   textContent is ''. */
.scenario-end-tagline:empty { display: none; }

/* Medal SVG retired — the Voryn portrait sits in its slot. */
.scenario-end-medal { display: none; }
.scenario-end-medal-name[hidden] { display: none; }

/* Voryn portrait — mirrors cold-open's `.scenario-intro-portrait`. */
.scenario-end-portrait {
  width: 132px; height: 132px;
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--faction-a) 60%, transparent);
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--faction-a) 18%, transparent),
    color-mix(in srgb, var(--faction-a) 4%, transparent));
  position: relative;
  overflow: hidden;
  filter: drop-shadow(0 0 22px color-mix(in srgb, var(--faction-a) 35%, transparent));
  opacity: 0;
  transform: translateY(8px) scale(0.95);
  animation: scenario-fade-in 900ms ease-out 500ms forwards,
             end-portrait-rise 900ms ease-out 500ms forwards;
}
@keyframes end-portrait-rise {
  to { opacity: 1; transform: translateY(0) scale(1); }
}

/* Codec dialogue block: portrait → speaker → typewriter line. SOTA
   end-screens (Hades, Helldivers 2 mission summary) treat these as one
   tight unit, then a clear margin gap to the score block — that visual
   separation is what makes the "who's speaking → result" beat read. */
.scenario-end-speaker {
  font: 700 11px/1 'Rajdhani', sans-serif;
  letter-spacing: 5px;
  color: var(--faction-a, #ffcc33);
  text-transform: uppercase;
  margin-top: 10px;
  opacity: 0;
  animation: scenario-fade-in 700ms ease-out 1000ms forwards;
}

.scenario-end-typewriter {
  font: 400 16px/1.5 'Rajdhani', sans-serif;
  font-style: italic;
  color: #f4ecd0;
  max-width: 540px;
  min-height: 48px;
  margin-top: 10px;
  text-align: center;
}

.scenario-end-score {
  font: 700 48px/1 'Anton', 'Rajdhani', sans-serif;
  letter-spacing: 2px;
  color: #fffadb;
  text-shadow: 0 0 22px color-mix(in srgb, var(--faction-a) 35%, transparent);
  font-variant-numeric: tabular-nums;
  margin-top: 32px;
  opacity: 0;
  animation: scenario-fade-in 700ms ease-out 2300ms forwards;
}
.scenario-end-score-label {
  font: 500 9px/1 'Rajdhani', sans-serif;
  letter-spacing: 4px;
  color: rgba(255, 255, 255, 0.45);
  text-transform: uppercase;
  margin-top: 4px;
  margin-bottom: 20px;
  opacity: 0;
  animation: scenario-fade-in 700ms ease-out 2500ms forwards;
}

.scenario-end-ledger {
  width: 100%;
  max-width: 460px;
  background: rgba(12, 16, 22, 0.7);
  border: 1px solid color-mix(in srgb, var(--faction-a) 18%, transparent);
  border-radius: 4px;
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  opacity: 0;
  animation: scenario-fade-in 800ms ease-out 2800ms forwards;
}
.scenario-end-ledger-row {
  display: flex;
  justify-content: space-between;
  font: 500 12px/1 'Rajdhani', sans-serif;
  letter-spacing: 0.5px;
  color: rgba(244, 236, 208, 0.85);
}
.scenario-end-ledger-row .val {
  color: #fffadb;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.scenario-end-ledger-row.negative .val { color: #f29080; }
.scenario-end-ledger-row.bonus   .val { color: var(--faction-a, #ffcc33); }
.scenario-end-ledger-row.total   { border-top: 1px solid color-mix(in srgb, var(--faction-a) 25%, transparent); padding-top: 8px; margin-top: 6px; font-weight: 700; font-size: 13px; }
.scenario-end-ledger-row.total .val { color: var(--faction-a); }

.scenario-end-daily {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 16px;
  opacity: 0;
  animation: scenario-fade-in 700ms ease-out 3200ms forwards;
}
.scenario-end-daily-line {
  font: 500 11px/1.4 'Rajdhani', sans-serif;
  letter-spacing: 1.5px;
  color: rgba(255, 255, 255, 0.6);
}

.scenario-end-cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin-top: 26px;
  opacity: 0;
  animation: scenario-fade-in 700ms ease-out 3500ms forwards;
}
.scenario-end-cta .scenario-end-btn { min-width: 220px; }
.scenario-end-btn {
  height: 42px;
  padding: 0 22px;
  border: 1px solid color-mix(in srgb, var(--faction-a) 40%, transparent);
  background: transparent;
  color: var(--faction-a, #ffcc33);
  font: 700 11px/1 'Rajdhani', sans-serif;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  cursor: pointer;
  transition: all 180ms;
}
.scenario-end-btn:hover {
  background: color-mix(in srgb, var(--faction-a) 15%, transparent);
  border-color: var(--faction-a);
  box-shadow: 0 0 14px color-mix(in srgb, var(--faction-a) 30%, transparent);
}
.scenario-end-btn.primary {
  border-color: var(--faction-a);
  background: var(--faction-a);
  color: #0a0d12;
}
.scenario-end-btn.primary:hover {
  box-shadow: 0 0 22px color-mix(in srgb, var(--faction-a) 50%, transparent);
  filter: brightness(1.08);
}
.scenario-end-btn.outline { font-size: 10px; letter-spacing: 2px; }

/* Shared with intro-fade-in — single keyframe used by both end screen + cold open. */
@keyframes scenario-fade-in { to { opacity: 1; } }

/* ── UX6 Cold-open intro overlay ───────────────────────────── */
#scenario-intro {
  /* Always laid out (no display:none) so opacity/visibility transitions
     fire in BOTH directions. display:none kills transitions, which
     killed the BEGIN→game crossfade and let the map flash through
     before the curtain faded in. */
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 9999;                               /* above all game chrome + floaters */
  background:
    radial-gradient(ellipse at center, #05080d 0%, #020304 70%, #000 100%),
    #000;
  color: #f4ecd0;
  /* No `cursor` here on purpose — the curtain has no click-to-dismiss
     handler (cold-open.js only binds click on `#scenario-intro-cta` and
     `#scenario-intro-skip`). A pointer cursor on the whole overlay
     falsely signals "clickable everywhere" and, more importantly,
     suppresses the cursor change the user expects when moving onto
     BEGIN / SKIP — both buttons already declare `cursor: pointer`. */
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Fade-OUT animation (curtain lifts as game chrome fades in). Visibility
     delayed so the overlay stays interactive until opacity reaches 0. */
  transition: opacity 700ms ease-out, visibility 0s 700ms;
}
/* Block any lower-z-index animations from peeking through the curtain. */
body.scenario-intro-playing #scenario-intro {
  background: #000;                             /* solid during reveal */
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  /* Fade IN must be INSTANT — on LAST STAND / PLAY AGAIN the game
     screen is still painted behind the curtain and the Pixi map
     repaints during the Supabase fetch. Any non-zero fade-in lets that
     flash through. Fade-OUT (class removal) uses the base
     #scenario-intro 700ms transition for the curtain-lifts crossfade. */
  transition: opacity 0s, visibility 0s 0s;
}
/* Hide game chrome while the intro is playing — everything stays present,
   just invisible, so nothing jumps on reveal. Fade-OUT is instant for the
   same reason as the curtain fade-IN above: chrome must pop in behind the
   still-opaque curtain, never crossfade through it. */
body.scenario-intro-playing #vp-bar,
body.scenario-intro-playing #phase-header,
body.scenario-intro-playing #scenario-score-strip,
body.scenario-intro-playing #card-layer,
body.scenario-intro-playing #roster,
body.scenario-intro-playing #unit-card,
body.scenario-intro-playing #battlefield-inner,
body.scenario-intro-playing #action-bar,
body.scenario-intro-playing #scenario-dialogue {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0s;
}

.scenario-intro-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  max-width: 820px;
  padding: 40px 60px;
  text-align: center;
}

/* Loading spinner visible while the curtain is up but scenario data
   hasn't landed (#scenario-intro WITHOUT .content-ready). Once
   _playColdOpen populates content and adds .content-ready, the spinner
   fades out and the portrait/speaker/text reveal animations play.
   Visually matches .forge-spinner (game/css/battle-forge.css) so
   loaders read consistently across the app. Centered both axes via
   `inset: 0` + `margin: auto`. */
.scenario-intro-loader {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 36px;
  height: 36px;
  border: 3px solid rgba(255, 255, 255, 0.08);
  border-top-color: color-mix(in srgb, var(--faction-a) 75%, transparent);
  border-radius: 50%;
  animation: scenario-loader-spin 0.85s linear infinite;
  opacity: 1;
  transition: opacity 220ms ease-out;
}
#scenario-intro.content-ready .scenario-intro-loader,
body:not(.scenario-intro-playing) #scenario-intro .scenario-intro-loader {
  opacity: 0;
  pointer-events: none;
}
/* Disable the opacity transition during fade-out so removing
   .content-ready (in _finishColdOpen) doesn't briefly reveal the
   spinner while the curtain itself fades. The spinner needs a fade-OUT
   transition (220ms) on the in→out path during normal load → reveal,
   but during dismiss we want it hidden instantly. */
body:not(.scenario-intro-playing) #scenario-intro .scenario-intro-loader {
  transition: none;
}
@keyframes scenario-loader-spin { to { transform: rotate(360deg); } }

/* Animation gated on `#scenario-intro.content-ready`. Without the gate
   the portrait/speaker/text faded in on fixed timers from curtain
   raise — if the Supabase fetch finished AFTER those timers, the slots
   faded in EMPTY and the real content snapped in mid-animation. JS in
   `_playColdOpen` adds `content-ready` only after speaker/portrait/text
   are populated, so the reveal animations always coincide with real
   content. Pre-populated state stays at opacity 0 so the curtain shows
   black during the fetch. */
.scenario-intro-portrait {
  width: 132px; height: 132px;
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--owning-faction) 60%, transparent);
  background: linear-gradient(135deg, var(--owning-faction-tint), color-mix(in srgb, var(--owning-faction) 4%, transparent));
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 70px/1 'Anton', 'Rajdhani', sans-serif;
  color: var(--owning-faction);
  letter-spacing: 2px;
  text-shadow: 0 0 22px color-mix(in srgb, var(--owning-faction) 55%, transparent);
  opacity: 0;
  transform: translateY(6px) scale(0.94);
  position: relative;
  overflow: hidden;
}
#scenario-intro.content-ready .scenario-intro-portrait {
  animation: intro-portrait-in 1200ms ease-out 400ms forwards;
}
.scenario-intro-portrait::after {
  content: '';
  position: absolute; inset: -2px;
  border-radius: 6px;
  box-shadow: 0 0 0 0 transparent;
  pointer-events: none;
}
#scenario-intro.content-ready .scenario-intro-portrait::after {
  animation: intro-portrait-pulse 2600ms ease-in-out 1200ms infinite;
}
@keyframes intro-portrait-in {
  to { opacity: 1; transform: translateY(0) scale(1); }
}
/* Pulse animates a single colour token; the color-mix is resolved at
   keyframe-evaluation time so it honours the side-cascade rebind of
   --owning-faction. */
@keyframes intro-portrait-pulse {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  50%      { box-shadow: 0 0 0 4px color-mix(in srgb, var(--owning-faction) 26%, transparent); }
}

.scenario-intro-speaker {
  font: 700 11px/1 'Rajdhani', sans-serif;
  letter-spacing: 5px;
  color: var(--owning-faction);
  text-transform: uppercase;
  opacity: 0;
}
#scenario-intro.content-ready .scenario-intro-speaker {
  animation: scenario-fade-in 800ms ease-out 800ms forwards;
}

.scenario-intro-text {
  font: 400 21px/1.55 'Rajdhani', sans-serif;
  font-style: italic;
  color: #f4ecd0;
  min-height: 66px;                     /* 2 lines reserve — prevents layout jitter */
  max-width: 720px;
  opacity: 0;
}
#scenario-intro.content-ready .scenario-intro-text {
  animation: scenario-fade-in 800ms ease-out 1200ms forwards;
}

/* Cold-open CTA (BEGIN). Visibility is what's animated (not
   pointer-events) so the element participates in hit-testing the
   moment it's rendered — prevents the "hover doesn't trigger until
   cursor moves" browser bug when pointer-events flips none → auto. */
.scenario-intro-cta {
  margin-top: 28px;
  padding: 14px 56px;
  font: 700 13px/1 'Anton', 'Rajdhani', sans-serif;
  letter-spacing: 6px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--owning-faction) 14%, transparent),
    color-mix(in srgb, var(--owning-faction) 5%, transparent));
  border: 1px solid color-mix(in srgb, var(--owning-faction) 55%, transparent);
  color: var(--owning-faction);
  cursor: pointer;                             /* always hand, even pre-reveal */
  visibility: hidden;
  opacity: 0;
  transform: translateY(6px);
  transition: background 180ms ease-out, border-color 180ms ease-out, color 180ms ease-out, box-shadow 180ms ease-out, opacity 500ms ease-out, transform 500ms ease-out, visibility 0s 500ms;
}
#scenario-intro.typewriter-done .scenario-intro-cta {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
  box-shadow: 0 0 10px var(--owning-faction-tint-strong);
  /* Skip appears first (200ms via `.scenario-intro-skip` below) so the
     bypass affordance reads first; BEGIN anchors at 700ms as the
     primary CTA. */
  transition: background 180ms ease-out, border-color 180ms ease-out, color 180ms ease-out, box-shadow 180ms ease-out, opacity 500ms ease-out 700ms, transform 500ms ease-out 700ms, visibility 0s 0s;
}
.scenario-intro-cta:hover,
.scenario-intro-cta:focus-visible {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--owning-faction) 26%, transparent),
    color-mix(in srgb, var(--owning-faction) 10%, transparent));
  border-color: var(--owning-faction);
  box-shadow: 0 0 18px var(--owning-faction-glow-strong);
  outline: none;
}
.scenario-intro-cta:active { transform: scale(0.985); }

.scenario-intro-skip {
  margin-top: 14px;
  padding: 4px 10px;
  background: transparent;
  border: none;
  font: 500 10px/1 'Rajdhani', sans-serif;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.35);
  cursor: pointer;
  visibility: hidden;
  opacity: 0;
  transition: opacity 600ms ease-out, color 160ms, visibility 0s 600ms;
}
/* Reveal SKIP as soon as the portrait fade-in completes (~1600ms after
   `content-ready` is added: 400ms delay + 1200ms anim). The user wants
   the bypass affordance available early — BEFORE the typewriter
   finishes — so an impatient player can skip straight to the in-game
   intro without sitting through 4-6s of typewriter. BEGIN still gates
   on `typewriter-done` (it's the primary CTA and shouldn't compete
   with skip during the read). */
#scenario-intro.content-ready .scenario-intro-skip {
  visibility: visible;
  opacity: 1;
  transition: opacity 500ms ease-out 1600ms, color 160ms, visibility 0s 1600ms;
}
.scenario-intro-skip:hover { color: rgba(255, 255, 255, 0.7); }

/* intro-fade-in consolidated into scenario-fade-in (defined above in UX4 block). */

body.scenario-mode #scenario-score-strip {
  display: flex;
}

body.scenario-mode.scenario-dialogue-visible #scenario-dialogue {
  display: flex;
}

/* HUD width hierarchy — two semantic layers:
   - Scenario layer (--scenario-stack-width): score-strip + dialogue. Both
     only exist in scenario mode and share gold-bordered panel chrome.
     Matching widths makes "scenario UI" a recognizable visual brand.
   - Game-loop layer (--phase-stack-width): phase-header + its in-flow
     warning banners. Narrower than the scenario layer so the two layers
     read as deliberately distinct (560 vs 420). */
body.scenario-mode {
  --scenario-stack-width: min(560px, 80vw);
  --phase-stack-width:    min(420px, 70vw);
}

/* ── Score strip — thin row between #vp-bar and #phase-header ─── */

#scenario-score-strip {
  /* Position explicitly below #vp-bar (which is abs-positioned at top:0, height ~48px).
     #phase-header is also abs-positioned at top:54px in the default chrome — we
     shift it down in scenario mode to make room (see rule below). */
  position: absolute;
  top: 48px;
  left: 50%;
  transform: translateX(-50%);
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 6px 22px;
  min-width: var(--scenario-stack-width, 560px);
  font-family: 'Rajdhani', sans-serif;
  font-size: 11px;
  letter-spacing: 2px;
  color: color-mix(in srgb, var(--faction-a) 85%, transparent);           /* Custodes gold, slightly dimmed */
  background: linear-gradient(180deg, rgba(8, 12, 18, 0.95) 0%, rgba(14, 20, 28, 0.82) 100%);
  border: 1px solid color-mix(in srgb, var(--faction-a) calc(0.22 * 100%), transparent);
  border-radius: 4px;
  text-transform: uppercase;
  user-select: none;
  /* Above #phase-header (z-overlay = 50) so the hover tooltip paints
     over the COMMAND/MOVE/etc. banner instead of being clipped behind
     it. Strip creates a stacking context for its descendants — the
     internal `.scenario-score-tooltip` z-index is RELATIVE to this. */
  z-index: 60;
  white-space: nowrap;
}

/* Make room for the strip by pushing the phase header down + clamp its
   width to the game-loop layer token so banners (which flow inside it
   as static children) inherit the same width. */
body.scenario-mode #phase-header {
  top: 94px;   /* was ~54px — slide 40px down to clear the strip */
  width: var(--phase-stack-width, 420px);
}

.scenario-score-star {
  color: var(--faction-a, #ffcc33);
  font-size: 14px;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--faction-a) 55%, transparent));
  transition: transform 180ms ease-out;
  display: inline-block;
}
.scenario-score-star.pulsing {
  animation: scenario-star-pulse 280ms ease-out;
}
@keyframes scenario-star-pulse {
  0%   { transform: scale(1);    filter: drop-shadow(0 0 4px color-mix(in srgb, var(--faction-a) 55%, transparent)); }
  40%  { transform: scale(1.35); filter: drop-shadow(0 0 14px color-mix(in srgb, var(--faction-a) 95%, transparent)); }
  100% { transform: scale(1);    filter: drop-shadow(0 0 4px color-mix(in srgb, var(--faction-a) 55%, transparent)); }
}

.scenario-score-value {
  font-weight: 700;
  font-size: 15px;
  color: #ffe188;
  letter-spacing: 1px;
  font-variant-numeric: tabular-nums;
  transition: color 160ms;
}

.scenario-score-medal {
  color: var(--faction-a, #ffcc33);
  font-weight: 600;
  transition: color 180ms, text-shadow 180ms, transform 180ms;
  display: inline-block;
}
.scenario-score-medal.threshold-crossed {
  animation: scenario-medal-flash 640ms ease-out;
}
@keyframes scenario-medal-flash {
  0%   { color: var(--faction-a); text-shadow: none; transform: scale(1); }
  35%  { color: #fffadb; text-shadow: 0 0 16px var(--faction-a), 0 0 28px color-mix(in srgb, var(--faction-a) 60%, transparent); transform: scale(1.12); }
  100% { color: var(--faction-a); text-shadow: none; transform: scale(1); }
}

.scenario-score-progress {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.scenario-score-bar {
  display: inline-block;
  width: 140px;
  height: 6px;
  background: color-mix(in srgb, var(--faction-a) calc(0.12 * 100%), transparent);
  border: 1px solid color-mix(in srgb, var(--faction-a) calc(0.25 * 100%), transparent);
  border-radius: 2px;
  overflow: hidden;
  position: relative;
}

.scenario-score-bar::after {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: var(--fill, 0%);
  background: linear-gradient(90deg, color-mix(in srgb, var(--faction-a) calc(0.3 * 100%), transparent) 0%, color-mix(in srgb, var(--faction-a) calc(0.75 * 100%), transparent) 100%);
  box-shadow: 0 0 8px color-mix(in srgb, var(--faction-a) calc(0.5 * 100%), transparent);
  transition: width 320ms ease-out;
  z-index: 1;
}

/* Tier checkpoint markers — small ticks anchored on the bar at each
   medal's proportional position. Cumulative-fill bar paints behind;
   ticks float above so they're always visible. `.reached` lights up
   in faction color once the player has crossed the threshold; the
   cap-tier tick is slightly wider/taller as the "you did it" anchor. */
.scenario-score-tier-mark {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 3px;
  margin-left: -1.5px;            /* center on its left% anchor */
  background: color-mix(in srgb, var(--faction-a) 35%, transparent);
  border-radius: 1.5px;
  z-index: 2;
  pointer-events: auto;
  transition: background 220ms ease-out, box-shadow 220ms ease-out;
}
.scenario-score-tier-mark.reached {
  background: var(--faction-a, #ffcc33);
  box-shadow: 0 0 6px color-mix(in srgb, var(--faction-a) 70%, transparent);
}
.scenario-score-tier-mark.is-cap {
  width: 4px;
  margin-left: -2px;
  top: -3px;
  bottom: -3px;
  /* Cap tick rendered as a slightly heavier anchor — it's the win
     state, the rest are checkpoints. */
  background: color-mix(in srgb, var(--faction-a) 55%, transparent);
}
.scenario-score-tier-mark.is-cap.reached {
  background: var(--faction-a, #ffcc33);
  box-shadow: 0 0 10px color-mix(in srgb, var(--faction-a) 85%, transparent),
              0 0 2px color-mix(in srgb, var(--faction-a) 100%, transparent);
}

.scenario-score-next {
  color: color-mix(in srgb, var(--faction-a) calc(0.6 * 100%), transparent);
  font-size: 10px;
}

.scenario-score-remaining {
  color: rgba(255, 255, 255, 0.55);
  font-size: 10px;
}

/* ── Hover-anywhere-on-strip breakdown tooltip (Option B) ── */

#scenario-score-strip { cursor: help; }
#scenario-score-strip:hover .scenario-score-tooltip,
#scenario-score-strip:focus-within .scenario-score-tooltip {
  display: block;
}

.scenario-score-tooltip {
  display: none;
  position: absolute;
  /* No gap between strip + tooltip — any gap breaks hover (cursor has
     to cross transparent space). Use padding-top to simulate the 8px
     visual gap while keeping the element's bounding box flush. */
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  padding: 22px 18px 14px;
  min-width: 320px;
  max-width: 360px;
  max-height: 520px;
  overflow-y: auto;
  cursor: default;
  background: rgba(8, 12, 18, 0.98);
  border: 1px solid color-mix(in srgb, var(--faction-a) calc(0.4 * 100%), transparent);
  border-radius: 4px;
  color: rgba(255, 255, 255, 0.85);
  font-size: 11px;
  letter-spacing: 0.5px;
  text-transform: none;
  line-height: 1.5;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
  z-index: 50;
}

.scenario-score-tooltip h4 {
  margin: 0 0 8px 0;
  color: var(--faction-a, #ffcc33);
  font-size: 12px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}

.scenario-score-tooltip .tt-section + .tt-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid color-mix(in srgb, var(--faction-a) 15%, transparent); }
.scenario-score-tooltip .tt-row { display: flex; justify-content: space-between; gap: 12px; padding: 2px 0; }
.scenario-score-tooltip .tt-row .val { color: #ffe188; font-variant-numeric: tabular-nums; }
.scenario-score-tooltip .tt-row.tt-total {
  border-top: 1px solid color-mix(in srgb, var(--faction-a) 25%, transparent);
  margin-top: 4px;
  padding-top: 6px;
  font-weight: 700;
}

/* Legend link — sits at the bottom of the hover tooltip and opens the
   scoring-legend modal. Looks like a subtle link, not a button. */
.scenario-score-legend-link {
  display: block;
  width: 100%;
  margin-top: 12px;
  padding: 8px 0 2px;
  border: none;
  border-top: 1px solid color-mix(in srgb, var(--faction-a) 15%, transparent);
  background: transparent;
  color: color-mix(in srgb, var(--faction-a) 80%, #ffe188);
  font: 500 10px/1.2 'Rajdhani', sans-serif;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;
  transition: color 160ms;
}
.scenario-score-legend-link:hover,
.scenario-score-legend-link:focus-visible {
  color: var(--faction-a);
  outline: none;
}
.scenario-score-legend-link::before { content: '\002b\0020'; }  /* "+ " prefix */

/* ── Scoring legend modal ────────────────────────────────────
   Opens on the "How is score calculated?" click. Dismissable via
   close button, backdrop click, or Escape. */
#scenario-score-legend-modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 500;
  align-items: center;
  justify-content: center;
}
body.scenario-score-legend-open #scenario-score-legend-modal {
  display: flex;
}

.scenario-score-legend-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(3, 6, 11, 0.68);
  backdrop-filter: blur(2px);
  animation: scenario-fade-in 200ms ease-out both;
  cursor: pointer;
}
.scenario-score-legend-panel {
  position: relative;
  max-width: 480px;
  width: calc(100% - 40px);
  padding: 28px 32px 22px;
  background: linear-gradient(180deg, rgba(12, 16, 22, 0.98), rgba(8, 12, 18, 0.98));
  border: 1px solid color-mix(in srgb, var(--faction-a) 42%, transparent);
  border-radius: 6px;
  box-shadow:
    0 16px 48px rgba(0, 0, 0, 0.7),
    0 0 26px color-mix(in srgb, var(--faction-a) 14%, transparent);
  color: #f4ecd0;
  animation: scenario-fade-in 240ms ease-out both;
}
.scenario-score-legend-close {
  position: absolute;
  top: 10px; right: 12px;
  width: 28px; height: 28px;
  background: transparent;
  border: none;
  color: rgba(244, 236, 208, 0.55);
  font: 400 24px/1 'Rajdhani', sans-serif;
  cursor: pointer;
  transition: color 160ms;
}
.scenario-score-legend-close:hover,
.scenario-score-legend-close:focus-visible {
  color: var(--faction-a);
  outline: none;
}
.scenario-score-legend-panel h3 {
  margin: 0 0 8px 0;
  font: 700 16px/1 'Anton', 'Rajdhani', sans-serif;
  letter-spacing: 5px;
  color: var(--faction-a);
  text-transform: uppercase;
}
.scenario-score-legend-lead {
  margin: 0 0 16px 0;
  font: 400 12px/1.55 'Rajdhani', sans-serif;
  color: rgba(244, 236, 208, 0.68);
}
.scenario-score-legend-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.scenario-score-legend-body .tt-row {
  display: flex;
  justify-content: space-between;
  padding: 4px 0;
  font: 500 12px/1.3 'Rajdhani', sans-serif;
  letter-spacing: 0.3px;
  border-bottom: 1px solid color-mix(in srgb, var(--faction-a) 8%, transparent);
}
.scenario-score-legend-body .tt-row:last-child { border-bottom: none; }
.scenario-score-legend-body .tt-row .val {
  color: var(--faction-a);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* ── Dialogue overlay — style A: bottom-center subtitle + small portrait ── */

#scenario-dialogue {
  /* Per-beat accent rides --owning-faction via the standard .side-a /
     .side-b cascade. dialogue.js applies one of those classes per beat
     by reading the linked unit's `side` from G; narrator beats with
     no linkedUnitId default to .side-a so the gold visual persists. */
  /* Centering uses inset:0 + margin-inline:auto + width:max-content
     instead of `left:50%; transform:translateX(-50%)`. The percentage
     translate caused a visible horizontal jump on first reveal: the
     transform transition (opacity+transform 300ms) decomposes the
     start/end into matrices using the panel's width AT the start of
     the transition, but the typewriter grows the panel mid-transition
     (min-width 380px → max-width 560px). The frozen pixel translate
     stays anchored to the old width, so the panel drifts off-center;
     when the transition ends and `translateX(-50%)` resumes resolving
     against the live width, the panel snaps back. Margin-auto centering
     re-resolves layout on every width change with no transform involved. */
  position: absolute;
  left: 0;
  right: 0;
  bottom: 72px;
  margin-inline: auto;
  width: max-content;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 22px 14px 14px;
  max-width: var(--scenario-stack-width, 560px);
  min-width: 380px;
  background: linear-gradient(180deg, rgba(8, 12, 18, 0.94) 0%, rgba(14, 20, 28, 0.94) 100%);
  border: 1px solid color-mix(in srgb, var(--owning-faction) 45%, transparent);
  border-radius: 6px;
  box-shadow:
    0 0 18px rgba(0, 0, 0, 0.6),
    0 0 24px color-mix(in srgb, var(--owning-faction) 14%, transparent),
    inset 0 0 22px color-mix(in srgb, var(--owning-faction) 5%, transparent);
  z-index: 120;                                /* above all game chrome/floaters */
  cursor: pointer;                             /* click-to-dismiss affordance */
  pointer-events: auto;                        /* ensure clickable when visible */
  /* Transition handles both fade-in + fade-out. Earlier revisions of
     this file used `animation: scenario-dialogue-in 0.5s ease-out both`
     which pinned opacity at 1 via the animation cascade level (higher
     priority than normal declarations). Removing
     body.scenario-dialogue-visible could not overpower the animation
     and the panel never visually closed — user-reported the click
     "didn't dismiss" because the panel stayed onscreen. The fix is to
     drop the keyframe animation entirely and let the transition handle
     both directions bidirectionally. */
  opacity: 0;
  transform: translateY(0);
  transition: opacity 300ms ease-out, transform 300ms ease-out;
}
body.scenario-mode.scenario-dialogue-visible #scenario-dialogue {
  opacity: 1;
  transform: translateY(0);
}
/* Children render normally — clicks are caught via document-level
   delegation in scenario-mode.js so any hit inside the panel dismisses.
   Side-class scoping (.side-a / .side-b) cascades --owning-faction
   automatically; no per-side override needed here. */
#scenario-dialogue:hover {
  border-color: color-mix(in srgb, var(--owning-faction) 60%, transparent);
  box-shadow:
    0 0 22px rgba(0, 0, 0, 0.7),
    0 0 34px color-mix(in srgb, var(--owning-faction) 24%, transparent),
    inset 0 0 22px color-mix(in srgb, var(--owning-faction) 8%, transparent);
}
body.scenario-mode:not(.scenario-dialogue-visible) #scenario-dialogue {
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
}

.scenario-dialogue-portrait {
  flex: 0 0 auto;
  width: 58px;
  height: 58px;
  border-radius: 4px;
  border: 1px solid color-mix(in srgb, var(--owning-faction) 55%, transparent);
  background: linear-gradient(135deg, color-mix(in srgb, var(--owning-faction) 25%, transparent), color-mix(in srgb, var(--owning-faction) 8%, transparent));
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dialogue-accent, #ffcc33);
  font-family: 'Anton', sans-serif;
  font-size: 30px;
  letter-spacing: 1px;
  text-shadow: 0 0 10px color-mix(in srgb, var(--owning-faction) 35%, transparent);
  position: relative;
  overflow: hidden;
}
.scenario-dialogue-portrait::after {
  /* Slow pulse glow — becomes the frame for S1 real portraits. */
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 4px;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--owning-faction) 70%, transparent);
  pointer-events: none;
  animation: scenario-portrait-glow 2400ms ease-in-out infinite;
}
/* Pause the infinite box-shadow animation when the dialogue is hidden.
   The overlay stays in the DOM (opacity 0, not display:none), so without
   this pause the GPU would keep repainting every 2.4s forever. */
body.scenario-mode:not(.scenario-dialogue-visible) .scenario-dialogue-portrait::after {
  animation-play-state: paused;
}
@keyframes scenario-portrait-glow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--owning-faction) 0%, transparent); }
  50%      { box-shadow: 0 0 0 3px color-mix(in srgb, var(--owning-faction) 28%, transparent); }
}

.scenario-dialogue-body {
  flex: 1 1 auto;
  min-width: 0;                       /* let the body shrink so text wraps inside the flex panel */
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: #f4ecd0;
  font-family: 'Rajdhani', sans-serif;
}

.scenario-dialogue-speaker {
  color: var(--dialogue-accent, #ffcc33);
  font-size: 10px;
  letter-spacing: 2.8px;
  text-transform: uppercase;
  font-weight: 700;
  opacity: 0.92;
  border-bottom: 1px solid color-mix(in srgb, var(--owning-faction) 25%, transparent);
  padding-bottom: 3px;
  display: inline-block;
  align-self: flex-start;
}

.scenario-dialogue-text {
  font-size: 14.5px;
  line-height: 1.45;
  font-style: italic;
  color: rgba(244, 236, 208, 0.96);
  min-height: 21px;                  /* prevent height jitter during typewriter */
  white-space: normal;               /* explicit so longer beats wrap inside the panel */
  word-wrap: break-word;
  overflow-wrap: break-word;
}

.scenario-dialogue-hint {
  font-size: 9px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.35);
  margin-top: 6px;
  font-family: 'Rajdhani', sans-serif;
}

/* ── Faction-color tint (UX5 will refine; UX1 ships basic palette) ─────
   The model-renderer sets stroke via SVG attribute every frame, which
   CSS cannot override (see rendering.md). In scenario mode the renderer
   itself reads a body attribute and picks scenario colors. Those
   renderer hooks are tiny and live in model-renderer.js.
   The rules below colour ancillary chrome (VP faction labels, etc).
   ─────────────────────────────────────────────────────────────────── */

body.scenario-mode .side-a .vp-name,
body.scenario-mode .side-a .vp-score {
  color: var(--faction-a, #ffcc33);                              /* Custodes gold */
  text-shadow: 0 0 8px color-mix(in srgb, var(--faction-a) calc(0.4 * 100%), transparent);
}

body.scenario-mode .side-b .vp-name,
body.scenario-mode .side-b .vp-score {
  color: var(--faction-b, #1f4ec2);                              /* Ultramarines cobalt */
  text-shadow: 0 0 8px color-mix(in srgb, var(--faction-b) calc(0.35 * 100%), transparent);
}

/* ── LAST STAND start-screen button scaffold (UX7 finishes) ─────── */

#btn-last-stand {
  /* placeholder styles; UX7 polishes with iconography + transitions */
  background: linear-gradient(180deg, color-mix(in srgb, var(--faction-a) calc(0.18 * 100%), transparent), color-mix(in srgb, var(--faction-a) calc(0.06 * 100%), transparent));
  border: 1px solid color-mix(in srgb, var(--faction-a) calc(0.5 * 100%), transparent);
  color: var(--faction-a, #ffcc33);
  font-weight: 700;
  letter-spacing: 3px;
}

#btn-last-stand:hover {
  background: linear-gradient(180deg, color-mix(in srgb, var(--faction-a) calc(0.3 * 100%), transparent), color-mix(in srgb, var(--faction-a) calc(0.1 * 100%), transparent));
  box-shadow: 0 0 12px color-mix(in srgb, var(--faction-a) calc(0.35 * 100%), transparent);
}

/* ── Scenario load-error modal (SE3) ─────────────────────────── */

#scenario-load-error {
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(90vw, 440px);
  z-index: 10000;
  border-top-color: #ff6b52;
}
#scenario-load-error .overlay-title { color: #ff6b52; }
#scenario-load-error .scenario-load-error__body {
  padding: 4px 14px 10px;
  color: var(--text-primary, #e8f4fa);
  font: 400 13px/1.4 'Rajdhani', sans-serif;
}

/* ── Codec portrait frame (MGS-style) ──────────────────────────────
   `.codec-frame` is added by `dom-helpers.applyPortrait` to the
   portrait host on first paint. The host is the existing
   `.scenario-dialogue-portrait` / `.scenario-intro-portrait` box —
   class additions, no DOM swap, so prior selectors keep matching.
   Children: 4 corner brackets + `.codec-face` (image, breathes) +
   `.codec-letter` (fallback char) + `.codec-scanlines` + `.codec-scan-roll`
   + `.codec-crt` (vignette).
   `--amp` is driven by scenario-vo's AnalyserNode RMS each rAF tick
   (0..1); the box-shadow scales with it so the glow tracks the voice.
*/
.codec-frame {
  /* Override the legacy flex centering — codec children are absolute. */
  display: block;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #0c0c0c;
  /* Layer the amp-driven glow on top of any existing box-shadow rule. */
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.6) inset,
    0 0 calc(20px + var(--amp, 0) * 32px) color-mix(in srgb, var(--owning-faction) 65%, transparent);
  transition: box-shadow 90ms linear;
}
.codec-frame .codec-face {
  position: absolute; inset: 0;
  background-size: cover;
  background-position: center 30%;
  z-index: 1;
  animation: codec-breathe 4.6s ease-in-out infinite;
}
@keyframes codec-breathe {
  0%, 100% { transform: scale(1.0); }
  50%      { transform: scale(1.025); }
}
.codec-frame .codec-letter {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: 'Anton', 'Impact', sans-serif;
  font-size: 0.72em;                  /* relative to host font-size */
  color: var(--owning-faction);
  text-shadow: 0 0 14px color-mix(in srgb, var(--owning-faction) 65%, transparent);
  z-index: 1;
}
.codec-frame.has-image .codec-letter { display: none; }

.codec-frame .codec-scanlines {
  position: absolute; inset: 0;
  background-image: repeating-linear-gradient(180deg,
    transparent 0 2px,
    rgba(0, 0, 0, 0.42) 2px 3px);
  mix-blend-mode: multiply;
  pointer-events: none;
  z-index: 2;
}
.codec-frame .codec-scan-roll {
  position: absolute; inset: -10% 0;
  background: linear-gradient(180deg,
    transparent 0%,
    color-mix(in srgb, var(--owning-faction) 65%, transparent) 46%,
    color-mix(in srgb, var(--owning-faction) 65%, transparent) 54%,
    transparent 100%);
  opacity: 0.15;
  pointer-events: none;
  mix-blend-mode: screen;
  z-index: 3;
  animation: codec-scan-roll 4.6s linear infinite;
}
@keyframes codec-scan-roll {
  0%   { transform: translateY(-100%); }
  100% { transform: translateY(100%); }
}
.codec-frame .codec-crt {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.55) 100%);
  pointer-events: none;
  z-index: 4;
}

/* 4 corner brackets — span elements, faction-tinted. */
.codec-frame .bracket-tl,
.codec-frame .bracket-tr,
.codec-frame .bracket-bl,
.codec-frame .bracket-br {
  position: absolute;
  width: 14px; height: 14px;
  border: 2px solid var(--owning-faction);
  pointer-events: none;
  z-index: 5;
}
.codec-frame .bracket-tl { top: -1px;    left: -1px;    border-right: none; border-bottom: none; }
.codec-frame .bracket-tr { top: -1px;    right: -1px;   border-left: none;  border-bottom: none; }
.codec-frame .bracket-bl { bottom: -1px; left: -1px;    border-right: none; border-top: none; }
.codec-frame .bracket-br { bottom: -1px; right: -1px;   border-left: none;  border-top: none; }

/* When the dialogue is visible, kick the scan-roll faster + flicker
   the brackets — matches the mockup's `is-speaking` state. The intro
   curtain has its own active-class hook below. */
body.scenario-dialogue-visible .scenario-dialogue-portrait .codec-scan-roll { opacity: 0.32; animation-duration: 2.6s; }
body.scenario-dialogue-visible .scenario-dialogue-portrait .bracket-tl,
body.scenario-dialogue-visible .scenario-dialogue-portrait .bracket-tr,
body.scenario-dialogue-visible .scenario-dialogue-portrait .bracket-bl,
body.scenario-dialogue-visible .scenario-dialogue-portrait .bracket-br {
  animation: codec-bracket-flicker 1.6s steps(1, end) infinite;
}
body.scenario-intro-playing .scenario-intro-portrait .codec-scan-roll { opacity: 0.32; animation-duration: 2.6s; }
body.scenario-intro-playing .scenario-intro-portrait .bracket-tl,
body.scenario-intro-playing .scenario-intro-portrait .bracket-tr,
body.scenario-intro-playing .scenario-intro-portrait .bracket-bl,
body.scenario-intro-playing .scenario-intro-portrait .bracket-br {
  animation: codec-bracket-flicker 1.6s steps(1, end) infinite;
}
@keyframes codec-bracket-flicker {
  0%, 92%, 100% { opacity: 1; }
  93%, 96%      { opacity: 0.32; }
}

/* Amp-modulated outer-frame glow on the dialogue panel + intro overlay.
   The scenario-vo amp driver writes `--amp` directly on the panel so
   the box-shadow tracks the voice envelope. */
#scenario-dialogue {
  box-shadow:
    0 0 18px rgba(0, 0, 0, 0.6),
    0 0 calc(18px + var(--amp, 0) * 26px) color-mix(in srgb, var(--owning-faction) 22%, transparent),
    inset 0 0 22px color-mix(in srgb, var(--owning-faction) 5%, transparent);
  transition: box-shadow 90ms linear, opacity 300ms ease-out, transform 300ms ease-out;
}
