:root {
  --bg: #0b0d12;
  --bg2: #12151d;
  --panel: #161a24;
  --panel2: #1c2230;
  --line: #2a3245;
  --fg: #e8ecf4;
  --dim: #8b95ab;
  --neon: #fcee0a;      /* Cyberpunk yellow */
  --cyan: #00f0ff;
  --pink: #ff2a6d;
  --green: #48ff9b;
  --red: #ff4d5e;
  --blue: #4d9fff;
  --yellow: #ffd21e;
  --purple: #b465ff;
  --shadow: 0 6px 24px rgba(0, 0, 0, .5);
}
* { box-sizing: border-box; }
/* Fullscreen-app shell (2026-09-01): the PAGE never scrolls. body is a
   viewport-sized flex column — the top bar is a fixed-height row, `main`
   fills the rest, and each view owns its own height (scale-to-fit for the
   playmats, internal scroll for the list views). */
html, body { margin: 0; height: 100%; overflow: hidden; }
body {
  background: radial-gradient(1200px 600px at 70% -10%, #1a2036 0%, var(--bg) 55%) fixed, var(--bg);
  color: var(--fg);
  font: 14px/1.45 'Segoe UI', system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
  display: flex; flex-direction: column;
}
/* Atmosphere (2026-09-15 polish): a faint cyberpunk grid + corner glows sit
   UNDER everything (fixed background layers — zero layout contribution). The
   pure-black voids that made panels look like they "failed to load" now read
   as a Night-City surface. Kept VERY dim so it never competes with the cards. */
body::before {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(900px 500px at 12% 108%, rgba(0, 240, 255, .05) 0%, transparent 60%),
    radial-gradient(900px 500px at 88% -8%, rgba(252, 238, 10, .045) 0%, transparent 60%),
    linear-gradient(rgba(42, 50, 69, .16) 1px, transparent 1px),
    linear-gradient(90deg, rgba(42, 50, 69, .16) 1px, transparent 1px);
  background-size: auto, auto, 44px 44px, 44px 44px;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 6px; }
::-webkit-scrollbar-track { background: transparent; }

/* ---------- top bar ---------- */
#topbar {
  display: flex; align-items: center; gap: 16px; flex: none;
  padding: 10px 18px; border-bottom: 1px solid var(--line);
  background: rgba(10, 12, 18, .8); backdrop-filter: blur(8px);
  position: relative; z-index: 40;
}
.brand {
  font-weight: 800; letter-spacing: 2px; font-size: 18px; cursor: pointer;
  color: var(--neon); text-shadow: 0 0 12px rgba(252, 238, 10, .5);
}
.brand span { color: var(--cyan); text-shadow: 0 0 12px rgba(0, 240, 255, .5); }
#nav { display: flex; gap: 6px; flex: 1; }
#nav button {
  background: transparent; color: var(--dim); border: 1px solid transparent;
  padding: 7px 14px; border-radius: 8px; cursor: pointer; font-size: 14px;
}
#nav button:hover { color: var(--fg); background: var(--panel); }
#nav button.active {
  color: #0b0d12; background: var(--neon); font-weight: 700;
  box-shadow: 0 0 16px rgba(252, 238, 10, .35);
}
/* Connection status (2026-09-15): a live dot + label instead of a bare dim
   word — green pulsing when connected, red when dropped. */
#whoami { font-size: 12px; color: var(--dim); display: inline-flex; align-items: center; gap: 6px; }
#whoami::before { content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--red); box-shadow: 0 0 6px rgba(255, 77, 94, .6); }
#whoami.connected { color: var(--fg); }
#whoami.connected::before { background: var(--green); box-shadow: 0 0 7px rgba(72, 255, 155, .8);
  animation: connpulse 2.4s ease-in-out infinite; }
@keyframes connpulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
#authbox { display: flex; align-items: center; gap: 8px; }
#authbox .avatar { width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--line); vertical-align: middle; }
#authbox .auth-name { color: var(--cyan); font-weight: 700; }
#authbox button { font-size: 12px; padding: 4px 10px; background: var(--bg2); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px; cursor: pointer; }
#authbox button:hover { border-color: var(--cyan); }
/* matchmaking (2026-09-13): full-width third panel + wait spinner */
#mm-box { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
#mm-box h2 { width: 100%; margin: 0; }
#mm-box label { display: flex; align-items: center; gap: 8px; }
#mm-hint { width: 100%; margin: 0; }
#mm-wait { display: inline-flex; align-items: center; gap: 8px; color: var(--neon); font-weight: 700; }
#mm-wait.hidden { display: none; }
.mm-spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--line);
  border-top-color: var(--neon); animation: mm-spin 0.9s linear infinite; }
@keyframes mm-spin { to { transform: rotate(360deg); } }
.deck-del { float: right; margin-left: 10px; color: var(--dim); font-size: 13px; padding: 0 4px; }
.deck-del:hover { color: var(--red); }

/* Full-width: the playmat region uses the whole window below the top bar
   (no 1400px cap, no centering gutter). main fills the remaining viewport
   height; each view fills main exactly, so the PAGE never scrolls. */
main { flex: 1 1 auto; min-height: 0; width: 100%; overflow: hidden; padding: 12px 16px; }
.view { height: 100%; overflow: hidden; }
.view.hidden { display: none; }
.hidden { display: none !important; }

h1 { font-size: 26px; margin: 4px 0 14px; letter-spacing: 1px; color: var(--neon);
  text-shadow: 0 0 18px rgba(252, 238, 10, .3); }
h2 { font-size: 17px; margin: 0 0 10px; }
h3 { font-size: 14px; margin: 0 0 8px; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; }
.hint { color: var(--dim); font-size: 12px; }
.small { color: var(--dim); font-size: 11px; }

/* ---------- generic controls ---------- */
button {
  background: var(--panel2); color: var(--fg); border: 1px solid var(--line);
  padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
  transition: border-color .15s, box-shadow .15s, background .15s, transform .08s;
}
button:hover { border-color: var(--cyan); box-shadow: 0 0 10px rgba(0, 240, 255, .2); }
/* Polish pass (2026-09-15): a pressed state — the button dips 1px and the
   glow tightens. transform only → no layout shift under any button. */
button:active:not(:disabled) { transform: translateY(1px); box-shadow: 0 0 6px rgba(0, 240, 255, .25); }
button.primary {
  background: linear-gradient(180deg, #2a2408, #1c1805); border-color: var(--neon); color: var(--neon);
  font-weight: 700;
}
button.primary:hover { box-shadow: 0 0 14px rgba(252, 238, 10, .4), inset 0 0 12px rgba(252, 238, 10, .12); }
button.primary:active:not(:disabled) { box-shadow: 0 0 8px rgba(252, 238, 10, .3), inset 0 0 8px rgba(252, 238, 10, .1); }
button.danger { border-color: var(--pink); color: var(--pink); }
button.danger:hover { box-shadow: 0 0 10px rgba(255, 42, 109, .3); }
/* Disabled: keep the border legible (was a near-invisible dim that read as
   "broken"), just desaturate + drop the glow. */
button:disabled { opacity: .55; cursor: not-allowed; box-shadow: none; filter: saturate(.4); }
input, select {
  background: var(--bg2); color: var(--fg); border: 1px solid var(--line);
  border-radius: 8px; padding: 8px 10px; font-size: 13px; outline: none;
  transition: border-color .15s, box-shadow .15s;
}
input:focus, select:focus { border-color: var(--cyan); box-shadow: 0 0 0 2px rgba(0, 240, 255, .18), 0 0 12px rgba(0, 240, 255, .15); }
/* Custom select chevron (2026-09-15): the stock OS dropdown arrow broke the
   theme — a thin neon triangle instead. appearance:none + a background SVG,
   so no extra DOM and no layout change (padding-right reserves the room). */
select {
  appearance: none; -webkit-appearance: none; padding-right: 30px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2300f0ff' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
}
/* Custom checkbox (2026-09-15): the OS square → a neon-accented toggle box. */
input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; width: 17px; height: 17px; flex: none;
  background: var(--bg2); border: 1px solid var(--line); border-radius: 5px; cursor: pointer;
  display: grid; place-content: center; transition: border-color .15s, box-shadow .15s, background .15s;
}
input[type="checkbox"]::before {
  content: ''; width: 9px; height: 9px; transform: scale(0); transition: transform .12s ease-out;
  background: var(--neon); clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
input[type="checkbox"]:checked { border-color: var(--neon); box-shadow: 0 0 8px rgba(252, 238, 10, .35); }
input[type="checkbox"]:checked::before { transform: scale(1); }
input[type="checkbox"]:disabled { opacity: .4; cursor: not-allowed; }
label { display: block; margin: 8px 0; color: var(--dim); font-size: 12px; }

/* ---------- panels / home ---------- */
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  padding: 20px; box-shadow: var(--shadow); }
/* Polish (2026-09-15): a hairline top highlight + deeper drop shadow give the
   panels real depth (they were flat 1px-border boxes). Background/shadow only
   — no size change. */
.panel { background: linear-gradient(180deg, rgba(255,255,255,.025) 0%, transparent 90px), var(--panel);
  box-shadow: 0 6px 24px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.05); }
/* Home + Play: the panel fills the view and scrolls internally if its
   content is tall; the page itself never scrolls. */
#view-home, #view-play { display: flex; flex-direction: column; }
#view-home > .panel, #view-play > .panel { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* Play: the panel shrinks to its content (no dead space under the cards) and
   centers in the view; it still scrolls if the lobby outgrows the viewport. */
#view-play > .panel { flex: 0 1 auto; margin: auto; }

/* Home — about text + disclaimer (2026-09-14) */
.home-about { max-width: 1080px; width: 100%; margin: 0 auto; }
.disclaimer {
  display: flex; gap: 10px; align-items: baseline;
  background: rgba(255, 77, 94, .06); border: 1px solid rgba(255, 77, 94, .35);
  border-left: 3px solid var(--red); border-radius: 10px;
  padding: 12px 14px; margin-bottom: 22px; font-size: 12.5px; color: var(--dim);
}
.disclaimer-tag {
  flex: none; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase;
  font-size: 11px; color: #0b0d12; background: var(--red);
  padding: 2px 8px; border-radius: 6px;
}
.about-lead { max-width: 860px; color: var(--fg); font-size: 14.5px; line-height: 1.6; margin: 0 0 24px; }
.home-features h3 { margin-bottom: 12px; }
.home-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
/* Polish (2026-09-15): feature cards get a hover lift + neon top hairline so
   they read as designed surfaces, not bare text boxes. transform/box-shadow
   only — the grid cells never resize. */
.home-grid .cardbox { position: relative; overflow: hidden; transition: transform .16s ease, box-shadow .16s ease, border-color .16s; }
.home-grid .cardbox::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, var(--cyan), transparent 70%); opacity: 0; transition: opacity .16s; }
.home-grid .cardbox:hover { transform: translateY(-2px); border-color: rgba(0, 240, 255, .45);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5), 0 0 14px rgba(0, 240, 255, .1); }
.home-grid .cardbox:hover::before { opacity: 1; }
/* Feature cards navigate (2026-09-19): pointer affordance + pressed state
   consistent with the neon hover lift; keyboard focus ring for tabindex=0. */
.home-grid .cardbox { cursor: pointer; }
.home-grid .cardbox:active { transform: translateY(1px); border-color: rgba(0, 240, 255, .6);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .5), 0 0 10px rgba(0, 240, 255, .18); }
.home-grid .cardbox:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.home-grid .cardbox p { margin: 6px 0 0; color: var(--dim); font-size: 13px; line-height: 1.55; }
/* Play view — unified setup (2026-09-14): one Format/Mode pair drives both
   "Create lobby" (unranked only) and "Matchmake"; Join sits opposite. The
   lobby replaces the grid in place once you're seated. */
.play-panel { max-width: 980px; width: 100%; margin: 0 auto; }
.play-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 18px; }   /* stretch = equal heights */
.cardbox { background: linear-gradient(180deg, var(--bg2) 0%, rgba(18, 21, 29, .6) 100%);
  border: 1px solid var(--line); border-radius: 14px; padding: 18px; }
.cardbox h2 { font-size: 15px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--dim); margin: 0 0 14px; }
.setup-box, .join-box { position: relative; overflow: hidden; }
.setup-box::before, .join-box::before {   /* neon hairline accent on top */
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, var(--neon), transparent 65%);
}
.join-box::before { background: linear-gradient(90deg, var(--cyan), transparent 65%); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
.field { display: flex; flex-direction: column; gap: 5px; font-size: 11.5px;
  letter-spacing: 1px; text-transform: uppercase; color: var(--dim); }
.toggle-row { display: flex; align-items: center; gap: 8px; margin: 2px 0 10px;
  font-size: 13px; cursor: pointer; user-select: none; }
.toggle-row input { accent-color: var(--neon); width: 15px; height: 15px; cursor: pointer; }
.toggle-row:has(input:disabled) { opacity: .45; cursor: not-allowed; }
.elo-line { display: block; margin-bottom: 8px; color: var(--cyan); }
#play-hint { min-height: 18px; margin: 0 0 12px; line-height: 1.5; }
.setup-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.setup-actions button { padding: 11px 0; font-size: 14px; letter-spacing: .5px; }
#btn-matchmake-cancel { margin-top: 10px; width: 100%; }
#mm-wait { display: flex; align-items: center; gap: 8px; margin-top: 12px; color: var(--dim); font-size: 13px; }
.code-input { text-align: center; font-size: 20px; letter-spacing: 8px; font-weight: 700;
  text-transform: uppercase; font-family: inherit; }
.join-box { display: flex; flex-direction: column; gap: 12px; }
.join-box .field { flex: 1; }
.join-box button { width: 100%; padding: 11px 0; font-size: 14px; letter-spacing: .5px; }
/* Lobby (in place of the grid) */
#lobby-status { display: flex; flex-direction: column; gap: 14px; }
.lobby-head h2 { margin: 0; }
#lobby-code { vertical-align: middle; }
.seat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.seat { display: flex; align-items: center; gap: 10px; padding: 12px 14px;
  background: var(--bg2); border: 1px solid var(--line); border-radius: 12px; font-size: 13.5px; }
/* dot semantics: amber = present (choosing), green = ready, hollow = empty */
.seatdot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: transparent; border: 2px solid var(--line); }
.seat.present .seatdot { background: var(--yellow); border-color: var(--yellow); box-shadow: 0 0 7px rgba(255, 210, 30, .6); }
.seat.ready .seatdot { background: var(--green); border-color: var(--green); box-shadow: 0 0 8px var(--green); }
.lobby-deckbox h3 { font-size: 14px; letter-spacing: 1px; text-transform: uppercase; color: var(--dim); margin: 0 0 10px; }
.deck-select { width: 100%; font-size: 14px; padding: 9px 10px; margin-bottom: 8px; }
.hint.okline { color: var(--green); }
.lobby-deck-actions { display: flex; align-items: center; gap: 12px; margin-top: 6px; }
#btn-lobby-ready { min-width: 130px; padding: 10px 0; }
#btn-lobby-ready[disabled] { opacity: .45; cursor: not-allowed; box-shadow: none; }
.code { font-size: 20px; color: var(--cyan); letter-spacing: 4px; font-weight: 800;
  background: var(--bg); padding: 2px 10px; border-radius: 8px; border: 1px solid var(--line); }

