:root {
  --bg-primary: #1a1a2e;
  --bg-secondary: #16213e;
  --bg-card: #0f3460;
  --text-primary: #e8e8e8;
  --text-secondary: #a0a0b0;
  --accent: #e94560;
  --accent-glow: rgba(233, 69, 96, 0.4);
  --gold: #ffd700;
  --spirit-blue: #4ea8de;
  --hp-green: #4ade80;
  --hp-red: #ef4444;
  --common-border: #6b7280;
  --uncommon-border: #22c55e;
  --rare-border: #3b82f6;
  --legendary-border: #ffd700;
  --ring-slot-bg: rgba(255, 255, 255, 0.05);
  --ring-slot-border: rgba(255, 255, 255, 0.15);
  --exhausted-overlay: rgba(40, 40, 50, 0.55);
  --fresh-overlay: rgba(255, 255, 100, 0.15);
  --locked-overlay: rgba(60, 90, 150, 0.55);
  --font-main: 'Segoe UI', system-ui, -apple-system, sans-serif;
  /* Voice-bubble font. Comic Neue self-hosted from assets/fonts/comic-neue/
     (see @font-face block near the bottom of this file). Falls back to
     Comic Sans MS on Windows / macOS and a generic cursive family
     elsewhere if the file fetch fails. The comic-letter feel reads
     the bubble as in-world speech rather than UI chrome. */
  --font-bubble: 'Comic Neue', 'Comic Sans MS', cursive, system-ui, sans-serif;
  --transition-speed: 0.3s;
  /* Battle UI sizing tokens — overridden in @media blocks below */
  --card-w: 130px;
  --card-h: 182px;
  --ring-w: 140px;
  --ring-h: 192px;
  --hand-h: 160px;
  --opp-hand-h: 100px;
  --opp-card-w: 90px;
  --opp-card-h: 126px;
  --card-fs: 10px;
  --board-pad-y: 8px;
  --board-pad-x: 12px;
  --board-gap: 4px;
  --ring-pad: 8px;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: var(--font-main);
}

#game {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  position: relative;
}

.game-board {
  flex: 1;
  /* Flex items default to `min-width: auto` which respects the item's
     own min-content. Without this override, an 8-card hand pushes the
     grid's min-content past the viewport — game-board then overflows
     #game to the right, the grid column inside reads the inflated
     width, and the hand-overlap math sees no deficit. `min-width: 0`
     lets game-board shrink below its content so it stays viewport-
     bounded; the grid column constraint below then propagates the
     bound down to the hand area. */
  min-width: 0;
  display: grid;
  /* Single column pinned to the grid's width. Without an explicit
     columns track the implicit column uses `grid-auto-columns: auto`
     (= `minmax(min-content, max-content)`), which lets the column
     grow with intrinsic content. Pair this with the `min-width: 0`
     above so the chain from #game → game-board → grid column →
     hand-area stays viewport-bounded all the way down. */
  grid-template-columns: minmax(0, 1fr);
  /* Symmetric 1fr rows above (opponent ring) and below (player ring) the
     battle-line so it sits at the vertical center, equidistant from the
     two ring rows, in every aspect ratio. With only ONE 1fr row the extra
     vertical space pooled on one side and pushed the battle-line off-center
     (most visible in tall portrait, e.g. iPad). */
  grid-template-rows: auto auto 1fr auto 1fr auto;
  gap: var(--board-gap);
  padding: var(--board-pad-y) var(--board-pad-x);
  max-width: 1400px;
  margin: 0 auto;
  width: 100%;
  height: 100%;
  position: relative;
  background:
    radial-gradient(ellipse at 50% 48%, rgba(15, 52, 96, 0.35) 0%, transparent 60%),
    linear-gradient(180deg, rgba(22, 33, 62, 0.4) 0%, transparent 30%, transparent 70%, rgba(22, 33, 62, 0.4) 100%);
}

/* ── Task list ─────────────────────────────────────────────────────
   TODO list showing the current player's available actions. Absolutely
   positioned over .game-board, anchored to the vertical center so it
   straddles the battle-line divider between the two sides. Populated by
   src/ui/TaskList.ts; hidden when it's the opponent's turn and the
   player isn't defending. */
.task-list {
  position: absolute;
  left: 16px;
  /* Anchored in the upper-left of the board in every aspect ratio, low
     enough to clear the opponent's speech balloon (which descends from the
     opponent header on this same side) but well above the midline — the
     player's ring cards sit below center and lift above it when staged to
     attack, so a centered panel collided with them. On wide screens the
     opponent's ring cards are centered, so the edge panel stays clear even
     this far down. Phones use a smaller offset (see the ≤600 media query):
     their header is shorter AND their ring stretches full-width just below
     the hand, so the panel can't drop as far without covering ring cards. */
  top: 130px;
  width: 240px;
  background: var(--bg-secondary);
  border-radius: 8px;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* Match `.game-log`'s z-index — cards hover at 10, ring overlays up to
     15, so anything lower lets card visuals draw on top of the panel. */
  z-index: 100;
}

.task-list.hidden { display: none; }

.task-list-toggle {
  /* Left-justified to match the left-anchored panel: the collapsed pill
     sits at the panel's left edge, so the toggle must too, or it would
     jump left→right as the panel expands. (The log toggle stays flex-end
     because its panel is right-anchored.) */
  align-self: flex-start;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-secondary);
  border: none;
  border-radius: 4px;
  padding: 2px 10px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  cursor: pointer;
}

.task-list-toggle:hover {
  background: rgba(255, 255, 255, 0.15);
  color: var(--text-primary);
}

.task-list-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task-list.collapsed {
  width: auto;
  padding: 4px 6px;
  gap: 0;
}

.task-list.collapsed .task-list-content { display: none; }

.task-list-header {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-secondary);
  font-weight: 700;
}

.task-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 17px;
  font-weight: 500;
  color: var(--text-primary);
  line-height: 1.25;
}

.task::before {
  content: "☐";
  font-size: 20px;
  line-height: 1.1;
  color: var(--accent);
  flex-shrink: 0;
}

.task.complete { color: var(--text-secondary); opacity: 0.55; }
.task.complete::before { content: "☑"; color: var(--text-secondary); }
.task.complete .task-text { text-decoration: line-through; }

.player-info {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 8px 16px;
  background: var(--bg-secondary);
  border-radius: 8px;
  font-size: 14px;
}

.player-info.opponent {
  flex-direction: row;
}

.player-info.player {
  flex-direction: row;
}

.player-label {
  font-weight: 700;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 1px;
  min-width: 80px;
}

/* Avatar variant: when the label holds a profile-card icon instead of
   the "Opponent" / "You" text, drop the min-width so the avatar's own
   36px footprint drives layout. The avatar itself uses the existing
   .tournament-ladder-avatar classes. */
