:root {
  --bg: #14100c;
  --panel: #211a13;
  --surface: var(--panel);
  --surface-elevated: #2a2115;
  --border: #7a5c2e;
  --gold: #d9b25c;
  --gold-bright: #f0d896;
  --magic: #8a6fd8;
  /* Fluid width for the rankings/music side docks (see the @media blocks
     using it below) — scales down smoothly from the original fixed 320px
     as the viewport narrows, instead of an all-or-nothing cutoff, so both
     docks keep working on ordinary laptop screens (1350px+) rather than
     only on very wide monitors. 474 = half the centered #lobby column
     (450) + the dock's own 1.5rem edge margin (24px); floors at 200px,
     the narrowest width its own contents (table columns, music buttons)
     still comfortably fit at. */
  --dock-width: clamp(200px, calc(50vw - 474px), 320px);
  --success: #6fd66a;
  --error: #e05c52;
  --warning: #e0a94d;
  --info: #5cb3e0;
  --text: #ecdfc4;
  --text-secondary: #a5947a;
  --dim: #a5947a;
  --disabled: #6b6156;
  --focus-ring: var(--gold);
  --hp: #b5322f;
  --mana: #3c7fd0;
  --vanguard: #3a2c1a;
  --rearguard: #241c12;
  --font-display: 'Cinzel', Georgia, 'Times New Roman', serif;
  /* System font stack — no external request, always available, reads as a
     modern UI face. Nothing set this before, so body/UI text silently fell
     back to the Georgia serif stack meant only for decorative headings. */
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  /* Global multiplier for card name/ability text size — adjustable via the
     stepper in the HUD-editing toolbar (applyCardTextScale() in client.js),
     persisted to localStorage and, when logged in, the player's account.
     Left at 1 (no-op) until JS applies a saved preference. */
  --card-text-scale: 1;

  /* Faction accents — the 10 hex values .faction-card[data-faction] already
     used as bare literals, named so they can be referenced elsewhere too. */
  --faction-beast: #4f8a3a;
  --faction-clock: #c17a2e;
  --faction-damned: #8a1f2b;
  --faction-dwarf: #d4a017;
  --faction-dynasty: #2f9e6e;
  --faction-elf: #3f9eae;
  --faction-fallen: #7a4fc4;
  --faction-human: #3c6fd0;
  --faction-orc: #b5432e;
  --faction-undead: #8a9e4a;

  /* Spacing scale — 4/8/12/16/24/32px. Applied to components touched in
     this pass, not a file-wide rewrite of every existing margin/padding. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* Type scale — clamp()-based so it's fluid across viewport widths rather
     than jumping at breakpoints. Applied to lobby headings/section titles
     in this pass; the ~55 pre-existing one-off font-size values elsewhere
     (card text, HUD text tuned against real layout constraints) are left
     alone rather than force-fit into this scale. */
  --text-xs: clamp(0.7rem, 0.67rem + 0.15vw, 0.78rem);
  --text-sm: clamp(0.82rem, 0.78rem + 0.2vw, 0.92rem);
  --text-base: clamp(0.95rem, 0.9rem + 0.2vw, 1.05rem);
  --text-md: clamp(1.05rem, 1rem + 0.3vw, 1.2rem);
  --text-lg: clamp(1.2rem, 1.1rem + 0.4vw, 1.4rem);
  --text-xl: clamp(1.5rem, 1.3rem + 0.8vw, 1.85rem);
  --text-2xl: clamp(1.9rem, 1.6rem + 1.4vw, 2.5rem);
  --text-3xl: clamp(2.3rem, 1.9rem + 2vw, 3.2rem);
}

* { box-sizing: border-box; }

/* A visible focus ring matching the theme for every interactive element —
   nothing suppresses the browser default elsewhere in this file, but the
   default ring reads inconsistently against a dark parchment/gold UI. */
button:focus-visible, input:focus-visible, select:focus-visible, a:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

body {
  margin: 0;
  position: relative;
  background:
    repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0 2px, transparent 2px 6px),
    radial-gradient(ellipse 900px 600px at 15% 85%, #8a6fd81a 0%, transparent 60%),
    radial-gradient(ellipse at top, #241c12 0%, var(--bg) 70%),
    radial-gradient(ellipse at 50% 50%, transparent 45%, #000 105%);
  color: var(--text);
  font-family: var(--font-body);
  min-height: 100vh;
  /* A UX deterrent against casual Ctrl+A/click-drag selection, not real
     protection — anyone can still bypass this via DevTools. Re-enabled
     below on every actual text-entry element so typing/selecting in forms,
     chat, and search boxes is completely unaffected. */
  user-select: none;
  -webkit-user-select: none;
}
input, textarea, [contenteditable] {
  user-select: text;
  -webkit-user-select: text;
}

/* Visually hidden but still reachable by screen readers/keyboard focus —
   used for form labels where a visible label would clutter an already-
   clear layout (login form, chat). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* A handful of slow-drifting motes for a faint sense of ambient magic —
   a separate fixed layer so it can animate independently of the static
   background gradients above, and be switched off entirely for users who
   asked for reduced motion. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  background-image:
    radial-gradient(2px 2px at 20% 30%, #f0d89670, transparent),
    radial-gradient(2px 2px at 75% 60%, #8a6fd870, transparent),
    radial-gradient(1.5px 1.5px at 50% 80%, #f0d89650, transparent);
  background-repeat: no-repeat;
  animation: drift-motes 40s ease-in-out infinite;
}
@keyframes drift-motes {
  0%, 100% { background-position: 0 0, 0 0, 0 0; }
  50% { background-position: 0 -18px, 0 14px, 0 -10px; }
}
@media (prefers-reduced-motion: reduce) {
  body::after { animation: none; }
  #arrowPath { animation: none; }
  /* Continuous/looping decorative pulses only — the state they signal
     (whose turn, legal target, playable slot, the front-line divider)
     still reads from the static color/border/outline underneath, so
     turning off just the looping animation loses nothing functional.
     Brief, one-shot feedback (combat hits, card-enter, coin flip) stays
     as-is since those convey a discrete event, not ambient motion. */
  .hp-panel.legal-target,
  .hp-panel.active-turn,
  .slot.legal-target,
  .slot.can-play,
  .slot.spell-target,
  .lane-divider,
  .turn-timer.timer-critical,
  .plan-castle-lethal {
    animation: none;
  }
}

.hidden { display: none !important; }

body.in-game {
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.lang-picker {
  display: flex;
  gap: 0.3rem;
  background: #00000040;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.2rem;
}
.lang-btn {
  background: transparent;
  border: none;
  color: var(--dim);
  font-family: var(--font-display);
  font-size: 0.7rem;
  letter-spacing: 1px;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.lang-btn:hover {
  color: var(--text);
}
.lang-btn.active {
  background: var(--gold);
  color: var(--bg);
}
.coin-badge {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  background: #00000040;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.3rem 0.7rem;
  font-family: var(--font-display);
  font-size: 0.8rem;
  color: var(--gold-bright);
  white-space: nowrap;
}
.coin-badge-icon { font-size: 0.9rem; line-height: 1; }
.coin-badge.coin-badge--guest { color: var(--dim); font-size: 0.72rem; letter-spacing: 0.5px; }
.buy-coins-btn {
  width: 1.8rem;
  height: 1.8rem;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--gold);
  color: var(--gold);
  font-family: var(--font-display);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.buy-coins-btn:hover { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }

/* ---------- Partner banner (Castle Battle cross-promo) ---------- */
/* Layout comes from .lobby-secondary-nav, which it shares with the other
   three secondary entries — only its own internals are styled here. */
.partner-banner { text-decoration: none; }
.partner-banner-logo { width: 32px; height: 32px; flex-shrink: 0; }
.partner-banner-text { display: flex; flex-direction: column; gap: 0.2rem; line-height: 1.25; }
.partner-banner-text strong {
  font-family: var(--font-display);
  font-size: 0.82rem;
  color: var(--gold-bright);
  letter-spacing: 0.3px;
}
.partner-banner-text small { font-size: var(--text-xs); color: var(--dim); line-height: 1.35; }

.logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  margin: 2rem 0 0.2rem;
}
.logo-sigil {
  width: 48px;
  height: 48px;
  filter: drop-shadow(0 0 8px #d9b25c60);
}
/* Real, always-visible entry points into the full SEO content pages —
   part of .logo itself so it inherits whichever grid-area "logo" gets in
   every admin layout, guaranteeing zero-scroll visibility everywhere,
   rather than depending on how far down .seo-home-content ends up. Opens
   in a new tab since navigating away in this tab would lose any
   in-progress login/match-setup state. */
.home-content-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.2rem;
}
.home-content-nav a {
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 0.78rem;
  letter-spacing: 0.3px;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: #00000030;
  transition: border-color 0.15s, color 0.15s;
}
.home-content-nav a:hover {
  color: var(--gold-bright);
  border-color: var(--gold);
}
h1 {
  text-align: center;
  font-family: var(--font-display);
  font-weight: 700;
  background: linear-gradient(180deg, var(--gold-bright), var(--gold));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-size: var(--text-3xl);
  letter-spacing: 3px;
  margin: 0;
  text-shadow: 0 2px 10px #00000090;
}
h1 span {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--dim);
  letter-spacing: 5px;
  text-transform: uppercase;
  -webkit-text-fill-color: var(--dim);
}

/* ---------- Buttons ---------- */
/* One shared primary/gold treatment, used everywhere a call-to-action
   button appears (lobby, account form, game controls, game-over overlay)
   instead of the same gradient/border/shadow declared five separate times.
   Context-specific selectors below only adjust sizing where needed. */
.btn {
  background: linear-gradient(#e7c877, #b8912f);
  border: 1px solid #5c4419;
  box-shadow: inset 0 1px 0 #ffe9b380, 0 3px 8px #0006;
  color: #241c12;
  font-family: inherit;
  font-weight: bold;
  padding: 0.6rem 1.2rem;
  border-radius: 6px;
  font-size: 1rem;
  cursor: pointer;
  transition: box-shadow 0.15s, transform 0.15s;
}
.btn:hover:not(:disabled) {
  box-shadow: inset 0 1px 0 #ffe9b380, 0 3px 10px #0007, 0 0 12px color-mix(in srgb, var(--gold) 50%, transparent);
}
.btn:active:not(:disabled) { transform: translateY(1px); box-shadow: inset 0 1px 3px #00000060; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn.btn-danger {
  background: linear-gradient(#d65d59, var(--hp));
  border-color: #4a1210;
  color: #fff2f0;
}
.btn.btn-danger:hover:not(:disabled) {
  box-shadow: inset 0 1px 0 #ffffff40, 0 3px 10px #0007, 0 0 12px color-mix(in srgb, var(--hp) 60%, transparent);
}

/* ---------- Lobby ---------- */
/* Wider than the old 900px because the play panel is now two real columns
   (mode + deck) plus a quests sidebar rather than one stack; everything else
   still shares this one centered measure so no panel is accidentally a
   different width. */
#lobby {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0.8rem 1.5rem 3rem;
  background: radial-gradient(ellipse at 50% 0%, #2a2013a0 0%, transparent 60%);
}

/* ---------- Lobby main row (play panel + quests sidebar) ---------- */
.lobby-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
}
/* Daily Quests only exists for a logged-in player — showLoggedIn()/
   showLoggedOut() toggle .hidden on the panel. Keying the second track off
   that directly means a guest gets a full-width play panel with no reserved
   empty gutter, and there's no second source of truth to keep in sync. A
   browser without :has() just falls through to the single-column rule above
   and stacks the panel underneath: still correct, only not side-by-side. */
.lobby-main:has(#questsPanel:not(.hidden)) {
  grid-template-columns: minmax(0, 1fr) 300px;
}
/* The panel centres itself when it's a standalone block further down the
   page; in the sidebar the grid track owns its width. */
.lobby-main > .quests-panel { max-width: none; margin: 0; }

/* ---------- Lobby header ---------- */
.lobby-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: 0.6rem 0 0.9rem;
  margin-bottom: var(--space-4);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 22%, transparent);
}
/* Overrides the tall, centered marquee treatment .logo carries for the
   pre-header layout — in the header it's a compact horizontal lockup. */
.lobby-header .logo {
  flex-direction: row;
  align-items: center;
  gap: 0.6rem;
  margin: 0;
}
.lobby-header .logo-sigil { width: 34px; height: 34px; }
.lobby-header h1 {
  text-align: left;
  font-size: var(--text-xl);
  letter-spacing: 1.5px;
  line-height: 1.05;
}
.lobby-header h1 span { font-size: 0.58rem; letter-spacing: 2.5px; }
.lobby-header .home-content-nav { margin-top: 0; justify-content: flex-start; }
.lobby-header-tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