/* ---------- card ---------- */
.card {
  --w: 92px;
  width: var(--w); background: linear-gradient(160deg, #20263a, #141826);
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  display: inline-flex; flex-direction: column; cursor: pointer; user-select: none;
  transition: transform .12s, box-shadow .12s, border-color .12s;
}
.card:hover { transform: translateY(-3px) scale(1.03); box-shadow: 0 8px 20px rgba(0,0,0,.6); z-index: 5; position: relative; }
.card.Red { border-color: rgba(255, 77, 94, .5); }
.card.Green { border-color: rgba(72, 255, 155, .45); }
.card.Blue { border-color: rgba(77, 159, 255, .5); }
.card.Yellow { border-color: rgba(255, 210, 30, .5); }
.card.hidden { filter: grayscale(1) brightness(.7); }
.card-img { aspect-ratio: 3/4; background: #0a0c11; }
.card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Play-feature visual QC (2026-09-21, t_a6362533) — pending-lane clip fix.
   `.card-img` is a flex item with the default `min-height: auto`, so when it
   wraps an `<img>` whose intrinsic ratio (733×1024 ≈ 1.397) is TALLER than the
   intended 3/4, the box inflates ~8px past its aspect-ratio height. A bare
   120px-wide in-play card therefore measured 167px instead of the intended
   159px (118 content × 4/3 + 2px border). Every board zone that holds bare
   cards is a fixed-height box with slack (hand 226, field 246, legend slot
   168) so it absorbed the overflow — but the pending-effect lane's card row
   (.fxl-cards) is exactly 162px with overflow:hidden, so each lane card's
   bottom strip (name/cost) was clipped ~5px. `min-height: 0` lets
   aspect-ratio govern again → cards return to a uniform 159px and the clip
   disappears. Scoped to #board-content (live play) per task scope; the SAME
   one-liner extends to #rp-content / #sc-content / #pz-content (shared
   renderTable — flagged in docs/PLAY-VISUAL-QC.md). No fixed zone box
   changes, so fitMat never re-scales (no-flicker invariant holds). */
#board-content .card-img,
/* Shared-root-cause extension (docs/PLAY-VISUAL-QC.md "flagged" section):
   renderTable is reused by the Replay viewer, Scenario Builder and Puzzles
   runner — same 5px pending-lane clip there; identical one-line fix. */
#rp-content .card-img,
#sc-content .card-img,
#pz-content .card-img { min-height: 0; }
/* Official Cyberpunk TCG card back — the ACTUAL printed image (packages/cards/
   art/back.png → /art/back.png), used on every face-down surface (rival hand
   backs, face-down Legends, Deck stack, Eddie backs). */
.cardback, .lgback {
  border-radius: 8px; border: 1px solid #000; overflow: hidden;
  background: url('/art/back.png') center / 100% 100% no-repeat #111;
}
.noart { width: 100%; height: 100%; display: grid; place-items: center; color: var(--dim); font-size: 11px; }
.card-name { font-size: 10px; font-weight: 700; padding: 4px 5px 2px; line-height: 1.15;
  display: flex; flex-direction: column; gap: 1px; }
.card-name small { font-weight: 400; color: var(--dim); font-size: 9px; }
/* keyword chips — SAME size/design as .tag (the "secondary" tags), but in
   the color the keyword is printed in on the card art (read off W2NC art).
   Bright fills (yellow/green/cyan) get dark text, dark fills (pink/red) get
   white; unknown future keywords fall back to a neutral outlined chip. */
.kws { display: flex; flex-wrap: wrap; gap: calc(2px * var(--cs, 1));
  padding: 0 calc(5px * var(--cs, 1)) calc(2px * var(--cs, 1)); }
.kw { font-size: calc(5.6px * var(--cs, 1)); line-height: 1.3;
  padding: calc(1px * var(--cs, 1)) calc(3px * var(--cs, 1)); border-radius: 3px;
  border: 1px solid transparent; font-weight: 800; letter-spacing: .2px; }
.kw-play, .kw-call, .kw-go-solo, .kw-adrenaline { background: var(--yellow); color: #101018; }
.kw-attack { background: var(--green); color: #0a1a12; }
.kw-spend { background: #fff; color: #0c0e14; font-weight: 700; }
.kw-blocker, .kw-quick { background: var(--pink); color: #fff; }
.kw-defeated { background: var(--red); color: #fff; }
.tags { display: flex; flex-wrap: wrap; gap: 2px; padding: 0 5px 5px; }
.tag { font-size: 5.6px; background: #0a0c11; border: 1px solid var(--line); color: var(--dim);
  padding: 1px 3px; border-radius: 3px; }
/* card footer (keywords + tags), pinned to the card's BOTTOM edge. The
   spacer (flex:1) grows to fill the space the name/stats rows don't use, so
   every card in a grid row ends its tags at the same baseline. */
.card-foot { display: flex; flex-direction: column; }
.card-foot-spacer { flex: 1 0 auto; min-height: 0; }
/* inside the footer the kws/tags padding is handled by the footer's gap;
   keep only horizontal padding + the last item's bottom padding. */
.card-foot .kws { padding-bottom: 0; margin-bottom: 2px; }
.card-foot .tags { padding-bottom: 5px; }

/* ---------- deckbuilder cards: rendered BIG (user request) ----------
    --cs scales every INTERNAL dimension of the deckbuilder card face uniformly
    (art box, name, chips, tags, and the +/n/- steppers + badges). Board /
    replay cards use the base sizes and are unaffected. 1.5 = the initial
    "50% bigger" pass; 1.65 = +10% more (user: "a little small now"). The card
    WIDTH is the grid track (set by applyDBCols, 100px * --cs per column), so
    the whole box grows with --cs. */
#db-cards { --cs: 1.65; }
/* BOTH deckbuilder panels (library `.db-card` + in-deck `.db-put`) share the
   EXACT same internal scaling — the user wants the deck card to be literally
   the same box as the library one (2026-09-11 item 5). --cs is defined on
   both #db-cards and #db-deck, so each selector resolves the same value. */
.db-card .card-name, .db-put .card-name { font-size: calc(10px * var(--cs));
  padding: calc(4px * var(--cs)) calc(5px * var(--cs)) calc(2px * var(--cs)); }
.db-card .card-name small, .db-put .card-name small { font-size: calc(9px * var(--cs)); }
.db-card .tags, .db-put .tags { gap: calc(2px * var(--cs));
  padding: 0 calc(5px * var(--cs)) calc(5px * var(--cs)); }
.db-card .tag, .db-put .tag { font-size: calc(5.6px * var(--cs));
  padding: calc(1px * var(--cs)) calc(3px * var(--cs)); }
.db-card .cardback, .db-put .cardback { font-size: calc(34px * var(--cs)); }
.db-card .noart, .db-put .noart { font-size: calc(11px * var(--cs)); }
.db-card .kws, .db-put .kws { gap: calc(2px * var(--cs));
  padding: 0 calc(5px * var(--cs)) calc(2px * var(--cs)); }
.db-card .kw, .db-put .kw { font-size: calc(5.6px * var(--cs));
  padding: calc(1px * var(--cs)) calc(3px * var(--cs)); }
.db-card .db-badges { top: calc(4px * var(--cs)); left: calc(4px * var(--cs));
  gap: calc(2px * var(--cs)); }
/* Alternate-art arrows (iconic/nova cycle): the badges move right of the
   top-left arrow so they never sit on top of it. */
.db-card:has(> .db-art-nav.prev) .db-badges { left: calc(44px * var(--cs)); }
/* The arrows: small round chips in the top corners of the art, only on cards
   that HAVE alternate printings. ‹ base/prev art · › next art. Neon border on
   hover; the count in the title (i/n) says which printing is shown. */
.db-art-nav { position: absolute; top: calc(4px * var(--cs)); z-index: 6;
  width: calc(18px * var(--cs)); height: calc(18px * var(--cs));
  border-radius: 50%; border: 1px solid var(--line); background: rgba(11,13,18,.72);
  color: var(--fg); font-size: calc(12px * var(--cs)); line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center; padding: 0 0 calc(2px * var(--cs)); }
.db-art-nav:hover { border-color: var(--cyan); color: var(--cyan); box-shadow: 0 0 calc(6px * var(--cs)) rgba(0,240,255,.5); }
.db-art-nav.prev { left: calc(4px * var(--cs)); }
.db-art-nav.next { right: calc(4px * var(--cs)); }
/* Alt-art printings get a subtle holographic frame while shown. */
.db-card.db-altart { box-shadow: 0 0 0 1px rgba(252,238,10,.35), 0 0 calc(10px * var(--cs)) rgba(252,238,10,.18); }
.badge.alt { background: linear-gradient(120deg, var(--neon), var(--pink)); }
.db-card .badge, .db-put .badge { font-size: calc(8px * var(--cs));
  padding: calc(1px * var(--cs)) calc(4px * var(--cs)); }
.db-card .db-step, .db-put .db-step { width: calc(20px * var(--cs)); height: calc(20px * var(--cs));
  font-size: calc(13px * var(--cs)); }
.db-card .db-nn, .db-put .db-nn { font-size: calc(10px * var(--cs));
  padding: 0 calc(4px * var(--cs)); }

/* ---------- board ---------- */
/* Fullscreen playmat (2026-09-02): the playmat region spans the whole window
   width minus a THIN LEFT rail (event log / card detail / chat) — the user
   wants the log on the left and the board to use the remaining full width
   (hand size). The whole table (rival mat · our mat) is ZOOM-TO-FIT into the
   stage; fitMat fills the stage edge-to-edge (no letterbox). */
#board-wrap { display: grid; grid-template-columns: 200px 1fr; grid-template-rows: 1fr;
  gap: 12px; height: 100%; min-height: 0; }
/* stage = the region the table is zoomed into (centers + clips the sizer). */
#board-stage { grid-column: 2; grid-row: 1; position: relative; min-height: 0; min-width: 0;
  overflow: hidden; display: flex; align-items: center; justify-content: center; }
#sidepanel { grid-column: 1; grid-row: 1; }
/* Shared zoom-to-fit plumbing (board #board-sizer/content + replay
   #rp-sizer/#rp-content): sizer holds the SCALED footprint; content is
   absolutely positioned at natural size and transformed (see fitMat). */
.fit-sizer { position: relative; }
.fit-content { position: absolute; top: 0; left: 0; transform-origin: top left;
  display: flex; flex-direction: column; gap: 8px; width: max-content; }
/* Playmat = ONE owner-orientation template (the 2026-09-02 picture), rendered
   for BOTH players. `.side` is a 3-col × 2-row grid:
     fixer | field | piles
     bottom bottom  bottom      (Legends | Hand | Eddies)
   The Fixer is a full-height LEFT column, Trash+Deck a full-height RIGHT
   column. The GIGS are NOT in the mats — a single SHARED bar sits in the
   center strip between the two mats (rival | friendly), flanked by the two
   per-player effect lanes (overlay layer, cards cross both mats).
   THE KEY MOVE: the rival's mat gets `transform: rotate(180deg)`. Both mats
   are built in the SAME owner orientation (each player's own layout), then
   the top mat is flipped in place — so every element mirrors across the
   center: each player reads their OWN Legends/Hand/Eddies from their own
   seat. `transform` is a pure visual flip (the grid cell is unchanged), so
   it composes cleanly with fitMat's scale on the parent content node. */
.side { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px;
  display: grid;
  grid-template-columns: 104px 1fr auto;
  /* v70 (2026-09-07): the mid row is LOCKED to the field's 246px so the whole
     row is uniform (the fixer's 6 dice no longer stretch the row taller than
     the field). The bottom row (Legends/Hand/Eddies) is 226 — the other uniform. */
  grid-template-rows: 246px auto;
  grid-template-areas:
    "fixer  field  piles"
    "bottom bottom bottom";
  gap: 10px; align-content: stretch; }
/* 2026-09-15 — CARD ORIENTATION (user request): the mat's 180° flip mirrors
   every element, which also turns the RIVAL's card FACES upside-down (you can
   see them but not read them). `--matrot` is that flip (0 on your own mat),
   and each card face below counter-rotates by it so EVERY face — units,
   legends, hand (faces AND backs) — reads UPRIGHT from the viewer's seat,
   while the mat layout itself still mirrors (zones swap sides as before).
   Pure transform on the faces → zero layout contribution → no fitMat re-scale. */
.side.mat-mine   { --matrot: 0deg; }
.side.mat-rival  { --matrot: 180deg; transform: rotate(var(--matrot)); }
/* Upright faces on the rival mat (the `rotate(var(--matrot))` is a no-op on
   your own mat). Spent pieces carry an extra 180° so their card stays seated
   in the tilted box (see the .unit.spent / .lgslot.spent rules below). */
.side.mat-rival :is(.unit-card, .lg-card, .lgback, .hand-back, .zone.hand .card) { transform: rotate(var(--matrot)); }
.side .zone.fixer { grid-area: fixer; }
.side .zone.field { grid-area: field; }
.side .mat-piles { grid-area: piles; }
.side .mat-bottom { grid-area: bottom; }
.side-label { font-size: 12px; font-weight: 700; color: var(--dim); text-transform: uppercase; letter-spacing: 1px; }
/* Two-mat TABLE: rival mat (top) · our mat (bottom). Their Gigs bars meet
   edge-to-edge at the center (no gap — the 2026-09-02 requirement). The
   table width is whatever fitMat sets on the parent content node. */
.table { display: flex; flex-direction: column; gap: 8px; width: 100%; }
/* Playmat (template 2026-09-02): each mat = mid row (Fixer|Field|Piles) +
   bottom row (Eddies · Hand · Legends). The GIGS are NOT in the mats — they
   live in the single CENTER bar between the two mats (rival | friendly), so
   every player sees their own Gigs on the RIGHT and the rival's on the LEFT.
   Hand is on the player's RIGHT (own edge), legends center, eddies left.
   Shared by the live board (#board-content) and replays (#rp-content).
   Cards are ~2x the old 84px (now 120px) — the user found them too small.
   Zones that change with card STATE (spent legends/eddies) are FIXED-SIZE
   boxes so toggling a tap never changes the mat's measured size (no
   auto-resize on tap — see fitMat). */
.mat { display: flex; flex-direction: column; gap: 10px; }
.ztag { font-size: 10px; font-weight: 700; letter-spacing: 1px; color: var(--yellow);
  text-transform: uppercase; margin: 0 0 5px 2px; }
/* Middle row (owner orientation): Fixer (LEFT, tall narrow vertical strip of
   the six dice) | Field (CENTER, flex) | Trash+Deck (RIGHT, stacked). The
   Fixer is a vertical column per the 2026-09-02 picture — NOT a top row. */
.mat-mid { display: flex; gap: 10px; align-items: stretch; }
.zone.fixer { border: 1px dashed var(--line); flex-direction: column; align-items: center;
  flex: none; width: 104px; gap: 5px;
  /* v70: locked to the field's 246px (the mid row is 246-uniform). Dice shrink
     to 26 + centered so all six fit without overflowing the row. */
  height: 246px; justify-content: center; overflow: hidden; }
.zone.fixer .ztag { margin: 0 0 2px 0; }
.zone.fixer .die { flex: none; min-width: 26px; width: 26px; padding: 3px; font-size: 8px; }
.mat-piles { display: flex; flex-direction: column; gap: 8px; flex: none; }
.pile { border: 1px dashed var(--line); border-radius: 8px; padding: 5px 7px; font-size: 11px; color: var(--dim); flex: none; }
.pile b { color: var(--fg); font-size: 12px; }
.pile .ztag { margin: 0 0 2px 0; }
.pile.clickable { cursor: pointer; }
.pile.clickable:hover { border-color: var(--yellow); }
/* Deck = a neat stack of official card backs (three offset backs + top).
   2026-09-07 v70: trimmed to 70px so the Trash+Deck column fits the field's
   246px height (mid-row uniform — the user's geometry request). */
.deckstack { position: relative; width: 74px; height: 70px; margin: 2px 0; }
.deckstack .db { position: absolute; inset: 0; border-radius: 7px; border: 1px solid #000;
  background: url('/art/back.png') center / 100% 100% no-repeat #111;
  transform: translate(calc(var(--i, 0) * 2px), calc(var(--i, 0) * -2px)); }
.deckstack .db.top { transform: none; }
/* Trash: face-up — the top card's art peeks out of the pile. */
.trashface { width: 74px; aspect-ratio: 3/4; margin: 2px 0; border-radius: 7px; overflow: hidden;
  border: 1px solid var(--line); background: #0a0c11; display: grid; place-items: center; }
.trashface img { width: 100%; height: 100%; object-fit: cover; display: block; }
.trashface span { color: var(--dim); font-size: 11px; }
/* Trash inspector modal — top of trash first, newest on top (CR 5.9). */
.trash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 12px; max-height: 65vh; overflow-y: auto; padding: 4px; }
.trash-item { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.trash-item .card { --w: 108px; }
.ti-idx { font-size: 10px; font-weight: 700; color: var(--yellow); letter-spacing: 1px; }
/* Field = middle row, CENTER column (owner orientation). FIXED height
   (246px = ztag + one 160px unit + 68px gear peek under an equipped unit —
   first gear's full text box, second gear's strip) so the mat's measured
   size is constant no matter how many units are played OR how much gear
   they carry — the tap/equip-resize class of bug is structurally dead.
   Extra units scroll inside (overflow-y) instead of growing the mat. */
.zone.field { border: 1px solid var(--line); height: 246px;
  flex-direction: row; flex-wrap: wrap; align-content: start; align-items: flex-start; overflow-y: auto; }
/* Field label = its own header row (2026-09-15 user report): the zone also
   matched the generic .field rule (flex-direction: COLUMN + wrap), so units
   laid out as one-card columns and the ztag sat at the HEAD of column 1 —
   the unit under "Field" started ~17px lower than every unit to its right.
   Explicit row direction + a full-basis label makes it a header line: every
   unit starts at one uniform distance from the top border, label stays
   visible. (246px fixed height unchanged — ztag row + 160 unit + gear peek.) */
.zone.field > .ztag { flex: 0 0 100%; margin: 0 0 2px; }
/* Polish (2026-09-15): a faint target-grid + center glow inside the Field —
   the big empty rectangle no longer reads as a blank box. Pure background
   (no size contribution → no-flicker invariant holds). */
.zone.field { background:
  radial-gradient(60% 80% at 50% 42%, rgba(0, 240, 255, .035) 0%, transparent 70%),
  linear-gradient(rgba(42, 50, 69, .12) 1px, transparent 1px),
  linear-gradient(90deg, rgba(42, 50, 69, .12) 1px, transparent 1px);
  background-size: auto, 34px 34px, 34px 34px; }
/* Bottom row (owner orientation): Legends (left) | Hand (center) | Eddies (right).
   Legends need the WIDE slot (3 × 160px slots ≈ 520px); Eddies the narrow
   one (~280px, two card-back stacks). Both are FIXED-SIZE boxes (constant
   across every spent-state) so toggling a tap never changes the mat's
   measured size — the root cause of the old "whole board resizes on tap" bug.
   Hand is the flexing middle (a single-row horizontal scroll strip). */
.mat-bottom { display: grid; grid-template-columns: max-content 1fr 280px; gap: 12px; align-items: start; }
.zone.legendzone, .zone.eddyzone { border: 1px dashed var(--line); }
/* v70 (2026-09-07): Legends / Hand / Eddies are ONE uniform height (226px).
   The old legendzone carried a 68px gear-peek padding that made it 266px
   (taller than the other two); the gear fan now extends into the mat's bottom
   margin instead (out-of-flow, so no-flicker is preserved). The 226px is the
   card height (160) + ztag + breathing room. */
.zone.legendzone, .zone.eddyzone, .zone.hand { height: 226px; }
.zone.legendzone { align-items: flex-start; }
.zone.legendzone .ztag, .zone.eddyzone .ztag { margin-bottom: 5px; }
.zone.hand { flex: none; }
/* Eddies (sold cards) — full card-size card BACKS, stacked with a small
   offset so the pile's visible slivers show how many are in it. Ready
   Eddies = upright stack (left); spent Eddies (CR 3.6/3.7, 90° tilt) = a
   second rotated stack (right). `--n` = cards in the stack. The whole area
   is a FIXED box (fills the 280px column, 160px tall) so tapping never resizes. */
.eddies { position: relative; width: 100%; height: 160px; margin-top: 24px; }
.eddstack { position: absolute; top: 0; }
.eddstack.ready { left: 0; }
.eddstack.spent { right: 0; left: auto; }
/* Eddies are 50% bigger than the 60px card backs (2026-09-02): 90×120, full
   size, fan offset 18px. The box stays the fixed 280×160 so taps never
   resize the table. */
.eddies .eddie { position: absolute; top: 0; left: 0; width: 90px; aspect-ratio: 3/4;
  border-radius: 8px; border: 1px solid #000; display: grid; place-items: center;
  font-size: 18px; font-weight: 800; color: #000;
  transform: translateX(calc(var(--i) * 18px));
  background: url('/art/back.png') center / 100% 100% no-repeat #111; }
.eddies .eddie.face { color: var(--dim); }
/* 2026-09-15 (user request): a SOLD Eddie stays face-up (its real card art)
   for the seller's turn + the opponent's turn — `revealed` is set by doSell /
   sellCard and cleared at the SELLER's next beginTurn. The inline style sets
   background-image to the card art; here we just drop the dim "face" color so
   the (now-hidden) center glyph doesn't show over the art, and let the art fill. */
.eddies .eddie.revealed { background-size: 100% 100%; color: transparent; }
/* Rival-mat revealed Eddie: counter-rotate like every other card face (see
   --matrot) so it reads upright from the viewer's seat. */
.side.mat-rival .eddies .eddie.revealed { transform: translateX(calc(var(--i) * 18px)) rotate(var(--matrot)); }
/* Spent + revealed on the rival mat: net to +90° screen (same as own spent). */
.side.mat-rival .eddstack.spent .eddie.revealed { transform: translateX(calc(var(--i) * -18px)) rotate(-90deg); }
/* Spent Eddies: the stack box is a 0×0 anchor at the RIGHT edge of .eddies,
   so the cards must be positioned relative to that anchor's RIGHT side
   (right, not left) and fan LEFTWARD (−i·18px) — with left:0 they rendered
   entirely outside the box, past the mat's right edge (off-screen, since the
   Eddies column is the owner's rightmost zone). right:18px keeps the
   rotated card (120px wide after 90°) inside the box. */
.eddstack.spent .eddie { top: 20px; right: 18px; left: auto;
  transform: translateX(calc(var(--i) * -18px)) rotate(90deg);
  transform-origin: 50% 50%; opacity: .85; }
.eddstack.empty { width: 90px; height: 120px; border: 1px dashed var(--line);
  border-radius: 8px; opacity: .5; }
.eddstack.spent.empty { top: 20px; right: 18px; left: auto; width: 120px; height: 90px; }
/* CR 3.6/3.7: spent = tilted 90° to the RIGHT. The card keeps its FULL
   size — only the footprint box swaps to landscape so the rotated card
   fits without scaling down. */
.unit.spent { position: relative; width: 168px; height: 136px; flex: none; }
/* Only the UNIT card rotates when spent — :not(.gear-card) keeps the tucked
   gear (also a .card) in its own under-unit fan (gear can ride a tapped unit). */
.unit.spent .unit-card { position: absolute; top: 50%; left: 50%; width: 120px;
  transform: translate(-50%, -50%) rotate(90deg); }
.unit.spent .unit-card:hover { transform: translate(-50%, -50%) rotate(90deg) scale(1.04); }
/* Rival-mat spent unit (2026-09-15): the mat's 180° flip would turn the tilted
   face the wrong way; -90° local nets to +90° in screen space — the SAME tilt
   as a spent unit on your own mat, so it reads upright (see --matrot above). */
.side.mat-rival .unit.spent .unit-card { transform: translate(-50%, -50%) rotate(-90deg); }
.side.mat-rival .unit.spent .unit-card:hover { transform: translate(-50%, -50%) rotate(-90deg) scale(1.04); }
.zone-row { display: flex; flex-direction: column; gap: 10px; }
.zones { display: grid; grid-template-columns: 1fr; gap: 10px; }
.zone { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-height: 40px;
  padding: 8px; border-radius: 10px; background: var(--bg2); border: 1px dashed transparent; }
.zone.field { border-color: var(--line); }
.zone .empty, .counts, .flabel { color: var(--dim); font-size: 11px; }
.unit { position: relative; }
/* In-play cards are ONE uniform size everywhere (field / hand / legends):
   bare printed faces only (opts.bare) — no name row, chips, or tags. 120px
   (was 84) — the user found 84 too small and asked for ~2x. */
.board-card, .unit .card, .lgslot .card, .zone.hand .card, .lgback { --w: 120px; }
.unit.spent .card { filter: brightness(.55) saturate(.7); }
/* Equipped gear: a FULL bare card tucked UNDER the unit/legend, fanned
   downward so the bottom TEXT BOX (keywords/effect — the part that can be
   inherited, CR 3.18.3) peeks out below the host. The host card sits on top
   (z-index), and the gear container is out-of-flow absolute so it never
   changes the host's box (no fitMat re-scale / flicker). Cascade = 32px per
   gear (2026-09-04): gear #1's full text box (card 160, box starts ~72%
   down ≈ 45px) is fully visible; each extra gear reveals its own ~32px
   box-strip, so N gear read as N stacked strips. Centered under the host
   (left:50% + translateX) so a SPENT (168-wide box) or a LEGEND (160-wide
   slot, card centered) shows NO side offset — the 2026-09-04 "shifted to
   the side" complaint. */
.unit-card { position: relative; z-index: 30; }
/* Polish (2026-09-15): a READY unit brightens + gets a soft neon rim on hover
   (the "you can act on me" affordance). filter only — no transform, so it
   composes with the rival-mat 180° counter-rotation and the spent rules. */
.unit:not(.spent) .unit-card:hover { filter: brightness(1.12) drop-shadow(0 0 8px rgba(0, 240, 255, .35)); }
.equipped { position: absolute; top: 0; left: 0; right: 0; z-index: 1; }
.gear-card { position: absolute; left: 50%; transform: translateX(-50%); }
.gear-card:hover { box-shadow: 0 0 0 2px var(--neon), 0 6px 16px rgba(0,0,0,.7); z-index: 6; }
/* Lag token (2026-09-04, user reference: degraded Wi-Fi/signal glyph + red
   "!" alert badge). Top-right corner, REPLACES the old 'lag' text tag. The
   arcs are flat white (readable on card art, unlike the reference's black);
   the badge is a flat red circle with a white "!". Out-of-flow → no flicker. */
.lag-tok { position: absolute; top: 2px; right: 2px; z-index: 45;
  display: flex; align-items: center; width: 30px; height: 18px;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.8)); }
.lag-signal { display: block; width: 18px; height: 16px; }
.lag-signal svg { width: 100%; height: 100%; display: block; }
.lag-alert { flex: none; width: 12px; height: 12px; margin-left: -5px;
  background: #e60012; color: #fff; font: 700 10px/12px system-ui;
  text-align: center; border-radius: 50%; box-shadow: 0 0 0 1.5px rgba(0,0,0,.6); }
/* ---------- status tokens (duration-limited effects, 2026-09-04) ----------
   One round chip per active modifier, bottom-LEFT cluster on the card
   (top-right = lag token, bottom-right = might badge). Colors = family:
   red = threat (must-attack / no-attack / doom), amber = no-ready (hourglass),
   green = protection (shield), cyan = attack-empowerment / gig ward,
   orange = steal / retaliation. Duration rings:
   .this-turn  = solid ring (turn_end — "this turn")
   .next-turn  = double ring (next_turn_* — carries into a next turn)
   .temp       = dashed outline (granted keyword / temporary haste)
   .armed      = pulsing ring (one-shot `when` charge — "the NEXT …") */
.tok-wrap { position: absolute; left: 2px; bottom: 2px; z-index: 44;
  display: flex; flex-direction: row; gap: 2px;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.8)); }
.tok { display: inline-flex; align-items: center; justify-content: center;
  min-width: 14px; height: 14px; padding: 0 2px; border-radius: 50%;
  font-size: 9px; line-height: 1; font-weight: 700; color: #fff;
  background: rgba(12,14,20,.85); box-shadow: 0 0 0 1px currentColor; }
.tok.tok-red    { color: #ff5d5d; background: rgba(64,10,14,.88); }
.tok.tok-amber  { color: #ffb02e; background: rgba(64,42,6,.88); }
.tok.tok-green  { color: #4ce07a; background: rgba(8,52,24,.88); }
.tok.tok-cyan   { color: #3fd6ff; background: rgba(6,44,60,.88); }
.tok.tok-orange { color: #ff9d2e; background: rgba(70,38,6,.88); }
.tok.tok-blue   { color: #6aa8ff; background: rgba(12,28,64,.88); }
.tok.this-turn  { box-shadow: 0 0 0 1.5px currentColor; }
.tok.next-turn  { box-shadow: 0 0 0 1.5px currentColor, 0 0 0 3.5px rgba(255,255,255,.28); }
.tok.temp       { box-shadow: 0 0 0 1.5px currentColor; outline: 1px dashed currentColor; outline-offset: 1.5px; }
.tok.armed      { animation: tok-arm 1.6s ease-in-out infinite; }
@keyframes tok-arm {
  0%, 100% { box-shadow: 0 0 0 1.5px currentColor, 0 0 0 3px rgba(255,255,255,.05); }
  50%      { box-shadow: 0 0 0 1.5px currentColor, 0 0 0 4.5px rgba(255,255,255,.28); } }
/* Owner-scoped tokens ride the Gig-area block (top-right of the half). */
.tok-wrap.gig-toks { top: 2px; bottom: auto; right: 2px; left: auto; }
/* Legend-slot tokens: the slot is a fixed 160×160 box, card centered —
   bottom-left of the SLOT (below the 120-wide card, above the gear fan). */
.tok-wrap.lg-toks { z-index: 46; }
.tok-wrap.lg-toks .tok { min-width: 15px; height: 15px; font-size: 10px; }
/* Legends (3 slots). Each slot is a FIXED 160×168 box (2026-09-07 v70: the
   height hugs the 160px card + 8px gear-peek headroom — the old 160×160 square
   left the card bottom flush with the box and made the AFFORDANCE PULSE render
   on the whole box. Now .lgslot.ix-cap pulses card-shaped). A ready Legend is
   a 120×160 card centered in it, and a SPENT Legend (CR 3.6/3.7, 90° tilt) is
   a 160×120 card in the SAME box — box size never changes on tap (no resize). */
.legends { display: flex; gap: 12px; }
.lgslot { position: relative; width: 160px; height: 168px; display: flex; align-items: center; justify-content: center; }
.lgslot.empty { display: grid; place-items: center; color: var(--dim); border: 1px dashed var(--line); border-radius: 8px; }
.lgback { width: 120px; aspect-ratio: 3/4; border-radius: 8px; }
/* The Legend card sits ABOVE its equipped gear (same stacking as a unit:
   .unit-card z-index:30 over .equipped z-index:1) — without this the
   positioned gear fan (z-index:1) would paint over the in-flow legend card. */
.lgslot .lg-card { position: relative; z-index: 30; }
/* Spent Legend (CR 3.6/3.7): the card tilts 90° in the SAME fixed 160×160
   box (no reflow). ONLY the card/face rotates — equipped Gear (the
   .equipped child, anchored to the slot's top-left) stays put and does NOT
   tilt, exactly like a spent unit (whose gear fan keeps its upright
   orientation — see .unit.spent .unit-card). The card-only dim filter
   matches the unit's, so the gear keeps its brightness while spent. */
.lgslot.spent > .lg-card, .lgslot.spent > .lgback { position: absolute; top: 50%; left: 50%; width: 120px;
  transform: translate(-50%, -50%) rotate(90deg);
  filter: brightness(.55) saturate(.7); }
/* Rival-mat spent Legend (2026-09-15): -90° local nets to +90° in screen space
   under the mat's 180° flip — same tilt as a spent Legend on your own mat. */
.side.mat-rival .lgslot.spent > .lg-card, .side.mat-rival .lgslot.spent > .lgback { transform: translate(-50%, -50%) rotate(-90deg); }
/* v70 (2026-09-07): the affordance pulse now RIDES the legend's card element
   (hugs the 120×160 card, and on a spent legend rotates with its 90° tilt —
   the class is on the same element that carries the transform). Guard the
   spent card against the generic #board-content .ix-cap { position:relative }
   (same id specificity, would wipe the absolute centering), and match the
   card's 8px corner so the ring follows the card shape exactly. */
#board-content .lgslot.spent > .lg-card.ix-cap,
#board-content .lgslot.spent > .lgback.ix-cap { position: absolute; }
#board-content .lg-card.ix-cap, #board-content .lgback.ix-cap { border-radius: 8px; }
.gigs { display: flex; align-items: center; gap: 6px; }
.sc { font-size: 12px; font-weight: 800; color: var(--neon); margin-right: 6px; }
/* Q3 (2026-09-01): each die wears a 2-D silhouette of its true polyhedron —
   D4 = 4 faces → square, D6 = 6 → hexagon, D8 = 8 → octagon, D10 = 10 →
   decagon, D12 = 12 → dodecagon, D20 = 20 → 20-gon. clip-path sets the shape
   and eats borders, so the identity colour is an inset box-shadow ring (which
   hugs the clipped silhouette). */
.die { display: inline-grid; place-items: center; aspect-ratio: 1; min-width: 46px; padding: 6px;
  background: var(--panel2); font-size: 10px; color: var(--dim);
  flex-direction: column; gap: 1px;
  clip-path: polygon(50% 0%, 65.5% 2.4%, 79.4% 9.5%, 90.5% 20.6%, 97.6% 34.5%, 100% 50%, 97.6% 65.5%, 90.5% 79.4%, 79.4% 90.5%, 65.5% 97.6%, 50% 100%, 34.5% 97.6%, 20.6% 90.5%, 9.5% 79.4%, 2.4% 65.5%, 0% 50%, 2.4% 34.5%, 9.5% 20.6%, 20.6% 9.5%, 34.5% 2.4%);
  box-shadow: inset 0 0 0 2px var(--pink); }
.die.d4  { clip-path: polygon(50% 0%, 100% 100%, 0% 100%); box-shadow: inset 0 0 0 2px var(--dim); }
.die.d6  { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); box-shadow: inset 0 0 0 2px var(--blue); }
.die.d8  { clip-path: polygon(50% 0%, 85.4% 14.6%, 100% 50%, 85.4% 85.4%, 50% 100%, 14.6% 85.4%, 0% 50%, 14.6% 14.6%); box-shadow: inset 0 0 0 2px var(--cyan); }
.die.d10 { clip-path: polygon(50% 0%, 79.4% 9.5%, 97.6% 34.5%, 97.6% 65.5%, 79.4% 90.5%, 50% 100%, 20.6% 90.5%, 2.4% 65.5%, 2.4% 34.5%, 20.6% 9.5%); box-shadow: inset 0 0 0 2px var(--green); }
.die.d12 { clip-path: polygon(50% 0%, 75% 6.7%, 93.3% 25%, 100% 50%, 93.3% 75%, 75% 93.3%, 50% 100%, 25% 93.3%, 6.7% 75%, 0% 50%, 6.7% 25%, 25% 6.7%); box-shadow: inset 0 0 0 2px var(--yellow); }
.die .dieval { font-size: 15px; font-weight: 800; color: var(--fg); }
.die.emptydie { opacity: .35; }
.edcount { font-size: 12px; font-weight: 700; color: var(--yellow); margin-right: 4px; }
/* Hand (2026-09-02 v6): a single-row strip, FIXED height 164px (120px-wide
   cards are 3:4 → 160px tall; 164 = card + 4px padding) so adding/removing a
   card NEVER resizes the table (the v5 128px min-height was SHORTER than the
   160px cards, so a 0→1 card grew the row 32px → fitMat re-scaled → the
   "layout flicker"). No scrollbar: the cards fan ADAPTIVELY (fitCardFan in
   app.js) — fully apart at the standard 120px step while they fit, then
   compressing toward the 34px floor only once they don't. */
.zone.hand { margin: 0; padding: 4px; flex-wrap: nowrap; gap: 0; overflow: hidden;
  align-items: flex-start; height: 226px; }   /* v70: 226 (was 164) — no more bottom cut; uniform w/ Legends+Eddies */
.zone.hand .card { flex: none; }
.zone.hand .card:hover { transform: translateY(-8px); z-index: 5; }
.zone.hand > .ztag { position: sticky; left: 4px; z-index: 2; flex: none;
  background: var(--bg2); padding-right: 6px; }
.hidden-hand { color: var(--dim); font-size: 12px; overflow: hidden; }
/* Gigs bar = the SINGLE shared block in the center strip between the two mats
   (2026-09-02: each mat used to carry its own bar with BOTH players' dice —
   duplicated). 1 row, 2 columns: left = the RIVAL's Gigs, right = the
   followed player's OWN Gigs; from either seat that reads "rival left /
   friendly right" (the rival mat's 180° rotation flips their side to match).
   It is the NARROW MIDDLE of the center row, flanked in-flow by the two
   per-player effect lanes (see .center-strip). FIXED min-height so the
   table's measured size never changes as gig dice are added/removed. */
.table { position: relative; }
/* The table's CENTER ROW (in flow, 2026-09-02 v5): [rival lane] | Gigs |
   [your lane] — one row. The Gigs block is a FIXED, CENTERED size — exactly
   wide/tall for 2 rows of 6 dice per player (the max), no more (the whole
   screen was useless for at most 12 dice each). The two pending-effect lanes
   take ALL the leftover width (flex: 1) — they can hold many effects — and
   their cards fan ADAPTIVELY (fitCardFan in app.js): fully apart at 120px
   while they fit, compressed to the narrowest fitting step otherwise. */
/* CENTER ROW (2026-09-02 v7): [rival Gigs] | [rival lane] | RESOLVING STAGE |
   [your lane] | [your Gigs]. The pending-effect lanes are NARROWED (capped at
   300px, still flex-shrink so a small window is fine), the Gigs block is SPLIT
   into two halves on the outer edges, and the freed center holds the RESOLVING
   effect stage — the origin of the target arrows (2026-09-02 v7). FIXED 190px
   height (every state-dependent zone inside is constant size) so the table's
   measured height never changes → fitMat never re-scales → no flicker. */
.center-strip { display: flex; gap: 10px; align-items: stretch; justify-content: center;
  width: 100%; height: 190px; }
/* GIG HALF (2026-09-04 v8 — 3-band fill) — one player's gig area: FILLs the
    center-strip height (190px, constant — no flicker) as FIVE horizontal
    bands: tag 10% · TOP dice row 40% · 5% gap · BOTTOM dice row 40% · 5%
    bottom. Each band is a direct child of .gig-half so the % are of the FULL
    area (the old flex-column with gap/centered content broke the %
    computation — justify-content + explicit % heights are exact).
    DICE FILL THEIR ROW (user 2026-09-04): .die is flex:1 + height:100%, so
    N dice tile the 205px row (6 → ~30px each, the historical size; 1 → ~116px
    — big, but that's what "fill the row" means and the row height is the
    constant ~70px, so a die is always a 1:1 silhouette). TOP row = dice that
    came from elsewhere (stolen / swapped — a die's id prefix never changes,
    so foreign = id owner digit != this area's seat); BOTTOM row = the owner's
    own. Constant size in every state; `fx-ref="gig-<seat>"` makes it an arrow
    target ("the Gig area"), each die `fx-die` its own. */
.gig-half { position: relative; display: flex; flex-direction: column; align-items: center;
  width: 225px; flex: none; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg2); }
.gig-half .gb-tag { flex: 0 0 10%; display: flex; align-items: center;
  font-size: 10px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
  color: var(--dim); white-space: nowrap; }
.gig-half .gb-you { color: var(--cyan); }
.gig-half .gig-row { flex: 0 0 40%; min-height: 0; width: 205px;
  display: flex; flex-wrap: nowrap; gap: 5px; align-items: stretch;
  justify-content: center; }
.gig-half .gig-row .empty { color: var(--dim); font-size: 10px; align-self: center; }
.gig-half .gig-spacer { flex: 0 0 5%; }
.gig-half .gig-spacer-bottom { flex: 0 0 5%; }
.gig-half .die { flex: 0 0 auto; min-width: 0; width: 30px; height: 30px; padding: 4px 2px; }
.gig-half .die .dieval { font-size: 15px; }   /* fitGigDice rescales both */
/* RESOLVING STAGE (2026-09-02 v7) — the fixed 172×190 center spot where the
   effect suspended on a choice is shown (moved here from the side rail).
   Constant size in every state (empty placeholder vs card) → no flicker; the
   card is the ORIGIN of the target arrows. */
.fx-stage { flex: none; width: 172px; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 4px; padding: 6px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--bg2); }
.fx-stage .fxs-head { font-size: 10px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase;
  color: var(--neon); }
.fx-stage .fxs-card { display: flex; align-items: center; justify-content: center; }
.fx-stage .fxs-card .card { --w: 120px; }
.fx-stage .fxs-card .empty { color: var(--dim); font-size: 10px; }
/* One-shot RESOLVED/PASSED frame (2026-09-03 ruling #1): the card shows in
   the stage with all its targets picked, dimmed — it has finished, the
   arrows (renderFxLayer) point at what it actually chose. */
.fx-stage .fxs-card.done .card { filter: brightness(.8) saturate(.9); }
.fx-stage .fxs-card.passed .card { filter: brightness(.55) grayscale(.4); opacity: .8; }
/* TARGET-ARROW OVERLAY (2026-09-02 v7) — the SVG layer laid over the whole
   (scaled) playmat: glowing lines + arrowheads from the resolving effect (or
   an attacking unit) to each target. Absolute + pointer-events:none + no size
   contribution → it never affects fitMat's measurements (no flicker) and
   never blocks clicks. Rebuilt every render; live + replay identical. */
.fx-layer { position: absolute; top: 0; left: 0; z-index: 50; pointer-events: none; }
/* SVG keeps its default overflow:hidden — and all points are clamped to
   RING_MARGIN inside the box (see renderFxLayer), so a target ring on a unit
   near the mat edge can never overflow into the content's scrollable area
   (that would change fitMat's measured height → re-scale → flicker).
   display:block is NOT cosmetic: an SVG's default inline display puts its box
   on the text baseline, and the line box's descender space pokes ~5px past
   the fixed-height layer — measured as content.scrollHeight and re-scaling
   fitMat (the flicker this overlay must never cause). */
.fx-layer svg { display: block; }
.fx-ring { animation: fx-pulse 1.4s ease-in-out infinite; }
@keyframes fx-pulse { 0%,100% { opacity: .9; } 50% { opacity: .35; } }
/* Valid-target pulse (2026-09-03, user ruling #6): the CURRENT pickable
   candidates get a pulsing border (no arrow — arrows are for CHOSEN targets).
   Animated via opacity so the geometry (and the SVG box it lives in) never
   changes → no fitMat re-scale → no flicker. */
.fx-pulse { animation: fx-valid-pulse 1.1s ease-in-out infinite; transform-box: fill-box; }
@keyframes fx-valid-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
/* Might badge (2026-09-03, user ruling #4): current might over the printed
   might, bottom-right corner of the unit card. Same font/size as the printed
   number (matches the card's own power box); neon-green when higher,
   neon-red when lower. Out-of-flow absolute → never changes the unit box. */
.might-badge { position: absolute; right: 3px; bottom: 3px; z-index: 40;
  min-width: 26px; padding: 0 4px; text-align: center; line-height: 1.25;
  font: 800 15px/1.25 'Segoe UI', system-ui, sans-serif;
  border-radius: 5px; color: #08120c; background: var(--green);
  border: 1px solid rgba(0,0,0,.55); box-shadow: 0 0 10px rgba(72,255,155,.7), inset 0 0 4px rgba(0,0,0,.25);
  text-shadow: 0 0 3px rgba(255,255,255,.35); }
.might-badge.down { background: var(--red); color: #fff;
  box-shadow: 0 0 10px rgba(255,77,94,.75), inset 0 0 4px rgba(0,0,0,.25);
  text-shadow: 0 0 3px rgba(0,0,0,.5); }
.might-badge::before { content: '⚔ '; font-size: 11px; }
/* Target-selection box + reveal faces (2026-09-03, user ruling #5/#6). */
.promptbox.targetbox { border-color: var(--green); background: #0a1a12; }
.promptbox.targetbox strong { color: var(--green); }
.targetpicks { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.targetpicks .reveal-face { --w: 88px; cursor: pointer; }
.targetpicks .reveal-face:hover { outline: 2px solid var(--green); outline-offset: 1px; }
.action.target { font-size: 12px; padding: 6px 10px; }
/* 1.5.0 visible auto-pick: the single-target frame. The box is calmer (no
   "choose" urgency), a small note says it's auto-picking, and the sole
   candidate is shown as an already-chosen target (not a clickable button).
   The fx layer draws it as a SOLID arrow (chosen style) instead of a pulse. */
.promptbox.targetbox.autopick { border-color: var(--neon); background: #0b1220; }
.promptbox.targetbox.autopick strong { color: var(--neon); }
.ap-note { margin-left: 8px; font-size: 10px; font-weight: 600; color: var(--dim); }
.ap-picked { font-size: 12px; font-weight: 700; color: var(--green);
  border: 1px solid var(--green); border-radius: 6px; padding: 6px 10px;
  background: rgba(72,255,155,.08); }
.fx-stage .fxs-head.autopick { color: var(--neon); }
/* Per-player pending-effect lane (2026-09-02 v5) — the POOL of that player's
   pending effects (CR 10.12: resolve in ANY order; 10.13: turn player first),
   drawn as the SOURCE CARDS side by side (bare faces, like a hand). NARROWED
   in v7 (max-width 300px — the user found the full-width lanes too wide) but
   still flexes down on small windows, fixed 168px tall. The effect currently
   RESOLVING is NOT here — it lives in the center STAGE (fx-stage). Clicking a
   card in your own lane resolves that effect. */
.fxlane { flex: 1 1 auto; max-width: 300px; min-width: 0; display: flex; flex-direction: column; gap: 4px;
  border: 1px dashed var(--line); border-radius: 8px; padding: 4px 6px; background: var(--bg2); }
.fxl-head { font-size: 10px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
  color: var(--dim); text-align: center; white-space: nowrap; }
.fxl-cards { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: flex-start;
  flex: 1 1 auto; min-height: 0; overflow: hidden; }   /* fills the lane; height is the lane's constant, not the card count */
.fxl-cards .empty { color: var(--dim); font-size: 10px; align-self: center; margin: 0 auto; }
/* Each effect card: 120px face, standard 120px step (fully apart). When the
   pool doesn't fit, fitCardFan compresses the step (34px floor) so it does. */
.fxl-cards .fx-card { position: relative; flex: none; --w: 120px; }
.fxl-cards .fx-card.clickable { cursor: pointer; }
.fxl-cards .fx-card.clickable:hover { outline: 2px solid var(--neon); outline-offset: 1px; }

/* midbar = the control strip BELOW the table (phase + actions + prompt).
   FIXED height (2026-09-02 v5): the action button count changes constantly
   (pending effects, attack windows, …) and a growing/shrinking bar used to
   resize the whole table → fitMat re-scaled everything (the "layout flicker"
   when a card entered the pending pool). Now the bar is a constant 84px box
   and the buttons scroll INSIDE it. */
#midbar { display: flex; flex-direction: column; gap: 6px; padding: 2px 4px;
  height: 84px; overflow: hidden; }
#midbar #prompt { flex: none; }
#midbar #actions { flex: 1; min-height: 0; overflow-y: auto; }
#phase-info { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.phase { font-weight: 800; letter-spacing: 1px; text-transform: uppercase; font-size: 13px; color: var(--cyan); }
.turn { font-size: 11px; padding: 2px 8px; border-radius: 6px; background: var(--panel); border: 1px solid var(--line); color: var(--dim); }
.turn.mine { color: #0b0d12; background: var(--green); border-color: var(--green); font-weight: 700; }
.attacknote { color: var(--pink); font-size: 12px; }
.result { font-size: 16px; font-weight: 800; color: var(--neon); text-shadow: 0 0 14px rgba(252, 238, 10, .5); }
#actions { display: flex; flex-wrap: wrap; gap: 6px; }
.action { font-size: 12px; padding: 6px 10px; }
.action .acost { margin-left: 6px; color: var(--yellow); font-weight: 700; font-size: 11px; }
.action.end { border-color: var(--green); color: var(--green); }
.action.danger { border-color: var(--pink); color: var(--pink); }
/* Auto-pay toggle (2026-09-08 #3): a per-browser preference in the bottom bar.
   Reads as a small ON/OFF switch — dim when off, neon-glowing when on. The
   label + a leading dot that fills green when active. */
.action.autopay { display: inline-flex; align-items: center; gap: 7px;
  border-color: var(--line); color: var(--dim); background: var(--panel2); }
.action.autopay::before { content: ''; width: 9px; height: 9px; border-radius: 50%;
  background: var(--line); box-shadow: none; transition: background .15s, box-shadow .15s; }
.action.autopay:hover { border-color: var(--cyan); color: var(--fg); }
.action.autopay.on { border-color: var(--green); color: var(--green); font-weight: 700;
  box-shadow: 0 0 10px rgba(72, 255, 155, .3); }
.action.autopay.on::before { background: var(--green); box-shadow: 0 0 8px var(--green); }
/* FX SPEED (2026-09-08): a per-browser slider in the bottom bar scales EVERY
   animation. --fx-scale is a DURATION MULTIPLIER (set on <html> by
   applyFxSpeed()): value = how many times the base length each beat runs, so
   1 = normal, 3 = slowest, 0.25 = fastest, 0 = OFF (instant). It already
   bakes in the x2 base (FX_BASE) and equals fxScale() on the JS side, so a
   CSS calc(Xs * var(--fx-scale)) and its mirrored JS X*fxScale() stay in
   sync. --fx-pulse is the same but floored at 1 for INFINITE animations
   (fixer pulse, burn glow): a 0s infinite would flicker, so those keep
   pulsing in OFF mode (affordance stays on). CSS keyframes read it via
   calc(); JS WAAPI/setTimeout read fxScale() directly. */
:root { --fx-scale: 2; --fx-pulse: 2; }
.action.fxspeed { display: inline-flex; align-items: center; gap: 6px;
  border-color: var(--line); color: var(--dim); background: var(--panel2); cursor: default; }
.action.fxspeed input[type=range] { width: 74px; accent-color: var(--cyan); cursor: pointer; }
.action.fxspeed .fxv { font-weight: 700; color: var(--cyan); min-width: 34px; text-align: right; }
/* ZONE-FLIGHT CLONES (2026-09-08): body-level position:fixed overlays — the
   "from→to" card for draws/trashes/defeats/bottom-decks. Never in the board
   tree → fitMat untouched (no-flicker invariant). Sized by JS; the inner
   .card/.cardback fills it at 3:4. */
.zone-fly { position: fixed; left: 0; top: 0; z-index: 130; pointer-events: none;
  will-change: transform, opacity; filter: drop-shadow(0 14px 26px rgba(0,0,0,.7)); }
.zone-fly .card { width: 100%; height: auto; cursor: default;
  box-shadow: 0 0 0 1px rgba(255,255,255,.08); }
.zone-fly .cardback { width: 100%; aspect-ratio: 3/4; display: block; }
/* SPEND TAP (2026-09-08): a spent Unit/Legend/Eddie TAPS — a quick 90° turn
   and back (a transient beat, not a resting state: the persistent "spent"
   look is carried by .unit.spent / .lgslot.spent filters + the Eddies' own
   90° rest rotation). Runs on the live element; the reflow-restart in JS
   makes it replayable. */
.spend-spin { animation: spendspin calc(.5s * var(--fx-scale)) cubic-bezier(.4,.1,.3,1) both; }
@keyframes spendspin { 0% { transform: rotate(0deg); } 50% { transform: rotate(90deg); } 100% { transform: rotate(0deg); } }
/* A soft "a card arrived here" pulse on a zone (hand / trash / deck). */
.zone-impact { animation: zoneimpact calc(.5s * var(--fx-scale)) ease both; }
@keyframes zoneimpact { 0% { box-shadow: 0 0 0 0 rgba(252,238,10,0); }
  35% { box-shadow: 0 0 0 3px rgba(252,238,10,.5); } 100% { box-shadow: 0 0 0 0 rgba(252,238,10,0); } }
/* Defeat variant (2026-09-15): same ring beat in RED — the trash/deck flashes
   hurt when a unit is defeated. box-shadow only → no layout contribution. */
.zone-impact.zone-impact-hurt { animation: zoneimpact-hurt calc(.5s * var(--fx-scale)) ease both; }
@keyframes zoneimpact-hurt { 0% { box-shadow: 0 0 0 0 rgba(255,77,94,0); }
  35% { box-shadow: 0 0 0 3px rgba(255,77,94,.6), 0 0 18px rgba(255,77,94,.35); } 100% { box-shadow: 0 0 0 0 rgba(255,77,94,0); } }
/* Respect reduced-motion: no flights/spins — cards still appear in-zone. */
@media (prefers-reduced-motion: reduce) {
  .zone-fly, .spend-spin, .zone-impact { animation: none; transition: none; }
}
/* ── BEAT FX (2026-09-08): die flash / turn banner / fixer pulse / fight ── */
/* Die-roll flash: a BIG shaped die spins up center-screen, then fades.
   Appended to #board-stage (absolute inside the stage — no layout impact). */
.die-flash { position: absolute; inset: 0; z-index: 40; display: grid; place-items: center;
  pointer-events: none; }
.die-flash .dieflash-shape { width: 118px; height: 118px; padding: 10px; font-size: 40px;
  background: var(--card-bg, #14161f); border-radius: 12px;
  filter: drop-shadow(0 0 18px rgba(252,238,10,.55)); }
.die-flash .dieflash-shape .dieval { font-size: 44px; color: var(--fg); }
.die-flash .dieflash-label { position: absolute; bottom: 18%; left: 50%; transform: translateX(-50%);
  font-size: 13px; font-weight: 800; letter-spacing: 1px; color: var(--neon);
  text-shadow: 0 0 8px rgba(252,238,10,.6); }
/* Turn-start banner: "Your turn — roll a die" center-screen, self-fading. */
.turn-banner { position: absolute; inset: 0; z-index: 41; display: grid; place-items: center;
  pointer-events: none; font-size: 26px; font-weight: 800; letter-spacing: 1px; color: var(--fg);
  text-shadow: 0 0 14px rgba(252,238,10,.7); }
.turn-banner b { color: var(--neon); }
/* Fixer pulse: the "roll a die" affordance — re-armed every step while the
   start phase is open. drop-shadow (applies AFTER clip-path — box-shadow on a
   clipped element paints nothing). */
#board-content .zone.fixer.fixer-pulse { animation: fixerpulse calc(1.4s * var(--fx-pulse)) ease-in-out infinite; }
@keyframes fixerpulse {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(252,238,10,0)); }
  50% { filter: drop-shadow(0 0 9px rgba(252,238,10,.75)); } }
/* Fight: the two bump clones ride .zone-fly (fixed overlays). */
.fight-clone { z-index: 45; }
/* Damage/power numbers: BOLD RED, tilted 45°, over the top half of each card.
   Body-level fixed; WAAPI handles motion + fade (opacity), CSS carries the
   45° tilt via transform in the keyframes. */
.dmg-num { position: fixed; z-index: 46; pointer-events: none; font-size: 30px; font-weight: 900;
  color: #ff2b3a; text-shadow: 0 0 10px rgba(255,43,58,.8), 0 2px 2px rgba(0,0,0,.7); }
/* Burning-out fight loser: a GLOWING FLIGHT CLONE (.zone-fly.burning) holds
   at the unit's pre-wipe spot for a beat, then the group flies. The clone is
   body-level (no-flicker); the burnout keyframes give it the ember glow. */
.zone-fly.burning > * { animation: burnout calc(.78s * var(--fx-pulse)) ease-in-out infinite alternate; }
@keyframes burnout {
  from { filter: brightness(.9) saturate(1.1) drop-shadow(0 0 4px rgba(255,120,30,.35)); }
  to   { filter: brightness(1.15) saturate(1.6) hue-rotate(-18deg) drop-shadow(0 0 16px rgba(255,90,20,.85)); } }
/* Gear glass-break shards (fixed body-level). */
.shard { position: fixed; z-index: 47; width: 16px; height: 20px; pointer-events: none;
  background: linear-gradient(160deg, rgba(200,235,255,.95), rgba(120,180,255,.55));
  clip-path: polygon(50% 0%, 100% 78%, 18% 100%); }
/* End-turn confirm (Space) — a compact bar above the midbar. */
#endturn-confirm { position: fixed; z-index: 60; left: 50%; bottom: 96px; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 10px;
  background: rgba(10,11,16,.94); border: 1px solid var(--neon); box-shadow: 0 0 18px rgba(252,238,10,.25);
  font-size: 12.5px; }
#endturn-confirm button { font-size: 12px; }
@media (prefers-reduced-motion: reduce) {
  .die-flash, .turn-banner, .dmg-num, .shard, .zone.fixer.fixer-pulse, .zone-fly.burning > * { animation: none; }
}
.action.prompt { border-color: var(--neon); color: var(--neon); font-weight: 700; }
.noact { color: var(--dim); font-size: 12px; padding: 4px; }
#prompt { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.promptbox { background: #241c06; border: 1px solid var(--neon); color: var(--neon); border-radius: 8px;
  padding: 6px 10px; font-size: 12px; }

/* Right rail: event log (flexes to fill + scrolls) · card detail · chat.
   The page no longer scrolls, so the old position:sticky is gone. */
#sidepanel { display: flex; flex-direction: column; gap: 10px; min-height: 0; height: 100%; }
#eventlog { flex: 1 1 auto; min-height: 80px; overflow-y: auto; }
#carddetail { flex: none; max-height: 240px; overflow-y: auto; }
#chat-log { flex: none; max-height: 160px; overflow-y: auto; }
.log { background: var(--bg2); border: 1px solid var(--line); border-radius: 10px; padding: 8px; font-size: 11px; }
.ev { padding: 2px 0; color: var(--dim); border-bottom: 1px solid rgba(42, 50, 69, .4); }
.ev.big { color: var(--neon); font-weight: 800; }
#carddetail { background: var(--bg2); border: 1px solid var(--line); border-radius: 10px; padding: 10px; }
#carddetail:empty { display: none; }
.detail-text { margin-top: 8px; font-size: 12px; }
.d-meta { color: var(--dim); font-size: 11px; margin-bottom: 4px; }
#chat { display: flex; flex-direction: column; gap: 6px; }
#chat-log { max-height: 120px; }
.chat { padding: 2px 0; }
.chat.me { color: var(--cyan); }
#chat-in { width: 100%; }

/* ---------- deckbuilder ---------- */
/* --db-cols / --db-w are set from JS (the "Width: N columns" control): the
   card area gets exactly N ~100px columns, so the list narrows/widens with
   the column count. 100 = per-column card width, 8 = gap, 24 = padding,
   6 = scrollbar gutter. */
/* Fill the view: the two panels each get the full height and scroll
   internally. (--db-cols / --db-w are set from JS for the list column width.) */
#db-wrap { display: grid; grid-template-columns: var(--db-w, 420px) 1fr; gap: 14px;
  height: 100%; min-height: 0; }
#db-cardlist { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px;
  display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.db-filters { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 10px; flex: none; }
.db-filters input { grid-column: 1 / 3; }
/* flex:1 + min-height:0 = bounded scroll region; scrollbar-width:thin forces
   a real (visible) scrollbar instead of a hover-only overlay one.
   grid-auto-rows:max-content — REQUIRED: when the grid is a bounded flex
   child, Chrome sizes implicit rows to min-content (cards collapse to a
   ~2px sliver). Verified via headless CDP probe: 2.44px baseline → 251px. */
#db-cards { flex: 1 1 auto; min-height: 0; overflow-y: scroll; overflow-x: hidden;
  overflow-anchor: none; grid-auto-rows: max-content;
  scrollbar-width: auto; scrollbar-color: var(--cyan) rgba(255,255,255,.07);
  /* FIXED calc(100px * --cs) = 165px tracks — the card is EXACTLY 165px, the
     same box as a deck-panel card (item 5: "literally the same size"). The
     panel width (--db-w, set by applyDBCols) is budgeted to fit N×165 + gaps
     + the scrollbar exactly, so nothing overflows into the scrollbar and the
     last column is never clipped (2026-09-11 item 3). No padding-right here —
     the old 6px pad double-counted with the scrollbar gutter and was part of
     what pushed the last column under the bar. */
  display: grid; grid-template-columns: repeat(var(--db-cols, 4), calc(100px * var(--cs)));
  gap: calc(8px * var(--cs)); align-content: start; }
.db-card { --w: 100%; }
.db-badges { position: absolute; top: 4px; left: 4px; display: flex; flex-direction: column; gap: 2px; z-index: 4; }
.db-card { position: relative; }
.db-stepper { position: absolute; right: 3px; bottom: 3px; z-index: 5;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  opacity: .3; transition: opacity .15s; }
.db-stepper.on { opacity: 1; }
.db-card:hover .db-stepper { opacity: 1; }
.db-step { width: 20px; height: 20px; border: none; border-radius: 5px; padding: 0;
  background: rgba(10, 12, 17, .55); color: #fff; font-size: 13px; line-height: 1;
  display: grid; place-items: center; box-shadow: none; }
.db-step:hover { background: rgba(0, 240, 255, .85); color: #0b0d12; }
.db-step:disabled { opacity: .25; cursor: not-allowed; }
.db-step:disabled:hover { background: rgba(10, 12, 17, .55); color: #fff; }
.db-nn { font-size: 10px; font-weight: 800; color: #fff; background: rgba(10, 12, 17, .6);
  padding: 0 4px; border-radius: 4px; }
.badge { font-size: 8px; font-weight: 800; padding: 1px 4px; border-radius: 3px; color: #0b0d12; }
.badge.leg { background: var(--purple); }
.badge.main { background: var(--cyan); }
#db-deck { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px;
  /* Fills the grid cell (the page no longer scrolls) and scrolls internally:
     a full deck makes this ~1200px, which used to grow the PAGE and the
     browser's window scroll-anchoring nudged the view on every +/− click. */
  min-height: 0; min-width: 0;
  overflow-y: auto; scrollbar-width: auto; scrollbar-color: var(--cyan) rgba(255,255,255,.07); }
/* Chrome/Safari: make the deckbuilder scrollbars OBVIOUS (the 'thin' default
   was nearly invisible — the user thought the list couldn't scroll). */
#db-cards::-webkit-scrollbar, #db-deck::-webkit-scrollbar { width: 12px; }
#db-cards::-webkit-scrollbar-track, #db-deck::-webkit-scrollbar-track {
  background: rgba(255,255,255,.06); border-radius: 8px; margin: 4px 0; }
#db-cards::-webkit-scrollbar-thumb, #db-deck::-webkit-scrollbar-thumb {
  background: var(--cyan); border-radius: 8px; border: 2px solid rgba(0,0,0,.35); }
#db-cards::-webkit-scrollbar-thumb:hover, #db-deck::-webkit-scrollbar-thumb:hover { background: #7df9ff; }
#db-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
#db-name { flex: 1; min-width: 160px; }
#db-saved { font-size: 11px; color: var(--green); }
.valid { font-size: 12px; padding: 6px 10px; border-radius: 8px; margin-bottom: 12px; }
.valid.ok { background: #0d2418; border: 1px solid var(--green); color: var(--green); }
.valid.bad { background: #2a1418; border: 1px solid var(--pink); color: var(--pink); }
.db-zone { margin-bottom: 14px; }
/* Item 6: Legends row no longer spans the full width — it sits LEFT in a
   flex row, sized to exactly its (max 3) cards; the freed space on the RIGHT
   holds #db-stats (the deck charts). */
#db-toprow { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; margin-bottom: 14px; }
/* Legends row sized to EXACTLY its 3 cards (item 6): 3×card + 2 gaps + the
   zone's 8px padding each side. --cs is re-declared here (it lives on
   #db-cards/#db-deck). --legend-card-w is set by applyDBCols (=100px*--cs). */
#db-legends { flex: 0 0 auto; --cs: 1.65;
  width: calc(3 * var(--legend-card-w, 165px) + 2 * (8px * var(--cs)) + 16px); }
#db-stats { flex: 1 1 0; min-width: 230px; background: var(--bg2); border: 1px solid var(--line);
  border-radius: 10px; padding: 10px 12px;
  /* flex-basis 0: the stats box's MAX-CONTENT (nowrap pie-legend rows ≈ 946px)
     is what line-breaking in a wrapping flex row measures — with basis:auto it
     forced a wrap even when 400px+ was free. Basis 0 + min-width: charts stay
     side-by-side with the fit-to-3 legends whenever min fits; only truly
     cramped decks wrap the row. */ }
#db-stats h3, #db-legends h3 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase;
  letter-spacing: .06em; color: var(--dim); }
/* The stats grid: 2x2 — PIE RAM/color, PIE card types (left col), PIE
   sellables + cost curve (right col). */
#db-stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; }
/* Cramped stats box: stack the four charts in one column (donuts + curve
   reflow fine; the legend rows wrap with the box). */
@container db-stats (max-width: 430px) { #db-stats-grid { grid-template-columns: 1fr; } }
#db-stats { container-type: inline-size; container-name: db-stats; }
.db-stat { min-width: 0; }
.db-stat .db-stat-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--dim); margin-bottom: 4px; display: flex; justify-content: space-between; gap: 6px; }
.db-stat .db-stat-val { font-weight: 700; color: var(--fg); }
/* donut */
.db-pie { display: flex; align-items: center; gap: 8px; }
.db-donut { width: 52px; height: 52px; flex: 0 0 auto; border-radius: 50%; position: relative; }
.db-donut::after { content: ''; position: absolute; inset: 11px; border-radius: 50%; background: var(--bg2); }
.db-legend { font-size: 10px; line-height: 1.5; color: var(--fg); min-width: 0; }
.db-legend .row { display: flex; align-items: baseline; gap: 5px; min-width: 0; overflow-wrap: anywhere; }
/* NO white-space:nowrap — the long color/type names made the stats box's
   max-content ~946px, which is what wrapping-flex measures; rows now wrap
   gracefully as the box narrows. */
.db-legend .sw { width: 8px; height: 8px; border-radius: 2px; flex: 0 0 auto; }
.db-legend .pct { color: var(--dim); margin-left: auto; }
/* cost curve */
.db-curve { display: flex; align-items: flex-end; gap: 3px; height: 56px; padding-top: 4px; }
.db-cbar { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; justify-content: flex-end;
  height: 100%; gap: 2px; }
.db-cbar .bar { background: linear-gradient(180deg, var(--cyan), #0a6f7a); border-radius: 2px 2px 0 0;
  min-height: 0; transition: height .15s; }
.db-cbar.zero .bar { background: rgba(255,255,255,.08); }
.db-cbar .n { font-size: 8.5px; text-align: center; color: var(--fg); line-height: 1; }
.db-cbar .x { font-size: 8.5px; text-align: center; color: var(--dim); line-height: 1; }
/* Deck-panel cards are the SAME size as the library (2026-09-11 item 5:
   "literally the same thing, use the current library size"): FIXED
   calc(100px * --cs) = 165px tracks — pixel-identical to a library card.
   auto-fill wraps however many whole 165px cards fit in the (narrower) deck
   panel at any window width. (--cs is defined on #db-cards; re-declare.) */
#db-deck { --cs: 1.65; }
.db-zone-cards { display: grid; grid-template-columns: repeat(auto-fill, calc(100px * var(--cs)));
  gap: calc(8px * var(--cs)); align-content: start; min-height: 60px;
  padding: 8px; border-radius: 10px; background: var(--bg2); border: 1px dashed var(--line); }
.db-zone-cards .db-put, .db-zone-cards .card { min-width: 0; }
/* Legends row = EXACTLY 3 slots (item 6 "cut it to fit the 3 cards"). The
   container width is sized to hold 3×165 + 2 gaps, so repeat(3) never wraps. */
#db-legends .db-zone-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.db-zone-cards .hint { grid-column: 1 / -1; }
.db-put { --w: 100%; position: relative; }
.db-put:hover { border-color: var(--pink); }
/* Unplayable promo previews: dimmed + a badge (the setCopies guard is the
   authority — clicking does nothing but toast). */
.card.unplayable .card-img img { filter: grayscale(.75) brightness(.75); }
.badge.up { background: var(--yellow); }
#db-counts { color: var(--dim); font-size: 12px; margin-top: 6px; }

/* ---------- packs ---------- */
#view-packs { display: flex; flex-direction: column; position: relative; }
#packs-wrap { display: flex; flex-direction: column; gap: 14px; height: 100%; min-height: 0; }
/* The opened-packs output grows (36-pack displays are long) — it scrolls
   inside the view; the description cardbox stays fixed on top. */
#pack-output { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.pack-controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* Layout (2026-09-14): 3 pack TILES per row, each 1/3 of the output width.
   Commons/uncommons are a small overlapping STACK; the Rare+ hits get the
   full-size stage inside the tile. */
.packgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-items: start; }
@media (max-width: 980px) { .packgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) { .packgrid { grid-template-columns: 1fr; } }
.pack-tile { background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 8px; min-width: 0; }
.packhead { font-size: 12px; color: var(--dim); margin-bottom: 6px; display: flex; align-items: center; gap: 8px; }
/* The hits (Rare+): full-size, centered — the chase cards get the stage. */
.pack-hits { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-top: 4px; }
.pack-hits .pk { --w: clamp(104px, 42%, 168px); }
/* Commons/uncommons: a small overlapping card stack (image only). */
.pack-stack { display: flex; flex-wrap: wrap; gap: 6px 0; margin-bottom: 8px; }
.pk.pk-stack { --w: 46px; }
.pack-stack .pk.pk-stack + .pk.pk-stack { margin-left: -13px; }
.pack-stack .pk.pk-stack:hover { z-index: 6; transform: translateY(-4px) scale(1.05); }
.pack-tile.kit .packhead { color: var(--neon); }
.pk { --w: 80px; position: relative; }
/* Rarity glows for pack/display openings (user spec 2026-09-12):
   Common — no glow · Uncommon — faint grey · Rare — blue · Epic — yellow
   · Secret — red · Iconic (non-secret) — strong orange
   · Iconic of a Secret — bright silver-white. */
.pk.gl-c { box-shadow: none; }
.pk.gl-u { box-shadow: 0 0 7px 1px rgba(200, 205, 214, .38); }
.pk.gl-r { box-shadow: 0 0 10px 2px rgba(77, 159, 255, .55); border-color: rgba(77, 159, 255, .5); }
.pk.gl-e { box-shadow: 0 0 13px 3px rgba(255, 210, 30, .6); border-color: rgba(255, 210, 30, .5); }
.pk.gl-s { box-shadow: 0 0 15px 4px rgba(255, 42, 66, .65); border-color: rgba(255, 42, 66, .55); }
.pk.gl-ic { box-shadow: 0 0 18px 6px rgba(255, 140, 20, .8), 0 0 40px 10px rgba(255, 120, 0, .35);
  border-color: rgba(255, 150, 30, .8); }
.pk.gl-ic::after { content: '★'; position: absolute; top: 2px; right: 4px; color: #ffb050; font-size: 12px; z-index: 4; }
.pk.gl-si { box-shadow: 0 0 20px 7px rgba(235, 240, 255, .95), 0 0 48px 14px rgba(190, 210, 255, .45);
  border-color: #fff; }
.pk.gl-si::after { content: '★'; position: absolute; top: 2px; right: 4px; color: #fff; font-size: 12px; z-index: 4; }
.pk.iconic::after { content: '★'; position: absolute; top: 2px; right: 4px; color: var(--neon); font-size: 12px; z-index: 4; }
/* Face-down (Epic and above; iconic). The GLOW stays on the wrapper — the
   user reveals the hit themselves by clicking. Pack cards are image-only, so
   only the art is swapped for the printed back. */
.pk.pk-hidden .card-img img { visibility: hidden; }
.pk.pk-hidden .card-img { background: url('/art/back.png') center / cover no-repeat #111; }
.pk.pk-hidden { cursor: pointer; }
.pk.pk-hidden:hover { filter: brightness(1.18); }
.pk.pk-hidden::before { content: 'CLICK TO REVEAL'; position: absolute; inset: auto 0 30% 0; z-index: 5;
  text-align: center; font-size: 7px; letter-spacing: .12em; color: #fff; text-shadow: 0 1px 3px #000; }
.pk.pk-stack.pk-hidden::before { content: ''; }
/* Reveal flip (runs once, on click). */
.pk.pk-flip { animation: pkflip .55s cubic-bezier(.2, .8, .3, 1); }
@keyframes pkflip {
  0% { transform: rotateY(90deg) scale(1.06); }
  100% { transform: rotateY(0) scale(1); }
}
/* Fresh-pack flight: cards deal in from the pack image tearing open. */
.pack-tile.anim .pk { animation: pkdeal .62s cubic-bezier(.2, .8, .3, 1) backwards; }
@keyframes pkdeal {
  0% { opacity: 0; transform: translateY(-46px) rotate(-5deg) scale(.6); }
  100% { opacity: 1; transform: none; }
}
/* Pack-open "flash card" overlay (2026-09-14): the wrapper itself pops up
   center-stage, big and glowing, then fades — the pack is a beat, not a row. */
.pack-flash { position: absolute; inset: 0; z-index: 40; display: grid; place-items: center;
  background: rgba(5, 7, 12, .72); pointer-events: none;
  animation: packflash 1.5s ease forwards; }
.pack-flash .pf-card { width: clamp(180px, 30vw, 300px); height: auto; border-radius: 12px;
  filter: drop-shadow(0 0 40px rgba(252, 238, 10, .5)) drop-shadow(0 26px 60px rgba(0, 0, 0, .8));
  animation: packflashin 1.5s cubic-bezier(.2, .9, .3, 1) forwards; }
.pack-flash .pf-label { margin-top: 14px; font-size: 15px; letter-spacing: .08em;
  color: #ffe64d; text-shadow: 0 2px 8px #000; text-align: center; }
@keyframes packflashin {
  0%   { transform: scale(.5) rotate(-4deg); opacity: 0; }
  18%  { transform: scale(1.06) rotate(0deg); opacity: 1; }
  30%  { transform: scale(1); }
  78%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(.94); }
}
@keyframes packflash {
  0% { opacity: 0; } 12% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; }
}
/* Product tiles ARE the actions — each picture opens on click (uniform:
   pack / box / kit). Hover = "you can open this". */
.pack-products { display: flex; gap: 10px; flex-wrap: wrap; margin: 8px 0; }
.pack-product { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 6px;
  cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 4px; width: 150px; }
.pack-product img { width: 100%; height: 92px; object-fit: contain; transition: transform .12s ease; }
.pack-product .pp-name { font-size: 11px; font-weight: 700; text-align: center; display: flex; flex-direction: column; gap: 1px; }
.pack-product .pp-name small { color: var(--dim); font-weight: 400; }
.pack-product:hover img { transform: scale(1.06); }
.pack-product:hover { border-color: var(--neon); box-shadow: 0 0 10px rgba(252, 238, 10, .35); }

/* ---------- replays ---------- */
#replays-wrap { display: grid; grid-template-columns: 280px 1fr; gap: 14px; height: 100%; min-height: 0; }
/* Left column: replay list (top) + event log (below) — both flex to their
   half of the full-height column and scroll internally. Thinner (280) to
   give the table more width. */
#replays-wrap .cardbox { display: flex; flex-direction: column; gap: 10px;
  height: 100%; min-height: 0; overflow: hidden; }
#replay-list { flex: 1 1 50%; min-height: 100px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
#replay-log { flex: 1 1 50%; min-height: 100px; display: flex; flex-direction: column; min-height: 0; }
#replay-log h3 { margin: 0 0 6px; flex: none; }
.replay-row { display: flex; align-items: center; gap: 6px; }
.replay-row .action { flex: 1; text-align: left; }
.repl-err { color: var(--pink); font-size: 11px; }
.filebtn { display: inline-block; cursor: pointer; }
/* Right column: the viewer fills the full height. Controls + meta are fixed
   on top; #rp-board is the stage — the .table inside is ZOOM-TO-FIT into it
   (fitMat sizes the sizer to the scaled footprint, scales the content to fill). */
#replay-viewer { display: flex; flex-direction: column; gap: 10px;
  height: 100%; min-height: 0; min-width: 0; }
#replay-controls { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; flex: none; }
#rp-seek { flex: 1; min-width: 120px; }
#rp-pos { font-size: 12px; color: var(--dim); min-width: 90px; }
#rp-meta { font-size: 12px; color: var(--dim); flex: none; }
#rp-board { flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden; position: relative;
  display: flex; align-items: center; justify-content: center; }
/* Replay uses the SAME card sizes as the live board (unified 120px, 2026-09-02)
   — the old per-zone 72px overrides are gone. fitMat (deterministic width)
   handles the zoom-to-fit for both. */
.rp-phase { text-align: center; color: var(--cyan); font-weight: 700; }
/* Event log: fills the bottom half of the left column, scrolls internally;
   newest entry on top (older ones scroll down out of view as it advances). */
#rp-events { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
#rp-speed-wrap { font-size: 11px; color: var(--dim); display: inline-flex; align-items: center; gap: 4px; }
#rp-speed { font-size: 11px; }

/* ---------- modal / toasts ---------- */
#cardzoom { position: fixed; inset: 0; background: rgba(5, 7, 10, .82); backdrop-filter: blur(3px);
  display: grid; place-items: center; z-index: 150; cursor: zoom-out; }
#cardzoom .card { --w: min(380px, 86vh); cursor: zoom-out; overflow-y: auto;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .8), 0 0 40px rgba(0, 240, 255, .15); }
/* readable typography at zoom scale */
#cardzoom .card-name { font-size: 17px; padding: 8px 9px 4px; }
#cardzoom .card-name small { font-size: 13px; }
#cardzoom .kws { gap: 3px; padding: 0 9px 3px; }
#cardzoom .kw { font-size: 11px; padding: 2px 5px; }
#cardzoom .tags { padding: 0 9px 8px; }
#cardzoom .tag { font-size: 11px; padding: 2px 5px; }
#cardzoom .card-zoomtext { font-size: 13.5px; line-height: 1.45; padding: 8px 10px;
  border-top: 1px solid var(--line); background: #0e1119; white-space: pre-wrap; }
#cardzoom .cz-hint { position: fixed; bottom: 20px; left: 0; right: 0; text-align: center;
  color: var(--dim); font-size: 12px; pointer-events: none; }
/* Alt+hover big-card peek (board/replay) — floating, anchored near the card,
   no backdrop: you can keep your hand on the board. pointer-events:none so
   the hover underneath survives; it closes on mouseout/Escape/Alt-up. */
#cardpeek { position: fixed; z-index: 140; width: 620px; pointer-events: none;
  display: flex; flex-direction: column; gap: 8px;
  filter: drop-shadow(0 18px 40px rgba(0, 0, 0, .8)); }