.player-label.player-label-avatar {
  min-width: 0;
  padding: 0;
  letter-spacing: normal;
}

.hp-bar-container {
  flex: 1;
  max-width: 300px;
  height: 20px;
  background: rgba(0, 0, 0, 0.4);
  border-radius: 10px;
  overflow: hidden;
  position: relative;
}

.hp-bar {
  height: 100%;
  background: linear-gradient(90deg, var(--hp-green), #22c55e);
  border-radius: 10px;
  transition: width var(--transition-speed) ease;
}

.hp-bar.low {
  background: linear-gradient(90deg, var(--hp-red), #dc2626);
}

.hp-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 11px;
  font-weight: 700;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

.spirit-display {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--spirit-blue);
  font-weight: 600;
}

.deck-count {
  color: var(--text-secondary);
  font-size: 12px;
}

.hand-area {
  display: flex;
  justify-content: safe center;
  gap: 8px;
  /* Extra top padding so a selected card's raise (−8px) and glow
     (16px blur) aren't clipped. */
  padding: 24px 4px 4px;
  min-height: var(--hand-h);
  align-items: center;
  /* Cards overlap themselves (via margin-left in BoardView's
     `applyHandOverlap`) when the natural row width would exceed the
     panel — so we clip horizontal overflow rather than scroll, and
     leave the vertical axis visible so a hovered card's translateY
     lift + box-shadow glow aren't trimmed.
     `overflow-x: clip` (not `hidden`) is intentional: when the X axis
     is `hidden`, the spec implicitly promotes a `visible` Y axis to
     `auto`, producing a vertical scrollbar whenever the hover lift
     pokes above the padding. `clip` does NOT promote, so the Y axis
     stays genuinely visible. */
  overflow-x: clip;
  overflow-y: visible;
  /* `.game-board` is a CSS Grid with an implicit single column whose
     default sizing (`minmax(auto, 1fr)`) lets the column grow with
     intrinsic content width. Without this, an 8+ card hand inflates
     the grid column past 100% — `applyHandOverlap` then measures the
     inflated clientWidth as "available", sees no overflow, and applies
     no margin overlap, so the rightmost cards sit off-screen behind
     `overflow-x: hidden`. `min-width: 0` lets the grid item shrink
     below its content's intrinsic width so the column stays viewport-
     bounded and the overlap math sees the real available room. */
  min-width: 0;
}

.hand-area .card {
  flex-shrink: 0;
}

.hand-area.opponent {
  min-height: var(--opp-hand-h);
}

.hand-area.opponent .card {
  transform: rotateY(180deg);
}

.ring-area {
  display: flex;
  justify-content: center;
  gap: 12px;
  padding: var(--ring-pad);
  align-items: center;
}

.ring-slot {
  width: var(--ring-w);
  height: var(--ring-h);
  border: 2px dashed var(--ring-slot-border);
  border-radius: 10px;
  background: var(--ring-slot-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--transition-speed);
  position: relative;
}

.ring-slot.empty {
  cursor: default;
}

.ring-slot.valid-target {
  border-color: var(--accent);
  box-shadow: 0 0 12px var(--accent-glow);
  cursor: pointer;
}

.ring-slot.valid-target:hover {
  background: rgba(233, 69, 96, 0.1);
}

.battle-line {
  height: 4px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  margin: 4px 0;
  opacity: 0.5;
}

.battle-line.active {
  opacity: 1;
  height: 6px;
  animation: pulse-line 1s ease infinite;
}

.phase-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px;
  background: var(--bg-secondary);
  border-radius: 8px;
}

.phase-indicator {
  display: flex;
  gap: 4px;
  align-items: center;
}

.phase-pip {
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--text-secondary);
  background: rgba(255, 255, 255, 0.05);
  transition: all var(--transition-speed);
}

.phase-pip.active {
  color: #ffffff;
  background: var(--spirit-blue);
  box-shadow: 0 0 8px rgba(78, 168, 222, 0.4);
}

.phase-pip.auto {
  font-size: 10px;
  opacity: 0.5;
}

.action-button {
  padding: 8px 24px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  background: var(--accent);
  color: white;
  transition: all 0.2s;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.action-button:hover {
  filter: brightness(1.2);
  transform: translateY(-1px);
}

.action-button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
}

.action-button.secondary {
  background: rgba(255, 255, 255, 0.1);
  color: var(--text-secondary);
}

.turn-label {
  font-size: 12px;
  color: var(--text-secondary);
  text-align: center;
}

.clock-display {
  font-size: 12px;
  font-weight: 600;
  color: #d6b75a;
  margin-left: 6px;
  white-space: nowrap;
}

.clock-display .clock-value {
  margin-left: 1px;
}

.game-log {
  /* Absolute (relative to `.game-board`) rather than fixed (viewport), so
     on wide screens — where the board is centered with a max-width — it
     insets from the play space's right edge instead of hugging the screen
     edge. This mirrors `.task-list`'s board-relative left:16px, keeping the
     two panels symmetric. (On narrow screens the board fills the viewport,
     so this is identical to the old fixed positioning.) */
  position: absolute;
  /* Matches `.task-list`'s top offset (clears the opponent speech balloon)
     and its 16px edge inset. See its `top` comment. */
  top: 130px;
  right: 16px;
  width: 280px;
  max-height: 200px;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 8px;
  padding: 6px 8px;
  font-size: 11px;
  color: var(--text-secondary);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.game-log-toggle {
  align-self: flex-end;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-secondary);
  border: none;
  border-radius: 4px;
  padding: 2px 10px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  cursor: pointer;
}

.game-log-toggle:hover {
  background: rgba(255, 255, 255, 0.15);
  color: var(--text-primary);
}

.game-log .log-entries {
  overflow-y: auto;
  min-height: 0;
  flex: 1;
}

.game-log.collapsed {
  max-height: none;
  width: auto;
  padding: 4px 6px;
  gap: 0;
}

.game-log.collapsed .log-entries {
  display: none;
}

.game-log .log-entry {
  padding: 2px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

/* Card-name link inside a log entry. Clicking opens the read-only
 * detail modal. Underline-on-hover signals interactivity without
 * cluttering the log when nothing is hovered. */
.game-log .log-card-link {
  color: var(--gold);
  cursor: pointer;
  text-decoration: none;
}

.game-log .log-card-link:hover {
  text-decoration: underline;
}

/* ── Tutorial Banner ─────────────────────────────────────────── */

.tutorial-banner {
  position: fixed;
  /* Anchor under the opponent's ring (upper-middle of the board) so the
   * banner doesn't cover the player's ring slots. translate(-50%, -50%)
   * still centers horizontally and pivots vertically off the same point so
   * the slide-in keyframe stays aligned. */
  top: 38%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--bg-secondary);
  border: 1px solid var(--spirit-blue);
  border-radius: 10px;
  padding: 12px 20px;
  display: flex;
  align-items: center;
  gap: 16px;
  z-index: 500;
  max-width: 600px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  animation: tutorial-slide-in 0.3s ease;
  /* Drags and drops should pass through the banner — its anchor sits right
   * above the battle line, so without this `elementFromPoint` returns the
   * banner instead of the drop target underneath and the player's attempt
   * to stage an attacker silently fails. The dismiss button re-enables
   * pointer events so it stays clickable. */
  pointer-events: none;
}