.header-menu { position: relative; }
.header-menu-btn {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 38px;
  padding: 0.4rem 0.7rem;
  background: #00000040;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-secondary);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.header-menu-btn:hover { border-color: var(--gold); color: var(--gold-bright); }
.header-menu-btn[aria-expanded='true'] {
  border-color: var(--gold);
  color: var(--gold-bright);
  background: linear-gradient(#3a2c14, #241c12);
}
.header-account-name {
  max-width: 10ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.header-menu-caret { font-size: 0.6rem; opacity: 0.8; }

.header-popover {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 60;
  width: max-content;
  max-width: min(320px, calc(100vw - 2rem));
  background: linear-gradient(180deg, #2a2115, var(--panel));
  border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: 10px;
  padding: 0.9rem 1rem;
  box-shadow: inset 0 0 20px #00000060, 0 10px 24px #000a;
  animation: popover-in 0.14s ease-out;
}
@keyframes popover-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .header-popover { animation: none; }
}
/* The popover already supplies the card chrome — the panels inside it are
   plain content, not nested cards. */
.header-popover .account-panel,
.header-popover .music-player-lobby {
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  margin: 0;
  padding: 0;
  max-width: none;
  width: 100%;
}
.header-popover .account-panel::before,
.header-popover .account-panel::after { display: none; }
.header-popover--music { width: 268px; }
.header-popover--music .music-track-list { max-height: 210px; }

/* ---------- Play panel (mode + deck + primary CTA) ---------- */
.play-panel {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: var(--space-4);
  background: linear-gradient(180deg, #2a2115, var(--panel));
  border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: 10px;
  padding: var(--space-4);
  box-shadow: inset 0 0 24px #00000060, 0 6px 18px #0007;
}
.play-panel::before, .play-panel::after {
  content: '';
  position: absolute;
  width: 9px;
  height: 9px;
  background: var(--gold);
  opacity: 0.7;
  transform: rotate(45deg);
  box-shadow: 0 0 6px #d9b25c90;
  pointer-events: none;
}
.play-panel::before { top: -4px; left: -4px; }
.play-panel::after { bottom: -4px; right: -4px; }
/* Draft's pick UI is a 3-card offer strip plus a running picks summary —
   far too wide for the mode column, and the deck panel is meaningless
   during a draft, so the panel goes single-column for that mode only. */
.play-panel[data-mode='draft'] { grid-template-columns: minmax(0, 1fr); }
.play-panel[data-mode='draft'] > .play-col--deck { display: none; }

.play-col { display: flex; flex-direction: column; min-width: 0; }
.play-col--deck {
  padding-left: var(--space-4);
  border-left: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
}
.play-col-title {
  margin: 0 0 var(--space-2);
  color: var(--gold);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  letter-spacing: 1.2px;
  text-transform: uppercase;
}

/* Exactly one mode panel is ever un-hidden, so exactly one .play-cta is ever
   visible — the dominant button on the page, with no JS coordinating it. */
.play-cta { margin-top: auto; padding-top: var(--space-3); }
.play-cta > .btn {
  display: block;
  width: 100%;
  padding: 0.85rem 1.2rem;
  font-size: var(--text-lg);
  letter-spacing: 1px;
}
.mode-panel-desc {
  margin: 0 0 var(--space-2);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.4;
}

/* ---------- Party window ---------- */
.party-panel {
  margin-top: 0.7rem;
  padding: 0.8rem 0.9rem;
  background: #00000035;
  border: 1px solid color-mix(in srgb, var(--gold) 35%, var(--border));
  border-radius: 8px;
}
.party-head { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.6rem; }
.party-title {
  flex: 1;
  margin: 0;
  color: var(--gold);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: 1px;
  text-transform: uppercase;
}
.party-mode-picker { display: flex; gap: 0.25rem; background: #00000040; border: 1px solid var(--border); border-radius: 999px; padding: 0.15rem; }
.party-mode-btn {
  background: transparent;
  border: none;
  color: var(--dim);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  letter-spacing: 0.5px;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.party-mode-btn.active { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; font-weight: bold; }
.party-mode-btn:disabled { cursor: default; }
.party-mode-btn:disabled:not(.active) { opacity: 0.45; }

.party-seats { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.party-seat {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  background: linear-gradient(var(--surface-elevated), var(--panel));
  border: 1px solid var(--border);
  border-radius: 6px;
}
/* A seat nobody has taken yet reads as a placeholder, not a player. */
.party-seat.is-empty { background: none; border-style: dashed; opacity: 0.75; }
.party-seat.is-you { border-color: color-mix(in srgb, var(--gold) 60%, var(--border)); }
.party-seat-who { flex: 1; display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.party-seat-name { font-family: var(--font-display); font-size: var(--text-sm); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.party-seat-meta { color: var(--dim); font-size: var(--text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.party-seat-tags { display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; }
.party-tag {
  font-size: var(--text-xs);
  color: var(--dim);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  white-space: nowrap;
}
.party-tag--leader { color: var(--gold); border-color: color-mix(in srgb, var(--gold) 50%, var(--border)); }
/* Ready is marked by text as well as colour, so it doesn't rely on green alone. */
.party-tag--ready { color: var(--success); border-color: color-mix(in srgb, var(--success) 55%, var(--border)); }
.party-seat-toggle { font-size: var(--text-xs); }

.party-blocker { margin: 0.6rem 0 0; color: var(--dim); font-size: var(--text-xs); }
.party-actions { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.7rem; }
.party-actions .btn { flex: 1; }
.party-actions .btn-secondary.is-active { border-color: var(--success); color: var(--success); }

.waiting-panel {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
  padding: 0.5rem 0.7rem;
  background: #00000035;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.waiting-text { flex: 1; min-width: 0; }
.waiting-elapsed { color: var(--gold-bright); font-family: var(--font-display); }
.waiting-spinner {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  border: 2px solid color-mix(in srgb, var(--gold) 35%, transparent);
  border-top-color: var(--gold);
  border-radius: 50%;
  animation: waiting-spin 0.9s linear infinite;
}
@keyframes waiting-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .waiting-spinner { animation: none; }
}

/* Join-by-code is a real second action for Ranked, deliberately demoted
   below the primary Create CTA rather than competing with it. */
.join-row {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid color-mix(in srgb, var(--gold) 18%, transparent);
}
.join-row-label {
  display: block;
  margin-bottom: 0.45rem;
  color: var(--dim);
  font-size: var(--text-xs);
  letter-spacing: 0.3px;
}
.join-row-controls { display: flex; gap: 0.5rem; }
.join-row-controls input {
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: #0f0c08;
  color: var(--text);
  font-size: var(--text-base);
  letter-spacing: 2px;
  text-align: center;
  text-transform: uppercase;
}
.join-row-controls .btn-secondary { flex-shrink: 0; }

/* ---------- Selected deck summary ---------- */
.deck-summary {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  text-align: center;
  padding: var(--space-3);
  background: #00000035;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.deck-summary--empty { justify-content: center; min-height: 190px; }
/* Auto margins centre the deck card against the taller mode column instead
   of stranding it at the top. Collapse to 0 once the columns stack, where
   the column is only as tall as its content. */
.play-col--deck > .deck-summary { margin-block: auto; }
.play-col--deck > #deckPicker { margin-bottom: auto; }
.deck-summary-art {
  width: 84px;
  height: 84px;
  object-fit: contain;
  filter: drop-shadow(0 2px 8px #000a);
}
.deck-summary-name {
  margin: 0;
  color: var(--gold-bright);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  letter-spacing: 0.5px;
}
.deck-summary-badges { justify-content: center; }
.deck-summary-tagline {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--text-xs);
  line-height: 1.4;
}
.deck-summary-empty-text {
  margin: 0;
  color: var(--dim);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.deck-summary-change { margin-top: 0.3rem; }

/* ---------- Secondary nav ---------- */
.lobby-secondary-nav {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0;
}
/* One shared card treatment for all four entries — the three button-backed
   ones and the Castle Battle link — so nothing looks accidentally sized. */
.lobby-secondary-nav > * {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  text-align: center;
  margin: 0;
  padding: 0.9rem 0.8rem;
  background: #00000030;
  border: 1px solid var(--border);
  border-radius: 8px;
  transition: border-color 0.15s;
}
.lobby-secondary-nav > *:hover { border-color: color-mix(in srgb, var(--gold) 55%, var(--border)); }
.lobby-secondary-nav p {
  margin: 0;
  color: var(--dim);
  font-size: var(--text-xs);
  line-height: 1.35;
}
.lobby-secondary-nav .btn-secondary { width: 100%; }

/* ---------- Deck chooser modal ---------- */
/* Qualified with .overlay-box deliberately: that class is defined further
   down the sheet, so an unqualified .deck-chooser-modal ties on specificity
   and loses its padding and text-align to it. */
.overlay-box.deck-chooser-modal {
  width: min(1040px, 94vw);
  max-height: 88vh;
  overflow-y: auto;
  padding: 1.4rem 1.6rem;
  text-align: left;
}
.deck-chooser-modal .faction-picker { margin: var(--space-3) 0 0; }
/* "My Custom Decks" lives here rather than down the lobby page so a deck
   belonging to any faction is one click from Change Deck. Its own card
   chrome would read as a box-in-a-box inside the modal, so it becomes a
   plain section under a divider. */
.deck-chooser-modal .my-custom-decks-panel {
  margin: var(--space-4) 0 0;
  padding: var(--space-3) 0 0;
  background: none;
  border: none;
  border-top: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
  border-radius: 0;
}

.faction-picker {
  display: grid;
  /* minmax(0,1fr), not bare 1fr: a grid track's implicit min is auto (its
     content's min-content size) unless overridden — .deck-badge's
     white-space:nowrap pills forced tracks wider than their fair 1fr share
     on narrow viewports, overflowing the page horizontally. minmax(0,*)
     lets the track actually shrink to the space it's given. */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 12px;
  margin: 2rem 0;
}
.faction-card {
  --accent: var(--gold);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  padding: 1rem 0.5rem;
  background: linear-gradient(#2a2115, var(--panel));
  border: 2px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
}
/* One shared component, themed per faction via a custom property keyed
   off the same data-faction attribute already used to pick a deck. */
.faction-card[data-faction="beast"] { --accent: #4f8a3a; }
.faction-card[data-faction="clock"] { --accent: #c17a2e; }
.faction-card[data-faction="damned"] { --accent: #8a1f2b; }
.faction-card[data-faction="dwarf"] { --accent: #d4a017; }
.faction-card[data-faction="dynasty"] { --accent: #2f9e6e; }
.faction-card[data-faction="elf"] { --accent: #3f9eae; }
.faction-card[data-faction="fallen"] { --accent: #7a4fc4; }
.faction-card[data-faction="human"] { --accent: #3c6fd0; }
.faction-card[data-faction="orc"] { --accent: #b5432e; }
.faction-card[data-faction="undead"] { --accent: #8a9e4a; }
.faction-card:hover {
  transform: translateY(-3px);
  border-color: var(--accent);
  box-shadow: 0 6px 16px #0008, 0 0 14px color-mix(in srgb, var(--accent) 45%, transparent);
}
.faction-card.selected {
  border-color: var(--accent);
  background: linear-gradient(#3a2c14, #241c12);
  box-shadow: 0 0 12px #00000080 inset, 0 0 14px color-mix(in srgb, var(--accent) 60%, transparent);
}
.faction-card.faction-card--locked {
  cursor: not-allowed;
  opacity: 0.55;
}
.faction-card.faction-card--locked:hover {
  transform: none;
  border-color: var(--border);
  box-shadow: none;
}
.faction-card .thumb {
  position: relative;
  width: 84px;
  height: 126px;
  border-radius: 8px;
  overflow: hidden;
  background: #0f0c08;
  border: 2px solid var(--border);
  box-shadow: inset 0 0 10px #000a;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.faction-card:hover .thumb, .faction-card.selected .thumb {
  border-color: var(--accent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 55%, transparent);
}
.faction-card .thumb img { width: 100%; height: 100%; object-fit: contain; }
.faction-card .label { font-family: var(--font-display); font-weight: 600; font-size: 0.85rem; text-align: center; letter-spacing: 0.5px; }

.deck-badge-row { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; }
.deck-badge {
  font-size: 0.62rem;
  font-family: var(--font-display);
  letter-spacing: 0.3px;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1px 6px;
  color: var(--dim);
  background: #00000030;
  white-space: nowrap;
}
.faction-card .deck-badge { color: var(--gold); border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
.deck-badge-difficulty[data-difficulty="Beginner"] { color: var(--success); border-color: var(--success); }
.deck-badge-difficulty[data-difficulty="Intermediate"] { color: var(--gold-bright); border-color: var(--gold); }
.deck-badge-difficulty[data-difficulty="Advanced"], .deck-badge-difficulty[data-difficulty="Expert"] { color: #e0897a; border-color: var(--hp); }
.deck-badge-status[data-status="owned"] { color: var(--success); border-color: var(--success); }
.deck-badge-status[data-status="locked"] { color: var(--gold-bright); border-color: var(--gold); }
.deck-badge-status[data-status="guest-restricted"] { color: var(--dim); border-color: var(--border); }
.faction-card .buy-deck-btn {
  font-family: inherit;
  font-size: 0.68rem;
  padding: 2px 10px;
  border: 1px solid var(--gold);
  background: transparent;
  color: var(--gold-bright);
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.faction-card .buy-deck-btn:hover { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
.faction-card .deck-tagline {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
  text-align: center;
  line-height: 1.35;
  min-height: 1.6em;
}
.faction-card .deck-action-row { display: flex; gap: 6px; margin-top: 2px; }
.faction-card .view-deck-btn, .faction-card .build-deck-btn {
  font-family: inherit;
  font-size: 0.68rem;
  padding: 2px 10px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--gold);
  border-radius: 6px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.faction-card .view-deck-btn:hover, .faction-card .build-deck-btn:hover { border-color: var(--gold); color: var(--gold-bright); }
.faction-card .view-deck-btn:active, .faction-card .build-deck-btn:active { background: color-mix(in srgb, var(--gold) 15%, transparent); }

.my-custom-decks-panel {
  margin: 0 0 1.4rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: #1c150d40;
}
.my-custom-decks-panel h3 {
  margin: 0 0 0.6rem;
  color: var(--gold);
  font-family: var(--font-display);
  font-size: 0.95rem;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.my-custom-decks-empty { margin: 0; color: var(--dim); font-size: 0.88rem; }
.my-custom-decks-list { display: flex; flex-direction: column; gap: 8px; }
.my-custom-deck-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #00000030;
}
.my-custom-deck-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.my-custom-deck-name { font-family: var(--font-display); color: var(--text); font-size: 0.95rem; }
.my-custom-deck-meta { color: var(--dim); font-size: 0.78rem; }
.my-custom-deck-actions { display: flex; gap: 0.9rem; flex-shrink: 0; }
.my-custom-deck-delete-btn.confirm-armed { color: var(--hp); }

/* The four mode panels are content inside .play-panel's card, not cards of
   their own — .play-panel supplies the border//background/shadow, and each
   fills the mode column so its .play-cta can sit flush at the bottom. */
.lobby-actions,
.ai-match-panel,
.team-match-panel,
.draft-panel {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}
.account-panel::before, .account-panel::after,
.rankings-panel::before, .rankings-panel::after {
  content: '';
  position: absolute;
  width: 9px;
  height: 9px;
  background: var(--gold);
  opacity: 0.7;
  transform: rotate(45deg);
  box-shadow: 0 0 6px #d9b25c90;
  pointer-events: none;
}
.account-panel::before, .rankings-panel::before { top: -4px; left: -4px; }
.account-panel::after, .rankings-panel::after { bottom: -4px; right: -4px; }
.room-code {
  margin-top: 0.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.6rem;
  letter-spacing: 5px;
  color: var(--gold-bright);
  text-align: center;
  background: radial-gradient(ellipse at center, #3a2c1480, #1c150d80);
  border: 1px solid var(--gold);
  box-shadow: 0 0 16px color-mix(in srgb, var(--gold) 35%, transparent) inset, 0 0 10px color-mix(in srgb, var(--gold) 25%, transparent);
  padding: 0.6rem 0.8rem;
  border-radius: 8px;
}
.room-code .copy-code-btn {
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--dim);
  font-size: 0.85rem;
  letter-spacing: 0;
  padding: 0.3rem 0.5rem;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.room-code .copy-code-btn:hover { border-color: var(--gold); color: var(--gold); }
.room-code .copy-code-btn.copied { border-color: var(--success); color: var(--success); }
.lobby-status { text-align: center; margin-top: 0.9rem; color: var(--dim); min-height: 1.5em; }

/* Ranked vs AI Match — deliberately the most visually distinct choice in the
   lobby (see the master prompt's "make the difference impossible to
   misunderstand"). Same active/hidden toggle idiom as .ranking-tab below,
   just wider so the always-visible one-line consequence copy fits without
   truncating. 2x2 rather than one row: four tabs carrying a full line of
   consequence copy each would be unreadably narrow in the mode column. */
.mode-tabs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  margin: 0 0 var(--space-3);
}
.mode-tab {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  align-items: flex-start;
  text-align: left;
  background: linear-gradient(var(--surface-elevated), var(--panel));
  border: 2px solid var(--border);
  border-radius: 8px;
  padding: 0.7rem 0.9rem;
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.15s, background 0.15s;
}
.mode-tab-title { font-family: var(--font-display); font-weight: 600; color: var(--text-secondary); font-size: var(--text-base); letter-spacing: 0.3px; }
.mode-tab-desc { color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.35; }
.mode-tab:hover:not(.active) { border-color: color-mix(in srgb, var(--gold) 45%, var(--border)); }
.mode-tab.active { border-color: var(--gold); background: linear-gradient(#3a2c14, #241c12); }
.mode-tab.active .mode-tab-title { color: var(--gold); }
.mode-tab.active .mode-tab-desc { color: var(--text); }

.difficulty-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; margin: 0 0 0.6rem; }
.difficulty-btn {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  background: #0f0c08;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.6rem 0.5rem;
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  transition: border-color 0.15s, color 0.15s;
}
.difficulty-name { font-weight: bold; color: var(--text-secondary); font-size: var(--text-sm); }
.difficulty-desc { color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.3; }
.difficulty-btn:hover:not(.active) { border-color: color-mix(in srgb, var(--gold) 50%, var(--border)); }
.difficulty-btn.active { border-color: var(--gold); background: linear-gradient(#3a2c14, #241c12); }
.difficulty-btn.active .difficulty-name { color: var(--gold); }
.difficulty-btn.active .difficulty-desc { color: var(--text); }

/* ---------- 2v2 Team Battle setup (lobby) ---------- */
.team-seat-list { display: flex; flex-direction: column; gap: 0.5rem; margin: 0 0 0.6rem; }
.team-seat-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 0.7rem;
  border-radius: 8px;
  background: #0f0c08;
  border: 1px solid var(--border);
}
.team-seat-label { font-family: var(--font-display); font-size: var(--text-sm); color: var(--text-secondary); min-width: 7rem; }
.team-seat-row[data-seat="seatB"] .team-seat-label { color: var(--gold-bright); }
.team-seat-toggle { display: flex; gap: 0.3rem; }
.team-seat-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--dim);
  font-family: inherit;
  font-size: var(--text-xs);
  padding: 0.35rem 0.6rem;
  border-radius: 999px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.team-seat-btn:hover:not(.active) { border-color: color-mix(in srgb, var(--gold) 50%, var(--border)); color: var(--text); }
.team-seat-btn.active { border-color: var(--gold); background: linear-gradient(#3a2c14, #241c12); color: var(--gold); }
.team-seat-difficulty { display: flex; gap: 0.3rem; margin-left: auto; }
.team-seat-diff-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--dim);
  font-family: inherit;
  font-size: var(--text-xs);
  padding: 0.3rem 0.55rem;
  border-radius: 6px;
  cursor: pointer;
}
.team-seat-diff-btn.active { border-color: var(--gold); color: var(--gold); }
.team-code-hint { font-size: var(--text-xs); color: var(--dim); margin: 0.4rem 0 0; }

/* ---------- Draft / Arena (lobby) ---------- */
.draft-panel > p { color: var(--text-secondary); font-size: var(--text-sm); margin-top: 0; }
.draft-entry-fee { color: var(--gold-bright); font-weight: bold; }
.draft-run-record { color: var(--text-secondary); font-size: var(--text-sm); }
.draft-pick-progress { color: var(--gold-bright); font-weight: bold; font-size: var(--text-sm); }
/* A real grid, not flex-wrap. .card is `width: 100%; aspect-ratio: 2/3`, so
   it contributes NO intrinsic width — under flex-wrap each .draft-offer-card
   therefore collapsed to the widest thing that does, which was the faction
   name pill. Card size literally equalled badge text width ("Mythic Beasts"
   -> 117px, "Damned Covenant" -> 148px), so the three offered cards rendered
   at different sizes depending on which factions came up. Fixed tracks plus
   min-width:0 make the track width independent of content, which is what
   .card's percentage width needs to resolve against. */
.draft-offer {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
  /* Without a cap the three cards stretch across the full-width draft panel
     and render enormous; this keeps them at a comfortable ~190px. */
  max-width: 600px;
  margin: 0.8rem 0 1.2rem;
}
.draft-offer-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  cursor: pointer;
  padding: 0.5rem;
  border-radius: 8px;
  border: 1px solid transparent;
  transition: border-color 0.15s, background 0.15s;
}
.draft-offer-card:hover { border-color: var(--gold); background: linear-gradient(#3a2c14, #241c12); }
.draft-source-badge {
  max-width: 100%;
  font-family: var(--font-display);
  font-size: var(--text-xs);
  color: var(--gold);
  background: #0f0c08;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  /* Belt and braces: the grid track no longer depends on this, but a long
     faction name should ellipsise rather than widen its own column. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.draft-picks-title { margin: 0.6rem 0 0.4rem; color: var(--text-secondary); font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; }
/* Same root cause as .draft-offer above — these rows collapsed to the width
   of their "x2" quantity label. auto-fill tracks give every picked card the
   same box however many there are. */
.draft-picks-summary {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: 0.7rem;
  max-height: 260px;
  overflow-y: auto;
}
.draft-pick-row { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; min-width: 0; }
.draft-pick-qty { font-family: var(--font-display); font-size: var(--text-xs); color: var(--gold); }

.ai-stats-line { color: var(--text-secondary); font-size: var(--text-sm); }

.board-mode-picker { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.6rem; margin: 0.8rem 0 1.2rem; }
.board-mode-btn {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  background: #0f0c08;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.6rem 0.5rem;
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  transition: border-color 0.15s, color 0.15s;
}
.board-mode-name { font-weight: bold; color: var(--text-secondary); font-size: var(--text-sm); }
.board-mode-desc { color: var(--text-secondary); font-size: var(--text-xs); line-height: 1.3; }
.board-mode-btn:hover:not(.active) { border-color: color-mix(in srgb, var(--gold) 50%, var(--border)); }
.board-mode-btn.active { border-color: var(--gold); background: linear-gradient(#3a2c14, #241c12); }
.board-mode-btn.active .board-mode-name { color: var(--gold); }
.board-mode-btn.active .board-mode-desc { color: var(--text); }
.game-over-ai-note { color: var(--text-secondary); font-size: var(--text-sm); max-width: 26em; }
.game-over-reward-note { color: var(--gold-bright); font-family: var(--font-display); font-size: var(--text-sm); max-width: 26em; }
/* Same slot, but it's an explanation for a zero payout rather than a reward
   — shouldn't read as celebratory gold. */
.game-over-reward-note.is-muted { color: var(--dim); font-family: var(--font-body); }

/* Rating movement is the headline of a ranked result — sized well above the
   supporting facts below it, and colour-coded plus signed so the direction
   reads without relying on colour alone. */
.game-over-rating {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  margin: 0.2rem 0 0.6rem;
}
.game-over-rating-delta {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-2xl);
  line-height: 1.1;
}
.game-over-rating.is-gain .game-over-rating-delta { color: var(--success); }
.game-over-rating.is-loss .game-over-rating-delta { color: var(--hp); }
.game-over-rating-total { color: var(--text-secondary); font-size: var(--text-xs); letter-spacing: 0.5px; }

.game-over-summary {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem 1.2rem;
  margin: 0 0 0.9rem;
  padding: 0.8rem 1rem;
  background: #00000035;
  border: 1px solid var(--border);
  border-radius: 8px;
  text-align: left;
}
.game-over-summary > div { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.game-over-summary dt {
  color: var(--dim);
  font-size: var(--text-xs);
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.game-over-summary dd {
  margin: 0;
  color: var(--text);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 480px) {
  .game-over-summary { grid-template-columns: minmax(0, 1fr); }
}

.btn-secondary {
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  color: var(--gold);
  font-family: inherit;
  font-weight: bold;
  padding: 0.55rem 1.1rem;
  border-radius: 6px;
  font-size: 0.95rem;
  cursor: pointer;
}
.btn-secondary:hover:not(:disabled) { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
.btn-secondary:active:not(:disabled) { transform: translateY(1px); }
.btn-secondary:disabled { opacity: 0.4; cursor: not-allowed; color: var(--disabled); border-color: var(--disabled); }

/* ---------- Account panel ---------- */
.account-panel {
  position: relative;
  max-width: 420px;
  margin: 1.2rem auto 0;
  background: linear-gradient(180deg, #2a2115, var(--panel));
  border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: 10px;
  padding: 0.9rem 1.1rem;
  box-shadow: inset 0 0 20px #00000060, 0 6px 16px #0007;
}
.account-view { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.account-view.hidden { display: none; }
.account-form { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; width: 100%; }
.account-form input {
  flex: 1 1 140px;
  padding: 0.5rem 0.7rem;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: #0f0c08;
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
}
.account-form .btn { padding: 0.5rem 1.1rem; font-size: 0.9rem; }
.link-btn {
  background: none;
  border: none;
  color: var(--gold);
  text-decoration: underline;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.85rem;
  padding: 2px 0;
  transition: color 0.15s;
}
.link-btn:hover:not(:disabled) { color: var(--gold-bright); }
.link-btn:active:not(:disabled) { color: var(--gold); }
.link-btn:disabled { color: var(--disabled); cursor: not-allowed; text-decoration: none; }
.account-status { color: var(--hp); font-size: 0.85rem; min-height: 1.2em; text-align: center; }
/* Stacks inside the header popover, which is far too narrow for the row the
   full-width lobby panel used to lay these out in. */
#accountLoggedIn { flex-direction: column; align-items: stretch; text-align: left; gap: 0.45rem; }
#accountLoggedIn strong { color: var(--gold); }
#accountLoggedIn .link-btn { text-align: left; }
.header-popover .account-form { flex-direction: column; flex-wrap: nowrap; }
.header-popover .account-form input { flex: 0 0 auto; width: 100%; }
.header-popover .account-form .btn { width: 100%; }

/* ---------- Rankings panel ---------- */
.rankings-panel {
  position: relative;
  max-width: 560px;
  margin: 1.5rem auto 0;
  background: linear-gradient(180deg, #2a2115, var(--panel));
  border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: 10px;
  padding: 1rem 1.2rem;
  box-shadow: inset 0 0 20px #00000060, 0 6px 16px #0007;
}
.ranking-tabs { display: flex; gap: 0.5rem; margin-bottom: 0.8rem; }
.ranking-tab {
  flex: 1;
  background: linear-gradient(var(--surface-elevated), var(--panel));
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-family: inherit;
  font-weight: bold;
  font-size: var(--text-sm);
  padding: var(--space-2);
  border-radius: 6px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.ranking-tab:hover:not(.active) { border-color: color-mix(in srgb, var(--gold) 50%, var(--border)); color: var(--text); }
.ranking-tab:active:not(.active) { background: linear-gradient(#1c150d, #150f0a); }
.ranking-tab.active { border-color: var(--gold); color: var(--gold); background: linear-gradient(#3a2c14, #241c12); }
/* table-layout:fixed + explicit column widths make the Players/Decks
   rankings tables strictly honor their container's width (never wider) at
   any size, including the narrow fixed-dock sidebar — the 2nd column
   (Player/Faction) gets the most room and truncates gracefully instead of
   forcing the whole table to overflow. Scoped away from .deck-compare-table,
   which has 5 columns and its own wider min-width layout (below). */
.ranking-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.ranking-table:not(.deck-compare-table) { table-layout: fixed; }
.ranking-table:not(.deck-compare-table) th,
.ranking-table:not(.deck-compare-table) td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 4-column layout (#, Faction, Win Rate, Games) — decksTable only, now
   that playersTable/team2v2Table carry a 5th Rating column (below). */
.ranking-table:not(.deck-compare-table):not(.rated-table) th:first-child,
.ranking-table:not(.deck-compare-table):not(.rated-table) td:first-child { width: 10%; }
.ranking-table:not(.deck-compare-table):not(.rated-table) th:nth-child(2),
.ranking-table:not(.deck-compare-table):not(.rated-table) td:nth-child(2) { width: 46%; }
.ranking-table:not(.deck-compare-table):not(.rated-table) th:nth-child(3),
.ranking-table:not(.deck-compare-table):not(.rated-table) td:nth-child(3) { width: 22%; }
.ranking-table:not(.deck-compare-table):not(.rated-table) th:nth-child(4),
.ranking-table:not(.deck-compare-table):not(.rated-table) td:nth-child(4) { width: 22%; }
/* 5-column layout (#, Player, Rating, Wins, Losses) — playersTable and
   team2v2Table, which both gained a Rating column for the unified Elo
   system (see accountsRegistry.js's handleRankings). */
.ranking-table.rated-table th:first-child,
.ranking-table.rated-table td:first-child { width: 8%; }
.ranking-table.rated-table th:nth-child(2),
.ranking-table.rated-table td:nth-child(2) { width: 42%; }
.ranking-table.rated-table th:nth-child(3),
.ranking-table.rated-table td:nth-child(3) { width: 18%; }
.ranking-table.rated-table th:nth-child(4),
.ranking-table.rated-table td:nth-child(4) { width: 16%; }
.ranking-table.rated-table th:nth-child(5),
.ranking-table.rated-table td:nth-child(5) { width: 16%; }
.ranking-table th { color: var(--gold); text-align: left; border-bottom: 1px solid var(--border); padding: 0.4rem 0.5rem; }
.ranking-table td { padding: 0.4rem 0.5rem; border-bottom: 1px solid #4a3a2250; color: var(--text); }
.ranking-table tbody tr:last-child td { border-bottom: none; }
.ranking-table .few-games { color: var(--text-secondary); font-size: var(--text-xs); }
.ranking-table .empty-row td, .ranking-table .loading-row td { text-align: center; color: var(--text-secondary); padding: 0.8rem; }

/* Same card look as .rankings-panel, duplicated rather than shared so this
   never picks up that class's per-layout grid-area rules (see the HTML
   comment above this panel) — it just flows after everything else in
   normal document order in every layout. */
.quests-panel {
  position: relative;
  max-width: 560px;
  margin: 1.5rem auto 0;
  background: linear-gradient(180deg, #2a2115, var(--panel));
  border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: 10px;
  padding: 1rem 1.2rem;
  box-shadow: inset 0 0 20px #00000060, 0 6px 16px #0007;
}
.quests-panel-title { color: var(--gold); font-size: var(--text-sm); margin: 0 0 0.6rem; }
.quests-panel-title:not(:first-child) { margin-top: 1rem; }
.quest-list, .achievement-list { display: flex; flex-direction: column; gap: 0.5rem; }
.quest-row, .achievement-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  background: linear-gradient(var(--surface-elevated), var(--panel));
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
}
.achievement-row.locked { opacity: 0.5; }
.quest-row-info, .achievement-row-info { flex: 1; min-width: 0; }
.quest-row-desc, .achievement-row-desc { font-size: var(--text-sm); color: var(--text); }
.quest-row-progress { font-size: var(--text-xs); color: var(--text-secondary); margin-top: 0.15rem; }
.quest-progress-bar { position: relative; height: 5px; border-radius: 3px; background: #1c150d; margin-top: 0.3rem; overflow: hidden; }
.quest-progress-bar-fill { position: absolute; inset: 0; width: 0%; background: linear-gradient(90deg, var(--gold), var(--gold-bright)); }
.quest-claim-btn {
  background: linear-gradient(var(--gold-bright), var(--gold));
  border: 1px solid var(--gold);
  color: #1a140d;
  font-family: inherit;
  font-weight: bold;
  font-size: var(--text-xs);
  padding: 0.35rem 0.7rem;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
}
.quest-claim-btn:disabled { opacity: 0.4; cursor: default; background: linear-gradient(var(--surface-elevated), var(--panel)); color: var(--text-secondary); }
.achievement-row-reward { font-size: var(--text-xs); color: var(--gold); white-space: nowrap; }

/* ---------- Game ---------- */
/* A fluid grid, not a fixed-resolution canvas scaled to fit — each row takes
   only the height it actually needs, so the board (the one flexible row)
   claims whatever is left over, at any viewport shape or aspect ratio. */
#game {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 8px var(--hand-col-width, 280px);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  grid-template-areas:
    "topbar   topbar topbar"
    "board    handle hand"
    "board    handle hand"
    "controls handle hand";
  width: min(98vw, 1900px);
  height: min(96vh, 1080px);
  max-width: 100%;
  max-height: 100%;
  overflow: hidden;
  gap: clamp(4px, 0.6vh, 8px);
  padding: clamp(4px, 0.5vh, 10px) clamp(6px, 0.8vw, 14px);
  background:
    radial-gradient(ellipse 900px 700px at 50% 45%, #2e2313 0%, transparent 65%),
    radial-gradient(ellipse at 50% 50%, transparent 55%, #000 100%);
}
#topBar { grid-area: topbar; }
.board-wrap { grid-area: board; min-height: 0; }
.controls { grid-area: controls; }
#hand { grid-area: hand; min-height: 0; }

/* ---------- HUD layout manager ---------- */
/* Once initHudLayout() (client.js) runs, every one of the 11 movable panels
   gets an inline position:absolute + left/top/width/height, which wins over
   the grid-area rules above (grid-area on an absolutely-positioned grid
   child is only consulted for its static-position fallback, never reached
   once explicit inset values are set) — no !important needed anywhere here. */
.hud-layout-controls {
  position: fixed;
  top: 12px;
  right: 12px;
  z-index: 500;
  display: flex;
  gap: 8px;
}
.hud-edit-toggle, .hud-reset-btn {
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  border-radius: 8px;
  color: var(--gold);
  font-family: inherit;
  font-weight: bold;
  padding: 0.4rem 0.7rem;
  font-size: 0.8rem;
  cursor: pointer;
  box-shadow: 0 4px 14px #000a;
  transition: background 0.15s, color 0.15s, transform 0.15s;
}
.hud-edit-toggle:hover:not(.active), .hud-reset-btn:hover { background: linear-gradient(#4a3a1c, #2e2416); }
.hud-edit-toggle:active, .hud-reset-btn:active { transform: translateY(1px); }
.hud-edit-toggle.active { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
.hud-edit-toggle.active:hover { background: linear-gradient(var(--gold-bright), var(--gold-bright)); }

/* Card text size stepper — same toolbar, only shown alongside Reset Layout
   while HUD editing is active (toggled in toggleHudEditing(), client.js). */
.hud-font-control {
  display: flex;
  align-items: center;
  gap: 6px;
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  border-radius: 8px;
  padding: 0.3rem 0.6rem;
  box-shadow: 0 4px 14px #000a;
}
.hud-font-label { color: var(--gold); font-size: 0.75rem; font-weight: bold; white-space: nowrap; }
.hud-font-btn {
  background: linear-gradient(#4a3a1c, #2e2416);
  border: 1px solid var(--gold);
  border-radius: 6px;
  color: var(--gold);
  font-weight: bold;
  width: 22px;
  height: 22px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
}
.hud-font-btn:hover:not(:disabled) { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
.hud-font-btn:disabled { opacity: 0.4; cursor: default; }
.hud-font-readout { color: var(--text); font-size: 0.75rem; font-weight: bold; min-width: 2.6em; text-align: center; }

/* Keybind rebind panel — same toolbar chrome as .hud-font-control, but a
   column of rows (one per action) instead of a single inline stepper. */
.keybind-control {
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  border-radius: 8px;
  padding: 0.3rem 0.6rem;
  box-shadow: 0 4px 14px #000a;
}
.keybind-rows { display: flex; flex-direction: column; gap: 4px; }
.keybind-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.keybind-row-label { color: var(--gold); font-size: 0.7rem; font-weight: bold; white-space: nowrap; }
.keybind-key-btn {
  background: linear-gradient(#4a3a1c, #2e2416);
  border: 1px solid var(--gold);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: bold;
  padding: 0.15rem 0.5rem;
  min-width: 3.4em;
  text-align: center;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.keybind-key-btn:hover { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
.keybind-key-btn.listening { background: linear-gradient(var(--gold-bright), var(--gold-bright)); color: #1a140d; animation: keybind-pulse 1s ease-in-out infinite; }
@keyframes keybind-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
.keybind-clear-btn {
  background: none;
  border: none;
  color: var(--gold);
  font-size: 0.75rem;
  cursor: pointer;
  opacity: 0.7;
  padding: 0 2px;
}
.keybind-clear-btn:hover { opacity: 1; }

/* Quick-command ping marker — a short-lived pulsing ring positioned via
   left/top (viewport coordinates from getBoundingClientRect, same space
   #targetArrow above already uses) rather than as a child of the slot, so
   it survives that slot being re-rendered/removed mid-animation. */
.ping-marker {
  position: fixed;
  width: 46px;
  height: 46px;
  margin-left: -23px;
  margin-top: -23px;
  border: 3px solid var(--gold-bright);
  border-radius: 50%;
  box-shadow: 0 0 16px 4px #f4c04580;
  pointer-events: none;
  z-index: 900;
  animation: ping-marker-pulse 2.5s ease-out forwards;
}
@keyframes ping-marker-pulse {
  0% { transform: scale(0.4); opacity: 1; }
  40% { transform: scale(1.15); opacity: 0.9; }
  100% { transform: scale(1.35); opacity: 0; }
}

/* Outlines the actual board element a quick-command targets (a unit's
   slot, the castle panel, or the whole enemy row for Clear Board) — an
   outline rather than a border so it never shifts layout, and generic
   enough to apply cleanly to elements of very different shapes/sizes. */
.ping-highlight {
  outline: 3px solid var(--gold-bright);
  outline-offset: 3px;
  animation: ping-highlight-fade 2.5s ease-out forwards;
}
@keyframes ping-highlight-fade {
  0%   { outline-color: var(--gold-bright); box-shadow: 0 0 24px 6px #f4c045a0; }
  60%  { outline-color: var(--gold-bright); box-shadow: 0 0 18px 4px #f4c04580; }
  100% { outline-color: transparent; box-shadow: 0 0 0 0 transparent; }
}

/* Quick-command banner — the readable "what did my ally just ping" text,
   deliberately separate from .top-notice (shared by many unrelated one-off
   messages elsewhere) so its timing/prominence can be tuned just for this:
   a slower fade than .top-notice's 3s and a brighter, more attention-
   grabbing treatment, since a teammate mid-combat needs a real beat to
   notice and read it. */
.command-banner {
  position: fixed;
  top: 64px;
  left: 50%;
  transform: translate(-50%, -12px) scale(0.92);
  z-index: 960;
  background: linear-gradient(180deg, #4a3a1e, #241c10);
  border: 2px solid var(--gold-bright);
  color: var(--gold-bright);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(0.95rem, 1.8vw, 1.25rem);
  letter-spacing: 0.3px;
  padding: 0.75rem 1.8rem;
  border-radius: 10px;
  box-shadow: 0 10px 30px #000d, 0 0 30px #f4c04570;
  pointer-events: none;
  opacity: 0;
  max-width: 92vw;
  text-align: center;
}
.command-banner.showing {
  animation: command-banner-cycle 4s ease-out;
}
@keyframes command-banner-cycle {
  0%   { opacity: 0; transform: translate(-50%, -12px) scale(0.9); }
  8%   { opacity: 1; transform: translate(-50%, 0) scale(1.04); }
  16%  { transform: translate(-50%, 0) scale(1); }
  78%  { opacity: 1; transform: translate(-50%, 0) scale(1); }
  100% { opacity: 0; transform: translate(-50%, 6px) scale(0.97); }
}

/* Right-click radial quick-command menu — a decorative hub circle at the
   click point with option "petals" arranged around it (see
   showQuickCommandMenu() in client.js), replacing the native context menu
   the game already suppresses everywhere. */
.qc-hub {
  position: fixed;
  width: 40px;
  height: 40px;
  margin-left: -20px;
  margin-top: -20px;
  border-radius: 50%;
  background: radial-gradient(circle, #4a3a1cd0, #1a140dd0 75%);
  border: 2px solid var(--gold);
  box-shadow: 0 0 18px #000c;
  z-index: 950;
  pointer-events: none;
  animation: qc-hub-in 0.12s ease-out;
}
.qc-option {
  position: fixed;
  transform: translate(-50%, -50%);
  background: linear-gradient(#4a3a1c, #2e2416);
  border: 1px solid var(--gold);
  border-radius: 8px;
  color: var(--gold);
  font-family: inherit;
  font-size: 0.68rem;
  font-weight: bold;
  padding: 0.35rem 0.55rem;
  cursor: pointer;
  white-space: nowrap;
  z-index: 951;
  box-shadow: 0 3px 12px #000a;
  animation: qc-option-in 0.15s ease-out;
}
.qc-option:hover { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
@keyframes qc-hub-in { from { transform: scale(0.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
/* Must restate the base rule's translate(-50%,-50%) here too — an
   animation's own transform value replaces the cascaded one for its
   duration, so omitting it would make the button jump into its centered
   position only once the (very short) animation ends. */
@keyframes qc-option-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

#hudOverlayLayer { position: absolute; inset: 0; pointer-events: none; z-index: 60; }
.hud-panel-overlay { position: absolute; display: none; }
#game.hud-editing .hud-panel-overlay { display: block; outline: 2px dashed var(--gold); outline-offset: 2px; }

.hud-drag-handle {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 0.65rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--gold-bright);
  background: linear-gradient(180deg, #3a2c14ee, #241c12cc);
  border: 1px solid var(--gold);
  border-radius: 4px 4px 0 0;
  cursor: move;
  pointer-events: auto;
  user-select: none;
}
.hud-resize-grip {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 16px;
  height: 16px;
  cursor: nwse-resize;
  pointer-events: auto;
  background: linear-gradient(135deg, transparent 50%, var(--gold) 50%);
}

#topBar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.3rem 1rem;
  background: linear-gradient(180deg, #2a2013c0, #1a140dc0);
  border: 1px solid #4a3a2270;
  border-radius: 10px;
  box-shadow: 0 3px 12px #0007;
  margin-bottom: 4px;
}
/* This panel is one of the HUD layout manager's independently drag-resizable
   panels (client.js HUD_PANELS, minW:150) — its rendered width is whatever
   the player dragged it to, continuously, not one of a few viewport
   breakpoints. A container query (keyed off the panel's OWN width, like
   .card's container-type:size for card text) is what actually tracks that,
   where @media never could. Flanks the turn indicator inside #topBar by
   default (an earlier attempt anchored these to the board's corners
   instead, but that visually collided with the side HP/deck/grave stat
   panels, which already occupy that same space) — still independently
   draggable via Edit HUD Layout if a player wants it elsewhere. */
.hp-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 190px;
  border: 2px solid transparent;
  border-radius: 10px;
  padding: 3px 10px;
  /* No negative margin here any more: it used to offset this panel's own
     padding so a hover/active border could expand outward without shifting
     surrounding flex-flow layout — but now that oppInfo/youInfo are
     absolutely positioned to an exact box computed in initHudLayout(), that
     same negative margin just shifted the visible border 6px above the
     panel's actual top edge, clipped by #game's overflow:hidden (the
     "border cut off on top" bug). */
  background: linear-gradient(#1a130c40, #1a130c70);
  transition: border-color 0.2s, box-shadow 0.2s;
  container-type: inline-size;
  container-name: hp-panel;
}
/* Name and faction sit side by side in one row instead of the faction
   wrapping onto its own line below — that kept the full faction name
   readable but made the whole panel noticeably taller. The name gets
   priority for space (flex-grow) and shrinks last; the faction tag now
   also has a shrink/ellipsis fallback of its own (rather than a rigid
   flex-shrink:0) so an unusually long faction name can't force the
   player's own name down to just a couple of characters — a longer
   faction name gives way first, since the player's name is what the
   user actually needs legible. */
.hp-panel .name-row { display: flex; align-items: center; justify-content: center; gap: 5px; max-width: 100%; }
.hp-panel .name {
  flex: 1 1 auto;
  font-weight: bold;
  color: var(--gold-bright);
  font-size: clamp(0.75rem, 10.5cqw, 1.4rem);
  line-height: 1.2;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* A small "name tag" band, the way most TCGs set the player's name off
     from the board art behind it, rather than loose gold text floating on
     a busy background. */
  background: linear-gradient(#00000060, #00000040);
  border: 1px solid #ffffff20;
  border-radius: 6px;
  padding: 2px 10px;
}
.hp-panel .faction-tag {
  flex: 0 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-display);
  font-size: clamp(0.6rem, 7cqw, 0.9rem);
  color: var(--gold);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 8px;
  background: #00000040;
}
.hp-panel .panel-row { display: flex; align-items: center; justify-content: center; gap: clamp(4px, 3cqw, 12px); flex-wrap: wrap; max-width: 100%; }
/* Covers both the top-bar (.hp-panel) and side-panel (.side-panel) shapes of
   .castle-panel — the actual castle-attack hit target is #oppPanel now (the
   crest banner), but this stays generic so it's correct wherever the class
   ends up toggled. */
.castle-panel.legal-target { border-color: var(--success); cursor: pointer; animation: pulse-green 1.3s ease-in-out infinite; }
/* Whose turn it is, at a glance — a slow gold pulse on that player's own
   HP panel, reusing the same "your available action" gold language
   already used for playable hand cards and legal-move slots. */
.hp-panel.active-turn { border-color: var(--gold); animation: pulse-gold 2.2s ease-in-out infinite; }

/* ---------- Castle (base HP) ---------- */
.castle-wrap { position: relative; width: clamp(34px, 3.6vw, 52px); height: clamp(34px, 3.6vw, 52px); flex-shrink: 0; }
/* #oppInfo/#youInfo (the top bar) carry the same .castle-wrap/.castle-img/
   .castle-hp-value markup as the side panels, shown only on mobile as a
   secondary display when the side-panel crest can get squeezed off-screen.
   The actual attack-castle click/drag target is #oppPanel (see
   onCastleClick()'s binding and castleCenter()) — this top-bar copy is
   purely decorative, not interactive. Hidden by default since the side
   panel already covers this on desktop. */
.hp-panel .castle-wrap, .hp-panel .castle-hp-value { display: none; }
.castle-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: saturate(1) brightness(1) drop-shadow(0 2px 4px #0009);
  transition: filter 0.4s ease;
}
.castle-panel.castle-damaged .castle-img { filter: saturate(0.7) brightness(0.85) sepia(0.15) drop-shadow(0 2px 4px #0009); }
.castle-panel.castle-critical .castle-img { filter: saturate(0.4) brightness(0.6) sepia(0.3) hue-rotate(-10deg) drop-shadow(0 2px 4px #0009); }
.castle-panel.castle-destroyed .castle-img { filter: grayscale(1) brightness(0.35); opacity: 0.5; }
.castle-hp-block { display: flex; flex-direction: column; align-items: center; gap: 2px; width: 100%; min-width: 0; }
.castle-owner-label {
  font-size: 0.62rem;
  font-weight: bold;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  text-shadow: 0 1px 2px #000;
  text-align: center;
}
/* Side panel only: the HP number lives directly on top of the crest
   (.castle-wrap already has position:relative). Deliberately much larger
   than the crest's default icon size (see the .side-panel .castle-wrap
   override below) so it reads as the reference image's "banner with your
   HP on it" rather than a small stat readout. */
.castle-hp-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display), Georgia, serif;
  font-weight: bold;
  font-size: clamp(1.3rem, 3.2vw, 2.2rem);
  color: #fff8e7;
  /* A dark outline (not a gold one) so the number stays legible over any
     faction's crest color — a gold stroke disappears against Dwarf's own
     gold-toned shield, for instance. The fill color below is what actually
     carries the HP-state signal. */
  -webkit-text-stroke: 2.5px #1a1410;
  paint-order: stroke fill;
  text-shadow: 0 2px 4px #000c;
  pointer-events: none;
}
/* Same escalation the removed bar's gradient used to carry, moved onto the
   number's fill color so low HP still reads clearly without a bar — the
   dark outline above stays constant for contrast regardless of state. */
.castle-panel.castle-damaged .castle-hp-overlay { color: #ffcf80; }
.castle-panel.castle-critical .castle-hp-overlay { color: #ff8f7a; }
.castle-panel.castle-destroyed .castle-hp-overlay { color: #9a8f80; }
/* Was the bar-fill's own hit-flash; now flashes the number instead since
   the bar is gone — same class/keyframe, new target. */
.castle-hp-overlay.bar-hit { animation: bar-hit-flash 0.4s ease-out; }
@keyframes bar-hit-flash {
  0% { filter: brightness(1); }
  30% { filter: brightness(1.8); }
  100% { filter: brightness(1); }
}
/* Side panel's crest is a real focal element (per the reference image), not
   the small icon-sized default .castle-wrap the top-bar panel still uses. */
.side-panel .castle-wrap { width: clamp(56px, 5.5vw, 92px); height: clamp(56px, 5.5vw, 92px); }
.castle-hp-value {
  font-weight: bold;
  color: #ffdede;
  font-size: 0.7rem;
  text-shadow: 0 1px 3px #000;
  white-space: nowrap;
}
.castle-wrap.castle-hit { animation: castle-hit-flash 0.6s ease-out; }
@keyframes castle-hit-flash {
  0% { filter: drop-shadow(0 0 0 #ff4d4d00); }
  15% { filter: drop-shadow(0 0 14px #ff4d4dcc); }
  100% { filter: drop-shadow(0 0 0 #ff4d4d00); }
}
.castle-panel.castle-destroyed .castle-wrap { animation: castle-crumble 0.9s ease-out forwards; }
@keyframes castle-crumble {
  0% { transform: scale(1) rotate(0deg); opacity: 1; }
  60% { transform: scale(0.92) rotate(-3deg); opacity: 0.7; }
  100% { transform: scale(0.85) rotate(-5deg); opacity: 0.45; }
}

.mana-crystals { display: flex; flex-wrap: wrap; justify-content: center; gap: clamp(2px, 1.5cqw, 6px); max-width: 100%; }
.crystal {
  width: clamp(8px, 7cqw, 22px);
  height: clamp(8px, 7cqw, 22px);
  border-radius: 2px;
  transform: rotate(45deg);
  display: inline-block;
  flex-shrink: 0;
}
.crystal.filled {
  background: radial-gradient(circle at 30% 30%, #a9d8ff, var(--mana) 70%);
  box-shadow: 0 0 7px #3c7fd0e0;
}
.crystal.spent { background: #1a2433; border: 1px solid #2c3b52; }
.mana { color: #bfe0ff; font-weight: bold; font-size: clamp(0.7rem, 8.5cqw, 1.5rem); text-shadow: 0 1px 3px #000; white-space: nowrap; }
.turn-indicator {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: 1.2rem;
  color: var(--gold);
  text-align: center;
  flex: 1;
  text-shadow: 0 2px 6px #000;
  letter-spacing: 1px;
}
.turn-timer {
  font-size: 0.85rem;
  font-weight: bold;
  letter-spacing: 0.5px;
  color: var(--dim);
}
.turn-timer.timer-critical {
  color: var(--hp);
  animation: pulse-timer-critical 1s ease-in-out infinite;
}
@keyframes pulse-timer-critical {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
.sfx-toggle-btn {
  background: none;
  border: none;
  color: var(--dim);
  font-size: 0.85rem;
  line-height: 1;
  padding: 2px;
  cursor: pointer;
  opacity: 0.75;
  transition: opacity 0.15s, color 0.15s;
}
.sfx-toggle-btn:hover { opacity: 1; color: var(--gold); }

.board-wrap {
  display: flex;
  flex-direction: row;
  flex: 1;
  min-height: 0;
  align-items: stretch;
  gap: 8px;
}
/* Was flex:1 — sharing board-wrap's width equally with .board-area meant
   these two thin stat-pill columns were claiming a full quarter of the
   board's width each. They only need enough room for .panel-stat itself. */
.side-panel {
  flex: 0 0 auto;
  width: clamp(70px, 6vw, 110px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 10px 4px;
  border-radius: 14px;
  background: radial-gradient(ellipse at center, #1c150d60 0%, transparent 70%);
  border: 1px solid #4a3a2250;
}
.panel-stat {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 70px;
  padding: 8px 14px;
  border-radius: 10px;
  background: linear-gradient(#2a2115, var(--panel));
  border: 1px solid var(--border);
  box-shadow: inset 0 0 10px #0007, 0 2px 6px #0007;
  font-size: 1.1rem;
  font-weight: bold;
  color: var(--text);
}
.panel-icon { font-size: 1.2rem; }
.board-area {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  max-width: min(92vw, 1650px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 6px;
  min-height: 0;
  padding: 10px 8px;
  border-radius: 16px;
  background: radial-gradient(ellipse at center, #1c150d 0%, #100c07 100%);
  box-shadow: inset 0 0 60px #000d, inset 0 0 0 1px #6b5330, 0 10px 34px #000a;
  border: 2px solid #7a5c2e80;
}
/* Small gold corner ornaments (diagonal corners only — a single element only
   has two pseudo-elements to spend), echoing the diamond mana-crystal motif
   already used in #topBar, so the board frame reads as a "war table" rather
   than a plain box. */
.board-area::before, .board-area::after {
  content: '';
  position: absolute;
  width: 10px;
  height: 10px;
  background: var(--gold);
  opacity: 0.7;
  transform: rotate(45deg);
  box-shadow: 0 0 6px #d9b25c90;
  pointer-events: none;
}
.board-area::before { top: -5px; left: -5px; }
.board-area::after { bottom: -5px; right: -5px; }
.lane-row {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 4px;
  border-radius: 10px;
  width: 100%;
  flex: 1;
  min-height: 0;
}
#oppRearguard, #youRearguard {
  background: linear-gradient(180deg, var(--rearguard), transparent);
}
#oppVanguard, #youVanguard {
  background: linear-gradient(180deg, var(--vanguard), transparent);
}
/* Row labels as an absolutely-positioned overlay rather than a layout row —
   Vanguard/Rearguard clarity without spending any of the board's hard-won
   vertical space on it. Also names the baseline row passive (rules.js
   effectivePower()/grantVanguardBonusIfNeeded()) here, since it applies to
   every unit regardless of card text and has no other on-screen home. */
#oppRearguard::before, #youRearguard::before { content: 'Rearguard +1 DMG'; }
#oppVanguard::before, #youVanguard::before { content: 'Vanguard +1 Max HP'; }
#oppRearguard::before, #youRearguard::before, #oppVanguard::before, #youVanguard::before {
  position: absolute;
  top: 3px;
  left: 8px;
  font-family: var(--font-display);
  font-size: clamp(0.5rem, 0.85vw, 0.62rem);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--dim);
  opacity: 0.55;
  pointer-events: none;
  z-index: 1;
}
/* Single Row board mode: no Rearguard, no row labels (neither passive
   applies on a plain board). Each .lane-row already carries flex: 1;
   min-height: 0, so hiding Rearguard lets Vanguard fill the freed space
   automatically — no other sizing rule needs to change. */
#game.single-row-mode #oppRearguard, #game.single-row-mode #youRearguard { display: none; }
#game.single-row-mode #oppVanguard::before, #game.single-row-mode #youVanguard::before { content: none; }
/* The actual front line where the two sides meet — given a slow pulse
   (reusing the existing pulse-gold keyframes at low intensity) so it reads
   as the battle line rather than a static rule between two boxes. */
.lane-divider {
  height: 3px;
  background: linear-gradient(90deg, transparent, var(--gold), transparent);
  margin: 2px 0;
  opacity: 0.75;
  box-shadow: 0 0 10px var(--gold);
  animation: pulse-divider 3s ease-in-out infinite;
}
@keyframes pulse-divider {
  0%, 100% { box-shadow: 0 0 8px var(--gold); opacity: 0.65; }
  50% { box-shadow: 0 0 16px var(--gold); opacity: 0.9; }
}

.slot {
  position: relative;
  height: 100%;
  max-height: 260px;
  min-height: 60px;
  aspect-ratio: 2 / 3;
  width: auto;
  flex-shrink: 1;
  border: 1px solid #4a3a22;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(ellipse at center, rgba(255,255,255,0.02), rgba(0,0,0,0.3));
  box-shadow: inset 0 0 10px #00000090;
}
.slot::before, .slot::after {
  content: '';
  position: absolute;
  width: 12px;
  height: 12px;
  border: 2px solid var(--border);
  opacity: 0.7;
  pointer-events: none;
}
.slot::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
.slot::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
.slot.legal-target { border-color: var(--success); cursor: pointer; animation: pulse-green 1.3s ease-in-out infinite; }
.slot.legal-target::before, .slot.legal-target::after { border-color: var(--success); }
.slot.can-play { border-color: var(--gold); cursor: pointer; animation: pulse-gold 1.3s ease-in-out infinite; }
.slot.can-play::before, .slot.can-play::after { border-color: var(--gold); }
.slot.spell-target { border-color: var(--magic); cursor: pointer; animation: pulse-magic 1.3s ease-in-out infinite; }
.slot.spell-target::before, .slot.spell-target::after { border-color: var(--magic); }

@keyframes pulse-magic {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--magic) 55%, transparent); }
  50% { box-shadow: 0 0 10px 3px color-mix(in srgb, var(--magic) 55%, transparent); }
}

@keyframes pulse-gold {
  0%, 100% { box-shadow: inset 0 0 10px #00000090, 0 0 6px 0 #d9b25c80; }
  50% { box-shadow: inset 0 0 10px #00000090, 0 0 16px 4px #d9b25ccc; }
}
@keyframes pulse-green {
  0%, 100% { box-shadow: inset 0 0 10px #00000090, 0 0 6px 0 #6fd66a80; }
  50% { box-shadow: inset 0 0 10px #00000090, 0 0 16px 4px #6fd66acc; }
}

.card {
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 3;
  height: auto;
  border-radius: 8px;
  overflow: hidden;
  /* Lets text inside (name plate, ability lines, stat badges) size itself
     via cqw/cqh — relative to THIS card's own rendered box — instead of
     vw (relative to the viewport). vw ties font size to browser width, so
     the exact same card renders at an unrelated font size in a small board
     slot vs. a full hand card vs. the hover preview at the same viewport
     width; cqw/cqh scale correctly with however big this particular card
     actually is. Safe to apply here specifically because .card's own box
     is already fully self-determined (width 100% of its parent + a fixed
     aspect-ratio) and never depends on its children's size. */
  container-type: size;
  container-name: card;
  border: 2px solid #1a140c;
  box-shadow: 0 2px 6px #000a;
  cursor: pointer;
  flex-shrink: 0;
  background-color: #100c08;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.card.face-down { background: linear-gradient(135deg, #2a2015, #1a1209); border-color: #5c4419; }
.card.hand-card:not(.unaffordable):hover {
  transform: translateY(-14px) scale(1.08);
  box-shadow: 0 12px 20px #000c, 0 0 14px var(--gold);
  z-index: 5;
}
/* Board cards get the same "hover lifts it" language as hand cards, just
   without the vertical lift (which would look odd pinned inside a fixed
   slot) — a slight scale and glow is enough to feel consistently tactile
   everywhere a card appears. */
.slot .card:hover {
  transform: scale(1.06);
  box-shadow: 0 6px 16px #000c, 0 0 12px var(--gold);
  z-index: 3;
}
@keyframes card-pop {
  0% { transform: scale(0.7); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.card.card-enter { animation: card-pop 0.25s ease-out; }
.card.selected { outline: 3px solid var(--gold); }
.card.sick { filter: grayscale(0.6) brightness(0.7); }
.card.attacked { filter: brightness(0.55); }
.card.unaffordable { filter: grayscale(0.8) brightness(0.5); cursor: not-allowed; }
.card.has-taunt { box-shadow: 0 0 0 2px var(--success), 0 2px 6px #000a; }
.card.spell-card { box-shadow: 0 0 0 2px var(--magic), 0 2px 6px #000a; }
.card.spell-card.hand-card:not(.unaffordable):hover {
  box-shadow: 0 12px 20px #000c, 0 0 14px var(--magic);
}
/* A quiet, static outline (not box-shadow, so it layers cleanly with
   .has-taunt/.selected instead of overriding them) marking every unit of
   yours that could currently act — left unanimated since several of these
   can be visible at once and a field of pulsing cards would be noisy. */
.card.attack-ready { outline: 2px solid color-mix(in srgb, var(--gold) 65%, transparent); outline-offset: 1px; }
/* touch-action:none so a finger-drag on a board card is delivered to us as
   pointermove events instead of being claimed by the browser as a page-pan
   gesture — without this, the attack drag-arrow never receives movement on
   touch devices (see onCardPointerDown in client.js). */
.slot .card { width: 100%; height: 100%; touch-action: none; }

.card-badge {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  /* cqw (this .card's own rendered width), not vw (the viewport) — a badge
     on a small board-slot card now scales down with that card instead of
     staying viewport-sized regardless of how small the card actually is. */
  min-width: clamp(10px, 12cqw, 28px);
  height: clamp(10px, 12cqw, 28px);
  padding: 0 3px;
  border-radius: clamp(5px, 6cqw, 14px);
  font-family: Georgia, serif;
  font-weight: bold;
  font-size: clamp(7px, 7.5cqw, 16px);
  color: #fff;
  text-shadow: 0 1px 2px #000;
  border: 1.5px solid var(--gold);
  box-shadow: 0 1px 4px #000a;
  pointer-events: none;
  z-index: 2;
}
.cost-badge { top: 3px; right: 3px; background: radial-gradient(circle at 30% 30%, #6fa8e0, #2a5d94); }
.dmg-badge { bottom: 3px; left: 3px; background: radial-gradient(circle at 30% 30%, #d8d8d8, #6b6b6b); }
.hp-badge { bottom: 3px; right: 3px; background: radial-gradient(circle at 30% 30%, #ff8b83, var(--hp)); }
.countdown-badge { top: 3px; left: 50%; transform: translateX(-50%); background: radial-gradient(circle at 30% 30%, #d8c9a0, #7a6a3f); }

/* ---------- Live template card renderer (card-render migration) ---------- */
/* Card-render migration Phase 3: composites newcardtemplate.png as the card's
   own background (never a separate DOM layer, so it needs no z-index of its
   own) plus three absolutely-positioned regions — art, name, rules text —
   whose percentages were measured directly against the 1024x1536 template
   (see cardsource/extract_art.ps1's header comment for the matching art-crop
   rectangle). These are a first pass tuned by eye against a live render;
   nudge them here if a future template revision shifts the frame's regions.
   Font sizing uses cqh (this .card's own rendered HEIGHT, the binding
   constraint for these short horizontal bands) — this is Phase 5's
   container-query mechanism, pulled forward into Phase 3 after a real hand
   card (~80x120px in testing) exposed vw-based sizing as a genuine
   readability bug, not just a future nice-to-have: at that size a
   viewport-relative font stayed near its ~13-16px ceiling regardless of the
   card being far too small to hold it, clipping most of the rules text
   inside its own (safely non-spilling, `overflow:hidden`) box. */
.card-templated { background-image: url('assets/cards/newcardtemplate.png'); background-size: 100% 100%; background-repeat: no-repeat; }
.card-art-frame { position: absolute; left: 20%; top: 20%; width: 58%; height: 38%; overflow: hidden; pointer-events: none; }
/* This frame is ~1.02:1 (near-square) and the source art is a square
   327x326 canvas, so object-fit:cover has almost no crop slack to give
   object-position — the crop those two properties can do here is tiny.
   The transform:scale below is the one lever that actually crops, and it
   zooms symmetrically around the frame's own center regardless of where a
   given character's head/weapon/wings/crest happen to sit. A previous pass
   used scale(1.5) (33% of the image trimmed, 16.7% off each edge) tuned
   against 4 sampled arts; a full-set visual audit this session (every
   faction, cheap/mid/expensive units) found several other cards' helmet
   spikes, wingtips and crests landing flush against that cropped edge —
   a blind uniform zoom can't be safe for 374 differently-posed arts at
   once. Keep this scale conservative: it should trim only the outermost
   dead vignette ring baked into the old template's compositing, not eat
   into character extremities. Re-check any increase against a full-set
   render grid (see the card-art audit technique used to find this bug),
   not just a handful of samples — that's exactly what missed it last time. */
.card-art { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%; transform: scale(1.1); pointer-events: none; }
.card-name-plate {
  position: absolute;
  left: 18%; top: 8%; width: 64%; height: 4%;
  display: flex; align-items: center; justify-content: center; text-align: center;
  overflow: hidden;
  color: #3a2510;
  font-family: var(--font-display);
  font-weight: bold;
  line-height: 1.05;
  font-size: calc(clamp(4px, 2.8cqh, 20px) * var(--card-text-scale, 1));
  letter-spacing: 0.02em;
  pointer-events: none;
}
.card-rules-panel {
  position: absolute;
  left: 13%; top: 69%; width: 74%; height: 20%;
  display: flex; flex-direction: column; justify-content: center; align-items: center;
  gap: 3%;
  overflow: hidden;
  color: #241a0e;
  text-align: center;
  line-height: 1.15;
  font-size: calc(clamp(4px, 2.6cqh, 15px) * var(--card-text-scale, 1));
}
/* A 2-ability card needs to fit one more line in the same panel height —
   cheap, fully covers the current corpus (max 2 ability lines today) with
   zero per-render measurement cost. See Phase 5 for the general case
   (arbitrary future line counts / unusually long text). */
.card-rules-panel:has(.ability-line:nth-child(2)) { font-size: calc(clamp(4px, 1.9cqh, 12px) * var(--card-text-scale, 1)); }
.ability-line { font-weight: 600; }
.ability-footer { font-style: italic; opacity: 0.7; font-size: 0.85em; }

/* ---------- Combat feedback effects (floating numbers / flashes) ---------- */
.fx-float {
  position: absolute;
  left: 50%;
  top: 30%;
  transform: translateX(-50%);
  font-family: Georgia, serif;
  font-weight: 800;
  font-size: 15px;
  pointer-events: none;
  text-shadow: 0 1px 3px #000, 0 0 6px #000;
  animation: fx-float-up 1s ease-out forwards;
  z-index: 50;
  white-space: nowrap;
}
.fx-float.fx-damage { color: #ff5c5c; }
.fx-float.fx-heal { color: #5cff8f; }
.fx-float.fx-buff { color: #ffd24d; }
/* Offset above the base 30% so this doesn't land exactly on top of the
   -N damage number spawned at the same anchor for the same hit — both
   need to stay independently readable, not stacked in the same pixels. */
.fx-float.fx-spell-impact { top: 15%; color: var(--magic); font-size: 20px; }
@keyframes fx-float-up {
  0% { opacity: 0; transform: translate(-50%, 0); }
  15% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -34px); }
}
.fx-hit { animation: fx-hit-flash 0.4s ease-out; }
@keyframes fx-hit-flash {
  0% { filter: none; }
  30% { filter: brightness(1.7) saturate(1.4); }
  100% { filter: none; }
}
.fx-death { animation: fx-death-fade 0.5s ease-out; }
@keyframes fx-death-fade {
  0% { filter: brightness(2) saturate(0); opacity: 1; }
  100% { filter: none; opacity: 1; }
}

/* A damage spell's board-wide flourish — layered on top of the normal
   per-slot unit-damage flashes, not replacing them, so it reads as "a
   spell went off across this row" rather than just several coincidental
   hits. Uses --magic (the game's existing "this is a spell" color, see
   .spell-target's pulse-magic) to read as a distinct visual language from
   the red/green/gold damage/heal/buff palette above. */
.spell-aoe-flash { animation: spell-aoe-pulse 0.6s ease-out; }
@keyframes spell-aoe-pulse {
  0% { box-shadow: inset 0 0 0 0 color-mix(in srgb, var(--magic) 70%, transparent); }
  40% { box-shadow: inset 0 0 40px 10px color-mix(in srgb, var(--magic) 70%, transparent); }
  100% { box-shadow: inset 0 0 0 0 color-mix(in srgb, var(--magic) 70%, transparent); }
}
.spell-aoe-burst {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--magic) 80%, transparent) 0%, transparent 70%);
  pointer-events: none;
  /* One above .card-reveal-overlay's z-index:100 — the reveal popup fires
     on every card play (including this same spell), sits at this exact
     center point, and would otherwise fully hide the ring underneath it.
     The ring scales past the reveal card's own footprint, so it still
     reads as emanating from behind/around the reveal art. */
  z-index: 101;
  animation: spell-aoe-burst 0.6s ease-out forwards;
}
@keyframes spell-aoe-burst {
  0% { transform: scale(0.3); opacity: 0.9; }
  100% { transform: scale(8); opacity: 0; }
}

/* A plain combat attack landing — deliberately sharper and much faster
   than the magic-purple spell-aoe-burst above, so a sword/arrow hit reads
   as a distinct, more physical kind of impact than a spell resolving. */
.combat-impact-burst {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff6d8 0%, #ffb04d 45%, transparent 75%);
  pointer-events: none;
  z-index: 50;
  animation: combat-impact-burst 0.3s ease-out forwards;
}
@keyframes combat-impact-burst {
  0% { transform: scale(0.4); opacity: 1; }
  100% { transform: scale(3.5); opacity: 0; }
}
.combat-impact-shake { animation: combat-impact-shake 0.25s ease-out; }
@keyframes combat-impact-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  40% { transform: translateX(3px); }
  60% { transform: translateX(-2px); }
  80% { transform: translateX(1px); }
}

.controls { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; padding: 0.35rem; }
.controls .btn { padding: 0.4rem 1.4rem; font-size: 0.95rem; }

.hand {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
  align-content: start;
  gap: 8px;
  padding: 10px;
  border-radius: 14px;
  background: radial-gradient(ellipse at center, #1c150d 0%, #100c07 100%);
  border: 2px solid #7a5c2e80;
  box-shadow: inset 0 0 30px #000c;
  overflow-y: auto;
}
/* Reserve room for the chat panel only while it's actually open — otherwise
   its expanded window (up to ~340px tall) would sit on top of whichever
   hand card happens to be at the bottom of this dock. */
body:has(#chatPanel:not(.collapsed)) .hand {
  padding-bottom: 340px;
}

.log {
  max-height: 240px;
  overflow-y: auto;
  font-size: 0.85rem;
  color: var(--dim);
  background: #0000004d;
  padding: 8px 10px;
}
.log div { padding: 1px 0; }

.overlay {
  position: absolute; inset: 0;
  background: #000c;
  display: flex; align-items: center; justify-content: center;
}
.overlay-box {
  background: var(--panel);
  border: 2px solid var(--gold);
  border-radius: 10px;
  padding: 2rem 3rem;
  text-align: center;
}
.overlay-box h2 { color: var(--gold); }
.overlay-box .btn { margin-top: 1rem; padding: 0.6rem 1.4rem; }

#coinFlipOverlay { position: fixed; z-index: 600; }
/* The HUD panels (client.js applyPanelGeometry) carry an explicit z-index
   so the battlefield can never bury another dragged-over panel — without a
   z-index of its own here, this end-of-match dialog (z-index:auto) painted
   BELOW all of them and was invisible even once its `hidden` class was
   removed. */
#gameOverOverlay { z-index: 600; }
.coin-flip-box { min-width: 260px; }
.coin-flip-coin {
  font-size: 4rem;
  margin: 1rem 0;
  animation: coin-flip-spin 1.4s ease-out;
}
@keyframes coin-flip-spin {
  0% { transform: rotateY(0deg) scale(1); }
  70% { transform: rotateY(1440deg) scale(1.15); }
  100% { transform: rotateY(1800deg) scale(1); }
}
.coin-flip-result { color: var(--text); font-size: 1.1rem; margin: 0; }

#discoverOverlay { position: fixed; z-index: 600; }
.discover-box { min-width: 320px; }
.discover-waiting { color: var(--text); font-size: 1.05rem; margin: 0.5rem 0 0; }
.discover-options { display: flex; gap: 16px; justify-content: center; margin-top: 1rem; }
.discover-option { width: 160px; }

/* ---------- Deck preview modal ---------- */
#deckPreviewOverlay, #deckCompareOverlay, #deckChooserOverlay, #deckBuilderOverlay, #adminOverlay, #starterDeckOverlay, #buyCoinsOverlay, #tutorialWelcomeOverlay { position: fixed; z-index: 600; }
/* Both open from inside the deck chooser (its View Deck / Build Deck
   buttons), so they have to outrank it rather than tie and depend on
   document order. */
#deckPreviewOverlay, #deckBuilderOverlay { z-index: 610; }

/* ---------- Tutorial selection menu (Learn to Play) ---------- */
.tutorial-select-modal { width: min(640px, 92vw); max-height: 88vh; overflow-y: auto; text-align: left; }
.tutorial-select-modal h2 { text-align: center; }
.tutorial-select-modal > p { color: var(--text-secondary); text-align: center; margin: 0 0 1.2rem; }
.tutorial-select-list { display: flex; flex-direction: column; gap: 0.7rem; }
.tutorial-select-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 1.1rem;
  background: linear-gradient(#2a2115, var(--panel));
  border: 2px solid var(--border);
  border-radius: 10px;
}
.tutorial-select-card-info h3 { margin: 0 0 0.25rem; color: var(--gold-bright); font-family: var(--font-display); font-size: 1rem; }
.tutorial-select-card-info p { margin: 0; color: var(--text-secondary); font-size: var(--text-sm); }
.tutorial-select-card-status {
  margin-left: 0.5rem;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--success, #4caf50) 25%, transparent);
  color: var(--success, #4caf50);
  font-size: 0.7rem;
  font-family: var(--font-body, inherit);
  vertical-align: middle;
}
.tutorial-select-card .btn { margin-top: 0; padding: 0.5rem 1.1rem; white-space: nowrap; flex-shrink: 0; }
#tutorialWelcomeSkipBtn { display: block; margin: 1rem auto 0; }

/* On-screen replacement for the browser's native confirm() dialog — a
   single shared overlay reused by every yes/no prompt in the app (deck
   purchase, surrender, skip tutorial, unsaved-changes, admin actions),
   driven by the customConfirm() Promise-based helper in client.js. Sits
   above the 600-tier overlay stack (so it can confirm an action taken
   from inside another overlay, e.g. deleting an account from the admin
   panel) and above the tutorial panel/spotlight (650/640), so a surrender
   confirmation mid-lesson is never obscured. */
#customConfirmOverlay { position: fixed; z-index: 660; }
.custom-confirm-box { max-width: 420px; }
#customConfirmMessage { color: var(--text); font-size: 1rem; line-height: 1.4; margin: 0; white-space: pre-line; }
.custom-confirm-actions { display: flex; gap: 0.8rem; justify-content: center; margin-top: 1.2rem; flex-wrap: wrap; }
.custom-confirm-actions .btn, .custom-confirm-actions .btn-secondary { margin-top: 0; padding: 0.6rem 1.4rem; }

/* ---------- Tutorial (in-match) ---------- */
/* z-index placement: above every board/HUD element and above the 600-tier
   .overlay modals, but below #cardPreview (700) and both drag-arrow layers
   (895/900) — so inspecting a card or dragging an attack arrow mid-lesson
   is never obscured by the dialogue panel or the dim spotlight. */
/* Docked top-left rather than bottom-center — the bottom of the screen is
   the game's own controls bar (Go to Combat/Attack All/End Turn/Surrender),
   so a bottom-docked panel both covers those buttons (making the very
   actions a lesson asks for unclickable) and hides the spotlight's dim
   "hole" behind itself whenever the highlighted element is also down there.
   Top-left is empty in the existing HUD layout. */
.tutorial-panel {
  position: fixed;
  left: 12px;
  top: 12px;
  z-index: 650;
  max-width: 300px;
  width: calc(100vw - 24px);
  background: var(--panel);
  border: 2px solid var(--gold);
  border-radius: 10px;
  padding: 0.7rem 1rem;
  font-size: 0.88rem;
  box-shadow: 0 8px 24px #000c;
}
.tutorial-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; gap: 0.6rem; }
.tutorial-step-badge { color: var(--gold); font-family: var(--font-display); font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase; }
#tutorialDialogueText { margin: 0; color: var(--text); font-size: 0.95rem; line-height: 1.4; }
#tutorialNextBtn { margin-top: 0.7rem; padding: 0.45rem 1.2rem; }

.tutorial-spotlight {
  position: fixed;
  pointer-events: none;
  z-index: 640;
  border: 2px solid var(--gold);
  border-radius: 8px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
  transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
  animation: tutorial-spotlight-pulse 2.2s ease-in-out infinite;
}
/* A separate outline-opacity pulse rather than reusing pulse-gold's own
   box-shadow keyframe directly — the spotlight's box-shadow is already
   doing double duty as the fixed dim "moat", so animating it too would
   fight this element's own static shadow instead of layering with it. */
@keyframes tutorial-spotlight-pulse {
  0%, 100% { border-color: color-mix(in srgb, var(--gold) 70%, transparent); }
  50% { border-color: var(--gold-bright); }
}
@media (prefers-reduced-motion: reduce) {
  .tutorial-spotlight { animation: none; transition: none; }
}
@media (max-width: 760px) {
  .tutorial-panel { top: 8px; left: 8px; padding: 0.7rem 1rem; font-size: 0.9rem; max-width: calc(100vw - 16px); }
}

.tutorial-completion-list { list-style: none; padding: 0; margin: 1rem 0; text-align: left; display: inline-block; }
.tutorial-completion-list li { color: var(--success); font-size: 0.9rem; padding: 0.15rem 0; }
.tutorial-completion-list li::before { content: '✓ '; }

/* Distinct from the plain .btn:disabled dimmed look — this reads as
   "do the lesson's action first", not "unavailable right now". */
#endTurnBtn.tutorial-locked:disabled {
  opacity: 0.55;
  border-color: var(--gold);
  box-shadow: 0 0 8px color-mix(in srgb, var(--gold) 45%, transparent);
}
#endTurnBtn.tutorial-locked:disabled::after { content: ' 🔒'; }



/* ---------- Starter deck selection ---------- */
.starter-deck-modal { width: min(760px, 92vw); max-height: 88vh; overflow-y: auto; text-align: left; }
.starter-deck-modal h2 { text-align: center; }
.starter-deck-desc { color: var(--text-secondary); text-align: center; margin: 0 0 1rem; }
.starter-deck-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.6rem;
}
.starter-deck-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 0.7rem 0.5rem;
  background: linear-gradient(#2a2115, var(--panel));
  border: 2px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 0.82rem;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.starter-deck-card:hover { border-color: var(--gold); }
.starter-deck-card.selected {
  border-color: var(--gold);
  background: linear-gradient(#3a2c14, #241c12);
  box-shadow: 0 0 12px color-mix(in srgb, var(--gold) 45%, transparent);
  color: var(--gold-bright);
}
.starter-deck-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1.2rem;
}
.starter-deck-count { color: var(--dim); font-family: var(--font-display); font-size: 0.9rem; }
.starter-deck-status { min-height: 1.4em; color: var(--warning); font-size: var(--text-sm); text-align: center; margin: 0.6rem 0 0; }

/* ---------- Buy Coins panel (stub — see economy.js COIN_PACKS) ---------- */
.buy-coins-modal { width: min(560px, 92vw); max-height: 88vh; overflow-y: auto; text-align: left; }
.buy-coins-disclaimer { color: var(--text-secondary); font-size: var(--text-sm); margin: 0 0 1rem; }
.buy-coins-body { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.7rem; }
.coin-pack-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 1rem 0.6rem;
  background: linear-gradient(#2a2115, var(--panel));
  border: 2px solid var(--border);
  border-radius: 10px;
}
.coin-pack-amount { font-family: var(--font-display); font-size: 1.15rem; color: var(--gold-bright); }
.coin-pack-price { color: var(--text-secondary); font-size: 0.85rem; }
.buy-coins-guest-note { color: var(--dim); text-align: center; padding: 1rem 0; }
.deck-preview-modal {
  width: min(1000px, 92vw);
  max-height: 88vh;
  padding: 1.4rem 1.6rem;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  /* Without this, a flex column's children default to min-height:auto (their
     natural content size), so the max-height above has nothing to actually
     constrain — the body never shrinks enough to trigger its own
     overflow-y:auto, and everything past it (including the footer button)
     renders outside the fixed overlay's bounds, unreachable by scrolling. */
  overflow: hidden;
}
.deck-preview-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-shrink: 0; }
.deck-preview-heading { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.deck-preview-heading h2 { margin: 0; color: var(--gold); font-family: var(--font-display); font-size: 1.6rem; }
.deck-preview-heading .deck-badge { font-size: 0.8rem; padding: 3px 9px; }
.deck-preview-close {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  width: 32px;
  height: 32px;
  font-size: 1rem;
  cursor: pointer;
  flex-shrink: 0;
}
.deck-preview-close:hover { border-color: var(--gold); color: var(--gold); }
.deck-preview-body {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: 1.5rem;
  overflow-y: auto;
  padding-right: 4px;
  flex: 1 1 auto;
  min-height: 0;
}
.deck-preview-description { color: var(--text); font-size: 1.05rem; line-height: 1.55; }
.deck-preview-block { margin-bottom: 1.1rem; }
.deck-preview-block h3 {
  margin: 0 0 0.35rem;
  color: var(--gold);
  font-family: var(--font-display);
  font-size: 0.95rem;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.deck-preview-block p { margin: 0.15rem 0; color: var(--text); font-size: 0.98rem; line-height: 1.5; }
.deck-preview-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.deck-preview-list { margin: 0; padding-left: 1.2rem; font-size: 0.95rem; color: var(--text); line-height: 1.55; }
.deck-preview-list-pro li::marker { color: var(--success); }
.deck-preview-list-con li::marker { color: var(--hp); }
.deck-mechanic-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.deck-mechanic-chip {
  font-size: 0.82rem;
  font-family: var(--font-display);
  letter-spacing: 0.3px;
  border: 1px solid var(--magic);
  color: var(--magic);
  background: color-mix(in srgb, var(--magic) 12%, transparent);
  border-radius: 10px;
  padding: 3px 9px;
}
.deck-stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
  gap: 0.6rem;
  text-align: center;
  margin-bottom: 0.6rem;
}
.deck-stats-grid > div { background: #00000030; border-radius: 6px; padding: 0.4rem 0.2rem; }
.deck-stats-grid span { display: block; font-family: var(--font-display); font-size: 1.3rem; color: var(--gold-bright); }
.deck-stats-grid label { display: block; font-size: 0.7rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.3px; }
/* Live deck analysis in the builder, using the same chart as the read-only
   deck preview. Shorter here — it sits above the card list in a column that
   also has to fit the name field, save bar and validation errors. */
.builder-stats {
  margin: 0.5rem 0 0.7rem;
  padding: 0.6rem 0.7rem;
  background: #00000035;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.builder-stat-row {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
  color: var(--dim);
  font-size: var(--text-xs);
}
.builder-stat-row strong { color: var(--gold-bright); font-family: var(--font-display); }
.builder-stats .mana-curve-chart { height: 52px; }

.mana-curve-chart { display: flex; align-items: flex-end; gap: 4px; height: 80px; }
.mana-curve-col { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; gap: 2px; }
.mana-curve-bar { width: 100%; background: linear-gradient(#e7c877, #b8912f); border-radius: 2px 2px 0 0; min-height: 2px; }
.mana-curve-label { font-size: 0.7rem; color: var(--dim); }

.deck-preview-cardlist { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.deck-filter-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 0.6rem; }
.deck-filter-bar input, .deck-filter-bar select {
  background: #0000004d;
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  padding: 6px 9px;
  border-radius: 5px;
}
.deck-filter-bar input { flex: 1; min-width: 120px; }
.deck-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 12px;
  max-height: 58vh;
  overflow-y: auto;
  padding-right: 4px;
  align-content: start;
  /* Without this, Grid's default stretch fights .card's aspect-ratio-driven
     auto height (.card has no in-flow children to establish a height on its
     own — name plate/art/rules panel are all position:absolute) — some
     engines resolve that circular dependency by collapsing the row to a
     sliver instead of respecting the aspect-ratio. */
  align-items: start;
}
/* These tiles are much bigger than a live hand/board card, so they can
   afford a richer cqh ratio than the shared .card-name-plate/.card-rules-panel
   rules (tuned against ~80px-wide real hand cards, where a bigger ratio
   would overflow) — scoped here rather than raised globally. */
.deck-card-grid .card-name-plate { font-size: calc(clamp(6px, 3.4cqh, 24px) * var(--card-text-scale, 1)); }
.deck-card-grid .card-rules-panel { font-size: calc(clamp(6px, 3.1cqh, 18px) * var(--card-text-scale, 1)); }
.deck-card-grid .card-rules-panel:has(.ability-line:nth-child(2)) { font-size: calc(clamp(6px, 2.3cqh, 15px) * var(--card-text-scale, 1)); }
.deck-card-grid-empty { color: var(--dim); text-align: center; grid-column: 1 / -1; padding: 1.5rem 0; }
/* Classic intrinsic-ratio box: padding-top:150% gives an exact 2:3 height
   derived purely from width (0 content height + 150% padding), with none of
   the aspect-ratio + Grid + container-type:size ambiguity that collapsed
   .card directly as a grid item in at least one real browser — see
   buildDeckPreviewCardEl() in client.js. */
.deck-card-tile { position: relative; width: 100%; padding-top: 150%; cursor: pointer; }
.deck-card-tile .card { position: absolute; inset: 0; width: 100%; height: 100%; }
.deck-card-qty {
  position: absolute;
  bottom: 4px;
  right: 4px;
  background: #14100cd0;
  border: 1px solid var(--gold);
  color: var(--gold-bright);
  font-size: 0.75rem;
  font-family: var(--font-display);
  border-radius: 8px;
  padding: 2px 7px;
  pointer-events: none;
}
.deck-preview-footer { display: flex; justify-content: flex-end; padding-top: 0.4rem; border-top: 1px solid var(--border); flex-shrink: 0; }

/* ---------- Deck Builder ---------- */
/* Shares .deck-preview-modal/.deck-preview-body/.deck-preview-cardlist/
   .deck-filter-bar/.deck-card-grid/.deck-card-tile wholesale with the
   read-only "View Deck" preview above — only the left info pane (deck-in-
   progress instead of kingdom lore) and each card tile's stepper (instead
   of a static ×N badge) are specific to the builder. */
.deck-builder-info { display: flex; flex-direction: column; min-height: 0; gap: 0.7rem; }
.deck-builder-name-input {
  background: #0000004d;
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  padding: 7px 10px;
  border-radius: 6px;
}
.deck-builder-summary-bar { display: flex; align-items: center; justify-content: space-between; gap: 0.8rem; }
.deck-builder-count-readout { font-family: var(--font-display); font-size: 1.1rem; color: var(--dim); }
.deck-builder-count-readout.at-target { color: var(--success); }
.deck-builder-count-readout.over-target { color: var(--hp); }
.deck-builder-errors { display: flex; flex-direction: column; gap: 3px; }
.deck-builder-error { margin: 0; color: var(--hp); font-size: 0.8rem; }
.deck-builder-list {
  flex: 1 1 auto;
  min-height: 80px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-right: 4px;
}
.deck-builder-list-empty { color: var(--dim); font-size: 0.85rem; text-align: center; padding: 1rem 0; }
.deck-builder-list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #00000030;
  font-size: 0.85rem;
  cursor: pointer;
}
.deck-builder-list-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Same dec/count/inc button look as the card grid's own .deck-builder-stepper
   (client.js:buildBuilderCardEl), just laid out inline instead of floated
   absolutely over a card tile — a list row has no card face to float over. */
.deck-builder-list-stepper { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }

/* Floats over the bottom of the card face, same convention as
   .deck-card-qty above — the tile's aspect ratio never needs to change to
   make room for it. */
.deck-builder-stepper {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  pointer-events: none;
}
.deck-builder-stepper-dec, .deck-builder-stepper-inc {
  pointer-events: auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--gold);
  background: #14100cd0;
  color: var(--gold-bright);
  font-family: var(--font-display);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}
.deck-builder-stepper-dec:disabled, .deck-builder-stepper-inc:disabled { border-color: var(--disabled); color: var(--disabled); cursor: not-allowed; }
.deck-builder-stepper-dec:hover:not(:disabled), .deck-builder-stepper-inc:hover:not(:disabled) { border-color: var(--gold-bright); }
.deck-builder-stepper-count {
  pointer-events: auto;
  min-width: 18px;
  text-align: center;
  background: #14100cd0;
  border: 1px solid var(--gold);
  color: var(--gold-bright);
  font-family: var(--font-display);
  font-size: 0.8rem;
  border-radius: 8px;
  padding: 2px 5px;
}

.deck-compare-modal { width: min(900px, 92vw); max-height: 85vh; text-align: left; }
.deck-compare-modal h2 { color: var(--gold); text-align: center; }
.deck-compare-table-wrap { max-height: 65vh; overflow: auto; margin: 1rem 0; }
.deck-compare-table-wrap .ranking-table { min-width: 640px; }

/* ---------- Music player (lobby) ---------- */
/* Shown in every layout, independent of [data-layout] — a normal card in the
   document flow (auto-places at the end of the column, same fallback every
   other "extra" lobby element already uses) until the viewport is wide
   enough to dock it to the left edge without any risk of overlapping the
   still-centered 900px column. Visual treatment matches .sidebar-widget
   (same card look used in the Sidebar + HUD layout) but isn't gated by it. */
.music-player-lobby {
  background: linear-gradient(180deg, #2a2115, var(--panel));
  border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: 10px;
  padding: 0.9rem 1rem;
  margin: 1.2rem 0;
  box-shadow: inset 0 0 20px #00000060, 0 6px 16px #0007;
}
.music-player-lobby h4 {
  margin: 0 0 0.6rem;
  color: var(--gold);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.music-now-playing {
  color: var(--text);
  font-size: var(--text-sm);
  margin-bottom: 0.7rem;
  min-height: 1.3em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.music-controls { display: flex; align-items: center; justify-content: center; gap: 0.7rem; margin-bottom: 0.8rem; }
.music-ctrl-btn {
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  color: var(--gold);
  border-radius: 50%;
  width: 2.2rem;
  height: 2.2rem;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.15s, transform 0.1s;
}
.music-ctrl-btn:hover { background: linear-gradient(var(--gold-bright), var(--gold)); color: #1a140d; }
.music-ctrl-btn:active { transform: translateY(1px); }
.music-ctrl-btn-main { width: 2.8rem; height: 2.8rem; font-size: 1.2rem; }
.music-volume-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.8rem; font-size: 0.9rem; }
.music-volume-row input[type="range"] { flex: 1; accent-color: var(--gold); }
.music-track-list {
  max-height: 220px;
  overflow-y: auto;
  border-top: 1px solid var(--border);
  padding-top: 0.5rem;
  /* Themed scrollbar (Firefox via scrollbar-color, everything else via the
     ::-webkit-scrollbar pseudo-elements below) instead of the browser's
     default gray, matching the site's gold-on-dark-brown palette. */
  scrollbar-width: thin;
  scrollbar-color: var(--gold) #1c150d;
}
.music-track-list::-webkit-scrollbar { width: 8px; }
.music-track-list::-webkit-scrollbar-track { background: #1c150d; border-radius: 8px; }
.music-track-list::-webkit-scrollbar-thumb { background: linear-gradient(var(--gold-bright), var(--gold)); border-radius: 8px; border: 1px solid #1c150d; }
.music-track-list::-webkit-scrollbar-thumb:hover { background: var(--gold-bright); }
.music-track-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s;
}
.music-track-row:hover { background: #ffffff10; color: var(--text); }
.music-track-row.active { color: var(--gold); font-weight: bold; background: color-mix(in srgb, var(--gold) 12%, transparent); }

/* ---------- Music player (in-match mini) ---------- */
/* Hidden outside a live match — body.in-game is toggled by showGame()/
   exitHudPreview() in client.js, the same class the HUD editor already
   relies on. */
#musicPlayerGame { display: none; position: fixed; z-index: 450; top: 110px; right: 16px; }
body.in-game #musicPlayerGame { display: block; }
.music-mini-toggle {
  display: none;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  color: var(--gold);
  font-size: 1.1rem;
  cursor: pointer;
  box-shadow: 0 4px 14px #000a;
}
#musicPlayerGame.collapsed .music-mini-toggle { display: block; }
#musicPlayerGame.collapsed .music-mini-panel { display: none; }
.music-mini-panel {
  background: linear-gradient(#3a2c14, #241c12);
  border: 1px solid var(--gold);
  border-radius: 8px;
  padding: 0.4rem 0.5rem;
  box-shadow: 0 4px 14px #000a;
  width: 240px;
}
.music-mini-bar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  cursor: grab;
  touch-action: none;
}
.music-mini-bar:active { cursor: grabbing; }
.music-mini-title { flex: 1; color: var(--text); font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.music-mini-controls {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.35rem;
  padding-top: 0.35rem;
  border-top: 1px solid #ffffff20;
}
.music-mini-vol-icon { font-size: 0.75rem; flex: 0 0 auto; }
.music-mini-volume { flex: 1; min-width: 0; accent-color: var(--gold); height: 4px; }
.music-mini-btn {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--gold);
  font-size: 0.95rem;
  cursor: pointer;
  padding: 2px 4px;
}
.music-mini-btn:hover { color: var(--gold-bright); }

/* ---------- Admin panel ---------- */
.admin-modal { width: min(560px, 92vw); max-height: 85vh; text-align: left; display: flex; flex-direction: column; gap: 1.1rem; overflow-y: auto; }
.admin-modal-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
.admin-modal-header h2 { margin: 0; color: var(--gold); font-family: var(--font-display); font-size: 1.4rem; }
.admin-section h3 { margin: 0 0 0.4rem; color: var(--gold); font-family: var(--font-display); font-size: var(--text-base); }
.admin-section p { margin: 0 0 0.6rem; color: var(--text-secondary); font-size: var(--text-xs); }
.admin-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.admin-account-list { max-height: 220px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px; }
.admin-account-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid #4a3a2250;
  font-size: var(--text-sm);
}
.admin-account-row:last-child { border-bottom: none; }
.admin-delete-btn {
  background: transparent;
  border: 1px solid var(--error);
  color: var(--error);
  font-family: inherit;
  font-size: var(--text-xs);
  padding: 0.3rem 0.6rem;
  border-radius: 5px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.admin-delete-btn:hover { background: var(--error); color: var(--bg); }
.admin-loading { padding: 0.7rem; color: var(--text-secondary); font-size: var(--text-sm); }
.admin-status { min-height: 1.4em; color: var(--gold); font-size: var(--text-sm); text-align: center; }

/* ---------- Card play reveal ---------- */
/* Fires once per server-signaled play (client.js computeEffects/spawnEffect,
   keyed on game.lastPlayedCard.seq) -- a single one-shot animation handles
   the whole fade-in/hold/fade-out lifecycle, so pulseClass() restarting it
   on a second rapid play is all the JS needs to do; no timers to clean up. */
.card-reveal-overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  pointer-events: none;
  opacity: 0;
}
.card-reveal-overlay.reveal-active {
  animation: card-reveal-cycle 2s ease-out;
}
@keyframes card-reveal-cycle {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
  12%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  78%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.96); }
}
.card-reveal-img {
  width: clamp(140px, 16vw, 220px);
  height: auto;
  border-radius: 14px;
  border: 3px solid var(--gold);
  box-shadow: 0 14px 40px #000f, 0 0 32px #d9b25c90;
  /* background-color, NOT the `background` shorthand — the shorthand resets
     every unlisted sub-property to its initial value, including
     background-image, which would silently wipe out .card-templated's
     newcardtemplate.png (this exact bug shipped once already: the frame
     never rendered on the reveal popup, leaving art+text with no visible
     boundary between them). */
  background-color: #100c08;
  display: block;
}
.card-reveal-label {
  font-family: var(--font-display);
  font-weight: bold;
  font-size: clamp(0.75rem, 1.2vw, 0.95rem);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--gold);
  text-shadow: 0 2px 6px #000d, 0 0 10px #000a;
  background: #100c08cc;
  padding: 2px 10px;
  border-radius: 6px;
  border: 1px solid var(--gold);
}
.card-reveal-overlay.reveal-opponent .card-reveal-img { border-color: var(--hp); box-shadow: 0 14px 40px #000f, 0 0 32px #b5322f90; }
.card-reveal-overlay.reveal-opponent .card-reveal-label { color: var(--hp); border-color: var(--hp); }

/* ---------- Top notice (e.g. "pick a faction first") ---------- */
/* Fixed to the viewport, not #lobbyStatus's spot far down the page — the
   one thing players kept missing entirely. Auto-dismissed by JS (see
   showTopNotice() in client.js); the class toggle here only drives the
   animation, not visibility timing. */
.top-notice {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translate(-50%, -140%);
  z-index: 550;
  background: linear-gradient(180deg, #4a3a1e, #2a2014);
  border: 2px solid var(--warning);
  color: var(--warning);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(0.8rem, 1.4vw, 1rem);
  letter-spacing: 0.3px;
  padding: 0.6rem 1.4rem;
  border-radius: 8px;
  box-shadow: 0 10px 28px #000c, 0 0 24px #e0a94d50;
  pointer-events: none;
  opacity: 0;
  max-width: 90vw;
  text-align: center;
}
.top-notice.showing {
  animation: top-notice-cycle 3s ease-out;
}
@keyframes top-notice-cycle {
  0%   { opacity: 0; transform: translate(-50%, -140%); }
  10%  { opacity: 1; transform: translate(-50%, 0); }
  85%  { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -20%); }
}

/* ---------- Your Turn / Opponent's Turn banner ---------- */
/* Deliberately viewport-fixed (not board-relative like
   .card-reveal-overlay) so it reads as a full-screen moment, same idea as
   Magic Arena/Hearthstone's own turn-transition banners. Sits above the
   card-reveal overlay (z-index 100) since both can plausibly fire in the
   same instant at a turn boundary, but below the 600-tier modals (coin
   flip, game over, discover) — those still take priority if one is up. */
.turn-banner {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2rem, 6vw, 4rem);
  letter-spacing: 3px;
  text-transform: uppercase;
  text-shadow: 0 4px 16px #000f, 0 0 40px #0009;
}
.turn-banner.showing {
  animation: turn-banner-cycle 2.5s ease-out;
}
.turn-banner.turn-you { color: var(--gold-bright); }
.turn-banner.turn-opponent { color: var(--hp); }
@keyframes turn-banner-cycle {
  0%   { opacity: 0; transform: scale(0.85); }
  15%  { opacity: 1; transform: scale(1); }
  75%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.05); }
}

/* ---------- Card hover preview ---------- */
#cardPreview {
  position: fixed;
  left: 16px;
  top: 50%;
  /* Above every 600-tier overlay (.overlay-based modals: coin flip, discover,
     deck preview/compare) — otherwise hovering a card inside one of those
     modals renders this preview UNDER the modal's own semi-transparent
     backdrop, which is what actually caused the "grayed out" look (the art
     itself was fine; it was being seen through a dark veil). pointer-events
     is none below, so this can never intercept clicks meant for what's
     underneath. */
  z-index: 700;
  pointer-events: none;
  /* aspect-ratio + width:auto (not an independent width/height/max-height
     triplet) so whichever dimension actually binds — the ideal size below,
     or the viewport-height cap on a short screen — the other always derives
     from it and the box stays exactly 2:3. The old fixed width alongside a
     height-only max-height let a short viewport squish the box (349x301,
     ratio 0.86, measured on a 1366x650 window) while every percentage-
     positioned child inside (art frame, name plate, rules panel) kept
     assuming a proper 2:3 box — that mismatch was the actual "card art
     looks broken" bug, not the art itself. */
  aspect-ratio: 2 / 3;
  width: auto;
  /* Centered via top:50%/translateY(-50%), so capping height alone keeps it
     off the log panel at the bottom-left on short viewports — a shorter box
     just pulls both edges in symmetrically toward center. */
  height: min(540px, calc(100vh - 340px));
  border-radius: 16px;
  overflow: hidden;
  border: 3px solid var(--gold);
  box-shadow: 0 14px 40px #000f, 0 0 32px #d9b25c70;
  /* background-color, not the `background` shorthand — see the identical
     comment on .card-reveal-img for why the shorthand silently breaks the
     template background image. */
  background-color: #100c08;
  opacity: 0;
  transform: translateY(-50%) scale(0.97);
  transition: opacity 0.08s, transform 0.08s;
}
#cardPreview.visible { opacity: 1; transform: translateY(-50%) scale(1); }
/* Pops up on whichever side the hovered card isn't on (set in
   showPreview() in client.js), so it never covers the card — or the board
   behind it — you're actually looking at. */
#cardPreview.preview-right { left: auto; right: 16px; }

/* ---------- Drag-to-target attack arrow ---------- */
/* Fixed, full-viewport, no viewBox — its coordinate space is plain CSS
   pixels matching getBoundingClientRect() directly, so the arrow can be
   drawn straight from real card geometry with no transform step. */
#targetArrow {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 900;
  pointer-events: none;
  overflow: visible;
}
#targetArrow.hidden { display: none; }
#arrowPath {
  fill: none;
  stroke: var(--gold);
  stroke-width: 5;
  stroke-linecap: round;
  filter: drop-shadow(0 0 6px #d9b25c90);
  /* Energy flowing toward the target rather than a static line — a dashed
     stroke sliding along the path, cheap (no filter/layout work per frame). */
  stroke-dasharray: 10 8;
  animation: arrow-energy-flow 0.5s linear infinite;
}
@keyframes arrow-energy-flow {
  to { stroke-dashoffset: -36; }
}
#arrowHead { fill: var(--gold); filter: drop-shadow(0 0 6px #d9b25c90); }
#targetArrow.arrow-illegal #arrowPath { stroke: var(--hp); filter: drop-shadow(0 0 6px #b5322f90); }
#targetArrow.arrow-illegal #arrowHead { fill: var(--hp); filter: drop-shadow(0 0 6px #b5322f90); }

/* ---------- Attack Planning ---------- */
/* A separate, persistent sibling to #targetArrow (never modified) — one
   dashed arrow per queued attack, distinct from the live drag arrow's solid
   animated stroke so the two are never confused at a glance. Sits just
   below #targetArrow's z-index so the live drag/selection arrow always
   draws on top of any already-queued plan arrows. */
#attackPlanArrows {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 895;
  pointer-events: none;
  overflow: visible;
}
#attackPlanArrows.hidden { display: none; }
.plan-arrow-path {
  fill: none;
  stroke: var(--gold);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-dasharray: 7 7;
  opacity: 0.8;
}
.plan-arrow-path.plan-arrow-lethal { stroke: var(--hp); }
.plan-arrow-path.plan-arrow-rejected { stroke: var(--disabled); opacity: 0.5; }
/* 2v2 only: a teammate's own live plan, relayed for visibility — a
   distinct blue (matching .card.owner-ally's language) so it reads as
   "not mine, but visible" at a glance, never confused with your own
   gold/red arrows. */
.plan-arrow-path.plan-arrow-ally { stroke: #3a7bd5; stroke-dasharray: 4 4; }
.plan-arrow-head { fill: var(--gold); opacity: 0.8; }
.plan-arrow-head.plan-arrow-lethal { fill: var(--hp); }
.plan-arrow-head.plan-arrow-rejected { fill: var(--disabled); opacity: 0.5; }
.plan-arrow-head.plan-arrow-ally { fill: #3a7bd5; }
.plan-order-badge-bg { fill: var(--gold); stroke: #1a1410; stroke-width: 1.5; }
.plan-order-badge-bg.plan-arrow-lethal { fill: var(--hp); }
.plan-order-badge-bg.plan-arrow-rejected { fill: var(--disabled); }
.plan-order-badge-bg.plan-arrow-ally { fill: #3a7bd5; }
.plan-order-badge { font: bold 11px Georgia, serif; fill: #1a1410; }

/* Death-preview overlay — reuses the same "this is a prediction, not the
   real state" visual language as .plan-hp-badge below: obviously distinct
   from the game's other death/damage feedback (unit-death, combat-impact),
   which only ever plays once a real attack actually lands. */
.card.plan-death-preview { filter: grayscale(0.85) brightness(0.55); }
.card.plan-death-preview::after {
  content: '☠';
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2em;
  color: #fff;
  text-shadow: 0 0 6px #000;
  pointer-events: none;
}

/* Castle equivalent of .plan-death-preview — no card element to grey out,
   so the castle panel itself gets a pulsing red outline instead. */
.plan-castle-lethal { outline: 2px solid var(--hp); outline-offset: 2px; animation: plan-castle-lethal-pulse 1s ease-in-out infinite; }
@keyframes plan-castle-lethal-pulse {
  0%, 100% { outline-color: color-mix(in srgb, var(--hp) 60%, transparent); }
  50% { outline-color: var(--hp); }
}

/* Ghost predicted-HP badge — deliberately NOT styled like the real
   .hp-badge (dashed border, muted gold, offset above the card) so it can
   never be mistaken for the unit's actual, already-changed HP. */
.plan-hp-badge {
  position: absolute;
  top: -10px;
  right: -6px;
  z-index: 3;
  background: rgba(20, 16, 12, 0.85);
  border: 1px dashed var(--gold);
  color: var(--gold-bright);
  font: bold 11px Georgia, serif;
  border-radius: 8px;
  padding: 1px 5px;
  pointer-events: none;
}

#attackPlanSummary {
  align-self: center;
  color: var(--gold-bright);
  font-size: 0.9rem;
}

/* ---------- Log panel ---------- */
/* Same collapsible-floating-panel pattern as #chatPanel below, mirrored to
   the left edge instead of the right. */
#logPanel { position: fixed; left: 16px; bottom: 16px; z-index: 400; width: 300px; max-width: calc(100vw - 32px); }
.log-toggle { display: flex; align-items: center; gap: 6px; width: 100%; background: linear-gradient(#3a2c14, #241c12); border: 1px solid var(--gold); border-radius: 8px; color: var(--gold); font-family: inherit; font-weight: bold; padding: 0.5rem 0.8rem; cursor: pointer; box-shadow: 0 4px 14px #000a; transition: background 0.15s; }
.log-toggle:hover { background: linear-gradient(#4a3a1c, #2e2416); }
.log-toggle:active { transform: translateY(1px); }
.log-body { margin-top: 6px; background: var(--panel); border: 1px solid var(--gold); border-radius: 8px; box-shadow: 0 8px 24px #000c; overflow: hidden; }
#logPanel.collapsed .log-body { display: none; }

/* ---------- Chat panel ---------- */
#chatPanel { position: fixed; right: 16px; bottom: 16px; z-index: 400; width: 300px; max-width: calc(100vw - 32px); }
.chat-toggle { display: flex; align-items: center; gap: 6px; width: 100%; background: linear-gradient(#3a2c14, #241c12); border: 1px solid var(--gold); border-radius: 8px; color: var(--gold); font-family: inherit; font-weight: bold; padding: 0.5rem 0.8rem; cursor: pointer; box-shadow: 0 4px 14px #000a; transition: background 0.15s; }
.chat-toggle:hover { background: linear-gradient(#4a3a1c, #2e2416); }
.chat-toggle:active { transform: translateY(1px); }
.chat-badge { margin-left: auto; background: var(--hp); color: #fff; border-radius: 999px; font-size: 0.7rem; padding: 1px 7px; font-weight: bold; }
.chat-body { margin-top: 6px; background: var(--panel); border: 1px solid var(--gold); border-radius: 8px; box-shadow: 0 8px 24px #000c; overflow: hidden; }
#chatPanel.collapsed .chat-body { display: none; }
.chat-messages { max-height: 240px; overflow-y: auto; padding: 8px 10px; font-size: 0.85rem; color: var(--text); }
.chat-line { margin-bottom: 6px; line-height: 1.35; }
.chat-line .who { font-weight: bold; color: var(--gold); margin-right: 4px; }
.chat-line.mine .who { color: var(--mana); }
.chat-form { display: flex; border-top: 1px solid var(--border); }
.chat-form input { flex: 1; background: #0000004d; border: none; color: var(--text); font-family: inherit; font-size: 0.85rem; padding: 8px 10px; }
.chat-form button { background: linear-gradient(#e7c877, #b8912f); border: none; padding: 0 14px; font-weight: bold; cursor: pointer; color: #241c12; transition: filter 0.15s; }
.chat-form button:hover { filter: brightness(1.08); }
.chat-form button:active { filter: brightness(0.94); }

/* Below this there isn't room for a 300px sidebar alongside the play panel's
   own two columns without squeezing them, so quests drops underneath instead
   — the play area keeps priority on the horizontal space. */
@media (max-width: 1200px) {
  .lobby-main:has(#questsPanel:not(.hidden)) { grid-template-columns: minmax(0, 1fr); }
  .lobby-main > .quests-panel { max-width: 560px; margin: 0 auto; }
}

@media (max-width: 980px) {
  /* Tablet range (roughly 721-980px, e.g. a portrait iPad): 5 columns here
     left each faction card too narrow for its badge row/tagline/View Deck
     button — the same crowding problem the 720px breakpoint below already
     fixes for phones, just one range up. 4 columns gives real width back. */
  .faction-picker { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
  .faction-card { padding: 0.6rem 0.4rem; gap: 0.4rem; }
  .faction-card .thumb { width: 60px; height: 90px; }
  .faction-card .label { font-size: 0.75rem; }
  .deck-preview-body { grid-template-columns: 260px 1fr; }
}

@media (max-width: 980px) {
  /* Two real columns need the width; below this they stack. The primary CTA
     lives inside whichever mode panel is showing, so leaving the deck column
     after it would strand "Create Match" in the middle of the flow — deck
     first means the stack still ends on the CTA: what am I playing -> how am
     I playing -> start. */
  .play-panel { display: flex; flex-direction: column; }
  .play-col--deck {
    order: -1;
    padding-left: 0;
    border-left: none;
    padding-bottom: var(--space-3);
    border-bottom: 1px solid color-mix(in srgb, var(--gold) 20%, transparent);
  }
  .lobby-secondary-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 720px) {
  #lobby { padding: 0.5rem 0.8rem 2rem; }
  .lobby-header { gap: 0.5rem; padding: 0.3rem 0 0.5rem; margin-bottom: var(--space-3); }
  .play-col-title { margin-bottom: 0.4rem; }
  .mode-panel-desc { margin-bottom: 0.4rem; }
  .lobby-header h1 { font-size: var(--text-lg); letter-spacing: 1px; }
  .lobby-header h1 span { display: none; }
  .lobby-header .logo-sigil { width: 28px; height: 28px; }
  /* The nav pills are the first thing worth dropping at this width — the
     same three destinations are linked again from .seo-home-content. */
  .lobby-header .home-content-nav { display: none; }
  /* Five controls can't fit one 375px row — wrap rather than run off the
     right edge (the language picker takes the second row on its own). */
  .lobby-header-tools { flex-wrap: wrap; justify-content: flex-end; gap: 0.35rem; row-gap: 0.35rem; }
  .lobby-header-tools .lang-picker-row { flex-basis: 100%; display: flex; justify-content: flex-end; }
  .header-account-name { max-width: 8ch; }
  /* Four tabs each carrying a full line of consequence copy pushed the
     primary CTA a screen and a half down. The copy still matters — but only
     for the mode you're actually about to play, so only the active tab keeps
     it. Tapping a tab reveals its own warning before any CTA is pressed. */
  .mode-tab:not(.active) .mode-tab-desc { display: none; }
  .mode-tab { padding: 0.55rem 0.75rem; }
  .board-mode-btn { padding: 0.5rem 0.7rem; }
  /* The 190px floor exists to balance the two desktop columns; stacked, it
     just pushes the mode list and the CTA down the page. */
  .deck-summary--empty { min-height: 0; }
  .deck-summary { padding: 0.7rem; gap: 0.4rem; }
  .deck-summary-art { width: 60px; height: 60px; }
  /* Anchored to the header rather than the button so a popover opened from
     the right-hand end can never hang off the edge of a narrow screen. */
  .header-popover {
    position: fixed;
    left: 0.8rem;
    right: 0.8rem;
    top: auto;
    width: auto;
    max-width: none;
  }
  .header-popover--music { width: auto; }
  .play-panel { padding: var(--space-3); gap: var(--space-3); }
  .play-cta > .btn { padding: 0.8rem 1rem; font-size: var(--text-base); }
  .mode-tabs { grid-template-columns: 1fr; }
  .difficulty-picker { grid-template-columns: 1fr; }
  .board-mode-picker { grid-template-columns: 1fr; }
  .lobby-secondary-nav { grid-template-columns: 1fr; gap: 0.6rem; }
  .join-row-controls { flex-direction: column; }
  /* Keep all three offered cards side by side so they can still be compared
     at a glance — tap-to-inspect covers the detail — but reclaim the gap and
     padding so each card is as large as the width allows. */
  .draft-offer { gap: 0.4rem; }
  .draft-offer-card { padding: 0.25rem; }
  .deck-chooser-modal { width: 96vw; max-height: 92vh; padding: 1rem; }
  /* Fewer, wider columns here (not the desktop 5-across grid) — the deck
     face now carries a badge row, tagline and View Deck button that need
     real width to stay legible, per the master prompt's "stack deck info
     vertically on mobile" requirement. */
  .faction-picker { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 1rem 0; }
  .faction-card { padding: 0.5rem 0.3rem; gap: 0.3rem; }
  .faction-card .thumb { width: 52px; height: 78px; }
  .faction-card .label { font-size: 0.68rem; line-height: 1.1; }
  #cardPreview { height: min(390px, calc(100vh - 340px)); }
  .deck-preview-modal { width: 96vw; max-height: 92vh; }
  .deck-preview-body { grid-template-columns: 1fr; }
  .deck-preview-columns { grid-template-columns: 1fr; }
  .deck-card-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); max-height: 44vh; }
  .deck-builder-list { max-height: 30vh; }
  /* .deck-builder-body reuses .deck-preview-body's single-column stack
     above rather than a separate drawer/bottom-sheet component — the one
     addition a builder needs on top of the read-only preview is this: the
     card count and Save button pinned in place while the (now much
     taller, single-column) card grid scrolls underneath them. */
  .deck-builder-summary-bar {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--panel);
    padding: 6px 0;
    margin-top: -6px;
  }
}

@media (min-width: 1400px) {
  #cardPreview { height: min(600px, calc(100vh - 340px)); }
}

@media (max-width: 480px) {
  .faction-picker { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .faction-card { padding: 0.5rem 0.4rem; }
  .faction-card .thumb { width: 64px; height: 96px; }
  .faction-card .label { font-size: 0.72rem; }
  #cardPreview { height: min(315px, calc(100vh - 340px)); }
  .deck-preview-header { flex-direction: column; align-items: stretch; gap: 0.5rem; }
  .deck-card-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
}

/* ---------- Mobile game screen ---------- */
/* A genuinely reflowed layout, not the desktop grid shrunk down — the
   desktop HUD system's own configured minimums (boardArea 320px + hand
   160px) already exceed a phone's entire width, so squeezing it further was
   never going to work. Below this breakpoint, initHudLayout()/
   applyPanelGeometry() (client.js) stop applying position:absolute to every
   panel and let them fall back to this grid's grid-area placement instead —
   the exact same fallback mechanism every panel already has for the one
   frame before the desktop JS takes over. */
@media (max-width: 760px) {
  /* #langPicker sits fixed at the top-right corner too — on a screen this
     narrow the centered notice's width reaches far enough right to
     overlap it, so it drops below instead. */
  .top-notice { top: 54px; }
  #game {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "topbar" "board" "hand" "controls";
    width: 100vw;
    height: 100vh;
    padding: env(safe-area-inset-top, 4px) env(safe-area-inset-right, 6px)
      env(safe-area-inset-bottom, 4px) env(safe-area-inset-left, 6px);
  }
  /* Extra top clearance only while the HUD preview's "Back to Lobby" button
     is showing — it's position:fixed in that same corner regardless of this
     grid, and would otherwise sit on top of the topbar's own content
     instead of above it. A real match never shows that button (edit/reset
     are hidden on mobile too), so it isn't worth reserving this space then. */
  #game:has(#hudPreviewExitBtn:not(.hidden)) {
    padding-top: max(env(safe-area-inset-top, 4px), 48px);
  }

  /* --- Top bar: name/mana panels fall back to a plain flex row instead of
     the desktop's centered-cluster math, which assumed real width either
     side of the turn indicator. --- */
  #topBar { flex-wrap: wrap; gap: 4px; padding: 4px 6px; }
  .hp-panel { min-width: 0; flex: 1 1 auto; }
  .hp-panel .name { font-size: clamp(0.65rem, 3.5vw, 0.9rem); }
  .turn-indicator { font-size: 0.85rem; flex: 0 0 100%; order: 3; }

  /* --- Mobile castle-attack fix: the side-panel castle (below, in the
     board row) can get squeezed off the visible viewport on a tall board,
     but #oppInfo/#youInfo here in the topbar row never do — and #oppInfo is
     already the actual attack-castle tap target (see onCastleClick() in
     client.js). Show the icon+HP that were hidden by default so mobile
     players have something to see and tap. --- */
  .hp-panel .castle-wrap { display: flex; width: clamp(24px, 8vw, 36px); height: clamp(24px, 8vw, 36px); margin: 0 auto; }
  .hp-panel .castle-hp-value { display: block; text-align: center; }

  /* --- Vitals strip: opponent above the board, you below it (right above
     the hand) — the side rails the desktop layout uses have no room next
     to 5 lane slots on a phone, so each becomes a full-width horizontal
     strip instead via flex-direction, not a new component. --- */
  .board-wrap { flex-direction: column; gap: 4px; }
  .side-panel {
    width: 100%;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 10px;
    padding: 4px 10px;
    border-radius: 10px;
  }
  .castle-hp-block { flex-direction: row; width: auto; flex: 1 1 auto; gap: 6px; }
  .castle-owner-label { display: none; }
  .side-panel .castle-wrap { width: clamp(40px, 12vw, 56px); height: clamp(40px, 12vw, 56px); }
  .side-panel .castle-hp-overlay { font-size: clamp(1rem, 6vw, 1.5rem); }
  .castle-hp-value { white-space: nowrap; }
  .panel-stat { min-width: 0; padding: 4px 8px; font-size: 0.85rem; gap: 4px; }
  .panel-icon { font-size: 0.95rem; }

  /* --- Board: full width, tighter gaps so 5 slots per lane actually fit. --- */
  .board-area { max-width: 100%; padding: 6px 4px; gap: 3px; }
  .lane-row { gap: 4px; padding: 2px; }
  /* Row labels shrink to icon+number on a phone — the full "Vanguard +1 Max
     HP" wording has no room next to slots this small and would overlap
     the first card's art. */
  #oppRearguard::before, #youRearguard::before { content: '🏹 +1'; }
  #oppVanguard::before, #youVanguard::before { content: '🛡 +1'; }
  .slot { min-height: 40px; }

  /* --- Hand: a horizontal scroll strip, never a multi-row wrap that would
     push the board out of view — tap-to-preview (client.js) covers reading
     a card's full text since there's no room to show it inline. --- */
  #hand {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    gap: 6px;
    padding: 6px;
    -webkit-overflow-scrolling: touch;
  }
  .hand-card { flex: 0 0 76px; }

  /* --- Controls: full-width bottom bar, comfortable thumb-sized buttons. --- */
  .controls { padding: 6px; gap: 0.5rem; }
  .controls .btn { padding: 0.6rem 1rem; font-size: 0.85rem; min-height: 44px; }

  /* Free-form HUD customization can't work with a fingertip on a screen
     this size (see isMobileLayout() in client.js) — only the always-needed
     "Back to Lobby" control (part of the same container) stays. */
  #hudEditToggle, #hudResetLayout { display: none; }

  /* --- Log/Chat: collapsed to small circular icon buttons pinned to the
     side edges, well clear of the bottom controls bar, instead of the
     desktop's full-width "📜 Log"/"💬 Chat" pills — those were both up to
     300px wide anchored at the SAME bottom-16px corner pair, which
     overlapped each other on anything narrower than ~650px and could sit
     on top of a wrapped 4th control button (Surrender) underneath. Expanded
     panels still grow upward from this anchor exactly as before (bottom
     stays fixed, height added pushes the top edge up) — only the collapsed
     resting position and size change. --- */
  #logPanel, #chatPanel {
    bottom: max(calc(env(safe-area-inset-bottom, 0px) + 128px), 128px);
  }
  #logPanel { left: 6px; }
  #chatPanel { right: 6px; }
  /* #logPanel/#chatPanel (ID selectors, specificity 1-0-0) already set
     width:300px in the base stylesheet — a plain .log-panel.collapsed class
     override (0-2-0) can never win that fight regardless of source order,
     so these repeat the ID to actually take effect. */
  #logPanel.collapsed, #chatPanel.collapsed { width: 46px; max-width: 46px; }
  #logPanel:not(.collapsed), #chatPanel:not(.collapsed) {
    width: min(280px, calc(100vw - 20px));
  }
  #logPanel.collapsed .log-toggle, #chatPanel.collapsed .chat-toggle {
    position: relative;
    width: 46px;
    height: 46px;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
  }
  #logPanel.collapsed .toggle-label, #chatPanel.collapsed .toggle-label { display: none; }
  .toggle-icon { font-size: 1.1rem; line-height: 1; }
  /* Corner badge instead of an inline margin-left:auto pill — there's no
     "far right of a wide button" left once the button is a small circle. */
  #chatPanel.collapsed .chat-badge { position: absolute; top: -2px; right: -2px; margin-left: 0; }

  /* --- Card preview, in-match only: docked to the bottom edge instead of
     vertically centered, and shrunk further. Centered-and-tall (the base
     rule further up, shared with the lobby's deck browser, which has no
     board to hide behind it) sat on top of a meaningful chunk of the board
     on a phone — exactly the space a small screen can least afford to
     lose. Gated on body.in-game since #cardPreview is one shared element
     used by both screens (it lives outside both #lobby and #game in the
     markup), so the lobby's own preview is untouched. --- */
  body.in-game #cardPreview {
    left: 50%;
    right: auto;
    top: auto;
    bottom: max(env(safe-area-inset-bottom, 8px), 8px);
    transform: translateX(-50%) scale(0.97);
    height: min(200px, 30vh);
  }
  body.in-game #cardPreview.visible { transform: translateX(-50%) scale(1); }
}

/* ---------- 2v2 Team Battle board ---------- */
/* A completely separate screen from #game (see index.html) — every rule
   here is scoped to #game2v2/.team2v2-*, so nothing below can ever affect
   1v1's board. Reuses .lane-row/.slot/.card/.mana-crystals/.castle-wrap
   wholesale from the 1v1 board's own CSS above (identical box model, card
   art, hover states, .attack-ready glow) — only the outer quadrant/grid
   layout and the MY/ALLY/ENEMY team-color language below are new.
   Deliberately a plain vertical flex stack, not #game's resizable-hand-
   column HUD grid — the HUD drag/resize manager (initHudLayout()) isn't
   extended to 2v2 in this pass, a disclosed simplification. */
#game2v2 {
  display: flex;
  flex-direction: column;
  width: min(98vw, 1900px);
  height: min(96vh, 1080px);
  max-width: 100%;
  max-height: 100%;
  margin: 0 auto;
  padding: clamp(4px, 0.5vh, 10px) clamp(6px, 0.8vw, 14px);
  gap: clamp(4px, 0.6vh, 8px);
  box-sizing: border-box;
  overflow: hidden;
}
#team2v2TopBar { display: flex; justify-content: center; flex: 0 0 auto; }
/* Smaller than 1v1's own .turn-indicator (which sits in a wider, roomier
   top bar) — this stacked turn-text/timer/sfx column was eating more
   vertical budget here than it needs to, at the direct expense of the
   hand strip below actually having room to show full cards. */
#team2v2TopBar .turn-indicator { font-size: 0.95rem; gap: 0; }

/* One "side" per TEAM (not per player) — a header (shared castle + both
   players' compact info strips) plus one shared 6-slot row. The enemy
   side's row sits above its header (units near the middle of the
   screen, castle at the outer top edge); the self side mirrors that
   (row nearest the middle, castle/header at the outer bottom edge) —
   same top=enemy/bottom=you convention 1v1 already uses. */
.team2v2-side {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.team2v2-side.enemy { flex-direction: column; }
.team2v2-side.self { flex-direction: column-reverse; }
/* The base .castle-wrap size (clamp 34-52px) is tuned for the small
   top-bar badge context — a real focal-element castle needs the same
   bump up 1v1's own side panel gives it (.side-panel .castle-wrap
   further up), or it reads as a barely-visible smudge next to the HP
   number instead of actual castle art. */
.team2v2-side-header .castle-wrap { width: clamp(56px, 5vw, 90px); height: clamp(56px, 5vw, 90px); }
.team2v2-side-header {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 0.6rem;
  padding: 0.2rem 0.6rem;
  border-radius: 10px;
  background: radial-gradient(ellipse at center, #1c150d 0%, #100c07 100%);
  border: 2px solid var(--border);
}
.team2v2-side.enemy .team2v2-side-header { border-color: #b3382c; }
.team2v2-side.self .team2v2-side-header { border-color: var(--gold); }
.team2v2-side-info { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; flex: 1; }
.team2v2-side-title {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  letter-spacing: 1px;
  color: var(--text);
}
.team2v2-side.enemy .team2v2-side-title { color: #ec9a8e; }
.team2v2-side.self .team2v2-side-title { color: var(--gold); }
/* Each player's own compact strip (name/faction/mana) — the SAME visual
   owner-color language the shared-row units use below, so a player's
   strip and their units on the board read as the same person at a
   glance. Enemy1/enemy2 intentionally share one color (attacking
   targets the shared enemy board as a whole, not "enemy 1's board"),
   self/ally get their own distinct colors. */
.team2v2-player-strip {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--text-xs);
  padding: 0.1rem 0.4rem;
  border-radius: 6px;
  border-left: 3px solid var(--border);
}
.team2v2-player-strip[data-role="self"] { border-left-color: var(--gold); }
.team2v2-player-strip[data-role="ally"] { border-left-color: #3a7bd5; }
.team2v2-player-strip[data-role="enemy1"], .team2v2-player-strip[data-role="enemy2"] { border-left-color: #b3382c; }
.team2v2-player-strip.active-turn { background: #ffd76a1a; animation: pulse-gold 2.2s ease-in-out infinite; }
.team2v2-player-strip.knocked-out { opacity: 0.5; text-decoration: line-through; }
.team2v2-player-name { font-family: var(--font-display); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 9em; }
.team2v2-player-faction { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.team2v2-player-mana { color: var(--gold-bright); font-family: var(--font-display); margin-left: auto; }
.team2v2-player-strip .mana-crystals { display: flex; gap: 1px; }
.team2v2-player-strip .mana-crystals .crystal { width: 7px; height: 7px; }

/* The shared 6-slot row itself — same box model/background as 1v1's
   .lane-row, and deliberately the SAME sizing approach too: slots are
   NOT flex:1 (that stretches every card into a flat, squeezed rectangle
   the moment the row is wider than 6 real card-shaped slots need — which
   it almost always is, this row spans nearly the full board width).
   Letting each .slot size itself from the row's height via its own
   aspect-ratio (see the base .slot rule) and centering the row with
   justify-content is what keeps them genuine card proportions, same as
   every other board in this game. */
.team2v2-shared-row {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 4px;
  border-radius: 10px;
}
.team2v2-side.enemy .team2v2-shared-row { background: linear-gradient(180deg, var(--vanguard), transparent); }
.team2v2-side.self .team2v2-shared-row { background: linear-gradient(0deg, var(--vanguard), transparent); }
/* Per-unit ownership border — MY unit / ALLY unit / ENEMY unit, the same
   3-color language as the player strips above, applied directly to the
   card so it reads correctly even once units from both teammates are
   mixed together in one row. */
.card.owner-self { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold) inset; }
.card.owner-ally { border-color: #3a7bd5; box-shadow: 0 0 0 1px #3a7bd5 inset; }
.card.owner-enemy { border-color: #b3382c; box-shadow: 0 0 0 1px #b3382c inset; }

#controls2v2 { display: flex; align-items: center; justify-content: center; gap: 0.6rem; flex-wrap: wrap; }
/* auto-fit (not auto-fill) so a half-empty hand's cards actually grow to
   fill the row instead of leaving empty ghost columns to their right —
   noticeably wrong at this hand's full-width, only-a-few-cards common
   case. Taller + a wider per-card cap than 1v1's narrow sidebar hand
   (the base .hand rule further up), since this hand strip spans nearly
   the full board width and has real room to spend on bigger cards —
   also absorbs the vertical space freed by not having a second always-
   open log panel down here anymore (see renderLog2v2() in client.js). */
#hand2v2 {
  flex: 0 0 auto;
  /* Sized to actually fit a full card at the grid's own max column width
     below (140px wide -> 210px tall via the 2:3 aspect-ratio, +20px for
     .hand's own top/bottom padding = 230px) with a little headroom — a
     mismatch here is exactly what was forcing a vertical scrollbar
     instead of showing the hand's cards fully. */
  height: clamp(170px, 25vh, 240px);
  grid-template-columns: repeat(auto-fit, minmax(100px, 140px));
  justify-content: center;
}

@media (max-width: 900px) {
  /* A single shared row per side (6 slots, not 2x2 quadrants) fits a
     phone far better than the old per-player-board layout did — no
     quadrant-focus/tab-switcher needed at all, every slot just gets
     smaller, same approach 1v1's own board already uses at this width. */
  .team2v2-side-header { padding: 0.25rem 0.4rem; gap: 0.4rem; }
  .team2v2-side-info { gap: 0.1rem; }
  .team2v2-player-strip { flex-wrap: wrap; font-size: 0.65rem; padding: 0.05rem 0.3rem; }
  .team2v2-player-name { max-width: 6em; }
  .team2v2-shared-row { gap: 4px; padding: 3px; }
  /* Width is the scarce resource here, not height — 8 cards must fit a
     ~360px-wide phone screen. max-height caps how tall (and via
     aspect-ratio, how WIDE) each card can grow to, since the row itself
     may have plenty of vertical room to give on a tall phone; without
     this cap the base .slot rule's 260px desktop ceiling lets 8 cards
     blow way past the viewport width. ~57px tall -> ~38px wide each,
     8 of them + 7 gaps comfortably fits 360px. */
  .team2v2-shared-row .slot { min-height: 30px; max-height: 57px; }
  /* Smaller cards fit more per row on a narrow phone (3 instead of 2 at
     the desktop min-width), which meaningfully cuts down how often a
     typical mid-size hand needs a 2nd row/scrolling at all — a maxed-out
     10-card hand will still scroll on any phone regardless of sizing,
     that's inherent to the hand cap, not something to chase pixel-
     perfect here. */
  #hand2v2 { grid-template-columns: repeat(auto-fit, minmax(78px, 105px)); gap: 6px; height: clamp(150px, 22vh, 190px); }
}

/* ---------- Landscape phone (height-constrained, not width-constrained)
   ---------- */
/* A phone turned sideways is wide but short — the opposite resource
   constraint from the max-width:760px block above, which assumes width is
   scarce and height is plentiful. Most modern phones in landscape are
   WIDER than 760px (iPhone 12 mini/14/15 land at 780-930px), so without a
   height-based breakpoint they'd fall straight into the desktop layout
   instead, which has no height floor at all: .slot's hard min-height:60px
   (below) alone exceeds the ~200-230px a ~375-430px-tall viewport actually
   has for 4 rows once the topbar/controls are subtracted, and #game/
   body.in-game both clip overflow rather than scroll it. isMobileLayout()
   in client.js needs this same height threshold, or the HUD manager's
   free-form inline positioning (which always wins over stylesheet rules)
   would silently override everything below.
   Placed after the max-width block so it wins the cascade for a phone
   that's BOTH narrow and short (e.g. iPhone SE landscape, 667x375). 500px
   comfortably covers every real phone in landscape while excluding
   tablets (~768px+ even sideways) and ordinary desktop windows — as a
   bonus this also fixes #cardPreview's existing short-window formula
   further down, which its own comment already flags as never having been
   tuned this short. */
@media (max-height: 500px) {
  #game {
    grid-template-columns: minmax(0, 1fr) 6px minmax(88px, 15vw);
    grid-template-rows: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "topbar   topbar topbar"
      "board    handle hand"
      "controls handle hand";
    width: 100vw;
    height: 100dvh;
    padding: env(safe-area-inset-top, 2px) env(safe-area-inset-right, 4px)
      env(safe-area-inset-bottom, 2px) env(safe-area-inset-left, 4px);
    gap: 3px;
  }
  #game:has(#hudPreviewExitBtn:not(.hidden)) {
    padding-top: max(env(safe-area-inset-top, 2px), 40px);
  }

  /* --- Top bar: same flex-wrap fallback as portrait mobile, just tighter
     since there's a fraction of the vertical room to spend on it. --- */
  #topBar { flex-wrap: wrap; gap: 3px; padding: 2px 4px; }
  .hp-panel { min-width: 0; flex: 1 1 auto; }
  .hp-panel .name { font-size: clamp(0.6rem, 2.5vw, 0.8rem); }
  .turn-indicator { font-size: 0.75rem; flex: 0 0 100%; order: 3; }
  .hp-panel .castle-wrap { display: flex; width: clamp(18px, 4vh, 28px); height: clamp(18px, 4vh, 28px); margin: 0 auto; }
  .hp-panel .castle-hp-value { display: block; text-align: center; }

  /* --- Board + side-panels: back to a row, like desktop, not portrait
     mobile's stacked horizontal strips — width is abundant here, height
     isn't, so wasting a whole horizontal band on each side-panel would be
     the wrong trade in this orientation. --- */
  .board-wrap { flex-direction: row; gap: 4px; }
  .side-panel {
    width: clamp(44px, 9vw, 72px);
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 4px;
    padding: 3px;
    border-radius: 8px;
  }
  .castle-hp-block { flex-direction: column; width: 100%; gap: 2px; }
  .castle-owner-label { display: none; }
  .side-panel .castle-wrap { width: clamp(32px, 7vh, 52px); height: clamp(32px, 7vh, 52px); }
  .side-panel .castle-hp-overlay { font-size: clamp(0.8rem, 4vh, 1.2rem); }
  .panel-stat { min-width: 0; padding: 2px 4px; font-size: 0.7rem; gap: 2px; }
  .panel-icon { font-size: 0.8rem; }

  /* --- Board: every pixel of row height is scarcer here than even on
     portrait mobile (which at least had a tall, narrow viewport to work
     with) — shrink the slot floor further than that breakpoint's 40px. --- */
  .board-area { padding: 3px; gap: 2px; }
  .lane-row { gap: 3px; padding: 1px; }
  .slot { min-height: 28px; }

  /* --- Hand: a side column (like desktop), not a bottom strip — a bottom
     strip would spend ~100px of the one dimension this orientation can't
     spare. Narrower than desktop's default column and scrollable, since
     only a card or two is visible at once at this width. --- */
  #hand {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
    gap: 4px;
    padding: 3px;
    -webkit-overflow-scrolling: touch;
  }
  .hand-card { flex: 0 0 auto; }

  /* --- Controls: compact, still thumb-usable. --- */
  .controls { padding: 3px; gap: 0.3rem; }
  .controls .btn { padding: 0.35rem 0.6rem; font-size: 0.7rem; min-height: 32px; }

  /* Free-form HUD customization can't work here either — see
     isMobileLayout() in client.js, which now also checks height. */
  #hudEditToggle, #hudResetLayout { display: none; }

  /* --- Card preview: the existing calc(100vh - 340px) formula (further
     down) was tuned for a short DESKTOP window, not a ~375-430px-tall
     phone — it collapses to an unreadable sliver there. --- */
  #cardPreview { height: min(280px, calc(100dvh - 40px)); }
  /* In-match only (see the portrait breakpoint's identical comment) —
     smaller still and bottom-docked, so it doesn't sit over the board in
     an orientation where height is already the scarce resource. */
  body.in-game #cardPreview {
    left: 50%;
    right: auto;
    top: auto;
    bottom: max(env(safe-area-inset-bottom, 4px), 4px);
    transform: translateX(-50%) scale(0.97);
    height: min(160px, 45vh);
  }
  body.in-game #cardPreview.visible { transform: translateX(-50%) scale(1); }

  /* --- Log/Chat: same collapsed-icon treatment portrait mobile uses,
     anchored tighter since there's no bottom hand strip to clear here —
     the controls bar is the only thing below them — and height-capped so
     an expanded panel can't grow past what's actually on screen. --- */
  #logPanel, #chatPanel {
    bottom: max(calc(env(safe-area-inset-bottom, 0px) + 40px), 40px);
  }
  #logPanel { left: 4px; }
  #chatPanel { right: 4px; }
  #logPanel.collapsed, #chatPanel.collapsed { width: 38px; max-width: 38px; }
  #logPanel:not(.collapsed), #chatPanel:not(.collapsed) {
    width: min(240px, calc(100vw - 16px));
    max-height: min(240px, calc(100dvh - 60px));
  }
  #logPanel.collapsed .log-toggle, #chatPanel.collapsed .chat-toggle {
    position: relative;
    width: 38px;
    height: 38px;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
  }
  #logPanel.collapsed .toggle-label, #chatPanel.collapsed .toggle-label { display: none; }
  #chatPanel.collapsed .chat-badge { position: absolute; top: -2px; right: -2px; margin-left: 0; }
}