#cardpeek .peek-card { display: grid; place-items: center; }
#cardpeek .peek-card .card { --w: min(500px, calc((100vh - 150px) * 3 / 4)); cursor: default; }
#cardpeek .peek-card .card-name { font-size: 22px; }
#cardpeek .peek-card .kws { gap: 4px; }
#cardpeek .peek-card .kw { font-size: 15px; }
#cardpeek .peek-card .tag { font-size: 15px; }

#modal { position: fixed; inset: 0; background: rgba(5, 7, 10, .7); backdrop-filter: blur(4px);
  display: grid; place-items: center; z-index: 100; }
#modal-body { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 20px;
  min-width: 320px; max-width: 520px; max-height: 80vh; overflow-y: auto; box-shadow: var(--shadow);
  display: flex; flex-direction: column; gap: 8px; }
#toasts { position: fixed; bottom: 18px; right: 18px; display: flex; flex-direction: column; gap: 8px; z-index: 200; }
.toast { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px;
  font-size: 13px; box-shadow: var(--shadow); transition: opacity .4s; }
.toast.err { border-color: var(--pink); color: var(--pink); }
.toast.ok { border-color: var(--green); color: var(--green); }

@media (max-width: 980px) {
  #board-wrap, #db-wrap, #replays-wrap, .play-grid, .seat-row, .home-grid { grid-template-columns: 1fr; }
  #sidepanel { position: static; }
}