.tutorial-banner .tutorial-dismiss {
  pointer-events: auto;
}

.tutorial-text {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-primary);
}

.tutorial-text strong {
  color: var(--spirit-blue);
}

.tutorial-dismiss {
  background: var(--spirit-blue);
  color: white;
  border: none;
  border-radius: 5px;
  padding: 6px 14px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}

.tutorial-dismiss:hover {
  background: #3b8abf;
}

@keyframes tutorial-slide-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
  to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

.game-over-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  backdrop-filter: blur(4px);
}

/* While the game-over modal is up, lift the game log above the overlay
   scrim so the player can review the final turns (the log sits on the
   right edge; the modal panel is centered, so they don't overlap).
   Sitting above the overlay also keeps it out of the backdrop blur. */
body.game-over-active .game-log {
  z-index: 1001;
}

.game-over-panel {
  background: var(--bg-secondary);
  padding: 48px;
  border-radius: 16px;
  text-align: center;
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
}

.game-over-panel h1 {
  font-size: 36px;
  margin-bottom: 8px;
}

.game-over-panel .subtitle {
  color: var(--text-secondary);
  margin-bottom: 24px;
}

.game-over-panel .win {
  color: var(--gold);
}

.game-over-panel .lose {
  color: var(--accent);
}

.game-over-panel .coins-earned {
  color: var(--gold);
  font-size: 18px;
  font-weight: 700;
  margin: 8px 0;
}

.game-over-panel .gems-earned {
  color: #5dd0ff;
  font-size: 18px;
  font-weight: 700;
  margin: 4px 0 8px;
}

/* Ticket reward line on the game-over modal. Green to match the
 * shop's FREE ribbon so the player learns to associate the color
 * with pack-redemption credits. */
.game-over-panel .tickets-earned {
  color: #22c55e;
  font-size: 18px;
  font-weight: 700;
  margin: 4px 0 8px;
}

/* Button row at the bottom of the game-over panel. Continue stays the
 * rightmost (default-affirmative) action; "Claim Pack" appears to its
 * left when applicable. Centered as a group. */
.game-over-panel .game-over-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 8px;
}

@keyframes pulse-line {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* ── Coin-flip ceremony ────────────────────────────────────── */

.coin-flip-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1500;
  backdrop-filter: blur(4px);
  animation: coin-flip-fade-in 0.25s ease;
}

.coin-flip-overlay.fade-out {
  animation: coin-flip-fade-out 0.3s ease forwards;
}

.coin-flip-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  position: relative;
  z-index: 1;
}

/* Fighting-game-style pre-battle portrait reveal. Two portraits slide
 * in from opposite sides — player from the left (lower), opponent
 * from the right (higher) — to give the pair some asymmetric depth
 * rather than mirror-symmetry. The starting player's portrait gets
 * a glowing border highlight after the result lands.
 *
 * Layout switches at .max-width:600px (typical phone portrait): the
 * pair anchors to lower-left / upper-right corners instead of the
 * landscape side-of-screen position, with both portraits shrunk so
 * they don't crowd the coin / result text in the middle. */
