/* ============================================================
   Ugly Co-op Deckbuilder — derived from
   /home/fre/GitHub/CursedGame/Ugly Co-op Deckbuilder Game/
     Ugly Deckbuilder Style Board.dc.html
   ============================================================
   Background  : CASINO FELT (radial dark green + 45° stripe overlay)
   Table chrome: PALETTE A — Basement Vegas
                 #0c3a22 felt · #1d7a45 felt-lit · #ffd21e gold
                 #ff2e88 magenta (danger) · #00e5ff cyan (mana) · #000 chrome
   Boss layer  : PALETTE B — Cursed Rainbow
                 #20002e void · #6a0dad grape · #aaff00 lime (primary CTA)
                 #ff6a00 orange (mana) · #ff00c8 magenta (curse) · #e8e8e8 text
   Type        : PAIRING 1
                 Luckiest Guy — card names, buttons, boss shouting
                 Verdana       — rules, GM narration, numbers
                 VT323         — logs, timestamps, fake system text
   ============================================================ */

/* Self-hosted, not linked: the container has no outbound internet, so a
   fonts.googleapis.com <link> silently falls back to cursive/monospace —
   which is exactly what the display type was rendering as. See
   public/fonts/README.md. */
@font-face {
  font-family: 'Luckiest Guy';
  src: url('/fonts/luckiest-guy.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'VT323';
  src: url('/fonts/vt323.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Palette A — table */
  --felt-bg: radial-gradient(ellipse at 50% 40%, #26a05c 0%, #12592f 55%, #08331c 100%),
             repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.07) 0 3px, transparent 3px 6px);
  --felt-bg-blend: multiply;
  --felt-deep: #0c3a22;
  --felt-mid: #1d7a45;
  --gold: #ffd21e;
  --magenta: #ff2e88;
  --cyan: #00e5ff;
  --chrome: #000;
  /* Palette B — boss layer */
  --boss-bg: #20002e;
  --boss-grape: #6a0dad;
  --boss-lime: #aaff00;
  --boss-orange: #ff6a00;
  --boss-curse: #ff00c8;
  --boss-text: #e3c6f5;
  /* Type */
  --font-display: 'Luckiest Guy', cursive;
  --font-body: Verdana, Geneva, sans-serif;
  --font-mono: 'VT323', monospace;
}

* { box-sizing: border-box; }

/* ============================================================
   GLOBAL TEXT-SELECTION SUPPRESSION
   Drag-to-select on cards is annoying — kill it everywhere except
   in the modal where players may want to copy flavor text.
   ============================================================ */
.card-frame, .hand-fan, .hand-fan-card, .hand-battlefield, .party-rail,
.party-avatar, .tab-bar, .tab, .hp-panel, .mana-pips, .lands-rail,
.turn-banner, .boss-strip, button, .land-chip, .status-chip, .play-grid {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;  /* suppress iOS long-press callout */
}
/* The enlarged card opens on long press — the same gesture that starts a text
   selection — so it used to arrive with its own rules text already highlighted.
   Nothing in the modal is meant to be copied, so selection stays off.
   (The rule here previously also named `.card-frame.selectable`, which never
   matched: the class tiles.js actually sets is `is-selectable`, and that marks
   a legal combat target, not selectable text.) */
.modal-card, .modal-card * {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

html, body {
  margin: 0; padding: 0; min-height: 100%;
  background: var(--felt-bg);
  background-blend-mode: var(--felt-bg-blend);
  box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.8);
  color: var(--boss-text);
  font-family: var(--font-body);
  overflow-x: hidden;
}

/* ============================================================
   Views + boss strip
   ============================================================ */
.view { display: none; padding: 16px; min-height: 60vh; }
.view.active { display: block; }

.boss-strip {
  /* PAINT SPLATTER over palette-B void — the template's boss background. */
  background: var(--boss-bg);
  background-image:
    radial-gradient(circle at 15% 30%, rgba(170, 255, 0, 0.35) 26px, transparent 27px),
    radial-gradient(circle at 85% 70%, rgba(255, 0, 200, 0.35) 34px, transparent 35px);
  border-bottom: 4px solid var(--boss-lime);
  padding: 8px;
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 9px;
  align-items: center;
  color: var(--boss-text);
}
.boss-portrait {
  width: 62px; height: 62px; flex: 0 0 auto;
  background: #0a0a0a; border: 4px outset var(--boss-curse);
  display: flex; align-items: center; justify-content: center; font-size: 34px;
}
.boss-info { min-width: 0; }
.boss-line { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.boss-hp-text { font-family: var(--font-body); font-weight: bold; font-size: 14px; color: #fff; }
.boss-next {
  margin-top: 4px; font-family: var(--font-mono); font-size: 15px; color: #fff;
  background: #000; border: 2px inset var(--boss-grape); padding: 2px 5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.boss-next span { color: var(--boss-orange); }
.boss-name {
  font-family: var(--font-display);
  font-size: 17px;
  color: var(--boss-lime);
  text-shadow: 2px 2px 0 var(--chrome);
}
.boss-phase {
  font-family: var(--font-mono);
  font-size: 18px;
  color: var(--boss-text);
}

/* ============================================================
   BOSS HP — striped bar (palette B)
   The template says: badge = players, striped bar = boss,
   pip row = creatures (≤5 HP). Boss always gets the striped bar.
   ============================================================ */
.boss-hp, .boss-hp-bar {
  width: 100%;
  height: 15px;
  background: #3a0018;
  border: 2px solid var(--chrome);
  padding: 2px;
  position: relative;
  overflow: hidden;
}
.boss-hp-fill, .boss-hp-bar > div {
  height: 100%;
  background: repeating-linear-gradient(90deg, var(--magenta) 0 6px, #ff5aa0 6px 12px);
  transition: width 0.3s ease;
}
.boss-hp span, .boss-hp-bar span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-body);
  font-weight: bold;
  font-size: 14px;
  color: #fff;
  text-shadow: 1px 1px 0 var(--chrome);
}

/* Boss screen gets the BIG boss hp */
.boss-hp-bar { height: 36px; margin: 16px 0; }
.boss-hp-bar span { font-family: var(--font-mono); font-size: 22px; }

/* ============================================================
   PLAYER HP — badge style (template: ❤️ + Verdana bold number,
   magenta outset border). Replaces the old hp-panel block.
   ============================================================ */
.hp-panel {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: #1a0010;
  border: 3px outset var(--magenta);
  padding: 4px 9px;
  align-self: start;
}
.hp-heart { font-size: 20px; }
.hp-big {
  font-family: var(--font-body);
  font-weight: bold;
  font-size: 19px;
  color: #fff;
  line-height: 1;
}
.hp-label {
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--gold);
  letter-spacing: 0.3px;
}

/* ============================================================
   TURN INDICATOR — gold bevel + pulse ring on active player.
   Boss turn = whole screen gets 6px magenta inset border.
   ============================================================ */
.phase-banner {
  font-family: var(--font-display);
  font-size: 20px;
  color: #0c3a22;
  background: var(--gold);
  border: 4px outset #fff;
  padding: 6px 12px;
  text-align: center;
  letter-spacing: 0.3px;
}
.phase-banner.is-my-turn {
  animation: pulseTurn 1.6s ease-out infinite;
}

/* When the boss turn starts, app.js toggles a class on body */
body.boss-turn::before {
  content: '';
  position: fixed;
  inset: 0;
  border: 6px inset var(--boss-curse);
  pointer-events: none;
  z-index: 1000;
}

/* Active player indicator on the avatar/HP badge */
.hp-panel.is-active {
  border-color: var(--gold);
  border-style: outset;
  animation: pulseTurn 1.6s ease-out infinite;
}

/* ============================================================
   MANA — cyan orb + pip row.
   The template says: pick orb + pip row, drop the rest.
   ============================================================ */
.mana-orb {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #9ff6ff, #00a8c0);
  border: 3px solid var(--chrome);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 18px;
  color: #00304a;
  flex: 0 0 auto;
}
.mana-pips { display: inline-flex; gap: 3px; align-items: center; }
.mana-pip {
  width: 14px;
  height: 22px;
  background: var(--cyan);
  border: 2px solid var(--chrome);
  display: inline-block;
}
.mana-pip.spent { background: #123; }

/* ============================================================
   STATUS EFFECT CHIPS — 22px black-bordered emoji + stack count.
   ============================================================ */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  background: #000;
  border: 2px solid var(--chrome);
  padding: 2px 5px;
  font-family: var(--font-mono);
  font-size: 14px;
  color: #fff;
}
.status-chip .emoji { font-size: 22px; }

/* ============================================================
   CARDS — V1 CASINO BEVEL (already defined; kept here for clarity)
   ============================================================ */
.card-frame { display: inline-block; }
.card-bevel {
  background: linear-gradient(var(--felt-mid), var(--felt-deep));
  border: 6px outset var(--gold);
  box-shadow: 8px 8px 0 var(--chrome);
  cursor: pointer;
  transition: transform 120ms cubic-bezier(.34, 1.56, .64, 1);
  font-family: var(--font-body);
  color: #c0c0c0;
}
.card-frame.interactive .card-bevel:hover { filter: brightness(1.08); }
.card-frame.interactive .card-bevel:active { transform: scale(1.06) rotate(0deg); }

.card-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 5px 7px;
  background: var(--gold);
  border-bottom: 3px solid var(--chrome);
}
.card-name {
  font-family: var(--font-display);
  font-size: 13px;
  color: var(--felt-deep);
  letter-spacing: 0.5px;
  line-height: 1;
  text-shadow: none;
}
.card-md .card-name { font-size: 15px; }
.card-lg .card-name { font-size: 20px; }

.card-cost {
  display: inline-flex; align-items: center; gap: 3px;
  flex: 0 0 auto;
}
.cost-pip {
  border-radius: 50%;
  border: 2px solid var(--chrome);
  display: flex; align-items: center; justify-content: center;
  line-height: 1; flex: 0 0 auto;
}
.cost-generic {
  width: 24px; height: 24px;
  background: var(--cyan);
  font-family: var(--font-display);
  font-size: 13px;
  color: #000;
}
.cost-themed {
  width: 22px; height: 22px;
  background: #0b1220;
  font-size: 13px;
}
.cost-themed-text { font-family: var(--font-display); font-size: 10px; color: var(--cyan); }
.card-md .cost-generic { width: 26px; height: 26px; font-size: 15px; }
.card-md .cost-themed { width: 24px; height: 24px; font-size: 14px; }
.card-lg .cost-generic { width: 34px; height: 34px; font-size: 20px; }
.card-lg .cost-themed { width: 30px; height: 30px; font-size: 20px; }

.card-art {
  background: repeating-linear-gradient(45deg, #0f4a2b 0 10px, #12592f 10px 20px);
  display: flex; align-items: center; justify-content: center;
  border-bottom: 3px solid var(--chrome);
  text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.5);
  line-height: 1;
}

.card-stats {
  display: flex; justify-content: space-between;
  padding: 6px 8px;
  background: #0a0a0a;
}
.card-atk, .card-hp {
  font-family: var(--font-body);
  font-weight: bold;
  font-size: 15px;
}
.card-md .card-atk, .card-md .card-hp { font-size: 19px; }
.card-lg .card-atk, .card-lg .card-hp { font-size: 24px; }
.card-atk { color: var(--magenta); }
.card-hp { color: #00e676; }

/* Rules text — what the card DOES. Its own box above the flavor rule, in
   the body face (not the flavor italic) so function and fiction read as two
   different registers rather than one blur of prose. Background is a shade
   lighter than .card-flavor's near-black so the two boxes are visually
   distinct even before the type style is noticed. Word-wraps rather than
   truncating: at md (210px card, ~194px of that inside the bevel padding)
   ability text runs a few lines, not one, and must stay inside the frame
   rather than overflow it. */
.card-rules {
  padding: 6px 8px;
  background: #14140f;
  border-top: 2px solid var(--gold);
  font-family: var(--font-body);
  font-style: normal;
  font-size: 11px;
  line-height: 1.35;
  color: #e8e0c0;
  overflow-wrap: break-word;
}
.card-md .card-rules { font-size: 12px; }
.card-lg .card-rules { font-size: 14px; padding: 8px 10px; }
/* Symbol chips inline in a sentence need to be small enough to sit on the
   text baseline rather than the full cost-row size the ambient
   `.card-md .cost-themed` rule would otherwise apply. The text-fallback
   variant (no emoji resolved yet) keeps its natural pill width instead of a
   fixed circle — a symbol's full name doesn't fit in 16px. */
.card-rules .cost-pip { display: inline-flex; vertical-align: -3px; margin: 0 1px; }
.card-rules .cost-themed { width: 16px; height: 16px; font-size: 10px; }
.card-lg .card-rules .cost-themed { width: 18px; height: 18px; font-size: 11px; }
.card-rules .cost-themed-text { width: auto; height: auto; padding: 1px 4px; border-radius: 8px; }

.card-flavor {
  padding: 6px 8px 9px;
  background: #0a0a0a;
  border-top: 2px dotted var(--gold);
  font-family: var(--font-body);
  font-style: italic;
  font-size: 11px;
  line-height: 1.35;
  color: #c0c0c0;
}
.card-md .card-flavor { font-size: 12px; }
.card-lg .card-flavor { font-size: 14px; padding: 8px 10px 12px; }

/* ============================================================
   MODAL — palette B backdrop (paint splatter bg + clip-art stars)
   ============================================================ */
.modal-backdrop {
  position: fixed; inset: 0;
  background: radial-gradient(circle at 20% 30%, rgba(170, 255, 0, 0.18) 18px, transparent 19px),
              radial-gradient(circle at 70% 60%, rgba(255, 0, 200, 0.18) 26px, transparent 27px),
              radial-gradient(circle at 45% 85%, rgba(255, 106, 0, 0.18) 14px, transparent 15px),
              rgba(0, 0, 0, 0.85);
  display: flex; align-items: center; justify-content: center;
  z-index: 100;
}
.modal-card {
  background: var(--boss-bg);
  border: 6px ridge var(--gold);
  padding: 20px;
  max-width: 90vw;
  text-align: center;
  color: var(--boss-text);
}

/* ============================================================
   TARGETING OVERLAY — pick a target for an on_play trigger.
   Mirrors .modal-backdrop (fixed, full-screen, z-index 100) so the
   prompt sits above the play grid. Without this, the overlay is an
   unstyled block in #modal-root that renders behind the board and is
   invisible — so a card that needs a target (effect.target "any" etc.)
   appears to do nothing when cast: beginCast sets targeting mode and
   renders the picker, but the player never sees it to choose, and
   play_card is never sent.
   ============================================================ */
.target-overlay {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.85);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 14px;
  z-index: 100;
  padding: 24px;
}
.target-prompt {
  font-family: var(--font-display);
  font-size: 20px;
  color: var(--gold);
  text-shadow: 2px 2px 0 var(--chrome);
  text-align: center;
  max-width: 80vw;
  line-height: 1.2;
}
.target-list {
  display: flex; flex-direction: column;
  gap: 10px;
  width: min(320px, 90vw);
}
.target-option {
  background: var(--gold);
  color: var(--felt-deep);
  min-height: 44px;
  border: 3px solid var(--chrome);
  padding: 8px 16px;
  font-family: var(--font-display);
  font-size: 16px;
  letter-spacing: 0.3px;
  box-shadow: 4px 4px 0 var(--chrome);
  cursor: pointer;
}
.target-option:active {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 var(--chrome);
}
.target-cancel {
  background: var(--magenta);
  color: #fff;
  min-height: 44px;
  border: 3px solid var(--chrome);
  padding: 8px 16px;
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.3px;
  box-shadow: 4px 4px 0 var(--chrome);
  cursor: pointer;
  width: min(320px, 90vw);
}
.target-cancel:active {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 var(--chrome);
}

/* ============================================================
   BUTTONS — lime primary CTA (palette B), magenta secondary
   ============================================================ */
/* Gold, not lime: lime is palette B, which the style board reserves for the
   boss layer. Table-side actions are palette A, whose primary CTA is gold.
   START outranks JOIN by size, not by hue. */
.ugly-btn, .btn-play, .btn-close {
  background: var(--gold);
  color: var(--felt-deep);
  min-height: 44px;
  border: 3px solid var(--chrome);
  padding: 8px 16px;
  font-family: var(--font-display);
  font-size: 16px;
  letter-spacing: 0.3px;
  box-shadow: 4px 4px 0 var(--chrome);
  cursor: pointer;
}
.ugly-btn.primary, .btn-play { background: var(--gold); color: var(--felt-deep); }
.ugly-btn.secondary, .btn-close { background: var(--magenta); color: #fff; }
.ugly-btn:active, .btn-play:active, .btn-close:active {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 var(--chrome);
}

/* ============================================================
   TOAST — black bg, dashed gold border, VT323
   ============================================================ */
.toast {
  background: #000;
  border: 3px dashed var(--gold);
  padding: 12px 16px;
  margin: 8px;
  max-width: 320px;
  font-family: var(--font-mono);
  font-size: 18px;
  color: var(--gold);
  animation: toastIn 0.4s ease-out;
}
.toast-error { border-color: var(--magenta); color: var(--magenta); }
@keyframes toastIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

/* ============================================================
   RECONNECT
   ============================================================ */
#reconnect-strip {
  position: fixed; bottom: 0; left: 0; right: 0;
  background: var(--magenta); color: #fff;
  padding: 8px; text-align: center;
  font-family: var(--font-display);
  font-size: 16px;
  display: none;
  z-index: 200;
}
#reconnect-strip.active { display: block; }

/* ============================================================
   HAND SCREEN
   ============================================================ */
.hand-root { display: grid; grid-template-rows: auto 1fr auto auto; gap: 12px; padding: 16px; min-height: 80vh; }
.battlefield {
  background: rgba(0, 0, 0, 0.4);
  border: 3px dashed var(--gold);
  padding: 16px;
  min-height: 200px;
  display: flex; gap: 12px; flex-wrap: wrap;
  align-items: center; justify-content: center;
}
.placeholder { color: var(--gold); font-family: var(--font-mono); font-size: 20px; }

.lands-column { display: flex; gap: 4px; flex-wrap: wrap; }
.land-chip {
  font-size: 32px;
  padding: 4px;
  background: var(--felt-deep);
  border: 2px solid var(--gold);
  border-radius: 4px;
}

/* ============================================================
   LOBBY
   ============================================================ */
/* The lobby is a three-step sequence — pick a campaign, get the party in,
   start — so the style board's numbered section headers are load-bearing here
   rather than decorative. Everything was previously in raw inline flow, which
   is why the buttons sat on shared line boxes and collided with the headings. */
.lobby { display: flex; flex-direction: column; gap: 18px; }

/* ---- Marquee: the style board's own header lockup, section 0 ---- */
.marquee {
  border: 6px ridge var(--gold);
  background: linear-gradient(90deg, #ff00c8, #ff8a00, #ffe600, #00e676, #00b0ff, #b400ff, #ff00c8);
  background-size: 400px 100%;
  animation: rainbowSlide 6s linear infinite;
  padding: 6px;
  box-shadow: 0 10px 0 var(--chrome), 0 0 0 3px var(--chrome) inset;
}
.marquee-inner {
  background: #0a0a0a; padding: 14px 12px;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
  position: relative;
}
.marquee-logo {
  font-family: var(--font-display); font-size: 34px; line-height: 0.95;
  color: var(--gold); text-shadow: 4px 4px 0 var(--boss-curse);
}
.marquee-logo span { color: var(--cyan); }
.marquee-tag {
  font-family: var(--font-display); font-size: 14px; color: #00e676;
  text-shadow: 2px 2px 0 var(--chrome); transform: rotate(-3deg);
  border: 3px dashed var(--boss-curse); padding: 5px 10px; background: #1a0022;
}
.marquee-meta {
  font-family: var(--font-mono); font-size: 16px; color: #c0c0c0; line-height: 1.3;
}
.marquee-blink { animation: blinkTacky 1s steps(1) infinite; color: #ff3b3b; }
.marquee-star {
  position: absolute; right: 10px; top: 10px; font-size: 34px;
  animation: bounceStar 1.4s ease-in-out infinite;
}
@keyframes rainbowSlide { 0% { background-position: 0 0; } 100% { background-position: 400px 0; } }

/* ---- Steps ---- */
.step { display: flex; flex-direction: column; gap: 10px; }
.step-head {
  display: flex; align-items: center; gap: 10px; margin: 0;
  font-family: var(--font-display); font-size: 19px; color: #fff;
  text-shadow: 3px 3px 0 var(--boss-grape);
}
.step-no {
  flex: 0 0 auto; width: 28px; height: 28px;
  background: var(--gold); color: var(--felt-deep); border: 3px solid var(--chrome);
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; text-shadow: none;
}

/* ---- Campaign spec sheet, in place of a bare dropdown ---- */
.campaign-spec {
  background: #0a0a0a; border: 3px solid var(--gold); padding: 10px;
  display: flex; flex-direction: column; gap: 8px;
}
.campaign-spec.is-unplayable { border-color: #7a5a00; opacity: 0.72; }
.spec-empty {
  background: #0a0a0a; border: 3px dashed #6a8c78; padding: 14px;
  font-family: var(--font-mono); font-size: 17px; color: #8fbfa4;
}
.spec-name { font-family: var(--font-display); font-size: 17px; color: var(--gold); }
.spec-symbols { display: flex; flex-wrap: wrap; gap: 5px; }
.sym-chip {
  font-family: var(--font-mono); font-size: 14px; color: var(--boss-orange);
  border: 2px solid var(--boss-orange); padding: 0 5px; white-space: nowrap;
}
.spec-counts {
  display: flex; flex-wrap: wrap; gap: 10px;
  font-family: var(--font-mono); font-size: 16px; color: #c0c0c0;
}
.spec-blocked { font-family: var(--font-mono); font-size: 16px; color: var(--magenta); }

/* ---- Party slots: the play screen's sprites, plus the seats still open ---- */
.party-slots { display: flex; gap: 14px; padding: 4px 0 2px; }
.party-slot-empty {
  width: 50px; height: 50px; border-radius: 50%;
  border: 4px dashed #4d7a60; color: #4d7a60;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-size: 22px;
}

/* ---- Deck identity picker: one card per themed symbol. Bounded height with
   its own scrollbar (the log-strip idiom) so a five-symbol campaign never
   pushes BEGIN far down a 390×844 screen — the list scrolls, so its height
   stays flat no matter how many symbols a campaign has. ---- */
.identity-hint { font-family: var(--font-mono); font-size: 14px; color: #8fbfa4; }
.identity-list {
  display: flex; flex-direction: column; gap: 6px;
  max-height: 168px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding-right: 2px; /* clear the scrollbar so it doesn't sit on the border */
}
.identity-card {
  display: flex; flex-direction: column; gap: 2px;
  /* flex-shrink: 0 is load-bearing, not decoration — a flex column with a
     bounded cross size otherwise compresses its items to fit rather than
     letting the container scroll, and min-height alone does not stop that.
     Without it every card was squashed to 44px and its evidence line (the
     whole point of this picker) was clipped off silently. */
  flex-shrink: 0;
  min-height: 44px; width: 100%;
  background: #0a0a0a; color: var(--boss-text);
  border: 3px solid var(--gold); padding: 6px 10px;
  font-family: var(--font-body); text-align: left; cursor: pointer;
}
.identity-card:active { transform: translate(1px, 1px); }
.identity-card.is-pending { border-color: var(--cyan); box-shadow: 0 0 0 2px var(--cyan) inset; }
.identity-card.is-chosen { border-color: #00e676; box-shadow: 0 0 0 2px #00e676 inset; }
.identity-symbol {
  font-family: var(--font-display); font-size: 15px; color: var(--gold);
  text-transform: uppercase;
}
.identity-archetype { font-family: var(--font-mono); font-size: 15px; color: var(--cyan); }
.identity-evidence { font-family: var(--font-mono); font-size: 13px; color: #c0c0c0; }
.identity-seats { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 2px; }
.identity-seat {
  font-size: 16px; line-height: 1; padding: 2px 4px;
  background: var(--felt-deep); border: 2px solid var(--gold);
}
.identity-seat.is-me { border-color: #00e676; }

/* ---- Inputs ---- */
.join-row { display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap; }
.join-row .ugly-input { flex: 1 1 120px; min-width: 0; }
.join-row .emoji-input { flex: 0 0 64px; text-align: center; }
.join-row .ugly-btn { flex: 0 0 auto; }
.ugly-input {
  background: #000;
  color: var(--gold);
  border: 3px solid var(--gold);
  padding: 8px;
  font-family: var(--font-mono);
  font-size: 18px;
  min-height: 44px;
}

/* GENERATE is slow and system-flavoured, so it wears palette C's Win98 chrome
   — which also demotes it below the two ways to actually start playing. */
.sys-btn {
  background: #c0c0c0; color: #000; border: 3px outset #fff;
  font-family: Tahoma, Verdana, sans-serif; font-size: 14px;
  padding: 9px 12px; min-height: 44px; cursor: pointer; text-align: left;
}
.sys-btn:active { border-style: inset; }
.generate-progress { font-family: var(--font-mono); font-size: 16px; color: var(--cyan); }

/* ---- The two ways to start. Gold is palette A's primary CTA. ---- */
.cta-row { display: flex; gap: 10px; }
.cta {
  flex: 1; min-height: 56px; cursor: pointer;
  font-family: var(--font-display); font-size: 20px;
  border: 4px outset #fff; box-shadow: 5px 5px 0 var(--chrome);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.cta small { font-family: var(--font-mono); font-size: 14px; font-weight: normal; }
.cta-primary { background: var(--gold); color: var(--felt-deep); }
.cta-secondary { background: var(--felt-deep); color: var(--gold); border-color: var(--gold); }
.cta:active:not([disabled]) { transform: translate(2px, 2px); box-shadow: 3px 3px 0 var(--chrome); }
.cta[disabled] {
  background: #222; color: #7d7d7d; border: 3px inset #555;
  box-shadow: none; cursor: not-allowed;
}
.cta-hint { font-family: var(--font-mono); font-size: 16px; color: #8fbfa4; }
.error { color: var(--magenta); font-weight: bold; margin: 4px 0; font-family: var(--font-body); }

@media (prefers-reduced-motion: reduce) {
  .marquee { animation: none; }
  .marquee-blink, .marquee-star { animation: none; }
}

/* ============================================================
   BOSS VIEW — palette B
   ============================================================ */
.boss-view { padding: 16px; background: transparent; }
.boss-view h3 {
  font-family: var(--font-display);
  color: var(--boss-lime);
  font-size: 18px;
  letter-spacing: 0.3px;
  margin: 14px 0 6px;
  border-bottom: 2px dotted var(--boss-curse);
  padding-bottom: 3px;
}
.boss-portrait-big { font-size: 96px; text-align: center; margin: 16px 0; }
.boss-name-big {
  font-family: var(--font-display);
  color: var(--boss-lime);
  text-shadow: 3px 3px 0 var(--boss-curse);
  text-align: center;
  font-size: 28px;
  letter-spacing: 0.3px;
  margin: 8px 0;
}
.boss-abilities { display: flex; flex-direction: column; gap: 6px; }
.boss-ability {
  background: rgba(0, 0, 0, 0.5);
  padding: 6px 12px;
  margin: 4px 0;
  border-left: 4px solid var(--boss-curse);
  font-family: var(--font-body);
  color: var(--boss-text);
  font-size: 14px;
  line-height: 1.4;
}
.boss-pending {
  font-family: var(--font-mono);
  font-size: 24px;
  color: var(--gold);
}

/* ============================================================
   LOG STRIP — VT323 mono, dotted gold rules
   ============================================================ */
.log-strip {
  background: #000;
  padding: 8px;
  max-height: 280px;
  overflow-y: auto;
  border: 2px solid var(--gold);
  font-family: var(--font-mono);
  font-size: 14px;
}
.log-row { padding: 2px 0; border-bottom: 1px dotted var(--gold); }
.log-damage { color: var(--magenta); }
.log-heal { color: #00e676; }
.log-narration { color: var(--gold); }
.log-system { color: #888; font-style: italic; }

/* ============================================================
   TABLE OVERVIEW
   ============================================================ */
.table-view { padding: 16px; }
.table-row {
  display: grid; grid-template-columns: 100px 1fr; gap: 12px;
  margin: 12px 0; padding: 8px;
  background: rgba(0, 0, 0, 0.4);
  border: 2px solid var(--gold);
}
.table-label {
  font-family: var(--font-display);
  color: var(--gold);
  letter-spacing: 0.3px;
}
.table-cards { display: flex; gap: 6px; flex-wrap: wrap; }

/* ============================================================
   RARITY TINTS — gold for rare, magenta for curse (blinks)
   ============================================================ */
.rarity-curse { animation: blinkTacky 1s steps(1) infinite; }
.rarity-curse .card-bevel { border-color: var(--boss-curse); }
.rarity-rare .card-bevel { border-color: var(--gold); }
.rarity-uncommon .card-bevel { border-color: var(--cyan); }

/* ============================================================
   ANIMATIONS
   ============================================================ */
@keyframes pulseTurn {
  0%, 100% { box-shadow: 0 0 0 0 var(--gold); }
  70% { box-shadow: 0 0 0 12px rgba(255, 210, 30, 0); }
}
@keyframes blinkTacky { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.15; } }
@keyframes bounceStar { 0%, 100% { transform: translateY(0) rotate(-8deg); } 50% { transform: translateY(-7px) rotate(8deg); } }
@keyframes toastIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 600px) {
  .boss-name-big { font-size: 22px; }
}

/* ============================================================
   RESPONSIVE — auto-scale for Android (390×844) and PC.
   Uses clamp() / min() / vw so the same CSS works at any width.
   Mobile is the design target (template mockup is 390px wide).
   ============================================================ */

/* The #app container scales itself via container queries + clamp on padding.
   On wide screens we cap at ~720px so the layout still feels phone-like
   (the template is a phone, not a desktop dashboard). */
#app {
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  min-height: 100vh;
  position: relative;
}

/* Boss strip + cards scale their font sizes with viewport width */
.boss-strip { padding: clamp(6px, 1.5vw, 10px) clamp(8px, 2vw, 16px); }
.boss-name { font-size: clamp(14px, 2vw, 17px); }
.boss-portrait { font-size: clamp(24px, 4vw, 32px); }
.boss-hp, .boss-hp-bar { height: clamp(18px, 3vw, 22px); }
.boss-hp-bar { height: clamp(28px, 4.5vw, 36px); }
.boss-hp span, .boss-hp-bar span { font-size: clamp(11px, 1.5vw, 14px); }
.boss-hp-bar span { font-size: clamp(16px, 2.5vw, 22px); }

/* HP badge, mana orb scale with vw */
.hp-panel { padding: clamp(3px, 0.8vw, 5px) clamp(6px, 1.5vw, 9px); gap: clamp(3px, 0.8vw, 5px); }
.hp-heart { font-size: clamp(15px, 2.5vw, 20px); }
.hp-big { font-size: clamp(14px, 2.5vw, 19px); }
.hp-label { font-size: clamp(11px, 2vw, 15px); }
.mana-orb { width: clamp(28px, 6vw, 38px); height: clamp(28px, 6vw, 38px); font-size: clamp(13px, 2.5vw, 18px); }
.mana-pip { width: clamp(10px, 2vw, 14px); height: clamp(16px, 3.5vw, 22px); }

/* Hand view: rails shrink + tighten on narrow screens */
@media (max-width: 720px) {
  .hand-body {
    padding: 10px 60px 320px 56px;  /* extra bottom for lifted deck (156px) + slider (50px) + margins */
  }
  .party-rail { right: 4px; top: 6px; gap: 6px; }
  .party-avatar { width: 42px; height: 42px; font-size: 14px; }
  .party-hp { font-size: 12px; }
  .own-hp-block { left: 4px; top: 6px; }
  .lands-rail { left: 4px; top: 100px; }
  .land-chip { width: 36px; height: 28px; font-size: 16px; }
  .hand-battlefield { min-height: 140px; padding: 8px; }
  .turn-label { font-size: 12px; padding: 4px 8px; }
  .end-turn-btn { font-size: 11px; padding: 6px 10px; min-width: 84px; }
  /* Narrow screens: tighter stride so 6–7 cards still fit horizontally
     without bleeding past the rails. Visual scale stays at 0.63× from
     the desktop default. */
  .hand-fan-deck { --fan-stride: 55px; height: 150px; }
  .inline-log { font-size: 12px; max-height: 90px; }
  .tab-bar { height: 36px; }
  .tab { font-size: 12px; }
  .tab-icon { flex-basis: 44px; font-size: 16px; }
}

@media (max-width: 420px) {
  /* Smallest phones — tighter still */
  .hand-body { padding: 8px 52px 300px 48px; }
  .party-avatar { width: 38px; height: 38px; font-size: 13px; }
  .hp-big { font-size: 13px; }
  .hand-fan-deck { --fan-stride: 42px; height: 140px; }
  .hand-fan { bottom: 140px; }
  .turn-banner { flex-wrap: wrap; gap: 4px; }
  .end-turn-btn { min-height: 40px; padding: 4px 8px; }
}

/* Wide screens (PC / tablet): rails move slightly inward; layout uses the
   full 720px container with comfortable spacing. */
@media (min-width: 900px) {
  #app {
    border-left: 1px solid rgba(0,0,0,0.3);
    border-right: 1px solid rgba(0,0,0,0.3);
  }
}

/* ============================================================
   HAND VIEW (template section 8)
   Tab bar + party rail + inline GM log + turn banner + END TURN.
   See Ugly Deckbuilder Style Board.dc.html lines 327-462.
   ============================================================ */

/* TAB BAR — 38px sticky, chrome bevel gray, gold-highlighted active tab */
.tab-bar {
  display: flex;
  background: #c0c0c0;
  border-bottom: 3px solid var(--chrome);
  height: 38px;
  position: sticky;
  top: 0;
  z-index: 10;
  margin: 0;
}
.tab {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 13px;
  color: #0c3a22;
  background: transparent;
  border: none;
  border-right: 3px solid var(--chrome);
  cursor: pointer;
  padding: 0;
  letter-spacing: 0.3px;
}
.tab:hover:not(.tab-active) { background: #a8a8a8; color: #000; }
.tab-active {
  background: var(--gold) !important;
  color: #0c3a22 !important;
}
.tab-icon {
  flex: 0 0 52px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  background: transparent;
  border: none;
  cursor: pointer;
}
.tab-icon.is-on { background: var(--gold); }

/* HAND BODY — padded container for absolutely-positioned rails */
.hand-body {
  position: relative;
  padding: 10px 70px 80px 60px;
  min-height: 60vh;
}

/* HAND PANELS — pre-rendered, swap visibility by class */
.hand-panel { display: none; }
.hand-panel.is-active { display: block; }

/* OWN HP BLOCK (top-left rail) */
.own-hp-block {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 4;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.mana-pips {
  display: inline-flex;
  gap: 3px;
  align-items: center;
  background: #0a0a0a;
  border: 3px outset var(--cyan);
  padding: 3px 6px;
}
.mana-count {
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--cyan);
  margin-left: 4px;
}
.status-chips-row {
  display: flex;
  gap: 3px;
  flex-wrap: wrap;
}

/* LANDS RAIL (left, below own HP) */
.lands-rail {
  position: absolute;
  top: 112px;
  left: 8px;
  z-index: 4;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
}
.lands-label {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--gold);
  letter-spacing: 0.5px;
}
.land-chip {
  width: 44px;
  height: 34px;
  background: var(--felt-deep);
  border: 3px outset var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 19px;
}
.lands-empty {
  font-family: var(--font-mono);
  color: #555;
  font-size: 18px;
}

/* BATTLEFIELD (center) */
.hand-battlefield {
  background: rgba(0, 0, 0, 0.4);
  border: 3px dashed var(--gold);
  padding: 12px;
  min-height: 140px;
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}
/* Cards in play on the battlefield are scaled down so multiple creatures
   fit comfortably on screen. transform-origin center keeps them shrunk
   in place; aspect ratio is preserved by the scale. */
.hand-battlefield .card-frame {
  transform: scale(0.45);
  transform-origin: 50% 50%;
  margin: 0 -55px;       /* compensate for the scaled visual width so adjacent cards overlap nicely */
}
/* In the battlefield, the inner .card-bevel shadow is overkill at 0.45×
   (it becomes a faint smudge). Drop it; the card border is enough. */
.hand-battlefield .card-bevel {
  box-shadow: none;
}

/* PARTY RAIL (right) */
.party-rail {
  position: absolute;
  top: 10px;
  right: 8px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.party-avatar {
  position: relative;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: 4px outset #555;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 17px;
  color: #fff;
  text-shadow: 2px 2px 0 var(--chrome);
}
.party-avatar.is-active {
  border-color: var(--gold);
  border-style: outset;
  animation: pulseTurn 1.8s ease-out infinite;
}
.party-avatar.is-dying { filter: saturate(0.4); }
.party-avatar.is-npc { filter: grayscale(0.5); }
.party-initials {
  font-family: var(--font-display);
  font-size: 17px;
  line-height: 1;
}
.party-hp {
  position: absolute;
  left: -6px;
  top: -8px;
  font-family: var(--font-mono);
  font-size: 14px;
  background: var(--chrome);
  border: 2px solid var(--magenta);
  color: #fff;
  padding: 0 3px;
  line-height: 1.1;
  text-shadow: none;
}
.party-hp.low { color: #ff5a5a; border-color: #cc0000; }

/* INLINE GM LOG — toggled by 📜 */
.inline-log {
  position: relative;
  margin-top: 12px;
  background: var(--chrome);
  border: 3px inset var(--gold);
  padding: 6px 8px;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.45;
  color: #e8e8e8;
  max-height: 120px;
  overflow-y: auto;
}
.inline-log.is-hidden { display: none; }
.gm-prefix {
  font-family: var(--font-mono);
  font-size: 16px;
  color: var(--gold);
  margin-right: 6px;
}
.gm-log-row { padding: 1px 0; border-bottom: 1px dotted #333; }

/* TURN BANNER + END TURN BUTTON (template lines 412-415) */
.turn-banner {
  position: relative;
  margin-top: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  z-index: 6;
}
.turn-label {
  font-family: var(--font-display);
  font-size: 15px;
  color: #0c3a22;
  background: var(--gold);
  border: 4px outset #fff;
  padding: 6px 12px;
  box-shadow: 4px 4px 0 var(--chrome);
  transform: rotate(-2deg);
  letter-spacing: 0.3px;
}
.turn-banner.is-my-turn .turn-label {
  animation: pulseTurn 1.6s ease-out infinite;
}
.end-turn-btn {
  font-family: var(--font-display);
  font-size: 13px;
  color: #fff;
  background: #cc0000;
  border: 4px outset #ff8a8a;
  padding: 8px 14px;
  min-height: 44px;
  min-width: 100px;
  cursor: pointer;
  box-shadow: 4px 4px 0 var(--chrome);
  letter-spacing: 0.3px;
}
.end-turn-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.end-turn-btn:active:not(:disabled) {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 var(--chrome);
}

/* HAND STACK — fixed bottom (template section 8 lines 418-419, restyled
   for narrow screens: horizontal poker-hand fan with overlap). Each
   .hand-fan-card is absolutely positioned at left: 50% (centered
   horizontally on the focused card), then translated horizontally by
   --fan-stride × offset. Back cards drop a bit so they peek behind the
   focused one without fully hiding it. Slider + counter let the player
   pick which card is on top. */
.hand-fan {
  /* Deck only — floats higher on screen so cards sit above the
     slider rather than fighting it for vertical space. */
  position: fixed;
  bottom: 150px;     /* above where the slider sits */
  left: 0;
  right: 0;
  padding: 6px 12px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  /* No background/border — the deck sits in space, the slider is the
     fixed "control bar" at the very bottom. */
  z-index: 8;
}
.hand-fan-deck {
  /* Cards are 210px wide in the card layout (preserves template
     proportions), but rendered at 0.63× via .card-frame scale so the
     visual width is ~132px. Stride of 75px gives ~57px visible overlap
     per card. */
  position: relative;
  --fan-stride: 75px;
  width: 100%;
  height: 156px;     /* visual card height (~132) + 8px shadow + breathing room */
  overflow: visible;
}
.hand-fan-card {
  position: absolute;
  left: 50%;
  top: 4px;          /* tight to top of deck */
  margin-left: -105px;   /* card layout width (210) / 2 — keeps layout centered
                            even though the visual is only ~132px wide */
  transition: transform 220ms cubic-bezier(.2,.9,.3,1.2),
              z-index 0s linear 220ms;
  touch-action: none;
  will-change: transform;
}
/* Render the card at 0.63× of its layout size. transform-origin is the
   card's center, so it shrinks in place. This preserves the template's
   210×~190 aspect ratio while making the card visually smaller. */
.hand-fan-card .card-frame {
  transform: scale(0.63);
  transform-origin: 50% 50%;
}
/* Restore the template's chunky card-bevel shadow inside the hand-fan. */
.hand-fan-card .card-bevel {
  box-shadow: 8px 8px 0 var(--chrome);
}
.hand-fan-card.is-focused { z-index: 100; }

/* Slider — pinned to bottom: 0 as its own fixed element, ALWAYS
   accessible at the very bottom of the screen. The deck (above) can't
   move it. */
.hand-fan-slider {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%;
  max-width: 100%;
  margin: 0;
  padding: 14px 12px 18px;
  background: linear-gradient(#0a0a0a00, #0a0a0acc 40%, #0a0a0a);
  border-top: 3px solid var(--gold);
  accent-color: var(--gold);
  cursor: pointer;
  z-index: 9;
  /* Force a touch-friendly hit area on mobile native slider thumbs. */
  touch-action: manipulation;
}
.hand-fan-slider::-webkit-slider-thumb {
  background: var(--gold);
  border: 2px solid var(--chrome);
  width: 24px;
  height: 24px;
}
.hand-fan-slider::-moz-range-thumb {
  background: var(--gold);
  border: 2px solid var(--chrome);
  width: 24px;
  height: 24px;
}

/* "3 / 7" counter — lives inside the deck (top-right corner) so it never
   overlaps the slider. pointer-events: none makes it purely decorative;
   any touch that lands on its pixels falls through to whatever is behind
   it (the deck or, if the deck is short, the slider). */
.hand-fan-counter {
  position: absolute;
  top: -4px;
  right: 4px;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--gold);
  text-shadow: 1px 1px 0 var(--chrome);
  z-index: 110;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}
.hand-fan .placeholder {
  margin: 60px 0 0;
  color: var(--magenta);
  font-family: var(--font-mono);
  font-size: 14px;
}

/* ============================================================
   DRAG-AND-DROP visual states
   ============================================================ */

/* Source card while dragging — fades out so the ghost clone stands out. */
.card-frame.is-dragging {
  opacity: 0.35;
  cursor: grabbing !important;
}
.card-frame { cursor: grab; }

/* Floating ghost that follows the pointer on touch devices. Desktop HTML5
   DnD shows its own native ghost; this clone is for pointer-event fallback. */
.drag-ghost {
  position: fixed;
  pointer-events: none;
  z-index: 1000;
  transform: translate(-50%, -50%) scale(1.05) rotate(-3deg);
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.55));
  transition: transform 60ms ease-out;
  will-change: transform, left, top;
}

/* Battlefield zone glows when a card is dragged over it. */
.hand-battlefield.drag-over {
  border-color: var(--boss-lime);
  background: rgba(170, 255, 0, 0.12);
  animation: pulseDrop 0.6s ease-in-out infinite alternate;
}
@keyframes pulseDrop {
  from { box-shadow: inset 0 0 0 0 var(--boss-lime); }
  to   { box-shadow: inset 0 0 24px 6px rgba(170, 255, 0, 0.5); }
}

/* ============================================================
   PLAY SCREEN — style board section 8, "YOUR TURN" (390×844)
   The template stacks: boss strip / HUD rails / creatures /
   GM log / turn banner / hand fan. Its body uses absolute
   positioning; these are grid rows instead, because absolute
   children are what put the HP panel on top of the battlefield.
   ============================================================ */
.play-grid {
  display: grid;
  grid-template-rows: auto 1fr auto auto auto;
  gap: 8px;
  min-height: calc(100vh - 140px);
}
/* Grid and flex children default to min-width:auto, so one long unbreakable
   token (a card id in the GM log, say) can push a row wider than the phone and
   drag the whole layout with it. Every row is pinned to the track instead. */
.play-grid > * { min-width: 0; max-width: 100%; }

/* ---- HUD: own vitals left, party sprites right (template rails) ---- */
/* Vitals sit on one line rather than stacked — the second row cost the board
   ~40px of height on a 844px screen, which is a creature row. */
.play-hud { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.hud-self { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.hud-party { margin-left: auto; display: flex; gap: 12px; padding: 4px 8px 0 0; }

.hp-badge {
  display: inline-flex; align-items: center; gap: 5px;
  background: #0a0a0a; border: 3px outset var(--magenta); padding: 3px 8px;
  font-family: var(--font-body); font-size: 17px; color: #fff;
}
.hp-badge b { font-weight: bold; }
.hp-badge i { font-style: normal; color: #b98; font-size: 13px; }

.mana-badge {
  display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap;
  background: #0a0a0a; border: 3px outset var(--cyan); padding: 3px 8px;
}
.mana-badge .mana-pip {
  width: 10px; height: 16px; background: var(--cyan);
  border: 2px solid var(--chrome); display: inline-block;
}
.mana-badge .mana-pip.is-spent { background: #123; }
.mana-count { font-family: var(--font-mono); font-size: 16px; color: var(--cyan); }
.mana-sym {
  font-family: var(--font-mono); font-size: 13px; color: var(--boss-orange);
  border: 1px solid var(--boss-orange); padding: 0 3px;
}

.status-chips { display: flex; gap: 3px; }
.status-chips .status-chip {
  font-size: 16px; background: #0a0a0a;
  border: 2px solid var(--boss-lime); padding: 1px 3px;
}

/* Party sprites: 50px, initials + emoji overlay + HP tag, gold bevel active. */
.party-sprite {
  position: relative; width: 50px; height: 50px; border-radius: 50%;
  border: 4px outset #555; display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-size: 17px; color: #fff;
  flex: 0 0 auto; filter: saturate(0.7);
}
.party-sprite.is-active {
  border-color: var(--gold); filter: none;
  animation: pulseTurn 1.8s ease-out infinite;
}
.party-sprite.is-dying { filter: grayscale(0.5); }
.party-sprite.is-dead { filter: grayscale(1) brightness(0.6); }
.sprite-emoji { position: absolute; right: -5px; bottom: -5px; font-size: 17px; }
.sprite-hp {
  position: absolute; left: -6px; top: -8px;
  font-family: var(--font-mono); font-size: 14px;
  background: #000; border: 2px solid var(--slot, var(--magenta));
  color: #fff; padding: 0 3px; line-height: 1.2;
}

/* ---- BOARD: a camera viewport. touch-action:none so the browser
        does not steal pan/pinch before boardView.js sees them. ---- */
.play-board {
  position: relative; overflow: hidden; min-height: 210px;
  border: 3px inset rgba(0, 0, 0, 0.5);
  touch-action: none; cursor: grab;
}
.play-board:active { cursor: grabbing; }
/* Two territories, boss above and yours below, divided by a hard rule. The
   boss side carries palette B so whose creatures are whose is a colour
   decision, not a position-memory one. */
.board-canvas {
  display: flex; flex-direction: column; gap: 8px; padding: 8px;
  width: 100%;   /* fits the viewport at scale 1; the camera moves it from there */
  transform-origin: 0 0; will-change: transform;
}
/* ---- SEATS: one builder for boss, allies, you. Phone layout is a column;
        the square table is the >=1000px block further down. ---- */
.table-felt { display: flex; flex-direction: column; gap: 8px; }

.seat-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
  font-family: var(--font-mono); font-size: 14px; color: var(--boss-lime);
  white-space: nowrap; overflow: hidden;
}
.seat-name { overflow: hidden; text-overflow: ellipsis; }
.seat-creatures {
  display: flex; flex-wrap: wrap; gap: 6px; min-height: 30px;
  align-items: flex-start; align-content: flex-start; justify-content: center;
}
.seat-lands { display: flex; flex-wrap: wrap; gap: 6px; }

/* The boss keeps palette B so whose creatures are whose stays a colour
   decision, not a position-memory one. */
.seat.is-boss {
  background: rgba(32, 0, 46, 0.55);
  border: 2px solid var(--boss-grape);
  padding: 5px 6px 7px;
}
.seat.is-boss .seat-lands { margin-top: 6px; }

/* PHONE PARITY (the acceptance bar for the whole desktop-table branch: the
   390x844 render must match the pre-branch one pixel for pixel, because
   .play-grid's board row is 1fr inside an overflow:hidden .play-board — every
   pixel of new chrome is a pixel of your own board clipped away).
   The old .field-label showed the boss's NAME and nothing else, so all three
   chips go. HP is not new *information* (bossStrip.js has it above every
   screen) but it is new *in this row*, and the row is nowrap/overflow:hidden,
   so at 390px it eats the boss's name. At desktop widths seat symmetry is the
   point and all three come back. */
@media (max-width: 999px) {
  .seat.is-boss .seat-hp,
  .seat.is-boss .seat-mana,
  .seat.is-boss .seat-hand { display: none; }
  /* The old .field-row had no justify-content; only your own .creature-field
     centred. Centring the boss too shifted its creatures ~95px right. */
  .seat.is-boss .seat-creatures { justify-content: flex-start; }
}

/* Allies are a desktop-only affordance: a phone has no room for them. */
.seat.is-ally { display: none; }

/* Your own side: lands rail left, creatures right, under the hard rule that
   used to be its own .field-rule element.
   No header row here — .my-side never had one, and every value it would carry
   (HP, mana, hand count) is already in .play-hud two grid rows above. The
   header is a desktop-only affordance, restored in the >=1000px block where
   four seats have to identify themselves. */
.seat.is-me {
  display: grid; gap: 10px;
  grid-template-columns: auto 1fr;
  grid-template-areas: "lands creatures";
  border-top: 3px dashed rgba(255, 210, 30, 0.5);
  padding-top: 8px;
}
.seat.is-me > .seat-head { display: none; color: var(--gold); }
.seat.is-me .seat-lands {
  grid-area: lands; flex-direction: column; flex-wrap: nowrap; flex: 0 0 auto;
}
.seat.is-me .seat-creatures { grid-area: creatures; flex: 1; min-width: 0; }
/* The old lands rail was labelled, and the label is not decoration: the rail
   is a column of 44x34 emoji tiles with no other cue that they are lands
   rather than very small creatures. It comes back as generated content so the
   shared seat markup stays identical for all four seats, and only below the
   breakpoint, where the rail is a labelled column rather than a row under a
   seat header that already names its owner. */
@media (max-width: 999px) {
  .seat.is-me .seat-lands::before {
    content: 'LANDS';
    font-family: var(--font-mono); font-size: 14px; color: var(--gold);
  }
}

/* ---- The table centre: stack + GM line ---- */
/* Two placements, exactly one visible. The felt copy is a grid cell of the
   square table; the row copy sits where the GM line has always sat. */
.table-centre.is-felt { display: none; }
.stack-strip {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-bottom: 4px;
}
.stack-label {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em;
  color: var(--gold);
}
.stack-tile {
  display: flex; align-items: center; gap: 4px;
  padding: 3px 6px; background: var(--felt-deep);
  border: 2px outset var(--gold);
}
.stack-art { font-size: 16px; }
.stack-name {
  font-family: var(--font-display); font-size: 9px; color: var(--gold);
  max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.rail-empty { font-family: var(--font-mono); font-size: 16px; color: #6a8c78; }
.field-empty { font-family: var(--font-mono); font-size: 17px; color: #6a8c78; padding: 20px 0; }

/* ============================================================
   CARD TILE: one frame for a card's face everywhere it appears
   outside the zoom modal — in hand, a creature on the battlefield,
   a land in play. Playing a card used to visually transmute it into
   a different object: a dark charcoal hand-card became a gold/green
   mini-card became a 34x44 emoji blob with no name at all, from
   three separate rulesets. Border, background, art well, name
   placement, cost placement and stat placement are ONE ruleset here;
   only `.is-hand` / `.is-board` / `.is-land` below change dimensions
   and which optional part (HP, a land's produced symbol) shows.
   ============================================================ */
.card-tile {
  position: relative; flex: 0 0 auto; overflow: hidden;
  background: linear-gradient(var(--felt-mid), var(--felt-deep));
  border: 4px outset var(--gold); box-shadow: 4px 4px 0 var(--chrome);
}
.tile-art {
  font-size: 28px;
  background: repeating-linear-gradient(45deg, #0f4a2b 0 8px, #12592f 8px 16px);
  border-bottom: 2px solid var(--chrome);
  display: flex; align-items: center; justify-content: center;
  /* Neither the old .hand-art nor .perm-art had this — it is a deliberate
     addition, not an oversight, made so the small tiles' art well finally
     matches renderCard's own .card-art in the zoom modal (card.js), which
     has carried this exact drop shadow (only at 3px, for its larger art)
     since before this task. */
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
}
.tile-name {
  font-family: var(--font-display); font-size: 8.5px; letter-spacing: -0.2px;
  color: var(--gold); text-align: center; padding: 3px 2px; min-height: 15px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile-stats {
  display: flex; justify-content: space-between; align-items: center;
  padding: 2px 4px 3px;
  font-family: var(--font-body); font-weight: bold; font-size: 11px;
}
.tile-atk { color: var(--magenta); }
.tile-hp { color: #00e676; }
.tile-cost { display: inline-flex; align-items: center; gap: 2px; color: var(--cyan); }
.tile-cost .cost-pip { width: 13px; height: 13px; border-width: 1.5px; }
.tile-cost .cost-generic { font-size: 9px; }
.tile-cost .cost-themed { font-size: 10px; }
.tile-cost .cost-themed-text { font-size: 7px; }
/* A land's produced mana symbol, filling the slot a creature's ATK would —
   same row, same position, so a land keeps the identical frame rather than
   getting a bespoke corner badge the way the old landTile drew it. */
.tile-produces { font-size: 13px; line-height: 1; }

/* Boss creatures wear the boss palette, matching the template's
   deteriorating INTERN. */
.card-tile.is-opposing {
  background: linear-gradient(var(--boss-grape), #2a0140);
  border-color: var(--boss-lime);
}
.card-tile.is-opposing .tile-art { background: #1a0022; }
.card-tile.is-opposing .tile-name { color: var(--boss-lime); }

/* Curses arrive from the boss's palette — "pasted in from a worse website". */
.card-tile.is-cursed { background: linear-gradient(var(--boss-grape), #2a0140); border-color: var(--boss-curse); }
.card-tile.is-cursed .tile-art { background: #1a0022; }
.card-tile.is-cursed .tile-name { color: var(--boss-curse); text-decoration: line-through; }

.card-tile.is-sick { opacity: 0.7; }
.card-tile.is-selectable { cursor: pointer; outline: 3px dashed #00e676; outline-offset: 2px; }
.card-tile.is-selected { outline: 4px solid var(--gold); outline-offset: 3px; transform: translateY(-6px) rotate(-3deg); }
.tile-badge { position: absolute; top: -8px; right: -8px; font-size: 16px; }
.tile-damage {
  position: absolute; bottom: -8px; right: -8px; background: #cc0000; color: #fff;
  font-family: var(--font-mono); font-size: 14px; padding: 0 4px; border: 2px solid var(--chrome);
}

/* A rotation does not change the layout box, so a tapped tile lays across its
   own height and shoves its neighbour — and the row below it — out of the
   way. The margin (set per size below) reserves the footprint the rotation
   actually occupies. The margin reservation applies to both creatures and
   lands now — the old landTile rotated on tap with no margin reserved at
   all. Dimming does NOT: landTile.is-tapped also had `filter:
   brightness(0.6)` (a spent-resource cue — a land taps for mana and goes
   dark to say "used"), but permanentTile.is-tapped never dimmed a tapped
   creature (tapped-from-attacking is an active combat state, not a spent
   one). That distinction is real, not incidental, so it is kept: only
   `.is-land.is-tapped` dims below. The old 0.6 figure is kept too — the
   land's own box grew (44x34 → 48x66), which already makes the rotation
   read more clearly than before, so there is no case for darkening it
   further on top of that. */
.card-tile.is-tapped { transform: rotate(90deg); }
.card-tile.is-selected.is-tapped { transform: rotate(90deg) translateY(-6px); }

/* ---- size: board — a creature on the battlefield, 78x106, the template's
   board treatment. ---- */
.card-tile.is-board { width: 78px; height: 106px; }
.card-tile.is-board .tile-art { height: 48px; }
.card-tile.is-board.is-tapped,
.card-tile.is-board.is-selected.is-tapped { margin: 0 14px; }

/* ---- size: land — a land in play. Used to be a 44x34 emoji tile with no
   name at all ("The Air Dryer That Doesn't Work" read as a grey blob once it
   hit the board); it is now the same frame as a creature, just smaller, with
   its produced symbol filling the stats row a creature's ATK/HP would. ---- */
.card-tile.is-land { width: 48px; height: 66px; border-width: 3px; box-shadow: 3px 3px 0 var(--chrome); }
.card-tile.is-land .tile-art { height: 28px; font-size: 18px; }
.card-tile.is-land .tile-name { font-size: 7px; min-height: 11px; padding: 2px 2px; }
.card-tile.is-land .tile-stats { font-size: 9px; padding: 1px 3px 2px; }
.card-tile.is-land .tile-cost .cost-pip { width: 10px; height: 10px; }
.card-tile.is-land.is-tapped { margin: 0 9px; filter: brightness(0.6); }

/* Camera chrome — bevel tiles, same idiom as the lands rail. */
.board-zoom { position: absolute; right: 6px; bottom: 6px; display: flex; gap: 4px; }
.zoom-btn {
  min-width: 34px; height: 30px; background: var(--felt-deep); color: var(--gold);
  border: 3px outset var(--gold); font-family: var(--font-mono); font-size: 17px;
  line-height: 1; cursor: pointer; padding: 0 5px;
}
.zoom-btn:active { border-style: inset; }
.zones {
  position: absolute; left: 6px; bottom: 6px;
  font-family: var(--font-mono); font-size: 15px; color: #8fbfa4;
  background: rgba(0, 0, 0, 0.45); padding: 1px 5px; pointer-events: none;
}

/* ---- GM log: black box, gold inset, VT323 prefix ---- */
.gm-log {
  background: #000; border: 3px inset var(--gold); padding: 6px 8px;
  font-family: var(--font-body); font-size: 12.5px; line-height: 1.45; color: #e8e8e8;
  max-height: 58px; overflow-y: auto;
  overflow-wrap: anywhere;
}
.gm-prefix { font-family: var(--font-mono); font-size: 16px; color: var(--gold); margin-right: 5px; }

/* ---- Turn banner: gold bevel + red END TURN, per the template ---- */
.phase-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.phase-name {
  font-family: var(--font-display); font-size: 15px; color: var(--felt-deep);
  background: var(--gold); border: 4px outset #fff; padding: 6px 12px;
  box-shadow: 4px 4px 0 var(--chrome); transform: rotate(-2deg);
}
.phase-primary {
  font-family: var(--font-display); font-size: 13px; color: #fff;
  background: #cc0000; border: 4px outset #ff8a8a; padding: 8px 14px;
  min-height: 44px; box-shadow: 4px 4px 0 var(--chrome); cursor: pointer;
}
.phase-primary[disabled] {
  background: #222; color: #c0c0c0; border: 3px inset #555;
  box-shadow: none; cursor: default;
}

/* ---- Hand: 92px tilted cards over the template's fade + gold rule ---- */
.play-hand {
  background: linear-gradient(#0a0a0a00, #0a0a0acc 40%, #0a0a0a);
  border-top: 3px solid var(--gold); padding: 10px 0 4px;
}
.hand-strip {
  display: flex; gap: 8px; padding: 6px 10px 10px;
  overflow-x: auto; overflow-y: hidden; align-items: flex-end;
}
.hand-strip::-webkit-scrollbar { height: 6px; }
/* ---- size: hand — a card held in hand, 92px, tilted. Same .card-tile frame
   as a battlefield creature or a land; only the size and the fan tilt are
   specific to being in a hand. ---- */
.card-tile.is-hand {
  width: 92px;
  box-shadow: 5px 5px 0 var(--chrome);
  transform: rotate(var(--tilt, 0deg)); transform-origin: 50% 120%;
  transition: transform 120ms cubic-bezier(.34, 1.56, .64, 1);
  cursor: pointer;
  /* Horizontal pans scroll the hand; vertical ones are ours. Without this the
     browser claims the gesture and fires pointercancel on the first move, so
     "drag up to play" died on touch while still working with a mouse. */
  touch-action: pan-x;
}
.card-tile.is-hand:hover { transform: rotate(0deg) scale(1.06); }
/* The ghost is a clone, so it carries .card-tile.is-hand — whose transform
   would otherwise win on source order and leave the ghost offset from the
   finger. */
.card-tile.is-hand.drag-ghost {
  transform: translate(-50%, -50%) rotate(-4deg) scale(1.05);
  transition: none;
}
.card-tile.is-hand .tile-art { height: 54px; font-size: 30px; }
.card-tile.is-hand .tile-name { font-size: 10px; }
.card-tile.is-hand .tile-cost .cost-pip { width: 15px; height: 15px; }
.card-tile.is-hand.is-unaffordable { filter: grayscale(0.8) brightness(0.55); }
.card-tile.is-hand.is-dragging { opacity: 0.35; }
.hand-hint {
  text-align: center; font-family: var(--font-mono); font-size: 15px; color: #8fbfa4;
}
.play-board.drag-over { outline: 4px dashed #00e676; outline-offset: -4px; }

@media (prefers-reduced-motion: reduce) {
  .party-sprite.is-active { animation: none; }
  .card-tile.is-hand { transition: none; }
}

/* ---- Full game log, opened by tapping the GM line ---------------------- */
/* The GM line only shows the latest beat; the boss acts between your turns,
   so the log is where you see what it actually did. */
.gm-log { cursor: pointer; display: flex; align-items: baseline; gap: 6px; }
.gm-more { margin-left: auto; color: #ffd21e; font-family: VT323, monospace; flex: 0 0 auto; }
.gm-log:active { filter: brightness(1.2); }

.log-panel {
  width: min(94vw, 460px); max-height: 78vh; display: flex; flex-direction: column;
  background: #0a0a0a; border: 5px outset #ffd21e; box-shadow: 8px 8px 0 #000;
}
.log-panel-head {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  background: #ffd21e; border-bottom: 3px solid #000; flex: 0 0 auto;
}
.log-panel-title { font-family: 'Luckiest Guy', cursive; font-size: 16px; color: #0c3a22; }
.log-panel-head .btn-close { margin-left: auto; min-width: 44px; min-height: 36px; }
.log-panel-body { overflow-y: auto; padding: 6px 8px; -webkit-overflow-scrolling: touch; }

.log-row {
  display: grid; grid-template-columns: auto auto 1fr; gap: 6px; align-items: baseline;
  padding: 5px 2px; border-bottom: 1px dotted #333;
  font-family: Verdana, Geneva, sans-serif; font-size: 12.5px; line-height: 1.4; color: #e8e8e8;
}
.log-turn { font-family: VT323, monospace; font-size: 14px; color: #8fbfa4; }
.log-who { font-weight: bold; color: #00e5ff; }
/* The boss gets the loud palette so its turns are scannable at a glance —
   spotting what it did is the whole reason this panel exists. */
.log-boss .log-who { color: #ff2e88; }
.log-narration .log-gm { font-family: VT323, monospace; font-size: 14px; color: #ffd21e; }
.log-narration .log-text { font-style: italic; color: #c9a8de; }
.log-empty { padding: 14px; text-align: center; font-family: VT323, monospace; color: #8fbfa4; }

/* ============================================================
   DESKTOP TABLE (>=1000px): four seats around a square.
   Boss north, allies west and east, you across the whole south
   edge — your board is the only one you act on, so it is largest.
   ============================================================ */
@media (min-width: 1000px) {
  /* Single source of truth for "how wide is the table". It used to be a flat
     fraction of the viewport (min(74vh, 100vw-40px, 860px)) — that left
     ~560px of dead felt on each side at 1920x1080 and still let the page
     scroll, because nothing in it accounted for what actually surrounds the
     felt. Size it by what is left over instead: subtract the real vertical
     chrome from the screen height, and the real side rails from the screen
     width, and take whichever is smaller.
     --chrome-v is everything vertical that is not the felt: boss-strip (94px
     — its clamp()s bottom out at this width) + .view's own top/bottom
     padding (32px) + the board's own canvas padding and inset border
     (2*12 + 6 = 30px). 94+32+30 = 156px, measured live at 1920x1080 and
     cross-checked at 1366x768 — both widths land on the same figure because
     the boss-strip's vw-clamps already saturate at their px ceiling by
     ~667px wide. .play-hand used to be in this sum (a separate row cost it
     150px), but it is now a floating overlay anchored to the felt's own
     near edge instead of a row of its own — a hand of cards sits in front
     of the table, not on a shelf below it — so it no longer taxes the
     vertical budget at all. A first pass at chrome-v (190px) guessed too low
     and silently clipped .play-hand against #app's new overflow:hidden
     instead of pushing it into a scrollbar — worse than the scroll this
     branch removes, because nothing on screen said it happened. Rounded up
     4px for rendering slop this time since there is one fewer term to absorb
     rounding error.
     Both --chrome-v and --hand-h below are numbers CSS cannot derive or
     verify on its own: if #boss-strip's rendered height changes for an
     unrelated reason (a longer boss name wraps, a status chip gets added,
     a font-size bump), nothing here would notice — --table-w would keep
     using the stale figure, overshoot the felt, and #app's overflow:hidden
     would clip it exactly as it did on the first-pass guess of 190px, except
     silently, with no failing test to catch it. `npm run check:desktop-layout`
     (scripts/check-desktop-layout.ts) exists specifically to catch that: it
     drives the real running app with Playwright at three sizes and asserts
     no scroll, no clipping, and a square felt within an expected size band.
     It is not wired into `npm test` (like scripts/smoke-frontend.ts, it needs
     a live server + a real browser, which the rest of the suite deliberately
     doesn't require) — run it by hand after touching anything in this block,
     boss-strip.js, or .play-hand's own markup/CSS. */
  body.view-hand {
    --canvas-pad: 12px;
    --rail-w: 240px;
    --hand-h: 150px;   /* .play-hand's real rendered height, measured live —
                           shared with .board-zoom/.zones below so they clear
                           the floating hand instead of sitting under it. */
    --chrome-v: 160px;
    /* The width term's own overhead: .view's left/right padding (16+16=32px)
       plus the two 8px column-gaps .play-grid puts between the rail/board/
       rail tracks (2*8=16px) = 48px. This was 40px through the first two
       passes at this branch — dormant at both tested sizes because the
       height term binds there, but it would have quietly cropped 8px off
       the felt's width in a width-constrained window nearer the 1000px
       floor, e.g. a tall-narrow one where the width term binds instead. */
    --table-w: min(calc(100dvh - var(--chrome-v)), calc(100vw - 2 * var(--rail-w) - 48px));
  }
  /* Pin the view to one screen — the whole point of this branch. #app used to
     be a plain block column that grew to fit its content and let the
     document scroll past 100vh; --table-w above is now computed so the
     content fits inside 100dvh exactly, so nothing here needs to scroll.
     dvh over vh: desktop treats them the same, and dvh is the unit that
     stays correct if this ever renders in a mobile browser at >=1000px. */
  body.view-hand #app {
    max-width: none;
    height: 100dvh;
    overflow: hidden;
  }

  /* The table is deliberately smaller than the window, but the boss strip and
     the hand have no reason to share its width — a wider hand shows more
     cards. Dropping their old max-width: var(--table-w) clamp lets them run
     the full window; nothing else was constraining them. */

  /* .play-board is the camera's viewport, so its box has to be the felt plus
     the canvas padding plus its own 3px inset border — otherwise that border
     frames the whole window and its absolutely-positioned .zones (left:6px)
     and .board-zoom (right:6px) sit at the screen corners, hundreds of pixels
     from the table they belong to. Height mirrors width for the same reason:
     the board is exactly the felt's box, no more. */
  .play-board {
    max-width: calc(var(--table-w) + 2 * var(--canvas-pad) + 6px);
    height: calc(var(--table-w) + 2 * var(--canvas-pad) + 6px);
    min-height: 0;
    margin-inline: auto;
    cursor: default;
  }
  .board-canvas { padding: var(--canvas-pad); }

  /* The vitals (play-hud) and the phase bar used to be full-width rows
     stacked above and below the table — dead weight on a 16:9 monitor, and
     every one of those rows was also height the square was not allowed to
     have. They move into the side margins instead, one rail each, flanking
     the square. .play-hand is no longer a grid row at all — see below. */
  .play-grid {
    position: relative;   /* containing block for .play-hand's overlay */
    grid-template-columns: var(--rail-w) 1fr var(--rail-w);
    grid-template-rows: auto;
    grid-template-areas: "hud board phase";
    align-items: center;
    min-height: 0;
  }
  .play-hud   { grid-area: hud; }
  .play-board { grid-area: board; }
  .phase-bar  { grid-area: phase; }

  /* Rail content stacks instead of running in a row — there is no spare
     width at --rail-w to lay HP/mana/party out side by side. */
  .play-hud {
    flex-direction: column; align-items: stretch; justify-content: center;
    gap: 12px;
  }
  .hud-self  { flex-direction: column; align-items: flex-start; }
  .hud-party { margin-left: 0; flex-wrap: wrap; padding: 0; }
  .phase-bar {
    flex-direction: column; align-items: stretch; justify-content: center;
    gap: 10px;
  }
  .phase-name { align-self: center; }
  .phase-primary { width: 100%; }

  /* The hand floats over the felt's near (south) edge instead of sitting in
     its own row below it — a hand of cards overlaps the table it belongs to,
     it does not sit on a shelf underneath. Deliberately NOT a grid item
     (.play-grid has no "hand" area any more): an absolutely positioned child
     with no assigned grid-area takes the grid CONTAINER's own padding box as
     its containing block, which is exactly what lets it be centered under
     the felt independent of the rail/board/rail column tracks. It is a
     sibling of .play-board, not a descendant, specifically so .play-board's
     own overflow:hidden (the camera viewport clip) never has a chance to
     clip it the way #app's overflow:hidden clipped it on the first pass at
     this branch. z-index keeps it painting over the felt's south seat rather
     than under it. */
  .play-hand {
    position: absolute;
    left: 50%; transform: translateX(-50%);
    bottom: 0;
    width: calc(var(--table-w) + 2 * var(--canvas-pad) + 6px);
    z-index: 2;
  }

  /* The floating hand covers the board viewport's bottom corners, where the
     zoom controls and the library/graveyard/exile counts live. Lifting them
     above --hand-h keeps both reachable instead of buried under the hand's
     backing. */
  .board-zoom, .zones { bottom: calc(var(--hand-h) + 10px); }

  .table-felt {
    display: grid;
    width: var(--table-w);
    height: var(--table-w);   /* square by construction, not by aspect-ratio
                                 losing an argument with the column minimums */
    margin: 0 auto;
    gap: 10px;
    /* minmax(0, …) everywhere: a floor in px is a floor the square cannot go
       below, which is exactly how the table stopped being square. The centre
       is deliberately the narrowest track — it holds the GM line, which is one
       clamped line of text, and a stack strip that is unreachable in live play
       (see tableCentre in tiles.js). The width it used to hoard belongs to the
       ally seats, which are the ones actually running out of room. */
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.8fr) minmax(0, 1.1fr);
    /* The middle band carries two ally boards side by side in a third of the
       width each; north carries one board across the whole table and south the
       same, so they need proportionally less height. South still gets far more
       area than any other seat — it is full-width. */
    grid-template-rows: 0.85fr 1.25fr 1.1fr;
    grid-template-areas:
      "north  north   north"
      "west   centre  east"
      "south  south   south";
    background: radial-gradient(ellipse at 50% 45%, rgba(29, 90, 68, 0.55) 0%, transparent 70%);
    border: 3px solid var(--chrome);
  }

  .seat-north { grid-area: north; }
  .seat-west  { grid-area: west; }
  .seat-east  { grid-area: east; }
  .seat-south { grid-area: south; }
  /* No grid-area, so an over-cap seat (see allySides in screens/play.js) is
     auto-placed into an implicit fourth row: visibly wrong, rather than
     silently stacked on top of another player's board. */
  .seat-extra { border: 3px dashed var(--magenta); }

  /* The floating hand always covers the felt's bottom --hand-h pixels — south
     is the seat that sits there, so it is the one whose content can end up
     rendered behind the hand's backing, not merely crowded but genuinely
     unreachable (hold-to-read routes through the same [data-permanent-id]
     as every other tile, per play.js, so a covered tile is dead to it too).
     Reserving --hand-h as padding-bottom does NOT touch --table-w or the
     felt — it only shrinks the south seat's own usable content height
     (already the tallest row at 1.1fr), so content that would have rendered
     into the hand's footprint instead pushes .seat's existing overflow-y:
     auto into scrollable range sooner. That trades directly-visible-without-
     scrolling south-seat height for guaranteed-reachable-by-scroll — the
     same tradeoff already accepted for ally seats (is-ally's own comment
     block above), not a new one. It is still an imperfect safety net (that
     comment's caveat about the board's wheel handler intercepting wheel
     scroll applies here too — the scrollbar thumb, not the wheel, is the
     reliable way to reach it), but reachable-via-scroll beats invisible-and-
     unreachable-via-anything, which is what a played land got without this
     at 1366x768 during this branch's own testing. */
  .seat-south { padding-bottom: var(--hand-h); }

  /* A seat is a fixed grid cell, so on a short window its board can outgrow
     it. Clipping it is the failure this branch addresses. overflow-y: auto is
     set but unreachable: the board's wheel handler preventDefaults and zooms
     instead, and no scrollbar paints. Below 1366x768 this matters; at and above,
     seats fit. Making overflow reachable requires a follow-up: exempt seats
     from the wheel handler or size so they always fit. */
  .seat {
    min-width: 0; overflow-y: auto; overflow-x: hidden;
    overscroll-behavior: contain;
    scrollbar-width: thin; scrollbar-color: var(--gold) rgba(0, 0, 0, 0.35);
  }
  .seat::-webkit-scrollbar { width: 8px; }
  .seat::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.35); }
  .seat::-webkit-scrollbar-thumb { background: var(--gold); }
  .seat.is-ally { display: block; }

  /* Scrolling is the safety net; fitting is the goal. Ally tiles are read, not
     acted on — the only gesture they answer is hold-to-read, which opens the
     full card frame anyway — so they run at roughly three quarters size, which
     is what turns a typical four-creature ally board from two scrolled rows
     into two visible ones. Your own tiles and the boss's keep full size. */
  .seat.is-ally .card-tile.is-board { width: 58px; height: 80px; border-width: 3px; }
  .seat.is-ally .card-tile.is-board .tile-art { height: 34px; font-size: 21px; }
  .seat.is-ally .card-tile.is-board .tile-name { font-size: 7px; min-height: 12px; padding: 2px 2px; }
  .seat.is-ally .card-tile.is-board .tile-stats { font-size: 9px; padding: 1px 3px 2px; }
  .seat.is-ally .tile-badge { font-size: 12px; top: -6px; right: -6px; }
  .seat.is-ally .card-tile.is-board.is-tapped { margin: 0 11px; }
  .seat.is-ally .card-tile.is-land { width: 32px; height: 44px; border-width: 2px; }
  .seat.is-ally .card-tile.is-land .tile-art { height: 18px; font-size: 13px; }
  .seat.is-ally .card-tile.is-land .tile-name { font-size: 6px; min-height: 9px; }
  .seat.is-ally .card-tile.is-land .tile-produces { font-size: 9px; }
  .seat.is-ally .field-empty { font-size: 14px; padding: 10px 0; }
  /* Tightened to match: at 1366x768 — the stock laptop this branch is aimed
     at — these few pixels are the difference between a full ally board and a
     scrollbar for the last 6px of it. */
  .seat.is-ally .seat-head { margin-bottom: 2px; }
  .seat.is-ally .seat-creatures { gap: 4px; min-height: 0; }
  .seat.is-ally .seat-lands { gap: 4px; margin-top: 4px; }

  /* The name is the seat's only identifier, and a 200px ally seat cannot fit
     it alongside three chips on one line — it ellipsised to "A…". Let the
     chips wrap to a second line instead; the name keeps the first. */
  .seat-head { flex-wrap: wrap; }
  .seat-name { flex: 1 1 auto; }
  .seat-hp, .seat-mana, .seat-hand { flex: 0 0 auto; }

  /* Every seat is a plain stack here; the phone's grid shape for your own
     side does not survive into the square. Your header comes back: with four
     seats on the felt, each one has to say whose it is. */
  .seat.is-me {
    display: block; border-top: none; padding-top: 0;
  }
  .seat.is-me > .seat-head { display: flex; }
  .seat.is-me .seat-lands { flex-direction: row; flex-wrap: wrap; margin-top: 6px; }

  .table-centre.is-row  { display: none; }
  .table-centre.is-felt {
    display: flex; flex-direction: column; justify-content: center; gap: 6px;
    grid-area: centre; min-width: 0; padding: 8px;
    background: rgba(0, 0, 0, 0.28);
    border: 1px solid rgba(255, 255, 255, 0.14);
  }
}