/* ---------- "card played" center flash (flow item 1, 2026-09-03) ----------
   position:absolute over the STAGE (live #board-stage / replay #rp-board are
   both position:relative, overflow:hidden) — zero layout contribution, so
   fitMat never re-scales on a play (no-flicker invariant holds). The card
   is ALREADY in its proper zone in the table; this is a ~2s transient beat
   "3d-wise above the board", then it's gone. */
.playflash { position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px; pointer-events: none;
  z-index: 30; animation: playflash 2s ease forwards; }
.playflash-card .card { --w: 240px; cursor: default;
  box-shadow: 0 26px 70px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.07); }
.playflash-card .card:hover { transform: none; box-shadow: 0 26px 70px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.07); }
/* PLAY & LAND (2026-09-07): MY unit/gear plays. Phase 1 — the card pops in
   at stage center (the familiar flash intro, no fade so it holds while it
   flies); Phase 2 — it flies to the unit's board slot (the transition is
   triggered by .land-fly; --land-x/y/scale are set from the live layout,
   post-fitMat, so the destination is exact at any zoom). The LABEL stays in
   the center and only fades with the whole wrap. */
.playflash.land .playflash-label { animation: landlabel calc(1.65s * var(--fx-scale)) ease forwards; }
@keyframes landlabel {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  55%  { opacity: 1; }
  80%  { opacity: 0; }
  100% { opacity: 0; }
}
.playflash .playflash-el.land-el {
  transform: translate(-50%, -50%) scale(1);
  /* fill-mode `backwards`, NOT `both`: with `both` the finished animation's
     final keyframe keeps overriding `transform` FOREVER (animations outrank
     normal declarations in the cascade) — that would freeze the .land-fly
     flight. `backwards` only covers the (nonexistent) delay; after the
     animation ends the element's own transform applies and the fly can run. */
  animation: landin calc(.5s * var(--fx-scale)) cubic-bezier(.2, 1.4, .4, 1) backwards;
  transition: transform calc(.68s * var(--fx-scale)) cubic-bezier(.5, .05, .9, .6), opacity calc(.25s * var(--fx-scale)) ease;
}
@keyframes landin {
  0%   { transform: translate(-50%, -50%) scale(.7) translateY(-18px); opacity: 0; }
  100% { transform: translate(-50%, -50%) scale(1) translateY(0); opacity: 1; }
}
.playflash .playflash-el.land-el.land-fly {
  transform: translate(calc(-50% + var(--land-x, 0px)), calc(-50% + var(--land-y, 0px))) scale(var(--land-scale, .5));
}
/* The card sits centered via flex; give the flier a definite centering so
   translate(-50%,-50%) is relative to its own box at the flex center. */