.coin-flip-portrait {
  position: absolute;
  width: min(26vw, 260px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  z-index: 0;
  pointer-events: none;
  /* Slide-in animation is data-side specific; see below. */
}
.coin-flip-portrait[data-side="player"] {
  left: 5%;
  top: 56%;
  transform: translateY(-50%);
  animation: coin-flip-portrait-slide-left 0.85s cubic-bezier(0.16, 0.84, 0.32, 1.0);
}
.coin-flip-portrait[data-side="opponent"] {
  right: 5%;
  top: 44%;
  transform: translateY(-50%);
  animation: coin-flip-portrait-slide-right 0.85s cubic-bezier(0.16, 0.84, 0.32, 1.0);
}
.coin-flip-portrait img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  object-position: top;
  border-radius: 12px;
  border: 2px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
  transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.coin-flip-portrait-name {
  font-size: 16px;
  font-weight: 800;
  color: var(--text-primary);
  letter-spacing: 1px;
  text-transform: uppercase;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
  text-align: center;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Starting-player highlight: the resolved-winner portrait gets a
 * gold glow once the ceremony settles. Pair-targeting via
 * `.first-player` / `.first-opponent` modifier classes set on the
 * overlay when the coin lands. */
.coin-flip-overlay.first-player.settled .coin-flip-portrait[data-side="player"] img,
.coin-flip-overlay.first-opponent.settled .coin-flip-portrait[data-side="opponent"] img {
  border-color: var(--gold);
  box-shadow: 0 0 36px rgba(255, 215, 0, 0.55), 0 6px 20px rgba(0, 0, 0, 0.6);
}
.coin-flip-overlay.first-player.settled .coin-flip-portrait[data-side="player"] .coin-flip-portrait-name,
.coin-flip-overlay.first-opponent.settled .coin-flip-portrait[data-side="opponent"] .coin-flip-portrait-name {
  color: var(--gold);
}

@keyframes coin-flip-portrait-slide-left {
  from { transform: translate(-120%, -50%); opacity: 0; }
  to   { transform: translate(0, -50%);     opacity: 1; }
}
@keyframes coin-flip-portrait-slide-right {
  from { transform: translate(120%, -50%); opacity: 0; }
  to   { transform: translate(0, -50%);    opacity: 1; }
}

/* Narrow viewports (phones, mostly portrait). Portraits move to the
 * corners so the coin + result text in the center stay legible.
 * Shrunk to ~32vw so two portraits + center text all fit without
 * overlap. Anchors stay diagonal: player lower-left, opponent
 * upper-right — same asymmetric depth as landscape, just rotated. */
@media (max-width: 600px) {
  .coin-flip-portrait {
    width: min(32vw, 180px);
    gap: 4px;
  }
  .coin-flip-portrait-name {
    font-size: 12px;
    letter-spacing: 0.5px;
  }
  .coin-flip-portrait[data-side="player"] {
    left: 4%;
    top: auto;
    bottom: 8%;
    transform: none;
    animation: coin-flip-portrait-slide-left-portrait 0.85s cubic-bezier(0.16, 0.84, 0.32, 1.0);
  }
  .coin-flip-portrait[data-side="opponent"] {
    right: 4%;
    top: 8%;
    transform: none;
    animation: coin-flip-portrait-slide-right-portrait 0.85s cubic-bezier(0.16, 0.84, 0.32, 1.0);
  }
  @keyframes coin-flip-portrait-slide-left-portrait {
    from { transform: translateX(-140%); opacity: 0; }
    to   { transform: translateX(0);     opacity: 1; }
  }
  @keyframes coin-flip-portrait-slide-right-portrait {
    from { transform: translateX(140%); opacity: 0; }
    to   { transform: translateX(0);    opacity: 1; }
  }
}

.coin-flip-coin {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff0a3, #c79110 65%, #6f4f08 100%);
  border: 3px solid #ffd700;
  box-shadow: 0 0 36px rgba(255, 215, 0, 0.55), inset 0 -8px 16px rgba(0, 0, 0, 0.35);
  animation: coin-spin 0.45s linear infinite;
}

/* Once the result is settled the coin stops spinning and gives a small
 * triumphant pulse so it doesn't feel frozen. */
.coin-flip-overlay.settled .coin-flip-coin {
  animation: coin-settle 0.5s ease-out forwards;
}

.coin-flip-text {
  font-size: 28px;
  font-weight: 800;
  color: var(--text-primary);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
  text-align: center;
}

.coin-flip-overlay.settled .coin-flip-text {
  animation: coin-text-pop 0.35s ease-out;
}

@keyframes coin-spin {
  from { transform: rotateY(0deg) scale(1); }
  to   { transform: rotateY(360deg) scale(1); }
}

@keyframes coin-settle {
  0%   { transform: rotateY(0deg) scale(1); }
  60%  { transform: rotateY(0deg) scale(1.18); }
  100% { transform: rotateY(0deg) scale(1.08); }
}

@keyframes coin-text-pop {
  0%   { opacity: 0; transform: translateY(8px) scale(0.92); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes coin-flip-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes coin-flip-fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ── Stage intro modal ─────────────────────────────────────────
 * Pre-battle briefing for campaign stages. Click-to-dismiss
 * (the OK button), unlike the auto-dismissing coin-flip overlay —
 * playtesters need to *read* it, especially for stages with
 * unusual starting conditions like Pharsalus' Cataphract opener. */
.stage-intro-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1500;
  backdrop-filter: blur(4px);
  animation: coin-flip-fade-in 0.25s ease;
  padding: 24px;
}

.stage-intro-overlay.fade-out {
  animation: coin-flip-fade-out 0.2s ease forwards;
}

.stage-intro-panel {
  background: var(--bg-secondary);
  padding: 32px 36px 28px;
  border-radius: 14px;
  border: 1px solid rgba(255, 215, 0, 0.35);
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.6), 0 0 24px rgba(255, 215, 0, 0.15);
  max-width: min(560px, 92vw);
  text-align: center;
}

.stage-intro-title {
  font-size: 28px;
  font-weight: 800;
  color: var(--gold);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  margin: 0 0 16px;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
}

.stage-intro-message {
  font-size: 16px;
  line-height: 1.55;
  color: var(--text-primary);
  margin: 0 0 24px;
  text-align: left;
}

/* Mission Conditions section inside the intro modal. The top margin
 * doubles as a visual break from the prose paragraph above; when the
 * stage has no prose, the heading sits directly below the title with
 * the same spacing. */
.stage-intro-conditions {
  margin: 0 0 24px;
  text-align: left;
}

.stage-intro-conditions-header {
  font-size: 12px;
  font-weight: 700;
  color: var(--gold);
  text-transform: uppercase;
  letter-spacing: 0.8px;
  margin-bottom: 8px;
}

.stage-intro-conditions-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.stage-intro-conditions-list li {
  font-size: 14px;
  color: var(--text-primary);
  line-height: 1.45;
  padding: 2px 0 2px 16px;
  position: relative;
}

.stage-intro-conditions-list li::before {
  content: "•";
  position: absolute;
  left: 4px;
  color: var(--gold);
}

.stage-intro-buttons {
  display: flex;
  justify-content: center;
  gap: 12px;
}

.stage-intro-ok,
.stage-intro-cancel {
  min-width: 120px;
}

/* ── Gem Shop ────────────────────────────────────────────────── */

.gem-shop-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1500;
  backdrop-filter: blur(4px);
  padding: 24px;
  overflow-y: auto;
}

.gem-shop-panel {
  position: relative;
  background: var(--bg-secondary);
  padding: 32px 36px 28px;
  border-radius: 14px;
  border: 1px solid rgba(120, 170, 220, 0.35);
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.6), 0 0 24px rgba(78, 168, 222, 0.18);
  max-width: min(720px, 96vw);
  width: 100%;
  text-align: center;
}

.gem-shop-close {
  position: absolute;
  top: 12px;
  right: 14px;
  background: transparent;
  border: none;
  color: var(--text-secondary);
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 10px;
  border-radius: 4px;
}
.gem-shop-close:hover { color: var(--text-primary); background: rgba(255, 255, 255, 0.05); }

.gem-shop-title {
  font-size: 26px;
  font-weight: 800;
  color: #5dd0ff;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  margin: 0 0 6px;
}

.gem-shop-balance {
  font-size: 14px;
  color: var(--text-secondary);
  margin: 0 0 22px;
}

.gem-shop-balance strong {
  font-size: 16px;
}

/* Match the main-menu currency colors so the store's balance row
 * reads as the same data the player just saw on the menu. Gold for
 * coins, spirit-blue for gems. */
.gem-shop-balance .gem-shop-coins { color: var(--gold); }
.gem-shop-balance .gem-shop-gems  { color: #5dd0ff; }

.gem-shop-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

@media (min-width: 720px) {
  .gem-shop-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.gem-offer-tile {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 10px;
  padding: 16px 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  transition: border-color 0.2s;
}
.gem-offer-tile:hover { border-color: rgba(255, 255, 255, 0.15); }

/* Subscription tile spans the full width on its own row so it has
 * room for its longer description. */
.gem-offer-subscription {
  grid-column: 1 / -1;
  background: linear-gradient(135deg, rgba(78, 168, 222, 0.10), rgba(34, 197, 94, 0.06));
  border-color: rgba(78, 168, 222, 0.40);
}

.gem-offer-active {
  background: linear-gradient(135deg, rgba(34, 197, 94, 0.12), rgba(78, 168, 222, 0.08));
  border-color: rgba(34, 197, 94, 0.55);
}

.gem-offer-name {
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
}

.gem-offer-detail {
  font-size: 13px;
  color: var(--text-secondary);
  text-align: center;
}

.gem-offer-buy-btn,
.gem-offer-claim-btn {
  min-width: 120px;
  margin-top: 4px;
}

.gem-offer-footnote {
  font-size: 11px;
  color: var(--text-secondary);
  font-style: italic;
}

/* Confirm modal stacked over the shop panel. */
.gem-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1600;
  padding: 24px;
}

.gem-confirm-panel {
  background: var(--bg-secondary);
  padding: 28px 32px 24px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  max-width: min(440px, 92vw);
  text-align: center;
  box-shadow: 0 0 32px rgba(0, 0, 0, 0.6);
}

.gem-confirm-message {
  font-size: 15px;
  line-height: 1.5;
  color: var(--text-primary);
  margin: 0 0 20px;
}

.gem-confirm-buttons {
  display: flex;
  justify-content: center;
  gap: 12px;
}

.gem-confirm-cancel,
.gem-confirm-ok {
  min-width: 110px;
}

/* ── Responsive: iPad landscape and shorter ──────────────────────
   Binding constraint is vertical space. Below 900px tall, shrink
   card/ring dimensions, compress hand rows, tighten board padding.
*/
@media (max-height: 900px) {
  :root {
    --card-w: 107px;
    --card-h: 150px;
    --ring-w: 115px;
    --ring-h: 158px;
    --hand-h: 130px;
    --opp-hand-h: 80px;
    --opp-card-w: 65px;
    --opp-card-h: 91px;
    --board-pad-y: 4px;
    --board-pad-x: 8px;
    --board-gap: 2px;
    --ring-pad: 4px;
  }
}

/* ── Responsive: older iPad (1024×768) and iPad mini landscape ── */
@media (max-height: 800px) {
  :root {
    --card-w: 94px;
    --card-h: 132px;
    --ring-w: 102px;
    --ring-h: 142px;
    --hand-h: 115px;
    --opp-hand-h: 70px;
    --opp-card-w: 55px;
    --opp-card-h: 77px;
    --card-fs: 9px;
  }
  .player-info {
    padding: 4px 10px;
    font-size: 12px;
  }
  .player-label {
    min-width: 60px;
    font-size: 11px;
  }
}

/* ── Responsive: tablet portrait ─────────────────────────────────
   Same token sizes as the tight break; also shrinks the game log
   so its collapsed pill never encroaches on the phase bar.
*/
@media (orientation: portrait) and (max-width: 900px) {
  :root {
    --card-w: 94px;
    --card-h: 132px;
    --ring-w: 102px;
    --ring-h: 142px;
    --hand-h: 115px;
    --opp-hand-h: 70px;
    --opp-card-w: 55px;
    --opp-card-h: 77px;
    --card-fs: 9px;
  }
  .game-log {
    width: 220px;
    max-width: 40vw;
  }
  /* Panel top-anchoring (68px) now lives in the base rule — this portrait
     tablet width uses the same opponent-header sizing, so no override. */
}

/* ── Responsive: phone ─────────────────────────────────────────
   Width-driven: below 600px the default gaps alone don't fit 5 ring
   slots + a 5-card hand. Shrinks tokens and gaps aggressively.
   Hand still scrolls horizontally for 7+ card hands (see `.hand-area`
   overflow-x rule applied globally above).
*/
@media (max-width: 600px) {
  :root {
    --card-w: 58px;
    --card-h: 81px;
    --hand-h: 95px;
    --opp-hand-h: 55px;
    --opp-card-w: 42px;
    --opp-card-h: 59px;
    --card-fs: 8px;
    --board-pad-y: 2px;
    --board-pad-x: 2px;
    --board-gap: 1px;
    --ring-pad: 2px;
  }
  /* On phones the ring slots stretch to fill the width, so the task list
     sits overlaid on the play area. Start it collapsed (only the toggle
     shows) — the user can expand it when they need the reminder. */
  .task-list { font-size: 14px; }
  .task-list-toggle { font-size: 11px; padding: 3px 10px; }
  /* Only 3 ring slots exist — on phones, let them stretch to fill
     width (bounded by 140px max) and derive height from aspect ratio.
     Much more legible than the 62×87 we had before. */
  .ring-area {
    gap: 6px;
    width: 100%;
  }
  .ring-slot {
    flex: 1 1 0;
    width: auto;
    height: auto;
    /* min-width: 0 prevents the slot's flex basis from being inflated by
       its content's intrinsic min-width (the card's `width: var(--card-w)`).
       Without this, a slot whose card is briefly absent (e.g. mid-drag with
       `position: fixed`, or KO'd) and a slot whose card is present can
       end up with different intrinsic sizes, leaving the row unbalanced
       even after the next render. */
    min-width: 0;
    max-width: 140px;
    aspect-ratio: 62 / 87;
    /* Limit the transition to visual properties only — the base .ring-slot
       rule applies `transition: all` which would animate width/height
       changes from flex distribution. With aspect-ratio in play, those
       animations can settle at intermediate sizes if interrupted. */
    transition:
      border-color var(--transition-speed),
      background-color var(--transition-speed),
      box-shadow var(--transition-speed);
  }
  .hand-area { gap: 3px; padding: 20px 2px 2px; }
  .player-info { font-size: 11px; padding: 3px 6px; gap: 8px; }
  .player-label { min-width: 50px; font-size: 10px; }
  .phase-pip { font-size: 9px; padding: 2px 4px; }
  .action-button { padding: 4px 12px; font-size: 12px; }
  .game-log { width: 140px; max-width: 60vw; }
  /* Phones use a smaller top offset than the base: the opponent ring
     stretches full-width just below the hand here, so the panel can't drop
     as far as on desktop without covering the opponent's ring cards. This
     sits below the header (clearing most of the speech balloon) while
     keeping the collapsed pill above the ring. */
  .task-list,
  .game-log {
    top: 76px;
  }
}

/* ── Drag-and-drop ─────────────────────────────────────────────────
   Applied by DragController during a pointer drag. Source cards use
   .dragging; drop targets use .drag-valid-target (all eligible) and
   .drag-over (currently under the pointer). Action-card effect
   previews use .preview-target. Staged attackers/blockers use
   .staged-attacker (+ .united-primary / .united-joiner).
*/

.card.dragging {
  position: fixed;
  top: 0;
  left: 0;
  margin: 0;
  z-index: 1000;
  transition: none !important;
  pointer-events: none;
  box-shadow: 0 16px 32px rgba(0, 0, 0, 0.6);
  cursor: grabbing;
  touch-action: none;
}

/* Placeholder slot kept in a hand while a card is being dragged, so the
   other cards don't reflow. Invisible — just reserves space. */
.drag-placeholder {
  flex-shrink: 0;
  visibility: hidden;
  pointer-events: none;
}

.game-board .card {
  touch-action: none;
}

/* Animate the dragged card back to its home rect when the drop is invalid.
   Needs !important to beat .card.dragging's `transition: none !important`,
   which we keep applied during the snap so position: fixed stays intact
   (otherwise translate() reinterprets relative to flex flow and jumps). */
.card.dragging.snap-back {
  transition: transform 0.2s ease !important;
}

.drag-valid-target {
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
}

.drag-over {
  outline-style: solid !important;
  background-color: rgba(233, 69, 96, 0.15);
}

.card.drag-over {
  box-shadow: 0 0 20px var(--accent-glow);
}

.battle-line.drag-valid-target {
  height: 8px;
  opacity: 0.95;
  outline: none;
  animation: pulse-line 0.8s ease infinite;
}

.battle-line.drag-over {
  height: 14px;
  opacity: 1;
  background: linear-gradient(90deg, transparent, var(--accent), var(--gold), var(--accent), transparent);
}

.preview-target {
  box-shadow: 0 0 16px rgba(255, 215, 0, 0.7) !important;
  animation: pulse-border 1s ease infinite;
}

.hp-bar-container.preview-target,
.deck-count.preview-target {
  outline: 2px solid var(--gold);
  border-radius: 4px;
}

/* Attack posture — a forward lean that persists from the moment the player
   stages an attacker through combat resolution. Two sources apply it:
   `.staged-attacker` (local pre-dispatch state, player only) and `.attacker`
   (engine `attackLine`, both sides). Direction is per-side: player leans up
   (toward opponent), opponent leans down (toward player).
   Selectors include .ring-slot and :hover so we beat card.css's
   `.ring-slot .card:hover { transform: none }` (0,2,1). */
.ring-area[data-side="player"] .ring-slot .card.staged-attacker,
.ring-area[data-side="player"] .ring-slot .card.staged-attacker:hover,
.ring-area[data-side="player"] .ring-slot .card.attacker,
.ring-area[data-side="player"] .ring-slot .card.attacker:hover,
.ring-area[data-side="player"] .ring-slot .card.blocker,
.ring-area[data-side="player"] .ring-slot .card.blocker:hover {
  transform: translateY(-25%);
}

.ring-area[data-side="opponent"] .ring-slot .card.attacker,
.ring-area[data-side="opponent"] .ring-slot .card.attacker:hover,
.ring-area[data-side="opponent"] .ring-slot .card.blocker,
.ring-area[data-side="opponent"] .ring-slot .card.blocker:hover {
  transform: translateY(25%);
}

/* Snap the lean instantly — don't animate from "no transform" to "leaned"
   over 200ms. With keyed-reconciliation rendering, the .attacker /
   .blocker / .staged-attacker classes get toggled on existing card nodes,
   which would otherwise lerp the transform via the base .card transition.
   That lerp leaves the combat-arrow overlay (drawn synchronously from
   `getBoundingClientRect` after each render) pointing at the card's
   pre-animation position — visible as a laggy arrow during the lean.
   Mirrors the `transition: none` treatment on .united-joiner above. */
.ring-slot .card.staged-attacker,
.ring-slot .card.attacker,
.ring-slot .card.blocker {
  transition: none;
}

.ring-slot .card.staged-attacker {
  box-shadow: 0 0 12px var(--accent-glow);
  border-color: var(--accent);
}

/* United-attack primary styling. Same selector relaxation as
   .united-joiner: applies during local staging and during attack/counter,
   on either side. Without this the primary would lose its z-index: 3
   when .staged-attacker is dropped at DECLARE_ATTACKERS, letting the
   joiners (which keep z-index: 2 from .united-joiner) draw over it. */
.ring-slot .card.united-primary,
.ring-slot .card.united-primary:hover {
  box-shadow: 0 0 16px rgba(78, 168, 222, 0.7);
  border-color: var(--spirit-blue);
  z-index: 3;
}

/* United-attack joiner styling. Position is driven by inline transform
   from BoardView (so the joiner can sit over its primary regardless of
   slot distance); these rules just supply the glow / border / opacity
   that go with the pose. Applied on either side, both during local
   staging (with .staged-attacker) and during attack/counter (without).

   transition is overridden so the inline transform applies instantly.
   .card carries `transition: all 0.2s ease`, which would animate the
   joiner from its initial no-transform state to the cascade position
   on every re-render — visible as a lerp every time the engine pushes
   a new state during attack resolution. */
.ring-slot .card.united-joiner,
.ring-slot .card.united-joiner:hover {
  box-shadow: 0 0 12px rgba(78, 168, 222, 0.6);
  border-color: var(--spirit-blue);
  z-index: 2;
  opacity: 0.9;
  transition: none;
}

/* Fallback joiner transform — sized down + nudged toward where the
   primary's slot most plausibly sits (one slot left of the joiner) so
   the united-attack pose is visible even without the precise inline
   transform from BoardView's position pass. The position pass uses
   `cardEl.style.transform` (inline, specificity 1,0,0,0) which always
   wins over these CSS rules, so this is a no-op when the pass runs
   normally. It's the cushion for any path where the pass is skipped
   (e.g. layout not ready, or a race we haven't yet diagnosed).
   Direction matches the per-side lean: player joiner sits slightly
   above its own slot center (peeking out below the primary which sits
   higher), opponent joiner sits slightly below. */
.ring-area[data-side="player"] .ring-slot .card.united-joiner,
.ring-area[data-side="player"] .ring-slot .card.united-joiner:hover {
  transform: translate(-50%, -5%) scale(0.85);
}
.ring-area[data-side="opponent"] .ring-slot .card.united-joiner,
.ring-area[data-side="opponent"] .ring-slot .card.united-joiner:hover {
  transform: translate(-50%, 45%) scale(0.85);
}

/* Trash icon — appears to the right of the player's ring slots whenever
   the player is over their hand limit. Anchored to the ring-area (not the
   hand) so it's reachable even when the hand overflows horizontally. */
.ring-area[data-side="player"] {
  position: relative;
}

/* On wide screens the player's ring row has vertical slack and the cards
   sit centered in it — close enough to the battle-line that staged
   attackers (which lift translateY(-25%) toward the line) cross it and
   graze the opponent's ring. Nudge the whole player ring down toward the
   hand to open up that headroom. The hand-area's 24px top padding absorbs
   the shift, so the ring doesn't crowd the hand cards. Phones are excluded:
   their row is tight and the ring slots stretch full-width, so there's no
   slack to spend.

   Use `top` (the area is already position:relative) rather than a
   transform: a transform on this element would make it the containing
   block for the fixed-position dragged card AND a new stacking context,
   which breaks the drag offset (card flies off-screen) and z-order (card
   draws under the hand). `top` does neither. */
@media (min-width: 720px) {
  .ring-area[data-side="player"] {
    top: 24px;
  }
}

.hand-trash {
  /* Landscape default — sit off to the right of the player ring area,
     out of the way of board state. The parent .ring-area[data-side=
     "player"] is `position: relative`, so `right: 12px` anchors to its
     right edge. Sized to match a ring slot so the drop target is
     visually obvious. The portrait override below moves it to the
     viewport center when there's no horizontal room to the side. */
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: var(--ring-w);
  height: var(--ring-h);
  border: 2px dashed var(--accent);
  border-radius: 10px;
  background: rgba(233, 69, 96, 0.1);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--ring-w) * 0.4);
  flex-shrink: 0;
  color: var(--accent);
  transition: all 0.2s ease;
  z-index: 5;
}