.playflash-card { position: relative; }
.playflash-card .playflash-el.land-el { position: absolute; left: 50%; top: 50%; }
.playflash-card { margin: 0; }
/* IMPACT: the unit squashes on landing (the mat's own shake carries the
   "earthquake" — see .mat-shake below). */
.unit.land-impact .unit-card, .lgslot.land-impact .lg-card { animation: landimpact calc(.42s * var(--fx-scale)) ease both; }
@keyframes landimpact {
  0%   { transform: scale(1.06, .92) translateY(2px); }
  30%  { transform: scale(.97, 1.03) translateY(-2px); }
  60%  { transform: scale(1.015, .99) translateY(0); }
  100% { transform: scale(1, 1) translateY(0); }
}
/* Polish (2026-09-15): a neon impact flash on landing — the card blooms cyan
   for a beat as it hits the mat (box-shadow only → composes with landimpact's
   transform and the rival-mat counter-rotation). */
.unit.land-impact .unit-card, .lgslot.land-impact .lg-card { animation: landimpact calc(.42s * var(--fx-scale)) ease both,
  landglow calc(.55s * var(--fx-scale)) ease-out both; }
@keyframes landglow {
  0%   { box-shadow: 0 0 0 1px rgba(0, 240, 255, .8), 0 0 22px 4px rgba(0, 240, 255, .5); }
  100% { box-shadow: 0 0 0 0 rgba(0, 240, 255, 0), 0 0 0 0 rgba(0, 240, 255, 0); }
}
/* MAT SHAKE (the "don't overdo it" earthquake): rides the SIZER, which has
   NO inline transform (fitMat scales the CONTENT) — so the shake never
   fights the zoom and never changes any measured size (no-flicker
   invariant). Amplitude comes from --shake-amp (set per unit might:
   impactAmp in app.js, capped at 7px) — damped, brief. */
.fit-sizer.mat-shake { animation: mattravel calc(.48s * var(--fx-scale)) cubic-bezier(.36, .07, .19, .97) both; }
@keyframes mattravel {
  0%   { transform: translate(0, 0) rotate(0deg); }
  12%  { transform: translate(calc(var(--shake-amp, 3px) * .8), calc(var(--shake-amp, 3px) * .5)) rotate(calc(var(--shake-amp, 3px) * .045deg)); }
  28%  { transform: translate(calc(var(--shake-amp, 3px) * -.7), calc(var(--shake-amp, 3px) * .35)) rotate(calc(var(--shake-amp, 3px) * -.04deg)); }
  44%  { transform: translate(calc(var(--shake-amp, 3px) * .5), calc(var(--shake-amp, 3px) * -.28)) rotate(calc(var(--shake-amp, 3px) * .03deg)); }
  60%  { transform: translate(calc(var(--shake-amp, 3px) * -.32), calc(var(--shake-amp, 3px) * -.16)) rotate(calc(var(--shake-amp, 3px) * -.02deg)); }
  76%  { transform: translate(calc(var(--shake-amp, 3px) * .18), calc(var(--shake-amp, 3px) * .08)) rotate(calc(var(--shake-amp, 3px) * .012deg)); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .playflash .playflash-el.land-el, .fit-sizer.mat-shake,
  .unit.land-impact .unit-card, .lgslot.land-impact .lg-card,
  .staged-card, .staged-card.materialize, .staged-card.flying, .sc-inner,
  .rise-card, .hand-back { animation: none; transition: none; }
  .pack-flash { display: none; }
}
/* ── PLAY & LAND v68: the CONTINUOUS CARD ────────────────────────────────
   The staged card (the drag ghost, or a spawned clone for rival/replay) sits
   at the STANDARDIZED staging spot (the stage's viewport center) while its
   costs are paid, then FLIES to the real board slot. It is a BODY-level
   position:fixed overlay — NEVER in the board tree — so fitMat's measured
   sizes are untouched (the no-flicker invariant holds). The in-hand original
   stays in the layout (opacity 0 via .drag-out) so the hand fan doesn't
   reflow either. */
.drag-out { opacity: 0 !important; pointer-events: none !important; }
.staged-card {
  position: fixed; left: 0; top: 0; z-index: 120; pointer-events: none;
  --w: 160px;
  box-shadow: 0 22px 54px rgba(0,0,0,.75), 0 0 0 1px rgba(255,255,255,.08);
  will-change: left, top, transform;
}
/* A fresh clone (rival / replay / menu play) pops in at the staging spot. */
.staged-card.materialize { animation: staged-in calc(.18s * var(--fx-scale)) ease both; }
@keyframes staged-in {
  0%   { opacity: 0; transform: scale(.8); }
  100% { opacity: 1; transform: scale(1); }
}
/* Holding while the pay phase runs: a soft breathing glow ("this is the card
   you're paying for — it's waiting"). */
.staged-card.holding { animation: staged-hold 1.5s ease-in-out infinite; }
@keyframes staged-hold {
  0%, 100% { box-shadow: 0 22px 54px rgba(0,0,0,.75), 0 0 0 1px rgba(255,255,255,.08), 0 0 18px rgba(252,238,10,.22); }
  50%      { box-shadow: 0 22px 54px rgba(0,0,0,.75), 0 0 0 1px rgba(255,255,255,.08), 0 0 30px rgba(252,238,10,.5); }
}
/* Glide from the cursor drop point to the staging spot (drag→staged). */
.staged-card.glide { transition: left calc(.3s * var(--fx-scale)) cubic-bezier(.4, 0, .2, 1), top calc(.3s * var(--fx-scale)) cubic-bezier(.4, 0, .2, 1); }
/* THE FLIGHT: left/top/transform transition to the slot; the JS sets the
   destination + scale, the CSS animates the 600ms ease-out. */
.staged-card.flying { transition: left calc(.6s * var(--fx-scale)) cubic-bezier(.5, .05, .85, .55),
  top calc(.6s * var(--fx-scale)) cubic-bezier(.5, .05, .85, .55), transform calc(.6s * var(--fx-scale)) cubic-bezier(.5, .05, .85, .55); }
/* v69: the MTG-Arena flip — the staged card is a 3D flipper (outer keeps the
   left/top/transform flight transitions; .sc-inner holds the 180° rotation
   transition). Two absolutely-stacked faces, backface hidden: the FACE (art)
   and the BACK (official card back). `.sc-flipped` turns the inner so the
   face lands. The outer's flight transform (scale) is unaffected because the
   flip lives on the INNER element. */
.staged-card.sc-flip { perspective: 900px; }
.sc-inner { position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d; transition: transform calc(.45s * var(--fx-scale)) cubic-bezier(.4, .1, .3, 1); }
.staged-card.sc-flipped .sc-inner { transform: rotateY(180deg); }
.sc-face, .sc-back { position: absolute; inset: 0; backface-visibility: hidden;
  border-radius: 8px; overflow: hidden; background: #0a0c11; }
.sc-face { transform: rotateY(180deg); }
.sc-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sc-back { background: url('/art/back.png') center / 100% 100% no-repeat #111; }
/* The ETB beat: a small copy of the unit rises from the landed slot to the
   pending zone. .rise-card is body-level fixed; left/top/transform are
   transitioned (armed by a reflow in riseCopy), then it fades out. */
.rise-card { position: fixed; z-index: 122; pointer-events: none; aspect-ratio: 3/4;
  transition: left calc(.55s * var(--fx-scale)) cubic-bezier(.45, .05, .55, .95), top calc(.55s * var(--fx-scale)) cubic-bezier(.45, .05, .55, .95),
    transform calc(.55s * var(--fx-scale)) cubic-bezier(.45, .05, .55, .95), opacity calc(.4s * var(--fx-scale)) ease;
  box-shadow: 0 10px 26px rgba(0,0,0,.7); animation: rise-in calc(.5s * var(--fx-scale)) ease both; }
@keyframes rise-in { 0% { opacity: 0; } 30% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; } }
.rise-card .card { width: 100%; height: 100%; --w: 100%; box-shadow: none; border-radius: 8px; overflow: hidden; }
/* v69 "materialize on impact": hide the freshly-landed unit until the flying
   card covers its slot (the state is already updated; this is presentation).
   Opacity-only — no layout change, no flicker. */
.unit.land-veil .unit-card, .unit.land-veil .might-badge, .unit.land-veil .equipped { opacity: 0 !important; }
/* v69 → v73 (2026-09-08): opponent hand = a ROW of card BACKS fanned exactly
   like the own hand (fitCardFan sets the per-back marginLeft inline — 128px
   step, compressing to the 34px floor; the zone is overflow:hidden so a huge
   hand never resizes the mat). The old -84px overlap + centered pile made the
   rival hand look structurally different from the own hand. */
.hand-back { width: 120px; aspect-ratio: 3/4; flex: none;
  box-shadow: 0 6px 14px rgba(0,0,0,.5); }
.hand-more { flex: none; align-self: center; margin-left: 6px; font-weight: 800; color: var(--dim); font-size: 14px; }
.playflash-label { font-size: 15px; font-weight: 700; letter-spacing: .06em; color: #fff;
  text-shadow: 0 2px 12px rgba(0,0,0,.95); }
@keyframes playflash {
  0%   { opacity: 0; transform: translateY(-22px) scale(.85); }
  12%  { opacity: 1; transform: translateY(0) scale(1); }
  80%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(6px) scale(.98); }
}
/* PERSISTENT CARD STAGE (1.10.0, 2026-09-06) — the shared surface for
   search beats and deck-trash beats. Supersedes the old 2s trashflash:
   same stage overlay (absolute over #board-stage / #rp-board, zero layout
   contribution → no fitMat re-scale, no-flicker invariant), but it
   PERSISTS — no auto-dismiss — until the next step clears it or × closes
   it. A centered, auto-sized panel (NOT a full-bleed overlay): the board
   stays visible and interactive around it. Grid: 5 columns, 3 visible
   rows, scrolls beyond (max-height = exactly 3 card rows). Highlighted
   cells = a search's legal targets; pickable cells = clickable (send the
   choose). Alt+hover on any face-up cell = the usual big-card peek (the
   cells' .card elements carry data-cardid; openPeek/trackPointer include
   `.card-stage .card`). Mystery cells render the cardback (CR 5.5.4 — the
   rival sees faces redacted to backs). */
.card-stage { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 35; display: flex; flex-direction: column; gap: 8px;
  /* width: % resolves against #board-stage (the abs-positioned stage's
     containing block — definite). NO custom property carries this % into
     other elements: a %-value substituted from a var() re-resolves against
     the USE-SITE's containing block (the cell), which collapsed the cards
     to ~18px thumbnails (probe-110 catch, 2026-09-06). */
  width: min(660px, calc(100% - 24px)); max-height: calc(100% - 32px);
  background: rgba(8,10,14,.93); border: 1px solid rgba(255,255,255,.15);
  border-radius: 12px; padding: 12px 14px; pointer-events: auto; overflow: hidden;
  box-shadow: 0 26px 70px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.05);
  animation: cardstage-in calc(.35s * var(--fx-scale)) ease both; }
@keyframes cardstage-in {
  0%   { opacity: 0; transform: translate(-50%, calc(-50% - 14px)) scale(.94); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
.card-stage-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.card-stage-title { font-size: 15px; font-weight: 700; letter-spacing: .04em; color: #fff; }
.card-stage-count { font-size: 11px; color: var(--dim); border: 1px solid rgba(255,255,255,.2);
  border-radius: 999px; padding: 1px 8px; }
.card-stage-note { font-size: 12px; color: var(--dim); flex: 1 1 100%; }
.card-stage-skip, .card-stage-x { margin-left: auto; font: inherit; font-size: 12px; font-weight: 700;
  color: var(--dim); background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.18);
  border-radius: 8px; padding: 3px 10px; cursor: pointer; }
.card-stage-skip:hover, .card-stage-x:hover { color: #fff; background: rgba(255,255,255,.14); }
.card-stage-x { margin-left: 6px; padding: 3px 8px; }
/* 5 columns × 3 VISIBLE rows; scrolls beyond. Track width = (grid width −
   4 gaps) / 5 — the % resolves against the grid's own content box (a
   definite block child of the fixed-width panel). Card = 100% of its cell.
   max-height = 3 card rows + 2 gaps at the 660px design width (track 120 →
   card 160 → 3×160+16 = 496). NO % here: a %-max-height resolves against
   the panel's AUTO height → 0, which collapsed the grid (probe-110 catch).
   Narrower windows shrink the cards (fewer than 3 rows may fit fully —
   still scrolls; the panel's own max-height clamps short stages). */
.card-stage-grid { display: grid; grid-template-columns: repeat(5, calc((100% - 32px) / 5));
  gap: 8px;
  max-height: 496px; overflow-y: auto; }
.card-stage-grid::-webkit-scrollbar { width: 8px; }
.card-stage-grid::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); border-radius: 4px; }
.card-stage-cell { display: grid; place-items: center; min-width: 0; }
/* the mystery back: the global .card.hidden dims (grayscale) unknown
   FACES — a cardback is already face-down, so un-dim it in the stage. */
.card-stage-cell .card.hidden { filter: none; }
.card-stage-cell .card { --w: 100%; cursor: default;
  box-shadow: 0 10px 26px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.06); }
.card-stage-cell .card:hover { transform: none; box-shadow: 0 14px 30px rgba(0,0,0,.7), 0 0 0 1px rgba(255,255,255,.1); }
/* a search's LEGAL target — a static neon glow (the "valid target" beat) */
.card-stage-cell.highlight .card { box-shadow: 0 0 0 2px var(--cyan, #29e6ff), 0 0 18px rgba(41,230,255,.55), 0 10px 26px rgba(0,0,0,.6); }
/* the seat that answers gets a pulsing affordance on the same cells */
.card-stage-cell.pickable .card { cursor: pointer; animation: cardstage-pick 1.6s ease-in-out infinite; }
.card-stage-cell.pickable.highlight .card { animation: cardstage-pick 1.6s ease-in-out infinite; }
@keyframes cardstage-pick {
  0%, 100% { box-shadow: 0 0 0 2px var(--cyan, #29e6ff), 0 0 10px rgba(41,230,255,.4), 0 10px 26px rgba(0,0,0,.6); }
  50%      { box-shadow: 0 0 0 3px var(--cyan, #29e6ff), 0 0 26px rgba(41,230,255,.85), 0 10px 26px rgba(0,0,0,.6); } }
.card-stage-empty { font-size: 12px; color: var(--dim); grid-column: 1 / -1; padding: 8px 2px; text-align: center; }
/* TRASH-TARGET FAN (2026-09-03, flow item 2) — when a suspended choose's
   candidates live in the TRASH (V - Streetkid's "add a Braindance from trash
   to your hand"), the trash PILE on the mat is a count-only box (no on-mat
   card elements), so resolveTargetEl would mis-aim at the deck. This overlay
   lays the fan of candidate cards OUT of the trash pile, side by side, with
   each card carrying data-uid → resolveTargetEl finds them and the fx layer
   draws the target arrow / valid-target pulse onto them (the "trash view
   pops up, valid targets highlighted" beat). On confirm (the recorded
   choose step) the card leaves the trash (card_to_hand) and the fan closes.
   It is part of the fx-layer (pointer-events:none, zero layout contribution)
   so it never re-scales the mat — live + replay identical (same renderFxLayer
   path, rebuilt from state each frame). */
.fx-trashfan { position: absolute; z-index: 45; pointer-events: none;
  display: flex; align-items: center; justify-content: center; gap: 12px;
  padding: 8px; background: rgba(6, 8, 12, .82); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: 0 18px 40px rgba(0,0,0,.7);
  animation: trashfan-in calc(.3s * var(--fx-scale)) ease both; }
.fx-trashfan .card { --w: 96px; cursor: default; }
/* Choose mode: the fanned cards ARE the targets — clickable (the fan lives
   in the pointer-events:none fx layer, so re-enable on the cards only).
   Green pulse comes from the shared #board-content .ix-cap rule. */
.fx-trashfan.pickable .tf-card.ix-choose-target { pointer-events: auto; cursor: pointer; }
.fx-trashfan .tf-tag { position: absolute; top: -9px; left: 8px; font-size: 9px;
  font-weight: 800; letter-spacing: 1px; text-transform: uppercase; color: var(--neon);
  background: #0b1220; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--line); }
@keyframes trashfan-in {
  0%   { opacity: 0; transform: translateY(10px) scale(.94); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

/* ═══════════════════════════════════════════════════════════════════════════ */
/* INTERACTION LAYER — MTG-Arena-style gestures (2026-09-05) */
/* ═══════════════════════════════════════════════════════════════════════════ */
/* Every LEGAL element pulses (green = action, the established affordance */
/* color from the steal/valid-target FX). While a gesture is in flight the */
/* relevant elements hot up (brighter) and drop zones glow. */

@keyframes ix-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(72, 255, 155, 0.95), 0 0 14px 3px rgba(72, 255, 155, 0.6); }
  50%      { box-shadow: 0 0 0 3px rgba(72, 255, 155, 0.5),  0 0 5px 1px rgba(72, 255, 155, 0.25); }
}
/* Kill the browser's native image/text drag on the board (the game
   implements its own pointer-based drag): -webkit-user-drag:none covers
   Chrome/Firefox, draggable="false" on the <img> + dragstart preventDefault
   in app.js cover the rest. */
#board-content img { -webkit-user-drag: none; user-select: none; }
#board-content .card { user-select: none; -webkit-user-select: none; }
#board-content .ix-cap {
  animation: ix-pulse 1.1s ease-in-out infinite;
  cursor: pointer;
  border-radius: 4px;
  position: relative;
  z-index: 4;
}
/* Dice are CLIPPED (clip-path polygon = the die's polyhedron silhouette) —
   a box-shadow gets clipped away with them. filter: drop-shadow() is applied
   AFTER clipping (from the clipped silhouette) → the correct glow for dice. */
#board-content .die.ix-cap {
  animation: ix-pulse-die 1.1s ease-in-out infinite;
}
@keyframes ix-pulse-die {
  0%, 100% { filter: drop-shadow(0 0 7px rgba(72, 255, 155, 0.98)) drop-shadow(0 0 3px rgba(72, 255, 155, 0.95)); }
  50%      { filter: drop-shadow(0 0 3px rgba(72, 255, 155, 0.55)); }
}
#board-content .ix-cap.ix-hot {
  animation: none;
  box-shadow: 0 0 0 2px rgba(72, 255, 155, 1), 0 0 16px 3px rgba(72, 255, 155, 0.7);
}
/* SELL affordance (2026-09-10): a Sell-Tag card in hand pulses GOLDEN when
   selling is its only legal action, and SPLIT — left half BLUE (playable),
   right half gold (sellable) — when both apply. Hand cards use blue for the
   playable glow (units/legends/dice stay green). */
#board-content .hand .card.ix-cap { overflow: visible; }
#board-content .hand .card.ix-cap { animation: ix-pulse-blue 1.1s ease-in-out infinite; }
@keyframes ix-pulse-blue {
  0%, 100% { box-shadow: 0 0 0 2px rgba(77, 159, 255, 0.95), 0 0 14px 3px rgba(77, 159, 255, 0.6); }
  50%      { box-shadow: 0 0 0 3px rgba(77, 159, 255, 0.5),  0 0 5px 1px rgba(77, 159, 255, 0.25); }
}
#board-content .hand .card.ix-sell {
  animation: ix-pulse-sell 1.1s ease-in-out infinite;
}
@keyframes ix-pulse-sell {
  0%, 100% { box-shadow: 0 0 0 2px rgba(255, 210, 74, 0.95), 0 0 14px 3px rgba(255, 210, 74, 0.6); }
  50%      { box-shadow: 0 0 0 3px rgba(255, 210, 74, 0.5),  0 0 5px 1px rgba(255, 210, 74, 0.25); }
}
/* Both playable AND sellable → split glow: left half BLUE (playable), right
   half GOLD (sellable). box-shadow can't do a true two-tone border (a ±2px
   offset just blurs into one color on a 108px thumb) — so the base pulse
   stays blue and two pseudo-rings, each clipped to its own half, overlay the
   gold right edge (2026-09-10). */
/* Both playable AND sellable → SPLIT glow (2026-09-10): left reads BLUE
   (playable), right reads GOLD (sellable). box-shadow can't do a true
   two-tone border, so the technique is: the BLUE base pulse is promoted to
   higher specificity than the gold .ix-sell rule (which would otherwise win
   at equal specificity and make the whole frame uniform gold), then a gold
   ring clipped to the RIGHT half overlays it. Left edge + left halves of the
   top/bottom borders read blue; right edge + right halves read gold. */
#board-content .hand .card.ix-cap.ix-sell { position: relative; animation: ix-pulse-blue 1.1s ease-in-out infinite; }
/* The gold side of the split — a ring clipped to the RIGHT half, overlaid on
   the blue base pulse. */
#board-content .hand .card.ix-cap.ix-sell::after {
  content: '';
  position: absolute;
  inset: -4px;
  border: 2px solid rgba(255, 210, 74, 0.95);
  border-radius: inherit;
  pointer-events: none;
  clip-path: inset(-8px -8px -8px 50%);
  animation: ix-split-pulse 1.1s ease-in-out infinite;
}
@keyframes ix-split-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}
/* Steal-step die (CR 9.23.3.3): a STATIC strong glow, not the idle
   ix-pulse-die animation (it dips to 0.55 alpha — read as "no highlight"
   on the small clipped dice, 2026-09-06). box-shadow is clipped away by
   the die's clip-path → drop-shadow (applied after clipping). */
#board-content .die.ix-hot {
  animation: none;
  filter: drop-shadow(0 0 8px rgba(72, 255, 155, 1)) drop-shadow(0 0 3px rgba(72, 255, 155, 1));
}
#board-content .ix-paysrc {
  animation: none;
  box-shadow: 0 0 0 2px rgba(255, 210, 74, 0.95), 0 0 12px 2px rgba(255, 210, 74, 0.55);
  cursor: pointer;
}
#board-content .ix-paysrc.ix-hot {
  box-shadow: 0 0 0 3px rgba(255, 210, 74, 1), 0 0 18px 4px rgba(255, 210, 74, 0.8);
  transform: scale(1.04);
}
/* drop zones while dragging a hand card */
#board-content .ix-drop {
  outline: 1px dashed rgba(72, 255, 155, 0.5);
  outline-offset: -3px;
  border-radius: 4px;
}
#board-content .ix-drop:hover {
  outline: 2px solid rgba(72, 255, 155, 0.9);
  background: rgba(72, 255, 155, 0.06);
}

/* PUZZLE board affordances (2026-09-22, t_aab58775): the SAME gesture layer
   drives #pz-content as it does #board-content — but every live glow rule above
   is scoped to #board-content, so #pz-content gets its own self-contained set
   (reusing the same @keyframes). Without these the .ix-cap class applied by
   applyAffordances() would be INVISIBLE in the puzzle board (acceptance
   criterion 1: "legal elements glow"). Mirrors the #board-content rules one-for-
   one. Scenario build (#sc-content) has no such set — that's an inherited gap,
   not something this task regresses or fixes. */
#pz-content .ix-cap {
  animation: ix-pulse 1.1s ease-in-out infinite;
  cursor: pointer;
  border-radius: 4px;
  position: relative;
  z-index: 4;
}
/* Dice are CLIPPED (clip-path = the polyhedron silhouette) — box-shadow gets
   clipped away with them; drop-shadow applies AFTER clipping → correct glow. */
#pz-content .die.ix-cap { animation: ix-pulse-die 1.1s ease-in-out infinite; }
#pz-content .ix-cap.ix-hot {
  animation: none;
  box-shadow: 0 0 0 2px rgba(72, 255, 155, 1), 0 0 16px 3px rgba(72, 255, 155, 0.7);
}
/* Hand cards glow BLUE when playable (units/legends/dice stay green). */
#pz-content .hand .card.ix-cap { overflow: visible; }
#pz-content .hand .card.ix-cap { animation: ix-pulse-blue 1.1s ease-in-out infinite; }
/* SELL affordance: golden pulse when selling is legal. */
#pz-content .hand .card.ix-sell { animation: ix-pulse-sell 1.1s ease-in-out infinite; }
/* Both playable AND sellable → split glow: left BLUE (play), right GOLD (sell). */
#pz-content .hand .card.ix-cap.ix-sell { position: relative; animation: ix-pulse-blue 1.1s ease-in-out infinite; }
#pz-content .hand .card.ix-cap.ix-sell::after {
  content: '';
  position: absolute;
  inset: -4px;
  border: 2px solid rgba(255, 210, 74, 0.95);
  border-radius: inherit;
  pointer-events: none;
  clip-path: inset(-8px -8px -8px 50%);
  animation: ix-split-pulse 1.1s ease-in-out infinite;
}
#pz-content .die.ix-hot {
  animation: none;
  filter: drop-shadow(0 0 8px rgba(72, 255, 155, 1)) drop-shadow(0 0 3px rgba(72, 255, 155, 1));
}
/* Payment sources (Eddie stack / Legend) — golden, tap to pay. */
#pz-content .ix-paysrc {
  animation: none;
  box-shadow: 0 0 0 2px rgba(255, 210, 74, 0.95), 0 0 12px 2px rgba(255, 210, 74, 0.55);
  cursor: pointer;
}
#pz-content .ix-paysrc.ix-hot {
  box-shadow: 0 0 0 3px rgba(255, 210, 74, 1), 0 0 18px 4px rgba(255, 210, 74, 0.8);
  transform: scale(1.04);
}
/* Drop zones while dragging a hand card. */
#pz-content .ix-drop {
  outline: 1px dashed rgba(72, 255, 155, 0.5);
  outline-offset: -3px;
  border-radius: 4px;
}
#pz-content .ix-drop:hover {
  outline: 2px solid rgba(72, 255, 155, 0.9);
  background: rgba(72, 255, 155, 0.06);
}

/* the card ghost that follows the cursor while dragging */
.ix-ghost { opacity: 0.92; transform: scale(1.03) rotate(-2deg); filter: drop-shadow(0 8px 18px rgba(0,0,0,.6)); }

/* cursor-follow arrow (attack target / choose target) */
.ix-arrow {
  position: fixed; left: 0; top: 0; z-index: 95; pointer-events: none;
  overflow: visible;
}
.ix-arrow .ix-line { stroke-width: 3; stroke-linecap: round; opacity: 0.9; }
.ix-arrow .ix-head { stroke: none; }
body.ix-attack .ix-arrow .ix-line, body.ix-attack .ix-arrow .ix-head { /* set inline */ }

/* action menu (a card with several legal actions) */
#ix-menu {
  position: fixed; z-index: 96; min-width: 190px; max-width: 260px;
  background: #101a2c; border: 1px solid rgba(72, 255, 155, 0.5);
  border-radius: 8px; padding: 6px; display: flex; flex-direction: column; gap: 4px;
  box-shadow: 0 10px 30px rgba(0,0,0,.6);
}
#ix-menu .ix-menu-head { font-size: 11px; font-weight: 600; color: #9fd8b4; padding: 2px 4px 4px; border-bottom: 1px solid rgba(255,255,255,.08); }
#ix-menu .ix-menu-opt {
  text-align: left; background: #182338; color: #e8f0ff; border: 1px solid rgba(255,255,255,.1);
  border-radius: 6px; padding: 7px 9px; font-size: 13px; cursor: pointer;
}
#ix-menu .ix-menu-opt:hover { background: #1f2e49; border-color: rgba(72, 255, 155, 0.6); }
#ix-menu .ix-menu-note {
  padding: 8px 12px; font-size: 12px; color: #ffd27a;
  border-top: 1px solid rgba(255,255,255,.08);
}
#ix-menu .ix-menu-x { align-self: flex-end; background: none; border: none; color: #8fa3c8; font-size: 15px; cursor: pointer; padding: 0 4px; }
/* Ability text is long — let the menu option wrap + cap its height (the card
   already shows the same text on hover, so this is the commit label). */
#ix-menu .ix-menu-opt { white-space: normal; line-height: 1.35; max-height: 132px; overflow-y: auto; }

/* "you may …" decision window (2026-09-07): a centered Do/Don't popup, styled
   like the ability menu but modal-centered. Do = green (proceed to targeting),
   Don't = muted (decline). */
#may-window {
  position: fixed; z-index: 97; left: 50%; top: 42%;
  transform: translate(-50%, -50%);
  min-width: 220px; max-width: 320px;
  background: #101a2c; border: 1px solid rgba(72, 255, 155, 0.5);
  border-radius: 10px; padding: 12px 14px;
  box-shadow: 0 12px 40px rgba(0,0,0,.65);
  animation: may-in .18s ease;
}
@keyframes may-in { from { opacity: 0; transform: translate(-50%, -46%) scale(.96); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
#may-window .may-head { font-size: 12px; font-weight: 700; color: #9fd8b4; margin-bottom: 4px; }
#may-window .may-q { font-size: 15px; color: #e8f0ff; margin-bottom: 12px; }
#may-window .may-btns { display: flex; gap: 10px; }
#may-window .may-do, #may-window .may-dont {
  flex: 1; padding: 9px 0; font-size: 15px; font-weight: 800; border-radius: 7px;
  cursor: pointer; border: 1px solid transparent;
}
#may-window .may-do { background: rgba(72, 255, 155, 0.16); border-color: rgba(72, 255, 155, 0.6); color: #7dffc4; }
#may-window .may-do:hover { background: rgba(72, 255, 155, 0.28); }
#may-window .may-dont { background: #1b2740; border-color: rgba(255,255,255,.14); color: #9fb0d0; }
#may-window .may-dont:hover { background: #233150; color: #c7d3ea; }
@media (prefers-reduced-motion: reduce) { #may-window { animation: none; } }

/* midbar: minimal now (End Turn / Concede / windows) */
#midbar { padding: 2px 4px; }
#midbar #actions { justify-content: flex-start; }
#midbar .promptbox { font-size: 12.5px; }

/* =====================================================================
   SCENARIO BUILDER (docs/SCENARIO-BUILDER.md) — v86/v78
   Sidebar (browser + controls) | zoom-to-fit board stage, same plumbing
   as the replay viewer (#sc-board / #sc-sizer / #sc-content).
   ===================================================================== */
#sc-wrap { display: grid; grid-template-columns: 340px 1fr; gap: 12px; height: 100%; min-height: 0; }
#sc-sidebar { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px;
  display: flex; flex-direction: column; gap: 10px; min-height: 0; }
#sc-tabs { display: flex; gap: 6px; }
#sc-tabs button { flex: 1; padding: 7px 0; border-radius: 8px; background: rgba(255,255,255,.05);
  color: var(--fg); cursor: pointer; font-weight: 700; letter-spacing: .04em; }
#sc-tabs button.on { background: var(--cyan); color: #04121a; }
#sc-controls { display: flex; flex-direction: column; gap: 8px; }
#sc-controls .sc-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
#sc-controls .sc-row label { font-size: 12px; color: var(--dim); }
#sc-controls button, #sc-controls select { font-size: 12px; padding: 5px 8px; border-radius: 7px;
  background: rgba(255,255,255,.06); color: var(--fg); cursor: pointer; border: 1px solid var(--line); }
#sc-controls button:hover { background: rgba(255,255,255,.12); }
#sc-controls button.on { background: var(--green); color: #04121a; border-color: transparent; }
#sc-controls input[type="text"], #sc-controls input:not([type]) { font-size: 12px; padding: 5px 8px;
  border-radius: 7px; background: rgba(0,0,0,.3); color: var(--fg); border: 1px solid var(--line); flex: 1; min-width: 60px; }
#sc-controls .sc-toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; cursor: pointer; user-select: none; }
#sc-browser { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--line); padding-top: 10px; }
#sc-q { font-size: 13px; padding: 7px 10px; border-radius: 8px; background: rgba(0,0,0,.3);
  color: var(--fg); border: 1px solid var(--line); width: 100%; box-sizing: border-box; }
#sc-cards { flex: 1 1 auto; min-height: 0; overflow-y: scroll; overflow-x: hidden;
  /* grid-auto-rows MUST be max-content: with the default `auto`, Chrome
     collapses each implicit row to ~2px in this scroll container (the card's
     aspect-ratio image contributes no auto-min-size) — every card renders as
     a thin sliver. (user report 2026-09-14; deckbuilder grid is unaffected.) */
  display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 92px));
  grid-auto-rows: max-content;
  justify-content: start; gap: 8px; align-content: start; }
#sc-board { position: relative; min-height: 0; min-width: 0; overflow: hidden;
  display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; }
/* Zoom-to-fit stage (table) — fills the space ABOVE the midbar. Same role as
   #board-stage (2026-09-22, t_bc5b7b23). */
#sc-stage { position: relative; flex: 1; min-height: 0; min-width: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center; }
/* Control strip BELOW the table — mirrors live #midbar (fixed 84px height,
   buttons scroll inside). Same .action/.promptbox/.phase classes as live. */
#sc-midbar { display: flex; flex-direction: column; gap: 6px; padding: 2px 4px;
  height: 84px; overflow: hidden; border-top: 1px solid var(--line); }
#sc-midbar #sc-prompt { flex: none; }
#sc-midbar #sc-actions { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 6px; }
#sc-phase-info { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#sc-phase { margin: 0 4px 4px; font-size: 12.5px; color: var(--dim); }
/* drop affordances (native HTML5 DnD — the builder's own gesture layer,
   independent of the live game's pointer state machine) */
#sc-content [data-sc-zone] { position: relative; }
#sc-content [data-sc-zone].sc-dropover { outline: 2px dashed var(--cyan); outline-offset: 2px; border-radius: 8px; }
#sc-cards .card { cursor: grab; }
#sc-cards .card:active { cursor: grabbing; }
/* card inspector (build mode) */
#sc-inspect { border-top: 1px solid var(--line); padding-top: 10px; display: none; flex-direction: column; gap: 8px; max-height: 46%; overflow-y: auto; }
#sc-inspect.open { display: flex; }
#sc-inspect h4 { margin: 0; font-size: 13px; }
#sc-inspect .sc-igrid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
#sc-inspect button, #sc-inspect select { font-size: 12px; padding: 4px 7px; border-radius: 6px;
  background: rgba(255,255,255,.06); color: var(--fg); cursor: pointer; border: 1px solid var(--line); }
#sc-inspect button:hover { background: rgba(255,255,255,.14); }
#sc-sanity { font-size: 12px; line-height: 1.5; }
#sc-sanity .ok { color: var(--green); }
#sc-sanity .warn { color: var(--amber, #ffd24a); }
#sc-handoff { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 30;
  background: rgba(0,0,0,.82); border: 1px solid var(--cyan); color: var(--fg); padding: 8px 16px;
  border-radius: 10px; font-weight: 700; pointer-events: none; white-space: nowrap; }
/* action lists (build + play surfaces) */
#sc-controls .sc-acts-head { font-size: 11.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .05em; margin-top: 2px; }
/* "whose move is it" indicator — highlighted so the user sees WHY only the
 * defender's react_pass (or the attacker's steal pick) is offered mid-attack. */
#sc-controls .sc-move-ind { text-transform: none; letter-spacing: 0; color: var(--cyan);
  background: rgba(0,240,255,.08); border: 1px solid var(--cyan); border-radius: 7px;
  padding: 6px 9px; margin-top: 6px; font-weight: 700; }
#sc-controls .sc-acts { display: flex; flex-direction: column; gap: 4px; max-height: 34vh; overflow-y: auto; padding-right: 2px; }
#sc-controls .sc-acts button { text-align: left; font-size: 12px; padding: 6px 9px; border-radius: 7px;
  background: rgba(0,240,255,.06); color: var(--fg); cursor: pointer; border: 1px solid var(--line); }
#sc-controls .sc-acts button:hover { background: rgba(0,240,255,.14); border-color: var(--cyan); }
/* Secondary action list (2026-09-22, t_bc5b7b23): the board is the PRIMARY
   surface now; this collapsed "or pick from list" fallback is visually demoted
   — smaller, dimmer, tucked under a <details> toggle. */
#sc-controls .sc-acts-fallback { margin-top: 8px; border: 1px dashed var(--line);
  border-radius: 8px; padding: 4px 8px; }
#sc-controls .sc-acts-fallback summary { font-size: 11px; color: var(--dim);
  cursor: pointer; user-select: none; list-style-position: inside; }
#sc-controls .sc-acts-fallback[open] summary { margin-bottom: 4px; }
#sc-controls .sc-acts-fallback .sc-acts button { font-size: 11.5px; padding: 5px 8px; opacity: .85; }
#sc-controls button.big { flex: 1; padding: 9px 0; font-weight: 800; letter-spacing: .06em;
  background: rgba(72,255,155,.12); border-color: var(--green); color: var(--green); }