.hand-trash.drag-valid-target {
  animation: pulse-border 1s ease infinite;
  outline: none;
}

.hand-trash.drag-over {
  background: rgba(233, 69, 96, 0.3);
  transform: translateY(-50%) scale(1.05);
}

/* Phones (narrow portrait): the 3 ring slots stretch to fill the
   width, leaving no room off to the side for a full-sized trash. The old
   approach pinned it to the viewport center, but the battle-line sits
   ABOVE center (the player side has more chrome below it — hand, info,
   phase), so "center" landed on the player's middle ring slot and players
   trashed cards they meant to play there. Anchor it to the player
   ring-area (position:relative) and lift it just above that area — i.e.
   just above the battle line — so it mostly overlaps the opponent's center
   ring slot. `bottom: 100%` is the ring-area's top edge regardless of the
   row's height; +12px clears the thin battle-line bar. The translucent
   fill lets the opponent card show through, and the slight downward offset
   from the opponent slot keeps the trash's outline distinct from it.
   Gated to portrait + max-width 600px so iPad portrait (~768px+), which has
   plenty of side margin, keeps the cleaner off-to-the-side layout. */
@media (orientation: portrait) and (max-width: 600px) {
  .hand-trash {
    position: absolute;
    left: 50%;
    top: auto;
    right: auto;
    bottom: calc(100% + 12px);
    transform: translateX(-50%);
  }
  .hand-trash.drag-over {
    transform: translateX(-50%) scale(1.05);
  }
}