#sc-controls button.big:hover { background: rgba(72,255,155,.22); }
/* pending-choice / play-mode overlay on the stage (positioned inside #sc-content,
   above the table; pointer-events on so its buttons are clickable) */
#sc-overlay { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); z-index: 25;
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: center; max-width: 92%;
  background: rgba(0,0,0,.85); border: 1px solid var(--cyan); border-radius: 10px; padding: 8px 12px; }
#sc-overlay .ov-head { font-size: 12.5px; font-weight: 700; color: var(--cyan); }
#sc-overlay button, #sc-sanity button { font-size: 12px; padding: 5px 10px; border-radius: 7px;
  background: rgba(255,255,255,.08); color: var(--fg); cursor: pointer; border: 1px solid var(--line); }
#sc-overlay button:hover { background: rgba(0,240,255,.16); border-color: var(--cyan); }
#sc-overlay .hint { font-size: 11px; color: var(--dim); }
/* click-to-add / die inspector mini menu (body-level, fixed) */
#sc-mini-menu { position: fixed; z-index: 200; min-width: 220px; max-height: 70vh; overflow-y: auto;
  background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 6px;
  display: flex; flex-direction: column; gap: 3px; box-shadow: var(--shadow); }
#sc-mini-menu .mm-head { font-size: 12px; color: var(--dim); padding: 4px 6px; border-bottom: 1px solid var(--line); margin-bottom: 3px; }
#sc-mini-menu button { font-size: 12.5px; text-align: left; padding: 6px 9px; border-radius: 6px;
  background: transparent; color: var(--fg); cursor: pointer; border: 1px solid transparent; }
#sc-mini-menu button:hover { background: rgba(0,240,255,.12); border-color: var(--line); }
#sc-err { color: var(--red); font-size: 13px; padding: 8px; }
@media (max-width: 900px) { #sc-wrap { grid-template-columns: 1fr; } #sc-board { min-height: 60vh; } }

/* ---- scenario builder v87 additions -------------------------------------- */
/* #4 anchored build-mode context menu — same design as the live #ix-menu. */
#sc-card-menu {
  position: fixed; z-index: 96; min-width: 190px; max-width: 260px;
  background: #101a2c; border: 1px solid rgba(72, 255, 155, 0.5);
  border-radius: 8px; padding: 6px; display: flex; flex-direction: column; gap: 4px;
  box-shadow: 0 10px 30px rgba(0,0,0,.6);
}
#sc-card-menu .ix-menu-head { font-size: 11px; font-weight: 600; color: #9fd8b4; padding: 2px 4px 4px; border-bottom: 1px solid rgba(255,255,255,.08); }
#sc-card-menu .ix-menu-head small { color: var(--dim); font-weight: 400; }
#sc-card-menu .ix-menu-opt {
  text-align: left; background: #182338; color: #e8f0ff; border: 1px solid rgba(255,255,255,.1);
  border-radius: 6px; padding: 7px 9px; font-size: 13px; cursor: pointer; white-space: normal; line-height: 1.25;
}
#sc-card-menu .ix-menu-opt:hover { background: #1f2e49; border-color: rgba(72, 255, 155, 0.6); }
#sc-card-menu .ix-menu-opt.on { border-color: rgba(72, 255, 155, 0.8); background: #14301f; }
#sc-card-menu .ix-menu-x { align-self: flex-end; background: none; border: none; color: #8fa3c8; font-size: 15px; cursor: pointer; padding: 0 4px; }
/* #3 Eddie count stepper, overlaid on the Eddies zone (build mode). */
.sc-eddie-step {
  display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;
  background: rgba(10, 16, 28, .85); border: 1px solid var(--line); border-radius: 999px; padding: 2px 6px;
}
.sc-eddie-step button {
  width: 20px; height: 20px; line-height: 1; border-radius: 50%; border: 1px solid var(--line);
  background: #182338; color: var(--fg); cursor: pointer; font-size: 14px; padding: 0;
}
.sc-eddie-step button:hover { border-color: var(--green); color: var(--green); }
.sc-eddie-step .cnt { min-width: 14px; text-align: center; font-size: 12px; color: var(--fg); }
/* #5 zone-browse header row (deck/trash browse in the left panel). */
#sc-zonebar {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 6px 8px; margin-bottom: 6px; font-size: 12px; color: var(--dim);
  border: 1px dashed var(--line); border-radius: 6px;
}
#sc-zonebar button { background: #182338; color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px; font-size: 12px; cursor: pointer; }
#sc-zonebar button:hover { border-color: var(--green); color: var(--green); }
/* zone-browse card grid + empty state */
#sc-cards .sc-zonecard { cursor: grab; }
#sc-cards .sc-zonecard:active { cursor: grabbing; }
.sc-empty { color: var(--dim); font-size: 13px; padding: 12px; text-align: center; }

/* #1 new-scenario deck picker (one select per player, from localStorage). */
.sc-deckpick-row { display: flex; gap: 16px; margin: 10px 0; }
.sc-deckpick { display: flex; flex-direction: column; gap: 5px; font-size: 13px; color: var(--dim); }
.sc-deckpick select {
  background: #101a2c; color: var(--fg); border: 1px solid var(--line); border-radius: 6px;
  padding: 7px 9px; font-size: 13px; min-width: 240px; max-width: 320px;
}
.sc-deckpick select:focus { border-color: var(--green); outline: none; }
.sc-deckpick-actions { display: flex; gap: 10px; margin-top: 16px; }

/* #6 play-mode dock in the live board's left column (above the event log). */
#sc-play-dock {
  border: 1px solid rgba(72, 255, 155, 0.4); border-radius: 8px; padding: 10px;
  margin-bottom: 10px; background: rgba(16, 26, 44, 0.92); display: flex;
  flex-direction: column; gap: 8px;
}
#sc-play-dock .sc-dock-head { font-size: 13px; font-weight: 700; color: var(--green); }
#sc-play-dock .sc-row { display: flex; align-items: center; gap: 8px; }
#sc-play-dock button {
  background: #182338; color: var(--fg); border: 1px solid var(--line); border-radius: 6px;
  padding: 6px 10px; font-size: 13px; cursor: pointer;
}
#sc-play-dock button:hover:not(:disabled) { border-color: var(--green); color: var(--green); }
#sc-play-dock button.big { background: #14301f; border-color: rgba(72, 255, 155, 0.5); }
#ladder-box { display: flex; flex-direction: column; gap: 10px; }
#ladder-box h2 { margin: 0; }
.lb-toolbar { display: flex; align-items: center; gap: 14px; }
.lb-toolbar label { display: flex; align-items: center; gap: 8px; }
#lb-body { width: 100%; }
#lb-body table { border-collapse: collapse; width: 100%; font-size: 13px; }
#lb-body th, #lb-body td { padding: 3px 8px; border-bottom: 1px solid var(--line); text-align: left; }
#lb-body .you { color: var(--neon); font-weight: 700; }
#db-side-toggle { font-size: 12px; padding: 3px 10px; margin: 6px 0; border-radius: 8px; border: 1px solid var(--line); background: var(--bg2); color: var(--fg); cursor: pointer; }
#db-side-toggle.on { border-color: var(--neon); color: var(--neon); box-shadow: 0 0 8px rgba(252,238,10,.25); }
#db-side .db-put { opacity: .95; }
#mm-elo { margin-left: auto; }

/* ── Legal pages + site footer (2026-09-15) ─────────────────────────── */
/* Footer: slim status bar under the app main (app is a fixed-height flex
   column, so it sits flush at the bottom on every view). */
.site-footer {
  flex: none; display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: baseline;
  padding: 7px 16px; font-size: 11.5px; color: var(--dim);
  border-top: 1px solid var(--line); background: rgba(10, 13, 20, .55);
}
.site-footer a { color: var(--cyan); text-decoration: none; }
.site-footer a:hover { text-decoration: underline; }

/* Standalone legal/terms pages: same shell as the app, but scrollable. */
.legal-page { height: 100%; overflow-y: auto; }
main:has(> .legal-page) { overflow: hidden; }
.legal { max-width: 860px; width: 100%; margin: 24px auto; padding: 28px 34px; }
.legal h1 { margin: 0 0 4px; font-size: 26px; letter-spacing: .5px; }
.legal h2 { margin: 26px 0 8px; font-size: 16px; color: var(--cyan); }
.legal p, .legal li { color: var(--fg); font-size: 14px; line-height: 1.65; max-width: 78ch; }
.legal ul { padding-left: 22px; margin: 8px 0; }
.legal a { color: var(--cyan); }
.legal-meta { color: var(--dim); font-size: 12.5px; margin-bottom: 18px; }
.legal-foot { margin-top: 30px; padding-top: 14px; border-top: 1px solid var(--line); color: var(--dim); }

/* ── Stage windows: mulligan + roll-a-Gig (2026-09-15 user request) ─────── */
/* Centered, unmissable, but NO backdrop — the hand must stay readable
   behind the mulligan decision. Same visual family as #may-window, larger. */
#stage-window {
  position: fixed; z-index: 97; left: 50%; top: 42%;
  transform: translate(-50%, -50%);
  min-width: 300px; max-width: 460px; text-align: center;
  background: #101a2c; border: 1px solid rgba(0, 240, 255, 0.55);
  border-radius: 14px; padding: 18px 22px;
  box-shadow: 0 12px 48px rgba(0,0,0,.7), 0 0 24px rgba(0, 240, 255, .12);
  animation: may-in .18s ease;
}
#stage-window .sw-head { font-size: 13px; font-weight: 800; letter-spacing: 2px;
  text-transform: uppercase; color: var(--cyan); margin-bottom: 6px; }
#stage-window .sw-q { font-size: 14.5px; color: #e8f0ff; margin-bottom: 14px; line-height: 1.4; }
#stage-window .sw-btns { display: flex; gap: 12px; justify-content: center; }
#stage-window .sw-keep, #stage-window .sw-mull {
  flex: 1; padding: 12px 0; font-size: 15px; font-weight: 800; border-radius: 9px;
  cursor: pointer; border: 1px solid transparent; transition: background .14s, transform .14s;
}
#stage-window .sw-keep { background: rgba(72, 255, 155, 0.16); border-color: rgba(72, 255, 155, 0.6); color: #7dffc4; }
#stage-window .sw-keep:hover { background: rgba(72, 255, 155, 0.3); transform: translateY(-1px); }
#stage-window .sw-mull { background: rgba(255, 77, 94, 0.14); border-color: rgba(255, 77, 94, 0.6); color: #ff8f9e; }
#stage-window .sw-mull:hover { background: rgba(255, 77, 94, 0.26); transform: translateY(-1px); }
/* Roll-a-Gig: the fixer dice as big clickable hex chips (same family as the
   board dice, scaled up). */
#stage-window .sw-dice { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
#stage-window .sw-die {
  min-width: 64px; padding: 14px 8px; font-size: 17px; font-weight: 900; letter-spacing: .5px;
  color: var(--fg); background: rgba(0, 240, 255, .08); border: 1.5px solid rgba(0, 240, 255, .5);
  border-radius: 12px; cursor: pointer; transition: background .14s, transform .14s, box-shadow .14s;
  animation: sw-pulse 1.6s ease-in-out infinite;
}
#stage-window .sw-die:hover { background: rgba(0, 240, 255, .22); transform: translateY(-2px) scale(1.05);
  box-shadow: 0 6px 18px rgba(0, 240, 255, .25); }
@keyframes sw-pulse { 0%,100% { border-color: rgba(0,240,255,.35); } 50% { border-color: rgba(0,240,255,.85); } }

/* ── Reaction banner (2026-09-15 user request) ───────────────────────────── */
/* Being attacked must be obvious: a pulsing red banner at the TOP center —
   over the rival's half, far from everything the defender clicks. The
   container is pointer-transparent (buttons opt back in), so block/quick/
   call gestures on the board below are NEVER blocked. */
#react-banner {
  position: fixed; z-index: 96; top: 54px; left: 50%; transform: translateX(-50%);
  display: flex; gap: 12px; align-items: center; flex-wrap: wrap; justify-content: center;
  max-width: min(760px, 92vw); padding: 10px 18px;
  background: rgba(30, 10, 18, .92); border: 1.5px solid rgba(255, 77, 94, .75);
  border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.6), 0 0 22px rgba(255, 77, 94, .22);
  animation: rb-in .2s ease, rb-glow 1.8s ease-in-out infinite;
  pointer-events: none; /* the banner itself never eats a board gesture */
}
#react-banner > * { pointer-events: auto; }
#react-banner .rb-title { font-size: 14.5px; font-weight: 900; letter-spacing: 1px;
  text-transform: uppercase; color: #ff6b7d; white-space: nowrap; }
#react-banner .rb-hint { font-size: 12.5px; color: #c8b3ba; }
#react-banner button {
  padding: 8px 16px; font-size: 13.5px; font-weight: 800; border-radius: 8px; cursor: pointer;
  border: 1px solid rgba(255,255,255,.18); background: #23304e; color: #dbe6ff;
  transition: background .14s, transform .14s;
}
#react-banner button:hover { background: #2d3d63; transform: translateY(-1px); }
#react-banner .rb-pass { background: rgba(255, 77, 94, .18); border-color: rgba(255, 77, 94, .65); color: #ff9aa8; }
#react-banner .rb-pass:hover { background: rgba(255, 77, 94, .32); }
@keyframes rb-in { from { opacity: 0; transform: translate(-50%, -8px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes rb-glow { 0%,100% { border-color: rgba(255,77,94,.45); box-shadow: 0 8px 32px rgba(0,0,0,.6); }
                     50% { border-color: rgba(255,77,94,.95); box-shadow: 0 8px 32px rgba(0,0,0,.6), 0 0 26px rgba(255,77,94,.4); } }
@media (prefers-reduced-motion: reduce) { #stage-window, #stage-window .sw-die, #react-banner { animation: none; } }

/* ── Game-over window (2026-09-15 user request) ─────────────────────────── */
#gameover-window {
  position: fixed; z-index: 98; left: 50%; top: 42%; transform: translate(-50%, -50%);
  min-width: 340px; max-width: 480px; text-align: center;
  background: #101a2c; border-radius: 16px; padding: 26px 30px 22px;
  box-shadow: 0 16px 60px rgba(0,0,0,.75); animation: may-in .22s ease;
}
#gameover-window.won  { border: 1.5px solid rgba(72, 255, 155, .65); box-shadow: 0 16px 60px rgba(0,0,0,.75), 0 0 30px rgba(72,255,155,.18); }
#gameover-window.lost { border: 1.5px solid rgba(255, 77, 94, .65);  box-shadow: 0 16px 60px rgba(0,0,0,.75), 0 0 30px rgba(255,77,94,.18); }
#gameover-window:not(.won):not(.lost) { border: 1.5px solid rgba(0, 240, 255, .55); }
#gameover-window .go-title { font-size: 34px; font-weight: 900; letter-spacing: 4px; margin-bottom: 2px; }
#gameover-window.won .go-title { color: #7dffc4; text-shadow: 0 0 18px rgba(72,255,155,.5); }
#gameover-window.lost .go-title { color: #ff6b7d; text-shadow: 0 0 18px rgba(255,77,94,.5); }
#gameover-window:not(.won):not(.lost) .go-title { color: var(--cyan); }
#gameover-window .go-sub { font-size: 12.5px; color: var(--dim); margin-bottom: 18px; letter-spacing: 1px; }
#gameover-window .go-btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
#gameover-window .go-btns button {
  padding: 11px 16px; font-size: 13.5px; font-weight: 800; border-radius: 9px; cursor: pointer;
  border: 1px solid rgba(255,255,255,.16); background: #1b2740; color: #dbe6ff;
  transition: background .14s, transform .14s;
}
#gameover-window .go-btns button:hover { transform: translateY(-1px); }
#gameover-window .go-analyse { background: rgba(0, 240, 255, .1); border-color: rgba(0, 240, 255, .5); color: var(--cyan); }
#gameover-window .go-analyse:hover { background: rgba(0, 240, 255, .22); }
#gameover-window .go-play { background: rgba(72, 255, 155, .13); border-color: rgba(72, 255, 155, .55); color: #7dffc4; }
#gameover-window .go-play:hover { background: rgba(72, 255, 155, .26); }

/* ── Effect prompt banner (2026-09-15 user request) ─────────────────────── */
/* Top-center instruction while resolving effects — same family as the react
   banner, cyan instead of red so "your move" reads differently from "react!". */
#fx-prompt-banner {
  position: fixed; z-index: 95; top: 54px; left: 50%; transform: translateX(-50%);
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; justify-content: center;
  max-width: min(720px, 92vw); padding: 8px 16px;
  background: rgba(8, 20, 26, .92); border: 1.5px solid rgba(43, 226, 209, .65);
  border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.6), 0 0 18px rgba(43, 226, 209, .18);
  animation: rb-in .2s ease; pointer-events: none;
}
#fx-prompt-banner.stacked { top: 118px; } /* below the react banner when both show */
#fx-prompt-banner .fpb-text { font-weight: 700; color: #2be2d1; font-size: 15px; text-shadow: 0 0 10px rgba(43,226,209,.35); }
#fx-prompt-banner .fpb-hint { color: var(--muted-foreground, #9ab); font-size: 13px; }

/* ── Blocked / unit-vs-unit fight link (2026-09-15 user request) ────────── */
/* Double-headed amber arrow + midpoint badge while the attack stands between
   attacker and blocker/target — unmistakable during the reaction window. */
.fx-arrow.clash line, .fx-arrow.clash polygon { animation: clash-pulse 1s ease-in-out infinite; }
@keyframes clash-pulse { 0%,100% { opacity: .95; } 50% { opacity: .45; } }
.fight-badge {
  position: absolute; transform: translate(-50%, -50%); z-index: 51;
  padding: 3px 12px; border-radius: 999px; font-weight: 800; font-size: 13px; letter-spacing: .08em;
  color: #ffd76a; background: rgba(40, 24, 4, .92); border: 1.5px solid #ffb020;
  box-shadow: 0 0 16px rgba(255, 176, 32, .45); animation: clash-pulse 1s ease-in-out infinite;
  white-space: nowrap; pointer-events: none;
}
.unit.in-fight { outline: 2px solid #ffb020; outline-offset: 2px; border-radius: 8px;
  box-shadow: 0 0 18px rgba(255, 176, 32, .5); animation: clash-pulse 1.2s ease-in-out infinite; }

/* ── Pending-entry glide ghost (2026-09-15 user request) ────────────────── */
.pend-ghost { filter: drop-shadow(0 6px 18px rgba(43,226,209,.35)); opacity: .95; }

@media (prefers-reduced-motion: reduce) { #fx-prompt-banner, .fight-badge, .unit.in-fight, .fx-arrow.clash line, .fx-arrow.clash polygon { animation: none; } }

/* ── Tournaments (docs/TOURNAMENTS.md, 1.5.0 — UI redesign 2026-09-18) ─── */
/* Presentation-only: the view shape + WS ops are unchanged (see tourney.js).
   Reuses the app design language: --neon/--cyan/--pink accents, cardbox,
   .action buttons, seat/field-row vocabulary. Distinct color per phase. */

/* ---------- create box ---------- */
#tourney-create-box { margin-bottom:.8rem; }
#tourney-create-box .tc-desc { margin:0 0 12px; }
.tc-form { display:grid; grid-template-columns:minmax(0,1.6fr) minmax(90px,.8fr) minmax(120px,.8fr); gap:12px; align-items:end; }
.tc-form .field { display:flex; flex-direction:column; gap:5px; font-size:11px; letter-spacing:.6px; text-transform:uppercase; color:var(--dim); }
.tc-form input, .tc-form select { width:100%; padding:8px 10px; }
#btn-tc-create { height:37px; }
.tc-orgnote { display:flex; align-items:center; gap:8px; margin:12px 0 0; padding:9px 12px;
  font-size:12.5px; color:var(--dim); background:rgba(77,159,255,.06); border:1px dashed var(--line); border-radius:8px; }
.tc-orgnote .lock { color:var(--blue); }
@media (max-width:640px){ .tc-form { grid-template-columns:1fr 1fr; } #btn-tc-create { grid-column:1/-1; } }

/* ---------- event list: cards ---------- */
#tourney-list.tl-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:12px; }
.tourney-card { position:relative; overflow:hidden; display:flex; flex-direction:column; gap:9px;
  text-align:left; width:100%; padding:13px 14px; border-radius:12px;
  background:linear-gradient(180deg,var(--bg2) 0%, rgba(18,21,29,.55) 100%);
  border:1px solid var(--line); transition:border-color .15s, box-shadow .15s, transform .08s; }
.tourney-card::before { content:''; position:absolute; top:0; left:0; right:0; height:2px;
  background:linear-gradient(90deg,var(--cyan),transparent 70%); opacity:.7; }
.tourney-card:hover { border-color:var(--cyan); box-shadow:0 0 14px rgba(0,240,255,.18); }
.tourney-card:active:not(:disabled){ transform:translateY(1px); }
.tourney-card.sel { border-color:var(--neon); box-shadow:0 0 0 1px var(--neon), 0 0 18px rgba(252,238,10,.28); }
.tourney-card.sel::before { background:linear-gradient(90deg,var(--neon),transparent 70%); opacity:1; }
.tourney-card.done { opacity:.62; filter:saturate(.85); }
.tc-top { display:flex; align-items:center; gap:8px; justify-content:space-between; }
.tc-name { font-weight:800; font-size:14.5px; letter-spacing:.3px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tc-mid { display:flex; flex-direction:column; gap:6px; }
.tc-progress { font-size:12px; font-weight:600; color:var(--cyan); letter-spacing:.2px; }
.tourney-card.done .tc-progress { color:var(--dim); }
.tc-chips { display:flex; flex-wrap:wrap; gap:5px; }
.tc-chip { font-size:10.5px; font-weight:700; letter-spacing:.3px; padding:2px 8px; border-radius:999px;
  background:rgba(255,255,255,.04); border:1px solid var(--line); color:var(--dim); white-space:nowrap; }
.tc-chip b { color:var(--fg); font-weight:800; }
.tc-foot { display:flex; align-items:center; gap:8px; margin-top:auto; padding-top:2px; }
.tc-champ { font-size:11.5px; font-weight:700; color:var(--yellow); text-shadow:0 0 10px rgba(255,210,30,.4); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.tc-hint { font-size:11px; color:var(--dim); margin-left:auto; }

/* ---------- phase badge (distinct hue per phase) ---------- */
.tp-badge { display:inline-flex; align-items:center; gap:5px; font-size:.66rem; font-weight:800; letter-spacing:.07em;
  padding:.2rem .55rem; border-radius:999px; text-transform:uppercase; white-space:nowrap; border:1px solid transparent; }
.tp-badge::before { content:''; width:6px; height:6px; border-radius:50%; background:currentColor; box-shadow:0 0 6px currentColor; }
.tp-open    { background:rgba(77,159,255,.14);  color:#8fc2ff; border-color:rgba(77,159,255,.4); }
.tp-checkin { background:rgba(255,210,30,.14);  color:#ffd76a; border-color:rgba(255,210,30,.4); }
.tp-swiss   { background:rgba(0,240,255,.12);   color:#5ffce0; border-color:rgba(0,240,255,.4); }
.tp-cut     { background:rgba(255,42,109,.14);  color:#ff8ab4; border-color:rgba(255,42,109,.42); }
.tp-finished{ background:rgba(72,255,155,.12);  color:#7dffb4; border-color:rgba(72,255,155,.38); }

/* ---------- detail header + phase stepper ---------- */
.td-head { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:6px; }
.td-head h2 { margin:0; font-size:20px; letter-spacing:.4px; }
.td-meta { width:100%; font-size:11.5px; color:var(--dim); letter-spacing:.3px; }
.td-stepper { list-style:none; display:flex; flex-wrap:wrap; gap:6px; margin:14px 0 4px; padding:0; counter-reset:step; }
.td-step { position:relative; display:flex; align-items:center; gap:7px; font-size:11px; font-weight:700;
  letter-spacing:.4px; text-transform:uppercase; color:var(--dim);
  padding:6px 12px 6px 10px; border:1px solid var(--line); border-radius:999px; background:rgba(255,255,255,.02); }
.td-step::before { counter-increment:step; content:counter(step); flex:none; width:17px; height:17px; display:grid; place-items:center;
  font-size:10px; border-radius:50%; background:var(--panel2); border:1px solid var(--line); color:var(--dim); }
.td-step.done { color:var(--cyan); border-color:rgba(0,240,255,.35); }
.td-step.done::before { content:'✓'; background:rgba(0,240,255,.16); border-color:var(--cyan); color:var(--cyan); }
.td-step.current { color:#0a0d14; font-weight:800; background:linear-gradient(180deg,var(--neon),#e7c800);
  border-color:var(--neon); box-shadow:0 0 16px rgba(252,238,10,.4); }
.td-step.current::before { background:rgba(10,13,20,.85); border-color:#0a0d14; color:var(--neon); }
.td-step.skipped { opacity:.4; text-decoration:line-through; }
.td-step.skipped::after { content:'no cut'; margin-left:2px; font-size:9px; letter-spacing:.5px; }

/* ---------- champion banner (finished moment) ---------- */
.td-champ { display:flex; flex-direction:column; align-items:center; gap:2px; margin:16px 0 6px; padding:14px 16px; text-align:center;
  border-radius:14px; background:radial-gradient(120% 160% at 50% 0%, rgba(255,210,30,.16), rgba(18,21,29,.4) 70%);
  border:1.5px solid var(--yellow); box-shadow:0 0 26px rgba(255,210,30,.3), inset 0 0 30px rgba(255,210,30,.08); }
.td-champ .crown { font-size:30px; filter:drop-shadow(0 0 12px rgba(255,210,30,.7)); animation:champ-glow 2.4s ease-in-out infinite; }
.td-champ .cap { font-size:11px; letter-spacing:2px; text-transform:uppercase; color:var(--yellow); opacity:.85; }
.td-champ .cname { font-size:26px; font-weight:800; letter-spacing:.5px; color:#ffe9a8; text-shadow:0 0 18px rgba(255,210,30,.6); }
@keyframes champ-glow { 0%,100%{ transform:translateY(0) scale(1);} 50%{ transform:translateY(-2px) scale(1.04);} }

/* ---------- actions cluster ---------- */
.td-actions { display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; margin:.7rem 0 .9rem; }
.td-actions .deck-select { max-width:260px; }
.td-actions .okline { color:#5ffce0; }
button.sm { padding:.18rem .5rem; font-size:.72rem; }

/* ---------- detail section headers + players ---------- */
.td-section { border-top:1px solid rgba(255,255,255,.08); margin-top:6px; padding:14px 0 4px; }
.td-section > h3 { margin:.1rem 0 .5rem; font-size:.78rem; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); }
.tpp { display:flex; gap:.6rem; align-items:center; padding:.28rem .5rem; border-radius:7px; }
.tpp.me { background:rgba(0,240,255,.06); }
.tpp.out { opacity:.45; text-decoration:line-through; }
.tpp .tpp-name { font-weight:600; }
.tpp .hint { margin-left:auto; }
.td-pill { font-size:10px; font-weight:700; letter-spacing:.3px; padding:1px 7px; border-radius:999px; border:1px solid var(--line); color:var(--dim); white-space:nowrap; }
.td-pill.in { color:#7dffb4; border-color:rgba(72,255,155,.35); background:rgba(72,255,155,.08); }
.td-pill.deck { color:#8fc2ff; border-color:rgba(77,159,255,.35); background:rgba(77,159,255,.08); }

/* ---------- match rows (structured) ---------- */
.td-round { margin-bottom:14px; }
.td-round h4 { margin:0 0 8px; font-size:.8rem; letter-spacing:.06em; text-transform:uppercase; color:var(--cyan); display:flex; align-items:center; gap:8px; }
.td-round .rflag { font-size:10px; font-weight:700; letter-spacing:.4px; padding:1px 8px; border-radius:999px; }
.td-match { display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px;
  padding:.5rem .6rem; margin-bottom:6px; border:1px solid var(--line); border-radius:9px; background:rgba(255,255,255,.02); font-size:.86rem; }
.td-match.pending { border-style:dashed; border-color:rgba(0,240,255,.4); animation:match-pulse 1.7s ease-in-out infinite; }
@keyframes match-pulse { 0%,100%{ box-shadow:0 0 0 rgba(0,240,255,0);} 50%{ box-shadow:0 0 12px rgba(0,240,255,.25);} }
.td-side { display:flex; align-items:center; gap:6px; min-width:0; color:var(--fg); }
.td-side.b { justify-content:flex-end; text-align:right; }
.td-side .seed { font-size:.7rem; color:var(--dim); }
.td-side .nm { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.td-match.done .td-side:not(.win) { color:var(--dim); }
.td-side.win { color:#5ffce0; font-weight:800; }
.td-side.win::after { content:'✓'; font-size:.8rem; }
.td-vs { display:flex; align-items:center; justify-content:center; min-width:2.6rem; text-align:center;
  font-size:11px; font-weight:800; letter-spacing:.5px; color:var(--dim); }
.td-vs.bye { color:#ffd76a; background:rgba(255,210,30,.14); border:1px solid rgba(255,210,30,.4); border-radius:999px; padding:2px 10px; }
.td-match .mfoot { grid-column:1/-1; display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-top:2px; }
.mchip { font-size:10px; font-weight:700; letter-spacing:.4px; text-transform:uppercase; padding:1px 8px; border-radius:999px; border:1px solid var(--line); color:var(--dim); }
.mchip.draw { color:#8fc2ff; border-color:rgba(77,159,255,.4); background:rgba(77,159,255,.08); }
.mchip.ruled { color:#ffd76a; border-color:rgba(255,210,30,.4); background:rgba(255,210,30,.08); }
.mchip.voided { color:var(--red); border-color:rgba(255,77,94,.4); background:rgba(255,77,94,.08); }
.mchip.pending { color:#5ffce0; border-color:rgba(0,240,255,.4); background:rgba(0,240,255,.06); }
/* organizer ruling cluster */
.td-rule { display:inline-flex; align-items:center; gap:5px; padding:3px 5px; border-radius:8px; background:rgba(255,255,255,.03); border:1px solid var(--line); }
.td-rule .rlbl { font-size:9.5px; font-weight:800; letter-spacing:.6px; text-transform:uppercase; color:var(--dim); margin-right:2px; }

/* ---------- standings table ---------- */
.td-standings { width:100%; border-collapse:collapse; font-size:.86rem; }
.td-standings th { font-size:.7rem; letter-spacing:.08em; text-transform:uppercase; color:var(--dim); text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--line); }
.td-standings th.l, .td-standings td.l { text-align:left; }
.td-standings th.c, .td-standings td.c { text-align:center; }
.td-standings td { padding:.42rem .5rem; text-align:right; border-bottom:1px solid rgba(255,255,255,.05); color:var(--fg); }
.td-standings td.rank { font-weight:800; color:var(--dim); }
.td-standings td.name { font-weight:600; }
.td-standings td.mp { font-weight:800; color:var(--cyan); }
.td-standings tr.me td { background:rgba(0,240,255,.07); }
.td-standings tr.me td.name { color:#5ffce0; font-weight:800; }
.td-standings tr.cutline td { border-bottom:2px solid var(--yellow); box-shadow:inset 0 -1px 0 rgba(255,210,30,.5); }
.td-cutmarker { display:inline-flex; align-items:center; gap:8px; margin:.4rem 0 .6rem; font-size:10.5px; font-weight:800; letter-spacing:.6px; text-transform:uppercase; color:var(--yellow); }
.td-cutmarker::before, .td-cutmarker::after { content:''; flex:1; height:1px; background:linear-gradient(90deg,transparent,var(--yellow)); }
.td-cutmarker::after { background:linear-gradient(90deg,var(--yellow),transparent); }

/* ---------- top cut bracket ---------- */
.tc-bracket { display:flex; gap:18px; overflow-x:auto; padding:4px 2px 8px; }
.tc-col { flex:none; min-width:200px; display:flex; flex-direction:column; gap:8px; position:relative; }
.tc-col + .tc-col::before { content:'→'; position:absolute; left:-14px; top:42%; color:var(--pink); font-size:16px; opacity:.7; }
.tc-col-h { font-size:.72rem; font-weight:800; letter-spacing:.6px; text-transform:uppercase; color:var(--pink); margin-bottom:2px; }
.tc-col.final { border:1.5px solid var(--yellow); border-radius:10px; padding:8px; background:rgba(255,210,30,.06); box-shadow:0 0 16px rgba(255,210,30,.2); }
.tc-col.final .tc-col-h { color:var(--yellow); }
@media (max-width:640px){ .tc-bracket { flex-direction:column; gap:14px; } .tc-col + .tc-col::before { display:none; } }

/* ---------- legacy replay filter (kept) ---------- */
#replay-tourney-filter { margin-left:auto; padding:.35rem .55rem; }
.replay-row.tourney button { border-left:3px solid #ff8ab4; }

@media (prefers-reduced-motion: reduce){ .td-champ .crown, .td-match.pending { animation:none; } }

/* =====================================================================
   PUZZLE RUNNER (docs/PUZZLES.md §4 — locked play mode)
   Mirrors the scenario-builder layout (#sc-*) so the board + sidebar
   share the same neon design language.
   ===================================================================== */
#pz-wrap { display: grid; grid-template-columns: 360px 1fr; gap: 12px; height: 100%; min-height: 0; }
#pz-sidebar { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px;
  display: flex; flex-direction: column; gap: 10px; min-height: 0; overflow-y: auto; }
#pz-board { position: relative; min-height: 0; min-width: 0; overflow: hidden;
  display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; }
/* Zoom-to-fit stage (table) — fills the space ABOVE the midbar. Same role as
   #board-stage / #sc-stage (2026-09-22, t_aab58775). */
#pz-stage { position: relative; flex: 1; min-height: 0; min-width: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center; }
/* Control strip BELOW the table — mirrors live #midbar / scenario #sc-midbar
   (fixed 84px height, buttons scroll inside). Same .action/.promptbox/.phase
   classes as live; the promptbox renderers reach its cells via $mid() →
   #pz-phase-info / #pz-actions / #pz-prompt. */
#pz-midbar { display: flex; flex-direction: column; gap: 6px; padding: 2px 4px;
  height: 84px; overflow: hidden; border-top: 1px solid var(--line); }
#pz-midbar #pz-prompt { flex: none; }
#pz-midbar #pz-actions { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 6px; }
#pz-phase-info { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#pz-events { flex: 1 1 auto; min-height: 80px; max-height: 30vh; overflow-y: auto; }
/* puzzle list (home of the view) */
#pz-list h3 { margin: 0 0 4px; }
.pz-card { cursor: pointer; transition: transform .16s ease, border-color .16s; }
.pz-card:hover { transform: translateY(-2px); border-color: rgba(0, 240, 255, .45); }
.pz-card h2 { margin: 0 0 4px; font-size: 15px; }
.pz-card p { margin: 0; color: var(--dim); font-size: 12.5px; }
/* start screen / detail */
#pz-detail h3 { margin: 0 0 6px; }
#pz-detail .hint { font-size: 12.5px; line-height: 1.5; }
.pz-meta { gap: 6px; flex-wrap: wrap; }
.pz-chip { font-size: 11.5px; padding: 3px 8px; border-radius: 999px; background: rgba(255,255,255,.06);
  border: 1px solid var(--line); color: var(--dim); text-transform: capitalize; }
.pz-chip.pz-stars { color: var(--yellow); border-color: var(--yellow); }
.pz-revealnote { font-size: 11.5px; color: var(--cyan); }
.pz-winrow b { color: var(--green); }
.pz-failrow b { color: var(--red); }
/* action bar — DEMOTED (2026-09-22, t_aab58775): the board is now playable with
   the SAME gestures as normal play; this list is a secondary fallback. Same
   treatment scenario BUILD mode got (.sc-acts-fallback): collapsed <details>,
   smaller + dimmer buttons. The par-moves counter (star-scoring UI) stays
   prominent and always visible during the user's turn. */
#pz-acts .sc-acts-head { font-size: 11.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .05em; margin-top: 2px; }
#pz-acts .pz-par { font-size: 12.5px; color: var(--fg); font-weight: 700; text-transform: none; letter-spacing: .02em;
  background: rgba(0,240,255,.08); border: 1px solid var(--cyan); border-radius: 7px; padding: 6px 9px; margin-top: 6px; }
#pz-acts .pz-rival-ind { color: var(--cyan); background: rgba(0,240,255,.08); border: 1px solid var(--cyan);
  border-radius: 7px; padding: 6px 9px; font-weight: 700; }
/* Action-list fallbacks are TEST-ONLY surfaces (2026-09-23 user rule): the board
   gestures are the only visible control UI. The DOM stays rendered (headless probes
   and debugging keep working) but nothing shows — until body.ui-lists is set via
   ?uiprobe=1, which reveals both fallbacks for manual/QA inspection. */
#pz-acts .pz-acts-fallback, #sc-controls .sc-acts-fallback { display: none; }
body.ui-lists #pz-acts .pz-acts-fallback, body.ui-lists #sc-controls .sc-acts-fallback { display: block; }
#pz-acts .pz-acts-fallback { margin-top: 8px; border: 1px dashed var(--line); border-radius: 8px; padding: 4px 8px; }
#pz-acts .pz-acts-fallback summary { font-size: 11px; color: var(--dim); cursor: pointer; user-select: none; list-style-position: inside; }
#pz-acts .pz-acts-fallback[open] summary { margin-bottom: 4px; }
#pz-acts .sc-acts { display: flex; flex-direction: column; gap: 4px; max-height: 26vh; overflow-y: auto; padding-right: 2px; }
#pz-acts .sc-acts button { text-align: left; font-size: 11.5px; padding: 5px 8px; border-radius: 7px; opacity: .85;
  background: rgba(0,240,255,.06); color: var(--fg); cursor: pointer; border: 1px solid var(--line); }
#pz-acts .sc-acts button:hover { background: rgba(0,240,255,.14); border-color: var(--cyan); }
/* hints */
#pz-hint { display: none; flex-direction: column; gap: 6px; border-top: 1px solid var(--line); padding-top: 8px; }
#pz-hint.open { display: flex; }
#pz-hint .pz-hintline { font-size: 12.5px; line-height: 1.5; color: var(--yellow); background: rgba(255,210,30,.06);
  border: 1px solid rgba(255,210,30,.35); border-radius: 8px; padding: 7px 9px; }
/* WIN / FAIL banner */
#pz-result { display: none; flex-direction: column; gap: 8px; border-top: 1px solid var(--line); padding-top: 10px; }
#pz-result.open { display: flex; }
.pz-banner { border-radius: 12px; padding: 14px 16px; text-align: center; }
.pz-banner h2 { margin: 0 0 6px; letter-spacing: .1em; }
.pz-banner p { margin: 4px 0 0; color: var(--dim); font-size: 13px; }
.pz-banner.pz-win { background: rgba(72,255,155,.1); border: 1.5px solid var(--green); box-shadow: 0 0 18px rgba(72,255,155,.18); }
.pz-banner.pz-win h2 { color: var(--green); }
.pz-banner.pz-fail { background: rgba(255,90,90,.08); border: 1.5px solid var(--red); box-shadow: 0 0 18px rgba(255,90,90,.15); }
.pz-banner.pz-fail h2 { color: var(--red); }
.pz-starline { font-size: 26px; letter-spacing: .18em; color: var(--yellow); text-shadow: 0 0 12px rgba(255,210,30,.5); }
.pz-why { font-size: 12.5px; line-height: 1.55; background: rgba(0,0,0,.3); border: 1px solid var(--line);
  border-radius: 8px; padding: 8px 10px; }
.pz-why .warn { color: var(--amber, #ffd24a); }
#pz-result .sc-row { display: flex; gap: 6px; }
#pz-result button { flex: 1; font-size: 12.5px; padding: 8px 0; border-radius: 8px; cursor: pointer;
  background: rgba(255,255,255,.08); color: var(--fg); border: 1px solid var(--line); }
#pz-result button:hover { background: rgba(0,240,255,.14); border-color: var(--cyan); }
#pz-result button.big { flex: 1.4; font-weight: 800; letter-spacing: .06em;
  background: rgba(72,255,155,.12); border-color: var(--green); color: var(--green); }
/* ---------- puzzle authoring panel (docs/PUZZLES.md §6) ---------- */
#modal-body .pz-auth-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 14px; margin: 8px 0; }
#modal-body .pz-auth-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--dim); }
#modal-body .pz-auth-row input, #modal-body .pz-auth-row select {
  background: rgba(0,0,0,.3); border: 1px solid var(--line); border-radius: 7px; color: var(--fg);
  font-size: 12.5px; padding: 5px 8px; flex: 1; min-width: 0; }
#modal-body .pz-auth-section { margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--line); }
#modal-body .pz-auth-preds { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0; }
#modal-body .pz-auth-preds button { font-size: 11.5px; padding: 4px 8px; border-radius: 7px; cursor: pointer;
  background: rgba(0,240,255,.06); color: var(--fg); border: 1px solid var(--line); }
#modal-body .pz-auth-preds button:hover { background: rgba(0,240,255,.14); border-color: var(--cyan); }
#modal-body .pz-auth-pred { display: flex; align-items: center; gap: 6px; margin: 4px 0; font-size: 12px; }
#modal-body .pz-auth-pred button { font-size: 11.5px; padding: 4px 8px; border-radius: 7px; cursor: pointer;
  background: rgba(72,255,155,.08); color: var(--green); border: 1px solid var(--green); }
#modal-body .pz-auth-arg { display: inline-flex; align-items: center; gap: 4px; }
#modal-body .pz-auth-arg i { font-style: normal; color: var(--dim); font-size: 11px; }
#modal-body .pz-tree { background: rgba(0,0,0,.3); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin: 6px 0; font-size: 12.5px; }
#modal-body .pz-tree-leaf { color: var(--cyan); }
#modal-body .pz-tree i { color: var(--dim); font-style: normal; font-size: 11px; }
#modal-body .pz-auth-preview { font-size: 12.5px; border-radius: 8px; padding: 7px 10px; margin: 6px 0; }
#modal-body .pz-auth-preview.ok { background: rgba(72,255,155,.08); border: 1px solid var(--green); color: var(--green); }
#modal-body .pz-auth-preview.no { background: rgba(255,90,90,.06); border: 1px solid var(--red); color: var(--fg); }
#modal-body .pz-auth-rec { font-size: 12.5px; border-radius: 8px; padding: 7px 10px; margin: 4px 0; }
#modal-body .pz-auth-rec.ok { background: rgba(72,255,155,.08); border: 1px solid var(--green); color: var(--green); }
#modal-body .pz-auth-rec.no { background: rgba(255,90,90,.06); border: 1px solid var(--red); color: var(--fg); }
#modal-body .pz-auth-verify { font-size: 12.5px; border-radius: 8px; padding: 8px 10px; margin: 6px 0; line-height: 1.5; }
#modal-body .pz-auth-verify.ok { background: rgba(72,255,155,.08); border: 1px solid var(--green); color: var(--green); }
#modal-body .pz-auth-verify.no { background: rgba(255,90,90,.06); border: 1px solid var(--red); color: var(--fg); }
#modal-body .pz-auth-blocked { margin-top: 6px; }
#modal-body .pz-auth-save button.big { font-weight: 800; letter-spacing: .04em; }
/* Tier B — §3b script editor (mode=script) + §2.1 multi-turn banner ---------- */
#pz-detail .pz-turnbanner { margin: 4px 0; padding: 5px 10px; border-radius: 8px; font-weight: 800;
  letter-spacing: .05em; font-size: 13px; background: rgba(255,196,0,.1); border: 1px solid var(--amber, #ffc400); color: var(--amber, #ffc400); }
#modal-body #pzauth-script { display: flex; flex-direction: column; gap: 8px; margin: 8px 0; }
#modal-body .pz-auth-script-node { display: flex; align-items: center; gap: 6px; font-size: 12px;
  background: rgba(0,0,0,.3); border: 1px solid var(--line); border-radius: 8px; padding: 7px 9px; }
#modal-body .pz-auth-script-node select, #modal-body .pz-auth-script-do, #modal-body .pz-auth-script-act {
  background: rgba(0,0,0,.35); border: 1px solid var(--line); border-radius: 7px; color: var(--fg); font-size: 12px; padding: 4px 6px; flex: 1; min-width: 0; }
#modal-body .pz-auth-script-composer { background: rgba(0,0,0,.3); border: 1px dashed var(--line); border-radius: 8px; padding: 7px 9px; margin: 4px 0; }
#modal-body .pz-auth-preview .pz-auth-script-previewline { font-family: ui-monospace, monospace; font-size: 11.5px; color: var(--fg); margin-top: 3px; }
@media (max-width: 900px) { #pz-wrap { grid-template-columns: 1fr; } #pz-board { min-height: 60vh; } }