/* ─────────────────────────────────────────────────────────────────
   Comic Neue — self-hosted from assets/fonts/comic-neue/ for the
   voice-bubble font stack (`--font-bubble`). Licensed under SIL OFL
   (see assets/fonts/comic-neue/OFL.txt). The bubble CSS uses
   font-weight: 600 which doesn't exactly match any of these faces —
   browsers will round to the nearest available weight (700 Bold for
   non-italic, BoldItalic for italic narrator variants).
   ───────────────────────────────────────────────────────────────── */

@font-face {
  font-family: 'Comic Neue';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../assets/fonts/comic-neue/ComicNeue-Light.ttf') format('truetype');
}
@font-face {
  font-family: 'Comic Neue';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url('../assets/fonts/comic-neue/ComicNeue-LightItalic.ttf') format('truetype');
}
@font-face {
  font-family: 'Comic Neue';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/comic-neue/ComicNeue-Regular.ttf') format('truetype');
}
@font-face {
  font-family: 'Comic Neue';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/comic-neue/ComicNeue-Italic.ttf') format('truetype');
}
@font-face {
  font-family: 'Comic Neue';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../assets/fonts/comic-neue/ComicNeue-Bold.ttf') format('truetype');
}
@font-face {
  font-family: 'Comic Neue';
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url('../assets/fonts/comic-neue/ComicNeue-BoldItalic.ttf') format('truetype');
}

/* ─────────────────────────────────────────────────────────────────
   Voice bubbles (Phase 2 — character + opponent voice lines)

   Two variants:
   - .voice-bubble-speaker: rounded corners + tail; standard comic
     speech bubble; non-italic body text.
   - .voice-bubble-narrator: rectangular box, italic text, no tail.
     Used for non-speaker stage directions.

   Two anchor styles:
   - .voice-bubble-card: positioned above a character card, pointer
     events disabled so the bubble doesn't intercept clicks.
   - .voice-bubble-opponent-header / .voice-bubble-player-header:
     descend from below the corresponding profile icon at the top of
     the screen, can span across the opponent's hand cards.
   ───────────────────────────────────────────────────────────────── */

.voice-bubble {
  position: fixed;
  z-index: 200;                       /* above damage popups, below modals */
  pointer-events: none;
  font-family: var(--font-bubble);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.25;
  color: #1a1a2e;
  background: #fdfdfd;
  padding: 10px 14px;
  max-width: 320px;
  text-align: center;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
  /* opacity transition is set inline by VoiceBubble.ts so the fade
     duration follows config.voiceLineFadeMs — keep this rule free of
     transition: opacity overrides. */
}

/* Card-anchored bubbles need `width: max-content` so the bubble takes
   its content width up to `max-width`. Without this, the CSS shrink-
   to-fit formula computes available width as `viewport_width - left`
   for a fixed-position element with only `left` set — so a bubble
   anchored on a right-edge ring slot ends up squeezed into a sliver
   of available space and wraps unnecessarily, even though the
   transform later centers it. The runtime clamp shifts the bubble
   back into the viewport when its content-width pushes it off an
   edge. Header variants set both left+right and don't need this. */
.voice-bubble-card {
  width: max-content;
}

.voice-bubble-speaker {
  border-radius: 16px;
  border: 2px solid #1a1a2e;
}

/* Tail for character-anchored speaker bubbles only. Bubble sits
   above the card (both player-side and opponent-side), tail at the
   bottom of the bubble pointing down at the card. Header bubbles
   don't get a tail — they emanate from the icon corner without one. */
.voice-bubble-speaker.voice-bubble-card::after {
  content: '';
  position: absolute;
  /* --tail-x is set by VoiceBubble.ts to keep the tail aimed at the
     speaker card even when the bubble's been horizontally shifted to
     stay inside the viewport (mobile portrait, edge ring slots). The
     `50%` fallback is the unshifted case. */
  left: var(--tail-x, 50%);
  bottom: -10px;
  width: 16px;
  height: 16px;
  background: #fdfdfd;
  border-right: 2px solid #1a1a2e;
  border-bottom: 2px solid #1a1a2e;
  transform: translateX(-50%) rotate(45deg);
}

.voice-bubble-narrator {
  border-radius: 4px;
  border: 1px solid #1a1a2e;
  font-style: italic;
  background: #f4f0e6;                /* slightly off-white parchment */
}

/* Header anchors. Position relative to viewport: descend from the
   profile icon's corner. The opponent profile icon currently lives in
   the upper-left of the play screen; the player icon mirrors at the
   upper-right. Exact pixel offsets follow the existing header-strip
   layout; if those change, update the `top` values here. */
.voice-bubble-opponent-header {
  position: fixed;
  top: 62px;                          /* below opponent HP bar */
  left: 8px;
  max-width: calc(100vw - 16px);
}

.voice-bubble-player-header {
  position: fixed;
  top: 62px;
  right: 8px;
  max-width: calc(100vw - 16px);
}

/* Mobile portrait: bubbles span nearly the full viewport so text is
   legible. Card-anchored bubbles use a comfortable cap; header
   bubbles already use viewport-relative max-width. */
@media (max-width: 600px) and (orientation: portrait) {
  .voice-bubble {
    font-size: 14px;
  }
  .voice-bubble-card {
    max-width: calc(100vw - 32px);
  }
  .voice-bubble-opponent-header,
  .voice-bubble-player-header {
    /* Stretch across the top — readability beats elegant geometry
       at this width. */
    left: 8px;
    right: 8px;
    max-width: none;
  }
}

/* ─────────────────────────────────────────────────────────────────
   End-game interstitial — full-screen overlay that plays between
   game-over detection and the game-over modal. Two scene cards
   slide in (responsible card from one side, opponent profile from
   the other) and each speaks a line. Click anywhere on the overlay
   to skip the rest of the sequence.
   ───────────────────────────────────────────────────────────────── */

.endgame-interstitial-overlay {
  position: fixed;
  inset: 0;
  z-index: 600;                   /* above the play board, below modals */
  background: rgba(0, 0, 0, 0.65);
  display: block;
  cursor: pointer;
  transition: opacity 200ms ease;
  opacity: 1;
}

.endgame-interstitial-overlay.endgame-interstitial-fading {
  opacity: 0;
}

.endgame-card {
  position: absolute;
  top: 30%;
  width: 180px;
  height: 252px;
  background: var(--bg-card);
  border: 3px solid var(--gold);
  border-radius: 12px;
  /* No overflow: hidden — we need the speech bubble (absolutely
     positioned above the card via `bottom: calc(100% + 12px)`) to
     escape the card's bounding box. The img + name children get
     their own border-radius below so the rounded card corners still
     read correctly. */
  display: flex;
  flex-direction: column;
  transition: transform 300ms ease, opacity 300ms ease;
  opacity: 0;
}

.endgame-card img {
  width: 100%;
  height: 80%;
  object-fit: cover;
  object-position: center top;
  /* Match the inner rounded corners (card radius 12px - border 3px). */
  border-top-left-radius: 9px;
  border-top-right-radius: 9px;
}

.endgame-card-name {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-main);
  font-weight: 700;
  font-size: 14px;
  text-align: center;
  padding: 4px 8px;
  color: var(--gold);
  border-bottom-left-radius: 9px;
  border-bottom-right-radius: 9px;
}

.endgame-deckout-plate,
.endgame-fallback-plate {
  background: var(--bg-secondary);
  border-color: var(--text-secondary);
}

.endgame-deckout-plate .endgame-card-name,
.endgame-fallback-plate .endgame-card-name {
  color: var(--text-secondary);
  font-style: italic;
  height: 100%;
}

/* Pre-slide-in resting state. Off-screen on the chosen side.
   Landing positions (left: 20%, right: 20%) put each card about a
   fifth of the viewport in from its edge — well inside the visible
   area rather than hugging the borders. The empty center between
   them stays substantial enough that both cards read as distinct
   focal points rather than blending together. */
.endgame-card.endgame-left {
  left: 20%;
  /* translateX must clear the card's own width + the 20% landing
     anchor on any reasonable viewport. -400% covers viewports up to
     ~2700px wide; below that the card is fully off-screen at the
     start of the slide, eliminating the visible "pop-in". */
  transform: translateX(-400%);
}

.endgame-card.endgame-right {
  right: 20%;
  transform: translateX(400%);
}

/* When both cards land on the SAME side (opponent-wins case — the
   responsible attacker AND the opponent profile both come from the
   right), stagger them so they don't overlap. Primary lands higher
   and further inward; secondary lands lower and closer to the right
   edge. Both still slide from the same off-screen direction. The
   gap between primary's `right: 32%` and secondary's `right: 8%`
   keeps the two visually distinct on landscape. */
.endgame-card.endgame-card-stacked-primary {
  top: 22%;
  right: 32%;
}
.endgame-card.endgame-card-stacked-secondary {
  top: 42%;
  right: 8%;
}

/* Slide-in landed state — opacity full, transform identity. */
.endgame-card.endgame-slide-in {
  opacity: 1;
  transform: translateX(0);
}

/* Bubble attached to a slide-in card. Positioned above the card so
   it's visible during the dwell.

   `width: max-content` is critical here. Without it, CSS's shrink-to-
   fit algorithm computes the bubble's available width as
   `containing-block-width - left-offset` (the card is 180px wide and
   `left: 50%` consumes 90px, leaving ~90px of "available" width). The
   bubble then wraps into a narrow column even though max-width allows
   more. `max-content` bypasses the shrink-to-fit constraint and lets
   the bubble use its content's preferred width, then max-width clamps
   it on wide viewports. */
.endgame-bubble {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 12px);
  transform: translateX(-50%);
  width: max-content;
  max-width: 360px;
}

.endgame-bubble.voice-bubble-speaker::after {
  /* tail pointing down at the card. `--tail-x` is set by
     EndGameInterstitial.ts when the bubble has been horizontally
     shifted to stay inside the viewport (corner cards on mobile
     portrait) so the tail still points at the card center. */
  content: '';
  position: absolute;
  left: var(--tail-x, 50%);
  bottom: -10px;
  width: 16px;
  height: 16px;
  background: #fdfdfd;
  border-right: 2px solid #1a1a2e;
  border-bottom: 2px solid #1a1a2e;
  transform: translateX(-50%) rotate(45deg);
}

@media (max-width: 600px) and (orientation: portrait) {
  /* Portrait positioning: responsible card upper-portion of screen,
     opponent profile lower-portion. Both sides as instructed by the
     spec (player card upper-left when player wins; opponent card
     upper-right + opponent profile lower-right when opponent wins). */
  .endgame-card {
    width: 140px;
    height: 196px;
  }
  /* Player-wins (non-stacked) case: the responsible card and the
     opponent profile card slide in from opposite sides. On portrait
     they'd otherwise land at the same vertical level, which gives
     their above-the-card bubbles enough viewport width to overlap
     horizontally and become unreadable. Split them onto upper-left
     and lower-right halves so the two bubbles can never collide. */
  .endgame-card.endgame-left {
    top: 8%;
    left: 6%;
  }
  .endgame-card.endgame-right {
    top: auto;
    bottom: 8%;
    right: 6%;
  }
  /* Stacked layout on the right side (opponent-wins case): primary
     stays upper-right, secondary drops to lower-right. The horizontal
     offset is the same (both flush with the right edge) — portrait
     real estate is too narrow to also stagger horizontally. Reset
     `bottom` since these elements also match the `.endgame-right`
     rule above which would otherwise leak through. */
  .endgame-card.endgame-card-stacked-primary {
    top: 8%;
    bottom: auto;
    right: 6%;
  }
  .endgame-card.endgame-card-stacked-secondary {
    top: 50%;
    bottom: auto;
    right: 6%;
  }
  .endgame-bubble {
    max-width: calc(100vw - 32px);
  }
}

