/* Deck King — new UI layout/chrome (UI redesign, Slice 2b). Ports the phone-
   shaped screen, status bar, and dark bronze/gold theme from the design artifact
   (design-references/Primary UI proposal.html → _ui_dk-mobile.css). Loaded AFTER
   style.css so its rules win on equal specificity.

   CARD FACES ARE FROZEN: cards keep rendering via cardFaceHTML() + card-styles.css
   + currency-variants.css. This file ports only LAYOUT/CHROME, never card-face
   rules; the artifact's .dk-card/.dk-plate/.dk-art placeholder rules are NOT ported.

   Built in chunks: (1) shell + tokens + status bar + board re-skin [this];
   (2) card-fan hands + opp zone + move banner; (3) piles; (4) action control. */

:root {
  /* design tokens (artifact :root) */
  --bg: #0c0c10; --bg-2: #16161d; --panel: #15141a; --panel-2: #1c1a22;
  --hair: rgba(var(--gold-rgb),0.10); --hair-2: rgba(var(--gold-rgb),0.18);
  --bronze-hi: #c9a468; --bronze: #8a6f3e; --bronze-lo: #6f5530; --rim: #3a2a14;
  --gold: #f0e2b8; --gold-dim: #b9a878; --verdigris: #5ea890;
  --enemy: #c93020; --enemy-hi: #e6533f; --friendly: #4f79c4; --ink-faint: rgba(var(--gold-rgb),0.42);

  /* -rgb channel companions — the same colours, as raw channels, for feeding into
     rgba(). A value-swap theme re-declares the hex token AND its -rgb twin together. */
  --gold-rgb: 240,226,184; --verdigris-rgb: 94,168,144;
  --enemy-rgb: 201,48,32; --enemy-hi-rgb: 230,83,63; --friendly-rgb: 79,121,196;
  --rim-rgb: 58,42,20; --bg-rgb: 12,12,16; --bg-void-rgb: 8,8,11;
  --bronze-hi-rgb: 201,164,104; --bronze-rgb: 138,111,62;
  --panel-solid-rgb: 18,19,25; --scrim-rgb: 8,9,12;   /* near-black docked-panel / modal-backdrop bases */
  --glow-amber-rgb: 255,210,120;   /* warm king-panic glow */
  --glint-hi: 255,235,180; --glint: 255,225,150;   /* warm specular highlights on bronze */
  --pin-rgb: 120,200,230;          /* cool staged/pin glow */

  /* extended chrome palette — values already used inline below; now named so they
     recolour with a theme swap instead of staying hardcoded. */
  --bg-void: #08080b;              /* deepest bg (body / scrims) */
  --bronze-glow: #d8bd80; --bronze-mid: #b08850; --bronze-ink: #1a1006; --bronze-ink-deep: #2a1e0a;
  --ink-dark: #0d0d11; --ink-cool: #dfe3ea; --ink-muted: #cfc7b4;
  --accent-go: #3fbf7f; --accent-go-deep: #2f6f4a; --accent-go-ink: #eafff1;   /* positive/confirm */
  --glow-amber: #ffe9a8; --warn-critical: #ff6a4a; --panic-enemy: #ff7a5f;

  --mono: "JetBrains Mono", ui-monospace, monospace;
  --serif: "Cormorant Garamond", Georgia, serif;
  --display: "Cinzel", Georgia, serif;
  /* re-theme the board cells/cues to the dark bronze palette (overrides style.css).
     The whole board palette lives here now — --destroyed/--warned consolidated in
     from style.css (same values, so no visual change). */
  --light: #342d24;
  --dark: #221d16;
  --destroyed: #2b2b31;
  --warned: #c9543a;
  --selected: #f0e2b8;
  --legal: rgba(var(--gold-rgb),0.55);

  /* ---- AURA SEVERITY BINS (wiki/plan/aura-visual-language.md) -----------------
     Hue answers "how much should this worry you", NOT who the aura targets and NOT
     the object it edits. Replaces the old --enemy/--friendly/--gold field tint,
     which spent the strongest danger hue on a SCOPE value: Bully (a gentle,
     harmless shove) rendered red while Push King (a pair of hard shoves that kill
     by jamming) rendered near-white.

     Four bins, not seven: composited at the real 0.42 core stop over all four
     board-cell colours and run through a Viénot dichromat simulation, a
     one-hue-per-object set measures ΔE 0.7 (Existence and Identity are literally
     the same colour under tritanopia). Four clears every vision type at 8.3.
     Raising alpha does not help — the binding constraint is dichromatic collapse,
     not the dark substrate.

     DELIBERATELY NOT RE-DECLARED under [data-theme="steel"] — which is the SHIPPED
     DEFAULT theme, not an alternate (see theme.js DEFAULT_THEME). These are
     MEANING-hues, and plan/theming's standing precedent is that meaning stays put
     across a value swap ("danger stays warm"). Separation was measured over both
     themes' cells, so one set serves both.

     Measured 2026-08-05, each hue composited at the 0.42 core stop against the CELL
     IT SITS ON (not just against the other bins), worst case over both cell shades:
     steel — Exotic 26.5/24.4/17.3 · Position 16.4 · Fatal 15.9 · Mild 13.8;
     warm  — Exotic 27.4/25.1/17.7 · Position 18.2 · Fatal 15.5 · Mild 14.7.
     Every bin clears the substrate comfortably in both themes and the two themes
     barely differ. Exotic is the MOST separated, not the least — it reads pale
     because it is light and low-chroma, which is a question about its character,
     not about whether it is legible. Mild is the tightest, and that is by design:
     it is the quiet bin. */
  --aura-fatal:    #e80004;   /* this ends a piece — destroy / blast */
  --aura-position: #9a6cff;   /* this moves a piece — 43% of the board, hence its own hue */
  --aura-exotic:   #ffc079;   /* survives, but no longer yours / no longer itself / elsewhere */
  --aura-mild:     #009c89;   /* what the piece may do, or what it carries */

  /* Exotic's in-bin lightness step = PERMANENCE. Lightness is the one channel
     dichromacy leaves fully intact, which is why "never coming back" lives here
     rather than on a hue. --aura-exotic above IS the temporary step. */
  --aura-exotic-reversible: #ffd8a5;   /* undone next move (Time Lord) */
  --aura-exotic-permanent:  #c98d46;   /* never coming back (Whispered Oath, Become Me) */

  /* The Fatal SECONDARY, for a displacement that kills by jamming. Spikes DARK,
     not bright: a spike to bright #e80004 is a composited luminance swing of only
     0.06 — invisible to a dichromat — where #830001 gives 0.15, a visible flicker
     regardless of colour vision. Counterintuitively you cannot fix this by going
     brighter; over a near-black cell the composite compresses. Darkening as it
     reddens also reads more like a threat. */
  --aura-fatal-spike: #830001;

  /* Dormant (resting-state) twins — NO CONSUMER YET. Slice 3 is deferred; these
     are authored here rather than computed at render so the dim colour is a design
     value, not a multiplier. Chroma ×0.45, lightness held: a flat opacity drop
     would push the outer 0.04 gradient stop below visibility and break Tier 1's
     baseline job (the tinted region says "an aura reaches here"). */
  --aura-fatal-dim:    #b36156;
  --aura-position-dim: #9284c3;
  --aura-exotic-dim:   #e5c8ab;
  --aura-mild-dim:     #649187;
}

/* ---- STEEL/BLUE — THE SHIPPED DEFAULT -----------------------------------------
   ⚠️ Read this before reasoning about "the" board colours: `theme.js` sets
   DEFAULT_THEME = 'steel', so a user who has never chosen gets THIS block, not the
   warm :root above. The warm palette is the base the tokens are declared in and the
   opt-in alternate — the reverse of what the old heading here said ("ALT THEME
   (dev/preview)"), which was written before the default flipped and cost a session's
   worth of wrong conclusions about which cell colours a hue actually sits on.
   Flip with data-theme="steel" on <html> (see the boot script + window.dkTheme in
   index.html; persisted to localStorage). Overrides ONLY the palette tokens — fonts,
   layout vars, and board sizing are inherited from the default :root. Cards are NOT
   themed (separate material surface, out of scope) and stay warm by design.

   Source: the designed 17 base tokens come verbatim from
   `design-references/Primary UI proposal with alt color scheme.html`; everything else
   is DERIVED — the -rgb companions are the steel hex as raw channels, the ~15 new
   tokens are cool equivalents, and per Maxx's call **danger stays warm** (king-panic
   glow, hurry-up clock, panic fill, warned squares keep their meaning-hue so danger
   still reads at a glance). Derived values are eyeball-tunable. --legal/--hair-family
   follow --gold-rgb / are set explicitly to the designed hairline tint. */
:root[data-theme="steel"] {
  /* designed base 17 (verbatim from the alt scheme) */
  --bg: #090b12; --bg-2: #131a2a; --panel: #141a28; --panel-2: #1b2334;
  --hair: rgba(206,222,246,0.10); --hair-2: rgba(206,222,246,0.18);
  --bronze-hi: #d6e0f1; --bronze: #8d98ad; --bronze-lo: #5a6376; --rim: #232d42;
  --gold: #e9eefb; --gold-dim: #9aa7bf; --verdigris: #4fc6d6;
  --enemy: #d2495f; --enemy-hi: #ee6a82; --friendly: #6f8fe0; --ink-faint: rgba(206,222,246,0.42);

  /* -rgb companions (steel hex as channels) */
  --gold-rgb: 233,238,251; --verdigris-rgb: 79,198,214;
  --enemy-rgb: 210,73,95; --enemy-hi-rgb: 238,106,130; --friendly-rgb: 111,143,224;
  --rim-rgb: 35,45,66; --bg-rgb: 9,11,18; --bg-void-rgb: 5,7,12;
  --bronze-hi-rgb: 214,224,241; --bronze-rgb: 141,152,173;
  --panel-solid-rgb: 16,21,34; --scrim-rgb: 5,7,13;
  --glow-amber-rgb: 255,210,120;                 /* DANGER — warm */
  --glint-hi: 235,242,255; --glint: 220,232,252; /* cool specular on steel */
  --pin-rgb: 120,200,230;                         /* already cool */

  /* new named tokens — cool equivalents; danger tokens kept warm */
  --bg-void: #05070c;
  --bronze-glow: #eaf1ff; --bronze-mid: #6f7c93; --bronze-ink: #0a0e18; --bronze-ink-deep: #131f38;
  --ink-dark: #0a0c14; --ink-cool: #dfe6f5; --ink-muted: #b6c2d8;
  --accent-go: #3fbf7f; --accent-go-deep: #2f6f4a; --accent-go-ink: #eafff1; /* positive = keeps green meaning */
  --glow-amber: #ffe9a8; --warn-critical: #ff6a4a; --panic-enemy: #ff7a5f;   /* DANGER — warm */

  /* board cells: steel; warned stays warm (danger), selected/legal follow --gold-rgb */
  --light: #2a3346;
  --dark: #1a2130;
  --destroyed: #23262e;
  --warned: #c9543a;
  --selected: #e9eefb;
  /* --legal inherits rgba(var(--gold-rgb),0.55) from the default :root → steel gold */
}

body { background: var(--bg-void); }

/* ---- the phone-shaped screen ------------------------------------------- */
/* Fluid phone proportion: bounded width, fills the viewport height under the
   dev header; the ~0.46 aspect keeps it phone-shaped with tolerance. */
.dk-app {
  /* tall phone portrait: fill the viewport height, derive a phone-shaped width
     (~0.475 ratio) from it; clamp to the viewport width on narrow screens. */
  height: 97vh;
  width: min(96vw, calc(97vh * 0.475));
  /* board cell size — pure CSS off the layout viewport (vw/vh), inherited by the
     board AND the bottom zone (for the fan→board overlap). --cols/--rows are set
     by sizeBoard(). Board ≈ 55% of height; width-fill secondary; cap 130 / floor 28. */
  --cols: 4; --rows: 7;
  --app-w: min(96vw, calc(97vh * 0.475));
  /* cell = min(width-fit, height-fit), so the board fits whichever axis is the
     limiting one (a wide board like the 8×8 is width-limited; a tall/narrow one is
     height-limited). The width subtraction leaves room for #board's 8px border +
     subpixel rounding so a width-limited board never clips. */
  --cell: max(28px, min(
    calc((var(--app-w) - 16px) / var(--cols)),
    calc(97vh * 0.54 / var(--rows)),
    130px));
  margin: 1.5vh auto;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 18px 50px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(var(--gold-rgb),0.06);
}
.dk-screen {
  width: 100%; height: 100%; position: relative;
  background: radial-gradient(ellipse at 50% -8%, var(--bg-2) 0%, var(--bg) 58%), var(--bg);
  color: var(--gold); font-family: var(--serif);
  display: flex; flex-direction: column; overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ---- top status bar ---------------------------------------------------- */
/* The header sits ABOVE everything, including a raised opponent hand (which can climb
   to z-index 48 when pulled up) — it must never be covered. position+z-index lift it
   over the absolutely-positioned hand overlays that share its stacking context. */
.dk-status {
  position: relative; z-index: 50;
  flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
  padding: 9px 13px 8px; border-bottom: 1px solid var(--hair);
  /* OPAQUE base (over the screen's top tone) so a tucked opponent peek riding up to the
     near edge is cleanly occluded behind the bar, not bleeding through it. */
  background: linear-gradient(180deg, rgba(255,255,255,0.018), transparent), var(--bg-2);
}
.dk-wordmark {
  font-family: var(--display); font-weight: 800; font-size: 13px;
  letter-spacing: 0.2em; color: var(--gold); text-transform: uppercase; white-space: nowrap;
}
.dk-wordmark .k { color: var(--verdigris); }
.dk-status-mid { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; gap: 8px; min-width: 0; }
.dk-turn {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--gold-dim); white-space: nowrap;
}
/* encroachment-turn tell (2026-07-24): the world bites THIS turn — the counter goes
   warning-red and carries a small pulsing ✕ (the Destroyed glyph). Small, but on the
   one element players already watch every turn. */
.dk-turn.enc-now { color: #ff6a4a; }
.dk-turn .enc-tell { display: inline-block; margin-left: 5px; color: #ff5f45; animation: enc-tell-pulse 0.9s ease-in-out infinite; }
@keyframes enc-tell-pulse {
  0%, 100% { opacity: 0.45; transform: scale(0.95); }
  50%      { opacity: 1; transform: scale(1.15); }
}
.dk-sim {
  font-family: var(--mono); font-size: 8px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--verdigris); border: 1px solid rgba(var(--verdigris-rgb),0.4); border-radius: 2px;
  padding: 2px 5px; white-space: nowrap;
}
/* Notable-event readout (player-qol-batch #3): the header line that replaced the
   turn-role pill — the most recent notable event, tappable → the log overlay. */
.dk-notable {
  font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--gold); background: none; border: none; padding: 2px 4px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 100%;
}
.dk-notable:hover { color: var(--gold-dim); }
.dk-status-right { display: flex; align-items: center; gap: 9px; }
.dk-king { display: flex; align-items: center; gap: 4px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; color: var(--gold-dim); }
.dk-king .glyph { font-size: 13px; line-height: 1; }
.dk-king.p1 .glyph { color: var(--gold); }
.dk-king.p2 .glyph { color: var(--verdigris); }
.dk-stack { display: flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; color: var(--gold-dim); }
.dk-stack .pip { position: relative; width: 15px; height: 21px; flex: 0 0 auto; }
.dk-stack .pip i {
  position: absolute; inset: 0; border-radius: 2px; border: 1px solid var(--rim);
  background: linear-gradient(135deg, var(--bronze) 0%, var(--bronze-lo) 100%);
  box-shadow: inset 0 1px 1px rgba(var(--glint),0.25);
}
.dk-stack .pip i:nth-child(1) { transform: translate(2.5px, -2.5px); opacity: 0.55; }
.dk-stack .pip i:nth-child(2) { transform: translate(1.2px, -1.2px); opacity: 0.8; }

/* ---- board STAGE (decoupled model 2026-06-23) -------------------------------
   The board-wrap fills all space below the status bar; the board is CENTERED in it
   and SLIDES vertically by a single phase variable (--board-y) — up on your card turn
   (room for your bottom fan), down on your move turn (room for the opponent's top fan),
   centred during reveal/resolution. The hands no longer reflow the board (they overlay).
*/
.dk-board-wrap {
  flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
  position: relative; z-index: 1; padding: 0 8px; min-height: 0; overflow: visible;
}
#board {
  border: 2px solid var(--rim); border-radius: 2px; background: var(--rim);
  box-shadow: 0 10px 30px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(var(--gold-rgb),0.12), 0 0 0 1px rgba(var(--gold-rgb),0.06);
  transform: translateY(var(--board-y, 0px));
  transition: transform 1.5s cubic-bezier(.45,0,.2,1); /* slow + dramatic — mirrors the reveal tuck */
}
/* Hurry-up fuse (networked turn clock): a thin line pinned to the top of the play
   field that burns down over the final 30s while you still owe a commit — "lock in
   or your move is skipped". Hidden by default; .lit fades it in, --fuse (1→0) is the
   remaining fraction the JS ticker writes each frame; .critical reddens the last 10s.
   Hotseat never sets it (no turn clock there). */
.dk-fuse {
  /* positioned relative to .dk-screen (its parent now) — pinned just under the status
     bar at the play-area top edge. z-index sits in the TOP tier (above the hands/cards,
     just under the header) so the turn clock is never buried under the fan. */
  position: absolute; top: 35px; left: 8px; right: 8px; height: 3px;
  pointer-events: none; opacity: 0; z-index: 49;
  transition: opacity 0.45s ease;
}
.dk-fuse.lit { opacity: 1; }
.dk-fuse::before { /* burnt-down track (faint) */
  content: ""; position: absolute; inset: 0; border-radius: 2px;
  background: rgba(var(--gold-rgb),0.08);
}
.dk-fuse::after { /* remaining fuse, retreating right→left toward 0 */
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; border-radius: 2px;
  width: calc(var(--fuse, 1) * 100%);
  background: linear-gradient(90deg, var(--gold-dim), var(--gold));
  box-shadow: 0 0 6px rgba(var(--gold-rgb),0.55);
  transition: background 0.6s linear; /* colour eases; width is rAF-driven, no transition */
}
.dk-fuse-spark { /* the burning tip riding the fuse's leading edge */
  position: absolute; top: 50%; left: calc(var(--fuse, 1) * 100%);
  width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%;
  background: var(--glow-amber); box-shadow: 0 0 8px 2px rgba(var(--glow-amber-rgb),0.85);
}
.dk-fuse.critical::after {
  background: linear-gradient(90deg, var(--enemy), var(--enemy-hi));
  box-shadow: 0 0 9px rgba(var(--enemy-rgb),0.75);
}
.dk-fuse.critical .dk-fuse-spark {
  background: var(--panic-enemy); box-shadow: 0 0 10px 3px rgba(var(--enemy-hi-rgb),0.95);
  animation: dk-fuse-flicker 0.5s infinite;
}
@keyframes dk-fuse-flicker { 0%,100%{ opacity: 1 } 50%{ opacity: 0.55 } }

/* board phase: up (your card turn) / down (your move turn) / centre (reveal+resolution).
   The card-turn (UP) shift travels FURTHER than the move-turn (DOWN) shift: on your card
   turn the opponent's hand up top is thin, so the board climbs into that otherwise-wasted
   space to give your large bottom hand + the staging slots more room (Maxx 2026-06-30).
   DOWN stays modest — the bottom has the action band + your (thin) hand to clear. */
.dk-screen { --board-travel: 70px; --board-travel-up: 150px; }
.dk-screen.board-up #board { --board-y: calc(-1 * var(--board-travel-up)); }
.dk-screen.board-down #board { --board-y: var(--board-travel); }
/* centre = no class → --board-y 0 */
/* piece glyphs scale with the cell so they fill the (now responsive) board */
.piece-layer .pc { font-size: calc(var(--cell) * 0.72); }
/* piece glyphs: gold p1, verdigris-rimmed dark p2 (artifact .dk-piece) */
.pc .glyph.p1 { color: var(--gold); text-shadow: 0 1px 2px rgba(0,0,0,0.8), 0 0 1px #000; }
.pc .glyph.p2 {
  color: var(--ink-dark);
  text-shadow: 0 0 1px var(--verdigris), 0 0 2px rgba(var(--verdigris-rgb),0.7), 1px 1px 0 rgba(var(--verdigris-rgb),0.4), -1px -1px 0 rgba(var(--verdigris-rgb),0.3);
}

/* ---- hands + band as ABSOLUTE OVERLAYS (decoupled model 2026-06-23) — they sit over
   the board edges and animate (raise/tuck) on their own; they never reflow the board. */
.dk-top, .dk-bottom {
  position: absolute; left: 0; right: 0; z-index: 4; overflow: visible;
  display: flex; padding: 0 8px;
}
.dk-top { top: 34px; align-items: flex-start; }          /* below the status bar */
.dk-bottom { bottom: 0; align-items: flex-end; }
/* Tuck BOTH hands while the detail view is up (Maxx 2026-07-02): a raised fan sits
   above the board (z 4/48) and under the modal backdrop (z 52), so it otherwise
   overlaps the zoomed board inside the vignette hole. Slide + fade the whole hand
   overlays out; they return to their exact previous state on dismiss (class-driven —
   no hand state is touched). */
.dk-top, .dk-bottom { transition: transform 0.35s ease, opacity 0.3s ease; }
body.dk-inspecting .dk-top { transform: translateY(-55vh); opacity: 0; pointer-events: none; }
body.dk-inspecting .dk-bottom { transform: translateY(55vh); opacity: 0; pointer-events: none; }
.dk-top .hand-zone, .dk-bottom .hand-zone { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; align-items: center; }
/* the Skip/status band: a CENTERED, content-width strip lifted clear of the bottom
   corners (it used to span nearly full-width at bottom:62px and overlap the deck/
   discard corner piles — Maxx). Sits above the tucked hand peek so it shows on your
   move turn. On your CARD turn the raised fan would cover it at the bottom, so it floats
   into the gap UNDER the board instead (.under-board; top set live in renderBand), where
   it's always reachable. max-width keeps it off the corner piles even when its text is long. */
.dk-band {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 48px; z-index: 2;
  width: max-content; max-width: calc(100% - 110px);
}
.dk-band.under-board { z-index: 20; } /* in the under-board gap (top set live); clears the hand */
/* post-game panel (rematch / leave): DOCKED at the bottom of the stage with NO dimming
   backdrop, so the final board + end state stay fully visible above it. Sits just under
   the status bar's z-tier so it's never buried by a (now-idle) hand overlay. */
.dk-endpanel {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 16px; z-index: 49;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 12px 22px; border-radius: 11px; text-align: center;
  background: rgba(var(--panel-solid-rgb),0.95); border: 1px solid var(--bronze);
  box-shadow: 0 12px 36px rgba(0,0,0,0.62), inset 0 0 0 1px rgba(var(--gold-rgb),0.06);
  max-width: calc(100% - 24px); pointer-events: auto;
}
.dk-endpanel.hidden { display: none; }
.ep-head { font-family: var(--display); font-weight: 800; font-size: 19px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--gold); }
.ep-score { font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; color: var(--gold-dim); }
.ep-note { font-family: var(--serif); font-size: 12.5px; color: var(--ink-faint); margin-top: 1px; }
.ep-note.hot { color: var(--gold); font-weight: 600; }
.ep-actions { display: flex; gap: 10px; margin-top: 6px; }
.ep-btn {
  font-family: var(--serif); font-size: 13px; font-weight: 600; letter-spacing: 0.02em;
  padding: 7px 18px; border-radius: 7px; cursor: pointer;
  border: 1px solid var(--bronze); background: var(--panel-2); color: var(--ink-cool);
}
.ep-btn.primary { background: var(--accent-go-deep); border-color: var(--accent-go); color: var(--accent-go-ink); }
/* the loser's rematch re-pick (backlog #75) — deck, and formation on an
   experimental board. Sits between the prompt and the buttons, and stays narrow:
   the panel is DOCKED over a live board, so it must not grow into the position
   the player is still reading. */
.ep-repick { display: flex; flex-direction: column; gap: 5px; margin-top: 7px; width: 100%; }
.ep-pick { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.ep-pick > span {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--gold-dim); white-space: nowrap;
}
.ep-pick select {
  font-family: var(--serif); font-size: 12px; padding: 4px 7px; border-radius: 6px;
  border: 1px solid var(--bronze); background: var(--panel-2); color: var(--ink-cool);
  max-width: 15rem; flex: 1 1 auto; min-width: 0;
}
.ep-btn:hover { filter: brightness(1.12); }
.ep-btn:active { transform: translateY(1px); }
/* dev controls (hidden by default); the move-confirm prompt is centred when shown. */
.dk-devzone { position: absolute; left: 0; right: 0; bottom: 0; z-index: 5; }
/* UNIFIED HAND MODEL (refactor 2026-06-23): one container per hand; EVERY card is placed
   by a single transform (renderHand) — fan arc (raised), peek off the near edge (tucked),
   or lifted toward the board (featured). No max-height clipping, no container rotation;
   the opponent mirrors yours by FLIPPING each card's face (.flip), so all positioning
   stays in screen space. The .fan-card transform transition animates every state. */
.dk-screen .hand { flex: 0 0 auto; background: none; border: 0; padding: 0; overflow: visible; position: relative; }
/* Staging dock (Maxx 2026-06-30): [CANCEL] [slots] [DONE|CONFIRM] in the gap UNDER the board.
   Absolute on .dk-screen, horizontally centred; `top` is set live (renderStageDock) just below
   the board so it tracks the board's phase slide. Each played card drops into a 25% slot; the
   dock gains .expanded once committed and the slot faces grow to fill the gap. */
.dk-stage-dock {
  position: absolute; left: 50%; transform: translateX(-50%); z-index: 20;
  display: flex; align-items: center; gap: 16px;
}
/* EXPANDED (committed/reveal): stack the enlarged cards over a centred CANCEL — so a wide row
   of 3+ cards can't shove the button off the side; the button sits in the gap above the hand. */
.dk-stage-dock.expanded { flex-direction: column; gap: 10px; }
.dk-dock-slots { display: flex; align-items: flex-end; gap: 10px; }
.dk-dock-spacer { display: inline-block; min-width: 64px; } /* balances the row when a button is hidden */
.dk-dock-btn { flex: 0 0 auto; }
.dk-stage-dock .dk-slot { display: flex; align-items: flex-end; justify-content: center; }
/* text styles scoped to the EMPTY placeholder only — otherwise the card face inside a filled
   slot inherits text-transform:uppercase and renders its body text in caps. */
.dk-stage-dock .dk-slot:not(.filled) {
  width: 64px; height: 104px; border-radius: 5px; padding-bottom: 8px; background: rgba(0,0,0,0.20);
  font-family: var(--display); font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-faint);
}
.dk-stage-dock .dk-slot.primary:not(.filled) { border: 2px solid var(--rim); }
.dk-stage-dock .dk-slot.free:not(.filled) { border: 2px dashed var(--hair-2); }
.dk-stage-dock .dk-slot.filled { filter: drop-shadow(0 5px 12px rgba(0,0,0,0.55)); border-radius: 6px; }
.dk-stage-dock.expanded .dk-slot:not(.filled) { display: none; } /* no empty placeholders once committed */
.dk-stage-dock .dk-slot > * { pointer-events: none; }
/* Commit countdown (Maxx 2026-06-30): the dock's Lock-In control wears a subtle depleting
   ring + seconds — a lone primary auto-commits when it empties; a tap commits now. --p (1→0)
   is set live in renderStageDock/updateDockCountdown. */
.dk-dock-commit { display: inline-flex; align-items: center; gap: 7px; }
.dk-dock-count {
  --p: 1; position: relative; width: 19px; height: 19px; flex: 0 0 auto; border-radius: 50%;
  display: inline-grid; place-items: center;
  background: conic-gradient(var(--gold) calc(var(--p) * 360deg), rgba(255,255,255,0.14) 0);
}
.dk-dock-count::after { content: ""; position: absolute; inset: 2px; border-radius: 50%; background: var(--panel-2); }
.dk-dock-count-n { position: relative; z-index: 1; font-family: var(--mono); font-size: 9px; color: var(--gold); }
/* The hand CONTAINERS are click-through — their full-width box otherwise blocks the board
   underneath. Only the CARDS capture pointer events (a card tap still bubbles up to raise
   a tucked hand); empty hand area falls through to the board / the inspect backdrop. */
.dk-top, .dk-bottom, .dk-top .hand-zone, .dk-bottom .hand-zone, .dk-screen .hand, .dk-fan { pointer-events: none; }
.dk-fan .fan-card { pointer-events: auto; }
/* BAND-AID (plan/auto-play-card-icon): auto/force-play reveal cards auto-play (no click
   needed) but overlap the board and steal clicks — let clicks fall through to the board so
   a piece under one is still movable. (They still visually cover pieces; the proper fix is
   collapsing them to a small icon-chip — see the plan.) Since #6b the class is applied
   only while the hand is NOT raised (the reveal/tuck overlap that motivated it) — a
   raised fan's auto cards take taps again, so they're focusable + inspectable. */
.dk-fan .fan-card.autoplay-passthrough { pointer-events: none; }
/* Lane-2 grayout (player-qol-batch #6): a voluntary card with no legal target right
   now — visibly "not playable," still fully tappable (focus + reason) + long-press
   inspectable. Distinct from .readonly/.auto's soft dim. */
.fan-hand-card.unplayable { filter: grayscale(0.55) brightness(0.78); }
/* Reason-on-inspect (#6c): the line above a focused not-playable card — why it
   can't be played right now (the hand-side sibling of #17's availabilityDetail).
   Lives inside the scaled wrap, so it rides the featured enlargement. */
.dk-fan .fan-card .dk-card-reason {
  position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  width: max-content; max-width: 150px; text-align: center;
  font-family: var(--serif); font-size: 8.5px; line-height: 1.35; letter-spacing: 0.02em;
  color: var(--gold); background: rgba(var(--bg-void-rgb), 0.88);
  border: 1px solid rgba(var(--gold-rgb), 0.35); border-radius: 4px; padding: 3px 7px;
  pointer-events: none; z-index: 31;
}
/* opponent hand: "above the card" for the viewer = toward the board = below the wrap */
.opp-zone .dk-fan .fan-card .dk-card-reason { bottom: auto; top: calc(100% + 6px); }
.dk-screen .lockin .slot { color: var(--gold-dim); font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; }
.dk-screen .lockin .slot b { color: var(--gold); }

/* (board travel = the #board transform / --board-y above) */

/* A pull/focus inspect raises a backdrop (#dk-handback) that catches outside taps (back
   out one level). TRANSPARENT — inspecting the hand must NOT darken the board (Maxx); the
   element only exists to catch the outside tap. Bump both hand overlays above it. */
.dk-handback { position: absolute; inset: 0; z-index: 45; background: transparent; }
.dk-handback.hidden { display: none; }
body.dk-handup .dk-top, body.dk-handup .dk-bottom { z-index: 48; }

.dk-hand-label { display: none; }
.dk-fan-empty { text-align: center; color: var(--ink-faint); font-size: 11px; font-family: var(--serif); padding: 8px; }

/* the fan — a fixed-height row; each card's transform does the placement. Anchored to the
   NEAR screen edge (yours bottom / opponent's top) so the cards sit at that edge. */
.dk-fan { position: relative; display: flex; justify-content: center; height: 226px; max-width: 100%; }
.you-zone .dk-fan { align-items: flex-end; }
.opp-zone .dk-fan { align-items: flex-start; }
.dk-fan .fan-card { position: relative; margin: 0 var(--fan-margin, -22px); transition: transform 0.32s cubic-bezier(.3,.6,.3,1); transform-origin: center; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.5)); cursor: pointer; will-change: transform; }
/* the REVEAL tuck is slow + dramatic (~1.5s), mirroring the board slide; normal hand
   interactions (select/focus/raise) stay snappy at 0.32s. */
body.dk-revealing .dk-fan .fan-card { transition: transform 1.5s cubic-bezier(.45,0,.2,1); }
/* the opponent's cards face AWAY (upside-down to you) — flip only the FACE, not the
   wrapper, so all lifts/tucks stay in screen space (no rotation compounding). The slot
   transition animates the flip-to-upright when the opponent's hand SPREADS (or a card
   features), in step with the wrapper's transform transition. */
.fan-slot { transition: transform 0.32s cubic-bezier(.3,.6,.3,1); }
.fan-card.flip .fan-slot { transform: rotate(180deg); }
/* a FEATURED (lifted) card — gold highlight, on top */
.fan-card.featured { filter: drop-shadow(0 0 14px rgba(var(--gold-rgb),0.8)) drop-shadow(0 8px 20px rgba(0,0,0,0.6)); }
/* a STAGED card (multi-card commit — plan/force-and-free-play § Commit UI): lifted
   with a cool pin-glow, distinct from featured's warm gold, so a staged play reads
   as "committed to this turn's sequence, awaiting CONFIRM". */
.fan-card.staged { filter: drop-shadow(0 0 12px rgba(var(--pin-rgb),0.85)) drop-shadow(0 7px 16px rgba(0,0,0,0.55)); }

/* --- Turn nudge: the hand (turnNudge/nudgeHand) ------------------------------
   The turn wants a CARD, so the hand blooms once — a brief gold wash across the
   fan, left to right, then gone. FILTER ONLY, and keyed off the fan rather than
   the cards: renderHand owns every `.fan-card`'s transform (arc, splay, tuck) and
   rewrites its className each render, so anything living on the card itself would
   either fight the fan or be wiped mid-animation. Start and end frames match the
   resting drop-shadow above, so the bloom leaves no seam. The pieces' half of this
   lives in style.css (.nudge-layer). */
.dk-fan.nudge > .fan-card {
  animation: hand-nudge 240ms ease-out both;
  animation-delay: var(--nudge-delay, 0ms);
}
@keyframes hand-nudge {
  0%   { filter: drop-shadow(0 4px 10px rgba(0,0,0,0.5)); }
  40%  { filter: drop-shadow(0 0 13px rgba(var(--gold-rgb),0.8)) drop-shadow(0 4px 10px rgba(0,0,0,0.5)); }
  100% { filter: drop-shadow(0 4px 10px rgba(0,0,0,0.5)); }
}
/* Already motion-free (it is a glow, not a move), so reduced motion keeps it. */

/* fan card sizing: the real cardFaceHTML face (native 260×440) scaled. */
.fan-slot { width: 132px; height: 223px; position: relative; flex: 0 0 auto; }
.fan-hand-card { width: 260px; height: 440px; transform: scale(0.508); transform-origin: top left; transition: none; }
.fan-slot.picked { outline: 2px solid var(--gold); outline-offset: 1px; border-radius: 3px; box-shadow: 0 0 14px rgba(var(--gold-rgb),0.55); }
.fan-hand-card.readonly { opacity: 0.92; }
.fan-slot:not(.tuck) .fan-hand-card:not(.readonly) { cursor: pointer; }
.fan-hand-card .card-auto { position: absolute; bottom: 2px; left: 0; right: 0; text-align: center; font-size: 11px; color: var(--gold); background: rgba(0,0,0,0.55); z-index: 5; }

/* drag-and-drop: a card flies as a ghost under the pointer; the hovered legal
   square lights up; the source card dims while its ghost is out. */
.dk-drag-ghost { position: fixed; left: 0; top: 0; z-index: 60; pointer-events: none; transform: translate(-50%, -62%); filter: drop-shadow(0 14px 30px rgba(0,0,0,0.65)); opacity: 0.96; }
.dk-dragging { cursor: grabbing; }
.dk-dragging .fan-slot.picked { opacity: 0.4; }
#board .cells .sq.drop-hover { box-shadow: inset 0 0 0 3px var(--gold); background: rgba(var(--gold-rgb),0.22) !important; }

/* (The hand rows are positioned as absolute overlays up top in the decoupled-model
   section — the old in-flow .dk-top/.dk-bottom rules were removed so they don't pull
   the hands back into the flex flow below the board.) */

/* ---- piles at the SIDE CORNERS (revised 2026-06-23 per Maxx) — absolute, tucked into
   the board's side margins so they don't steal the board's vertical space. Owner-
   anchored: your Played/Deck bottom-left/right, the opponent's top-left/right. Captions
   hidden (read count by thickness; the face-up deck shows its faces). --------------- */
.dk-pile.corner { position: absolute; z-index: 3; }
.dk-pile.corner .cap { display: none; }
.pile-tl { top: 44px; left: 3px; }
.pile-tr { top: 44px; right: 3px; }
/* your piles anchor at the BOTTOM corners so the face-up deck cascades UP the margin */
.pile-bl { bottom: 8px; left: 3px; }
.pile-br { bottom: 8px; right: 3px; }

/* Player-carry tray (plan/player-carry): a per-seat token of a PENDING play-modifier,
   distinct from the Played pile — the render home for the game's first player-held
   carry-state. Both seats visible (perfect-info-present). Sits beside the discard
   corner, out toward the board edge so it never overlaps the pile. Empty = collapsed. */
.dk-carry.corner { position: absolute; z-index: 4; }
/* The trays live in the PILE BANDS (between the statusbar/hand chrome and the
   board edge), horizontal so they fit — a full-width board (8x8 variant-standard
   spans the whole column) put the old vertical top:118 tray ON its corner
   squares, hiding the a8 piece. Beside the discard pile, clear of the fan. */
.dk-carry.carry-top { top: 46px; left: 38px; }
.dk-carry.carry-bottom { bottom: 80px; left: 50px; }
.dk-carry:empty { display: none; }
.dk-carry .tray {
  display: flex; flex-direction: row; align-items: center; gap: 5px;
  padding: 3px 6px 3px 3px; border-radius: 6px;
  background: rgba(var(--bg-void-rgb), 0.72);
  border: 1px solid rgba(var(--gold-rgb), 0.5);
  box-shadow: 0 0 8px rgba(var(--glow-amber-rgb), 0.28);
}
.dk-carry .tray .mini { border-radius: 4px; overflow: hidden; box-shadow: 0 1px 4px rgba(0 0 0 / 0.5); }
.dk-carry .tray .badge {
  font: 700 10px/1.1 var(--font-ui, system-ui), sans-serif; letter-spacing: 0.04em;
  color: var(--gold, #c6a04a); text-align: center; text-transform: uppercase;
}
.dk-carry .tray .glyph { font-size: 15px; line-height: 1; color: var(--gold, #c6a04a); }

.dk-pile { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.dk-pile .stack { position: relative; width: var(--pw, 46px); height: calc(var(--pw, 46px) * 1.5); }
.dk-pile .stack > .layer {
  position: absolute; inset: 0; border-radius: 3px;
  background: linear-gradient(135deg, var(--bronze-mid) 0%, var(--bronze) 42%, var(--bronze-lo) 100%);
  box-shadow: inset 0 0 0 1px var(--rim), inset 0 1px 1px rgba(var(--glint),0.3), 0 1px 3px rgba(0,0,0,0.5);
}
.dk-pile .stack > .layer:nth-child(2) { transform: translate(4px, -4px); filter: brightness(0.82); }
.dk-pile .stack > .layer:nth-child(3) { transform: translate(2px, -2px); filter: brightness(0.92); }
.dk-pile .stack > .top { position: absolute; inset: 0; z-index: 2; border-radius: 3px; overflow: hidden; }
.dk-pile.discard .stack > .top { filter: saturate(0.55) brightness(0.62); }
/* #4: the Played top is inspectable — a pointer + a hover un-dim signal "tap to review". */
.dk-pile .stack > .top.discard-top { cursor: pointer; }
.dk-pile.discard .stack > .top.discard-top:hover { filter: saturate(0.85) brightness(0.92); }
.dk-pile .badge {
  position: absolute; z-index: 4; top: -6px; right: -6px;
  min-width: 17px; height: 17px; padding: 0 4px; border-radius: 9px;
  background: var(--rim); border: 1px solid var(--bronze-hi); color: var(--gold);
  font-family: var(--mono); font-size: 9px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.dk-pile .cap { font-family: var(--mono); font-size: 8px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint); }
.dk-back {
  position: absolute; inset: 0; z-index: 2; border-radius: 3px;
  background: radial-gradient(ellipse at 50% 30%, rgba(var(--verdigris-rgb),0.2) 0%, transparent 55%), linear-gradient(135deg, var(--bronze-mid) 0%, var(--bronze) 42%, var(--bronze-lo) 100%);
  box-shadow: inset 0 0 0 1px var(--rim), inset 0 0 0 3px rgba(var(--rim-rgb),0.5), inset 0 1px 1px rgba(var(--glint),0.3), 0 2px 5px rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
}
.dk-back::before { content: ""; width: 44%; height: 44%; border: 1px solid rgba(var(--rim-rgb),0.7); transform: rotate(45deg); background: radial-gradient(circle, rgba(var(--verdigris-rgb),0.35), transparent 70%); }
.dk-back .mono { position: absolute; font-family: var(--display); font-weight: 800; font-size: calc(var(--pw,46px) * 0.26); letter-spacing: 0.06em; color: var(--bronze-ink-deep); text-shadow: 0 1px 0 rgba(var(--glint),0.3); }

/* scaled mini card face (discard top, etc.) — the real frozen face, scaled */
.scaled-card-slot { position: relative; }
.scaled-card { width: 260px; height: 440px; transform-origin: top left; }

/* ---- empty pile (ui-refinements item 1): an empty Played/Deck pile reads as a
   clear slot, NOT a fake card stack. ---------------------------------------- */
.pile-empty {
  width: var(--pw, 46px); height: calc(var(--pw, 46px) * 1.5);
  border: 1px dashed var(--hair-2); border-radius: 3px;
  background: rgba(var(--gold-rgb),0.03);
}
.dk-pilen { color: var(--gold-dim); }

/* ---- your deck pile (ui-refinements item 3, revised): just the TOP card face-up on a
   thickness stack — click opens the full-deck view. (The old vertical fan was retired:
   the click-to-preview made the cascade redundant clutter.) If the top card admits next
   turn it gets the verdigris glow. */
.dk-pile.deck .stack .top .admit-next::after {
  content: ""; position: absolute; inset: 0; border-radius: 3px; pointer-events: none;
  box-shadow: inset 0 0 0 1.5px var(--verdigris), 0 0 10px rgba(var(--verdigris-rgb),0.5);
}

/* ---- card-admission flight (begin-turn admission): the just-admitted cards glide
   deck → hand together in one batched motion. ------------------------------- */
.dk-admit-fly {
  position: absolute; top: 0; left: 0; z-index: 39; pointer-events: none;
  transform-origin: 50% 50%; will-change: transform;
  filter: drop-shadow(0 8px 18px rgba(0,0,0,0.55));
}

/* ---- status + controls band UNDER the board (ui-refinements items 2/6/8; placement
   revised 2026-06-23 per Maxx — moved off the top so the layout flows). A centered
   one-line status + the secondary controls (Skip / Cancel / Lock In). Replaces both the
   old redundant banner AND the retired #dk-action bar. z low so the raised fan lifts
   over it (lower the hand to reach Skip — "for now"). */
.dk-band {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 4px 10px; min-height: 30px; border-radius: 3px;
  background: var(--panel); border: 1px solid var(--hair);
}
.dk-band:empty { display: none; }
.dk-band-status { flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; line-height: 1.12; text-align: center; }
.dk-band-status b {
  font-family: var(--display); font-weight: 700; font-size: 11.5px; letter-spacing: 0.04em; color: var(--gold);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dk-band-status span { font-family: var(--mono); font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }
.dk-band-status.tappable { cursor: pointer; }
.dk-band-status.tappable b::after { content: " ⤿"; color: var(--bronze-hi); font-size: 10px; }
/* #7: the safety-window countdown, legible in any lock-in state (the fuse goes dark once you commit). */
.dk-band-clock { font-variant-numeric: tabular-nums; color: var(--ink); opacity: 0.9; }
.dk-band-clock.critical { color: var(--warn-critical); font-weight: 700; opacity: 1; }
.dk-band-ctrls { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; }
.dk-band-btn {
  flex: 0 0 auto; cursor: pointer; height: 28px; padding: 0 12px; border-radius: 3px;
  font-family: var(--display); font-weight: 700; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  border: 1px solid var(--rim); color: var(--gold-dim); background: var(--panel-2);
}
.dk-band-btn.primary {
  color: var(--bronze-ink); border-color: var(--bronze-hi);
  background: radial-gradient(ellipse at 50% 0%, var(--bronze-glow) 0%, transparent 60%), linear-gradient(180deg, var(--bronze-hi) 0%, var(--bronze) 60%, var(--bronze-lo) 100%);
  box-shadow: inset 0 1px 1px rgba(var(--glint-hi),0.6), 0 2px 6px rgba(0,0,0,0.4); text-shadow: 0 1px 0 rgba(var(--glint-hi),0.4);
}
.dk-band-btn.skip { color: var(--gold); border-color: var(--bronze); }
.dk-band-btn.ghost { color: var(--ink-faint); border-color: var(--hair); background: none; }
.dk-band-btn:active { transform: translateY(1px); }

/* ---- action controls (chunk 4): reskin the turn-controls + lock-in into the
   bronze CTA bar. Behaviour unchanged (skip-move / pass-card / resolve + the
   auto-resolve & speed toggles). The networked Cancel/Lock-In/Waiting MORPH is
   deferred — it needs the localPlayer/commit-privacy notion (M3). ------------ */
.dk-screen .lockin { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; padding: 3px 11px 0; }
.dk-screen .lockin .slot { font-family: var(--mono); font-size: 9px; letter-spacing: 0.05em; color: var(--ink-faint); padding: 2px 7px; border-radius: 2px; border: 1px solid var(--hair); }
.dk-screen .lockin .slot.set { color: var(--gold); border-color: var(--bronze); }
.dk-screen .lockin .slot b { color: var(--gold-dim); font-weight: 600; }

.dk-screen .turn-controls {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding: 6px 11px 9px; border-top: 1px solid var(--hair);
  background: linear-gradient(0deg, rgba(0,0,0,0.25), transparent);
}
.dk-screen .turn-controls button {
  flex: 1 1 0; height: 42px; border-radius: 3px;
  font-family: var(--display); font-weight: 700; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  border: 1px solid var(--rim); cursor: pointer; color: var(--gold-dim); background: var(--panel-2);
}
.dk-screen .turn-controls #resolve {
  color: var(--bronze-ink); border-color: var(--bronze-hi);
  background: radial-gradient(ellipse at 50% 0%, var(--bronze-glow) 0%, transparent 60%), linear-gradient(180deg, var(--bronze-hi) 0%, var(--bronze) 60%, var(--bronze-lo) 100%);
  box-shadow: inset 0 1px 1px rgba(var(--glint-hi),0.6), 0 2px 6px rgba(0,0,0,0.4); text-shadow: 0 1px 0 rgba(var(--glint-hi),0.4);
}
.dk-screen .turn-controls button:disabled { opacity: 0.4; cursor: not-allowed; }
.dk-screen .turn-controls .auto-resolve {
  flex: 0 0 auto; font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-faint); display: flex; align-items: center; gap: 4px;
}
.dk-screen .turn-controls .auto-resolve select { background: var(--panel); color: var(--gold-dim); border: 1px solid var(--hair); font-family: var(--mono); font-size: 9px; padding: 1px 2px; }

/* (The full-width #dk-action bottom bar is RETIRED — ui-refinements item 8. Its
   Skip/Cancel/Lock-In controls now live in the .dk-band strip above the board.) */

/* ---- reveal freeze (Slice 3): telegraph the committed move + played card for a
   beat before the resolution cascade animates ----------------------------- */
body.dk-revealing .sq.pending::after,
body.dk-revealing .sq.reveal-from::after,
body.dk-revealing .sq.card-committed::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  box-shadow: inset 0 0 0 3px var(--gold), inset 0 0 16px rgba(var(--gold-rgb),0.6);
  background: radial-gradient(circle at 50% 45%, rgba(var(--gold-rgb),0.25), transparent 70%);
  animation: dk-reveal-pulse 0.7s ease-in-out infinite;
}
/* the card's target square reads in verdigris (its play colour), distinct from the move */
body.dk-revealing .sq.card-committed::after {
  box-shadow: inset 0 0 0 3px var(--verdigris), inset 0 0 16px rgba(var(--verdigris-rgb),0.6);
  background: radial-gradient(circle at 50% 45%, rgba(var(--verdigris-rgb),0.28), transparent 70%);
}
@keyframes dk-reveal-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }
/* the played card sits above its neighbours and stays featured (lifted + glowing)
   through the reveal AND the early cascade beats (dk-cardheld), until it flies. The
   LIFT transform is computed in renderHand (rotation-aware, so it reads upright in
   the opponent's mirrored fan); here we only own the z-order + glow. */
.fan-card.committed { z-index: 24 !important; }
.dk-fan .fan-card.committed { filter: drop-shadow(0 0 9px rgba(var(--gold-rgb),0.75)) drop-shadow(0 6px 14px rgba(0,0,0,0.55)); }
body.dk-revealing .fan-card.committed,
body.dk-cardheld .fan-card.committed {
  filter: drop-shadow(0 6px 18px rgba(var(--gold-rgb),0.55)) !important;
  transition: transform 0.3s ease;
}

/* committed/pending MOVE read-out: origin ring → path dots → destination (.pending,
   styled in style.css). Shown for your pending move and either side's locked move. */
#board .cells .sq.move-from { box-shadow: inset 0 0 0 3px var(--bronze-hi); }
/* Last-move trace (player-qol-batch #2): Tier-1, owner-NEUTRAL ("what happened",
   not "whose") — a muted desaturated tint on the origin + actual-rest squares of
   the most recent chess move, chess.com-style but quiet enough not to compete
   with effect colors. background-image, so the ::before/::after channels stay
   free for the other square languages. .lastmove-nominal is the faint "aimed
   here, ended there" tick; the hairline itself lives in .trace-layer. */
#board .cells .sq.lastmove { background-image: linear-gradient(rgba(178,172,150,0.18), rgba(178,172,150,0.18)); }
#board .cells .sq.lastmove-nominal { background-image: linear-gradient(rgba(178,172,150,0.09), rgba(178,172,150,0.09)); }
/* Path ring (Maxx 2026-07-25): each traversed square of the last move wears a
   faint UNFILLED circle, sized like the legal-move dot (22px). A real child
   element — the .sq pseudo channels belong to legal/target/encroach. */
#board .cells .sq .lastmove-ring {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid rgba(178,172,150,0.45);
  pointer-events: none; z-index: 0;
}
.trace-layer {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 1; pointer-events: none; overflow: visible;
}
.trace-layer .lastmove-trail {
  stroke: rgba(178,172,150,0.55); stroke-width: 1.5px;
  vector-effect: non-scaling-stroke; stroke-linecap: round; stroke-dasharray: 5 4;
}
/* Relocate-landing preview (backlog #78): the targeting-phase "where would this
   piece end up" layer for Rewind Move / Return Home. Same dashed hairline
   markup as the last-move trace, a touch brighter so it reads as live UI rather
   than history (must come after .lastmove-trail — same specificity, the stroke
   override wins by order). Ghost = a barely-there copy of the piece at the
   landing square; X = the landing is blocked, i.e. why this piece is greyed. */
.trace-layer .preview-trail { stroke: rgba(214,206,176,0.75); }
/* A BLOCKED leg carries the refusal in the line itself — dimmer and ember-red,
   because the X at the far end can hide under an occupying piece (Maxx,
   2026-08-20) while the leg always stays visible. Same specificity trick:
   after .preview-trail, so the stroke override wins by order. */
.trace-layer .preview-trail.blocked { stroke: rgba(224,96,52,0.5); }
.trace-layer .preview-ghost {
  font-size: 0.72px; text-anchor: middle; dominant-baseline: central;
  opacity: 0.32; pointer-events: none;
}
.trace-layer .preview-ghost.p1 { fill: var(--gold, #c6a04a); }
.trace-layer .preview-ghost.p2 { fill: var(--verdigris, #4a9c8c); }
.trace-layer .preview-x {
  stroke: rgba(224,64,52,0.6); stroke-width: 3px;
  vector-effect: non-scaling-stroke; stroke-linecap: round;
}
/* Sightline preview (the LoS companion, 2026-08-20): a fine dotted ray from
   each offered target of a line-of-sight-to card (Commander) to the piece its
   sightline lands on, ringed at the far end. Gold — it explains an OFFER, so it
   wants the affirmative accent, distinct from the neutral rewind hairline. */
.trace-layer .los-ray {
  stroke: rgba(var(--gold-rgb, 198,160,74), 0.55); stroke-width: 1.5px;
  vector-effect: non-scaling-stroke; stroke-linecap: round; stroke-dasharray: 2 4;
}
.trace-layer .los-anchor {
  fill: none; stroke: rgba(var(--gold-rgb, 198,160,74), 0.5); stroke-width: 1.5px;
  vector-effect: non-scaling-stroke;
}
#board .cells .sq.path::before {
  content: ""; position: absolute; left: 50%; top: 50%; width: 24%; height: 24%;
  transform: translate(-50%, -50%); border-radius: 50%; pointer-events: none; z-index: 1;
  background: radial-gradient(circle, rgba(var(--gold-rgb),0.85), rgba(var(--gold-rgb),0.2) 70%, transparent);
}

/* ---- card flight (Phase 2): the committed card flies from hand → its target
   square, stamps (effect burst), then flutters to the discard --------------- */
#dk-cardflight {
  position: absolute; top: 0; left: 0; z-index: 40; pointer-events: none;
  transform-origin: 50% 50%; will-change: transform, opacity;
  filter: drop-shadow(0 12px 26px rgba(0,0,0,0.6));
}
/* quick impact shake on landing — animates the inner slot so it composes with the
   outer positioning transform on #dk-cardflight (no conflict). */
#dk-cardflight.shake > .scaled-card-slot { animation: dk-card-shake 0.24s ease-in-out; }
@keyframes dk-card-shake {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  18% { transform: translate(-3px, 1px) rotate(-4deg); }
  38% { transform: translate(3px, -1px) rotate(4deg); }
  58% { transform: translate(-2px, 1px) rotate(-2.5deg); }
  78% { transform: translate(2px, 0) rotate(1.5deg); }
}
/* Chess-piece set-down choreography (plan/ui-redesign § Reveal → resolution): the
   mover is RAISED while it hovers in-transit at its destination (substep 1,
   piece_lifted) — uncommitted while the card resolves — then DROPS with the same
   impact shake as the card on its set-down (substep 3, piece_landed). Applied to
   the inner .glyph so it composes with the .pc positioning transform. */
/* While hovering in-transit the piece floats CLEARLY off its square (~⅓ cell up
   + a cast shadow below), so it reads as uncommitted — not set down — until the
   card resolves; then it DROPS (transition down) and shakes into place. The raise
   is cell-relative so it scales with the board. */
.piece-layer .pc.lifted { z-index: 5; filter: drop-shadow(0 calc(var(--cell) * 0.18) 5px rgba(0, 0, 0, 0.45)); }
.piece-layer .pc .glyph { transition: transform 0.16s ease-out; }
/* Lift-in FIRST (a smooth ~⅓-cell rise) THEN the perpetual hover — dk-piece-hover's
   0% is already the raised pose, so applying it alone POPS the glyph up with no motion
   (invisible in a normal move, since the piece also glides to its destination; naked
   and jarring in a trip-up, where it rises in place first). The one-shot rise plays,
   holds (forwards), and the hover takes over from the same raised pose (delayed to match). */
.piece-layer .pc.lifted .glyph { animation: dk-piece-liftin 0.17s cubic-bezier(.3, .7, .3, 1) forwards, dk-piece-hover 1.1s ease-in-out 0.17s infinite; }
.piece-layer .pc.shake .glyph { animation: dk-card-shake 0.26s ease-in-out; }
/* Shove-into-obstacle impact (#2): the obstacle BRACES — a quick compress + recoil
   — while the driven piece slams with the shared .shake. */
.piece-layer .pc.braced .glyph { animation: dk-piece-brace 0.3s ease-out; }
/* THE THROW's flight (plan/throw #137; keyframes in style.css). Sited HERE, after
   .lifted / .shake / .braced, because all four target `.glyph` at equal specificity
   and the last declaration wins — in style.css this rule silently lost to the
   launcher's slam and the piece never left the ground.
   `!important` is the second, order-independent defence: a flight OWNS the glyph for
   its duration, and a slam beat landing mid-air must not steal it back. The JS strips
   the competing classes at launch; this covers one being re-added while the piece is
   still in the air. */
.piece-layer .pc.throw-fly .glyph {
  animation: throwArc var(--throw-ms, 380ms) linear forwards !important;
  transform-origin: 50% 60%; /* tumble about the body, not the crown */
  transition: none !important; /* .pc .glyph has a 0.16s transform transition that would lag the arc */
  z-index: 6; /* over the pieces it flies above */
}
.piece-layer .pc.throw-land .glyph { animation: throwLand 220ms cubic-bezier(.2,.85,.3,1) both !important; }
@keyframes dk-piece-brace {
  0% { transform: scale(1); }
  30% { transform: scale(0.88, 1.08); }
  60% { transform: scale(1.06, 0.96); }
  100% { transform: scale(1); }
}
@keyframes dk-piece-hover {
  0%, 100% { transform: translateY(calc(var(--cell) * -0.32)) scale(1.1); }
  50% { transform: translateY(calc(var(--cell) * -0.4)) scale(1.1); }
}
/* smooth rise from the resting pose into the hover's raised pose (ground-transit) */
@keyframes dk-piece-liftin {
  from { transform: translateY(0) scale(1); }
  to { transform: translateY(calc(var(--cell) * -0.32)) scale(1.1); }
}
/* the impact burst on the stamped square — a quick bright ring that snaps to size */
.cells .sq.stamp { z-index: 2; }
.cells .sq.stamp::after {
  content: ""; position: absolute; inset: -2px; z-index: 3; pointer-events: none; border-radius: 4px;
  box-shadow: inset 0 0 0 3px var(--gold), 0 0 20px 5px rgba(var(--gold-rgb),0.8);
  background: radial-gradient(circle at 50% 50%, rgba(var(--gold-rgb),0.6), transparent 70%);
  animation: dk-stamp 0.6s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes dk-stamp {
  0%   { transform: scale(1.7); opacity: 0; }
  35%  { opacity: 1; }
  100% { transform: scale(1);   opacity: 0; }
}

/* ---- inspect sheet (Slice 4): bottom sheet over a dimmed board -------- */
/* Modal tier (inspect/logview/deckview/stepper/surface) sits at z 52–55: ABOVE a
   raised hand (48), the hurry-up bar (49) and the header (50) — an open modal owns
   the screen — but UNDER the drag ghost (60), whose active gesture always wins.
   (live-game-backlog #19: the sheet used to sit at z 9/10 and a raised/lifted
   hand or focused card drew over the detail view.) */
#dk-inspect:empty { display: none; }
.dk-insp-backdrop { position: absolute; inset: 0; z-index: 52; background: rgba(var(--bg-void-rgb),0.55); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.dk-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 53; max-height: 66%;
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, var(--panel) 0%, var(--bg) 60%);
  border-top: 1px solid var(--hair-2); border-radius: 16px 16px 0 0;
  box-shadow: 0 -14px 34px rgba(0,0,0,0.55); animation: dk-sheet-up 0.22s ease;
}
@keyframes dk-sheet-up { from { transform: translateY(34px); opacity: 0; } to { transform: none; opacity: 1; } }
/* depth stack (detail-view-rework bucket F): layers below the top render as static,
   slightly-offset sheets peeking out above it — depth reads visually; only the top
   layer is live (perf: the offsets are decorative, not interactive). --stk = depth
   below the top (0 = top), set inline by renderInspect. */
.dk-sheet.under {
  animation: none; pointer-events: none;
  transform: translateY(calc(var(--stk, 0) * -13px)) scale(calc(1 - var(--stk, 0) * 0.025));
  transform-origin: 50% 100%;
  filter: brightness(0.72) saturate(0.85);
}
/* ---- board zoom-in-place (bucket D · reference Direction D) ---------------- */
/* the zoom overrides the board's resting slide transform; a snappier transition
   than the 1.5s phase slide so inspect feels responsive */
#board.dk-zoomed { transition: transform 0.55s cubic-bezier(.3,0,.25,1); }
/* the tile-inspect backdrop is a focus VIGNETTE: transparent over the zoomed focus
   region (--vx/--vy/--vr set by applyBoardZoom), darkening outward; no blur so the
   zoomed board stays crisp (the flat-dim backdrop is the card-inspect path) */
.dk-insp-backdrop.zoomv {
  -webkit-backdrop-filter: none; backdrop-filter: none;
  background:
    radial-gradient(circle at var(--vx, 50%) var(--vy, 30%),
      rgba(var(--bg-void-rgb),0) calc(var(--vr, 160px) * 0.55),
      rgba(var(--bg-void-rgb),0.55) var(--vr, 160px),
      rgba(var(--bg-void-rgb),0.88) calc(var(--vr, 160px) * 2.1)),
    linear-gradient(0deg, rgba(var(--bg-void-rgb),0.9) 4%, rgba(var(--bg-void-rgb),0) 38%);
}
.dk-zoom-ring {
  position: fixed; z-index: 53; pointer-events: none; border-radius: 5px;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 0 2px var(--gold), 0 0 18px 2px rgba(var(--gold-rgb),0.5);
  transition: left 0.55s cubic-bezier(.3,0,.25,1), top 0.55s cubic-bezier(.3,0,.25,1),
              width 0.55s cubic-bezier(.3,0,.25,1), height 0.55s cubic-bezier(.3,0,.25,1);
}
.dk-zoom-chip {
  position: fixed; z-index: 53; top: 42px; left: 12px; pointer-events: none;
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--verdigris); background: rgba(var(--bg-void-rgb),0.7);
  border: 1px solid var(--hair-2); border-radius: 3px; padding: 4px 8px;
}
/* effect muting while a tile inspect is open (bucket D "mute other effects"):
   unrelated aura fields/markers + ground glyphs/badges fade so the focused piece's
   effect picture is singled out; the vignette handles the spatial dimming */
/* ---- the PHASED SECONDARY (plan/aura-visual-language Axis 3) -----------------
   A field holds its primary hue continuously and briefly SPIKES to a secondary, so
   it can say what it is and what else it carries without spending a hue slot or a
   texture. Hard-pushing Position fields spike to Fatal: a hard shove is a
   displacement that KILLS by jamming, with no `destroy` verb to read off, so
   lethality here is a downstream consequence rather than a family.

   ASYMMETRIC ON PURPOSE — 2.4s hold · 0.4s ramp up · 0.5s spike · 0.4s ramp back.
   A symmetric cross-fade leaves the field red as often as purple and surrenders
   primary identity; a ~14% duty cycle reads as "a purple field that flinches".

   THE PERIOD IS CHOSEN AGAINST THE BOARD'S EXISTING RESTING PULSES, not picked for
   feel. Three animations already run at rest — encroach-pulse 1.5s, dk-twin-pulse
   1.6s, dk-warm-pulse 2.6s — and the stylesheet already carries a note that
   encroach-warn had to stay "distinct from the steady-red king-panic pulse so a
   square about to encroach never reads as check". A 3.0s period would be exactly
   2× the encroach pulse and would visibly lock and beat against it. 3.7s is clear
   of 1.5 / 1.6 / 2.6 and their harmonics, so it drifts instead of beating. Red +
   rhythm already means "something here is about to die"; a hard push joining that
   vocabulary needs its own gait.

   Phase is anchored to page time via a negative animation-delay set inline, because
   the aura layer is fully rebuilt (svg.replaceChildren) on every render — without
   it each field would restart from zero on every cascade beat and drift out of sync
   with its neighbours. */
@keyframes aura-spike {
  0%, 64.9% { opacity: 0; }   /* 2.4s hold on the primary */
  75.7%     { opacity: 1; }   /* 0.4s ramp up */
  89.2%     { opacity: 1; }   /* 0.5s spike */
  100%      { opacity: 0; }   /* 0.4s ramp back */
}
/* The primary dips out on the SAME envelope the spike rises on — a true cross-fade,
   not an overlay. Painting the spike on top of a still-full primary leaves 58% of the
   pixel showing the primary and roughly halves the signal: measured relative
   luminance drop at the peak is -31%..-40% for an overlay against -68%..-74% for the
   cross-fade. That matters because the spike's job is to be a LUMINANCE event —
   colour alone reaches no dichromat, which is also why it spikes dark rather than
   bright. Both halves must carry the same animation-delay or the cross-fade tears. */
@keyframes aura-primary-dip {
  0%, 64.9% { opacity: 1; }
  75.7%     { opacity: 0; }
  89.2%     { opacity: 0; }
  100%      { opacity: 1; }
}
.aura-layer .aura-spike { animation: aura-spike 3.7s linear infinite; }
.aura-layer .aura-primary-dip { animation: aura-primary-dip 3.7s linear infinite; }
/* Suppressed (cascade / reduced motion): the primary returns to FULL, which is the
   base opacity, so dropping the animation is all that is needed. */
.aura-layer .aura-primary-dip.paused { animation: none; }
@media (prefers-reduced-motion: reduce) { .aura-layer .aura-primary-dip { animation: none; } }
/* Suppress during resolution: auraFiredFx owns motion through the cascade, and
   ambient breathing underneath it would compete.

   `animation: none`, NOT `animation-play-state: paused`. A merely PAUSED animation
   still asserts its keyframe value, and an animated property beats a normal
   declaration — so pausing alone leaves the field frozen at whatever opacity the
   spike happened to be at, and a cascade that begins mid-spike would hold the field
   FULL RED for its entire duration. That is worse than the breathing this rule
   exists to stop: a frozen red field reads as a Fatal aura. Dropping the animation
   removes the override and lets the base opacity apply. Re-entry is safe — the layer
   is rebuilt every render with a freshly computed negative delay, so the field
   rejoins the shared cycle in phase. */
.aura-layer .aura-spike.paused { animation: none; opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  /* Hold the primary rather than flashing. The spike is a secondary read; losing it
     costs less than motion someone cannot tolerate, and Fatal-as-a-PRIMARY (the bin)
     is unaffected — a genuinely lethal aura is still red without any animation. */
  .aura-layer .aura-spike { animation: none; opacity: 0; }
}

/* THE FIRING RING — an instance-triggered aura's instant, extended (Maxx, 2026-08-11).
   A trigger-cadence aura fires in zero frames: the pieces move and the field that
   moved them shows nothing, so the cause is never on screen. This holds the field
   bright for a FIXED window (default 500ms, scaled by the pace control in main.ts and
   passed in as --aura-fire-ms) long enough to overlap the displacements it caused.

   Brightness + a tinted drop-shadow rather than opacity, because the field's strength
   lives in its gradient's stop-opacity (0.42/0.2/0.04) and a group at opacity 1 has
   nowhere up to go. The shadow is the part that actually reads as "the ring lit up" —
   it puts light OUTSIDE the union's edge, where there was none, so the boundary
   momentarily exists. --aura-fire-tint is the field's own severity hue, so the pulse
   never contradicts what the field already says about how much to worry.

   ASYMMETRIC, like aura-spike above and for the same reason: a fast ramp (~70ms) reads
   as an impact, a slow one reads as breathing. The long tail is what carries it past
   the piece_moved beats that follow.

   NOT suppressed under prefers-reduced-motion, unlike the ambient rules below. Those
   kill INFINITE resting animations — motion with no information in it. This is a
   one-shot causal indicator, and it is precisely what a motion-sensitive player needs
   kept: without it the pieces still fly, just with the explanation removed. */
@keyframes aura-fire {
  0%        { filter: none; opacity: var(--aura-fire-from, 1); }
  14%       { filter: brightness(2) drop-shadow(0 0 8px var(--aura-fire-tint, #ffd83d)); opacity: 1; }
  62%       { filter: brightness(2) drop-shadow(0 0 8px var(--aura-fire-tint, #ffd83d)); opacity: 1; }
  100%      { filter: none; opacity: var(--aura-fire-from, 1); }
}
/* `forwards` is load-bearing, not tidiness. The pulse is cleared by a timeout ~30ms
   after it ends, and the `latent` class only comes back on the re-render that timeout
   triggers. Without a retained final frame the element reverts to its base opacity for
   those 30ms — a triggered field would POP to full brightness at the exact moment it
   finished firing, which is the opposite of what the whole treatment says. */
.aura-layer g.aura-field.firing { animation: aura-fire var(--aura-fire-ms, 500ms) ease-out 1 forwards; }

/* LATENT — an instance-triggered field AT REST (engine auraCadenceFor; Maxx,
   2026-08-11: *"instance triggered auras need to communicate the opposite"*).

   The problem this solves: every field looked active-ish all the time, which for a
   presence-based aura is true — step in and something happens — and for a triggered
   one is a promise it does not keep. Standing in Attack Blast's ring does nothing at
   all; it fires on a capture and is otherwise inert.

   TWO CHANNELS, NOT ONE, and that is forced rather than decorative. Opacity alone has
   nowhere to sit: lit is 1, dark is 0.65, and the useful room below that runs out fast
   — the stylesheet already records that 0.4 was "near-invisible at the table". So
   latent takes a smaller opacity step and adds DESATURATION, which `dark` never uses.
   The two then read as different statements rather than as two dimnesses:
       dark   = off right now, but it is the kind of field you can step into
       latent = on, and stepping into it is not how it works
   Desaturation is the honest carrier for the second one — a greyed field reads inert
   rather than merely dim, and it survives colour-blindness, which a hue change would
   not.

   The saturate() rides the FILL PATH, not the group, so it never contends with the
   firing pulse's group-level filter; and the class is dropped outright for the
   duration of a pulse, so the flash returns in full hue and full opacity. That
   contrast — inert grey to saturated flash and back — is doing most of the work here.
   --aura-fire-from is what returns it to 0.6 rather than to 1 when the pulse ends. */
.aura-layer g.aura-field.latent { opacity: var(--aura-latent-rest, 0.6); }
.aura-layer .latent-fill { filter: saturate(0.4); }

/* A DARK sensor field (activeWhile not holding — plan/field-model slice 4): the
   coverage stays visible at reduced opacity (inactive ≠ absent), first-pass
   treatment per the 2026-08-07 ruling. Declared BEFORE .muted so focus-muting
   (the stronger suppression) wins when both classes apply. */
.aura-layer g.dark { opacity: 0.65; } /* 0.4 was near-invisible at the table (Maxx eyeball, 2026-08-07) */
/* AUTHORED faintness (effect `coverageFx: "dim"`, 2026-08-13). The other tiers are
   derived from what the aura IS; this one is the author saying the footprint is
   nearly not worth drawing — a board-radius field whose coverage means "everywhere".
   Sits BELOW dark (0.65) because it is a stronger statement than "off", and above
   muted (0.12), which is a transient focus state and must still win when both apply
   — hence its declaration order here. `none` never reaches CSS: the renderer skips
   the gradient outright, reusing the lost-every-cell path. */
.aura-layer g.aura-field.faint { opacity: 0.3; }
.aura-layer g.muted { opacity: 0.12; }
.aura-icon-layer .muted { opacity: 0.15; }
#board .sq.insp-mute .ground-fx,
#board .sq.insp-mute .fire-fx,
#board .sq.insp-mute .dur-badge,
#board .sq.insp-mute .dwell-badge,
#board .sq.insp-mute .link-badge { opacity: 0.15; }
/* ── PER-PIECE EFFECT-CHANGE CALLOUTS (live-game-backlog #39, ruled 2026-08-05).
   A one-shot name label beside the piece when an effect is added / removed /
   altered on it — the TRANSITION channel to the affect chip's STATE channel.
   Sans, not the display serif: this is functional text read to understand what
   just happened (visual-design-board § Information Tiering's typographic rule).

   Three changes, three motions, so the KIND of change reads without parsing
   the words: added RISES in (arriving), removed SINKS away struck-through
   (leaving), altered PULSES in place (still there, different). Colors are
   explicit — no undefined-token fallbacks (the --ink lesson) — with a dark
   halo so the label survives any square underneath.

   The outer .fx-callout carries the cell-position transform; the inner
   .fx-callout-text carries the keyframes — one element animating transform
   would fight its own positioning. Durations are set inline (pace-scaled).

   Like aura-fire above, NOT suppressed under prefers-reduced-motion: a
   one-shot causal indicator, not ambient motion. */
.fx-callout-overlay { position: absolute; pointer-events: none; z-index: 44; overflow: visible; }
.fx-callout {
  position: absolute; left: 0; top: 0;
  width: calc(var(--cell) * 3); margin-left: calc(var(--cell) * -1);
  display: flex; justify-content: center; pointer-events: none;
}
.fx-callout-text {
  font: 700 clamp(10px, calc(var(--cell) * 0.26), 15px)/1.15 system-ui, sans-serif;
  letter-spacing: 0.02em; white-space: nowrap; text-transform: lowercase;
  text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.65);
  will-change: transform, opacity;
}
.fx-callout-added .fx-callout-text {
  color: #ffd76a;
  animation: fx-co-add var(--co-ms, 950ms) cubic-bezier(0.2, 0.7, 0.3, 1) 1 forwards;
}
.fx-callout-removed .fx-callout-text {
  color: #cfc8bd; text-decoration: line-through; text-decoration-thickness: 2px;
  animation: fx-co-remove var(--co-ms, 950ms) ease-in 1 forwards;
}
.fx-callout-altered .fx-callout-text {
  color: #7ec8ff;
  animation: fx-co-alter var(--co-ms, 950ms) ease-in-out 1 forwards;
}
@keyframes fx-co-add {
  0%   { opacity: 0; transform: translateY(calc(var(--cell) * -0.05)) scale(0.8); }
  16%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.3)) scale(1.06); }
  70%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.48)) scale(1); }
  100% { opacity: 0; transform: translateY(calc(var(--cell) * -0.72)) scale(1); }
}
@keyframes fx-co-remove {
  0%   { opacity: 0; transform: translateY(calc(var(--cell) * -0.4)); }
  14%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.38)); }
  62%  { opacity: 0.85; transform: translateY(calc(var(--cell) * -0.12)); }
  100% { opacity: 0; transform: translateY(calc(var(--cell) * 0.18)); }
}
@keyframes fx-co-alter {
  0%   { opacity: 0; transform: translateY(calc(var(--cell) * -0.4)) scale(0.9); }
  15%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.4)) scale(1.12); }
  34%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.4)) scale(0.97); }
  52%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.4)) scale(1.08); }
  75%  { opacity: 1; transform: translateY(calc(var(--cell) * -0.4)) scale(1); }
  100% { opacity: 0; transform: translateY(calc(var(--cell) * -0.4)) scale(1); }
}

/* ambient foresight glow (bucket G, red tier only): a piece with an adverse event
   resolving on/before its owner's next action pulses enemy-red at rest */
.pc.fs-warn .glyph { animation: dk-pc-fswarn 1.05s ease-in-out infinite; }
@keyframes dk-pc-fswarn {
  0%, 100% { filter: none; }
  50% { filter: drop-shadow(0 0 7px rgba(var(--enemy-hi-rgb),0.95)) drop-shadow(0 0 2px rgba(var(--enemy-rgb),0.8)); }
}
.dk-sheet .sheet-grip { flex: 0 0 auto; width: 42px; height: 4px; border-radius: 3px; background: var(--hair-2); margin: 8px auto 4px; }
.dk-insp-titlerow { flex: 0 0 auto; display: flex; align-items: center; gap: 11px; padding: 4px 14px 8px; }
.dk-insp-titlerow .ttl-glyph {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 5px;
  display: flex; align-items: center; justify-content: center; font-size: 26px; line-height: 1; color: var(--gold);
  background: linear-gradient(160deg, var(--light), var(--dark)); box-shadow: inset 0 0 0 1.5px rgba(var(--gold-rgb),0.4);
}
.dk-insp-titlerow .ttl-glyph.p2 { color: var(--ink-dark); text-shadow: 0 0 2px var(--verdigris), 0 0 3px rgba(var(--verdigris-rgb),0.7); }
.dk-insp-titlerow .ttl-main { flex: 1 1 auto; min-width: 0; }
.dk-insp-title { font-family: var(--display); font-weight: 800; font-size: 19px; color: var(--gold); }
.dk-insp-titlerow .dk-insp-sub { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-faint); margin-top: 2px; }
.dk-insp-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 2px 14px 6px; }
.dk-insp-h { display: flex; align-items: center; gap: 8px; margin: 14px 0 7px; font-family: var(--display); font-weight: 700; font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--gold-dim); }
.dk-insp-h::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--hair); }
.dk-insp-h .hc { flex: 0 0 auto; order: 1; font-family: var(--mono); font-size: 9px; color: var(--ink-faint); border: 1px solid var(--hair-2); border-radius: 9px; padding: 0 6px; line-height: 15px; }
.dk-insp-eff { display: flex; align-items: center; gap: 11px; padding: 9px; border-radius: 5px; background: var(--panel-2); border: 1px solid var(--hair); }
.dk-insp-eff + .dk-insp-eff { margin-top: 6px; }
/* DARK sensor row (slice 4): the effect is installed but its condition doesn't
   hold — dimmed, with the via line naming why. */
.dk-insp-eff.dark { opacity: 0.45; }
.dk-insp-eff .ef-card { flex: 0 0 auto; border-radius: 4px; padding: 2px; filter: drop-shadow(0 3px 7px rgba(0,0,0,0.55)); }
.dk-insp-eff .ef-main { flex: 1 1 auto; min-width: 0; }
.dk-insp-eff .ef-name { display: flex; align-items: center; gap: 7px; font-family: var(--serif); font-weight: 600; font-size: 16px; color: var(--gold); }
.dk-insp-eff .ef-ico { flex: 0 0 auto; display: inline-flex; }
.dk-insp-eff .ef-ico img { filter: brightness(0) invert(0.82); }
.dk-insp-eff .ef-src { margin-top: 2px; font-family: var(--mono); font-size: 9px; letter-spacing: 0.04em; color: var(--ink-faint); }
.dk-insp-eff .ef-meta { flex: 0 0 auto; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 1px; padding: 5px 9px; border-radius: 4px; min-width: 46px; }
.dk-insp-eff .ef-meta + .ef-meta { margin-left: 4px; }
.dk-insp-eff .ef-meta.trn { background: rgba(var(--enemy-rgb),0.14); border: 1px solid rgba(var(--enemy-hi-rgb),0.45); }
.dk-insp-eff .ef-meta.chg { background: rgba(var(--friendly-rgb),0.16); border: 1px solid rgba(var(--friendly-rgb),0.5); }
/* honest-counter chips (detail-view-rework bucket B): dwell/carry countdown (amber)
   + the explicit PERMANENT pip (muted gold — absence would read as a bug, DD2) */
.dk-insp-eff .ef-meta.dwl { background: rgba(var(--glow-amber-rgb),0.13); border: 1px solid rgba(var(--glow-amber-rgb),0.5); }
.dk-insp-eff .ef-meta.prm { background: rgba(var(--gold-rgb),0.06); border: 1px solid var(--hair-2); }
.dk-insp-eff .ef-meta.prm .ef-n { color: var(--gold-dim); font-size: 10px; line-height: 15px; }
.dk-insp-eff .ef-n { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--gold); }
.dk-insp-eff .ef-u { font-family: var(--mono); font-size: 7.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint); }
/* pivot affordance (bucket F): the row's text body pushes the source piece's tile */
.dk-insp-eff .ef-main.ef-pivot { cursor: pointer; }
.dk-insp-eff .ef-main.ef-pivot .ef-src::after { content: " ›"; color: var(--gold-dim); }
.dk-insp-empty { font-family: var(--serif); font-size: 13px; color: var(--ink-faint); padding: 6px 2px; }
.dk-insp-cardhero { display: flex; justify-content: center; padding: 12px 0; }
/* per-effect description rows under the card hero (bucket C) */
.dk-insp-fx { padding: 8px 10px; border-radius: 5px; background: var(--panel-2); border: 1px solid var(--hair); }
.dk-insp-fx + .dk-insp-fx { margin-top: 6px; }
.dk-insp-fx .fx-name { display: flex; align-items: center; gap: 7px; font-family: var(--serif); font-weight: 600; font-size: 14px; color: var(--gold); }
.dk-insp-fx .fx-name img { width: 15px; height: 15px; filter: brightness(0) invert(0.82); }
.dk-insp-fx .fx-layer { margin-left: auto; font-family: var(--mono); font-size: 8px; letter-spacing: 0.12em; color: var(--ink-faint); border: 1px solid var(--hair-2); border-radius: 3px; padding: 1px 5px; }
.dk-insp-fx .fx-desc { margin-top: 4px; font-family: var(--serif); font-size: 12.5px; line-height: 1.45; color: var(--gold-dim); }
.dk-insp-fx .fx-desc .fx-chip { color: var(--gold); }
/* ---- effect-name chips + recursive hovercards (plan/effect-hovercards) ------
   A `.fx-chip` is a tappable effect name embedded in a card body / inspect row /
   another hovercard. main.ts binds a delegated tap → a `describe` hovercard; names
   inside that describe are themselves chips (recursion). Mobile-first: tap, not hover. */
.fx-chip {
  cursor: pointer; color: var(--fx-chip, var(--gold)); white-space: nowrap;
  border-bottom: 1px dotted currentColor;
  -webkit-tap-highlight-color: transparent;
}
.fx-chip:hover, .fx-chip.hc-open { text-shadow: 0 0 6px rgba(var(--gold-rgb), 0.4); filter: brightness(1.12); }
/* #33 (Maxx 2026-08-05): a chip on a card still sitting in the fan is INERT — the
   card has to be inspected (focused out of the fan) first, and the tap that lands on
   the chip is the tap that focuses it. Drop the affordance so the chip doesn't
   advertise a tap it won't take. The dotted underline STAYS: it marks "this is an
   effect", which is true whether or not you can open it yet. Mirrors chipIsLive(). */
.fan-card:not(.focused) .fx-chip { cursor: inherit; }
.fan-card:not(.focused) .fx-chip:hover { text-shadow: none; filter: none; }
/* On a CARD FACE the chip must read against the face GROUND, not the dark chrome:
   plain gold washes out on the light L1 parchment and blends into the L2 gold body.
   Give each face a legible, on-theme `--fx-chip` (+ a touch of weight). The dark
   hovercard / inspect-sheet surfaces keep the gold fallback. */
.card-body .fx-chip { font-weight: 600; }
.style-currency-promissory .card-body { --fx-chip: var(--border-color); }   /* polarity accent on cream */
.style-currency-specimen .card-body   { --fx-chip: var(--gris-type); }      /* olive type-accent on glass */
.style-currency-coupon.layer-L2 .card-body { --fx-chip: #f3e4b0; }          /* bright gold, distinct from the gold body */
/* the overlay layer sits above the inspect sheet (z 52/53) and the drag ghost (60) */
.dk-hc-layer { position: fixed; inset: 0; z-index: 70; pointer-events: none; }
.dk-hc {
  position: fixed; box-sizing: border-box; width: var(--hc-w, 236px);
  pointer-events: auto; cursor: pointer;
  background: linear-gradient(180deg, var(--panel) 0%, var(--bg) 130%);
  border: 1px solid var(--hair-2); border-radius: 10px;
  box-shadow: 0 12px 28px rgba(0,0,0,0.62); padding: 9px 11px;
  animation: dk-hc-in 0.12s ease;
}
@keyframes dk-hc-in { from { transform: translateY(5px) scale(0.98); opacity: 0; } to { transform: none; opacity: 1; } }
/* an older card compressed into the pile — only its shadowed leading edge peeks */
.dk-hc.piled { box-shadow: 0 5px 13px rgba(0,0,0,0.55); }
.dk-hc-name { font-family: var(--serif); font-weight: 600; font-size: 14px; color: var(--gold); margin-bottom: 3px; line-height: 1.15; }
.dk-hc-desc { font-family: var(--serif); font-size: 12.5px; line-height: 1.45; color: var(--gold-dim); }
.dk-hc-desc .fx-chip { color: var(--gold); }
.dk-hc-desc.empty { font-style: italic; opacity: 0.7; }
/* foresight callouts (bucket G): red = adverse & resolves before your next action,
   amber = adverse further out, soft blue = upcoming benign event */
.dk-insp-fs { display: flex; align-items: center; gap: 9px; padding: 7px 10px; border-radius: 5px; margin-top: 6px; font-family: var(--serif); font-size: 13px; color: var(--gold); border: 1px solid var(--hair); background: var(--panel-2); }
.dk-insp-fs .fs-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--gold-dim); }
.dk-insp-fs.warn { border-color: rgba(var(--enemy-hi-rgb),0.5); background: rgba(var(--enemy-rgb),0.10); }
.dk-insp-fs.warn .fs-dot { background: var(--enemy-hi); box-shadow: 0 0 7px rgba(var(--enemy-hi-rgb),0.8); animation: dk-fs-pulse 1.1s infinite; }
.dk-insp-fs.attn { border-color: rgba(var(--glow-amber-rgb),0.45); background: rgba(var(--glow-amber-rgb),0.07); }
.dk-insp-fs.attn .fs-dot { background: var(--glow-amber); }
.dk-insp-fs.info .fs-dot { background: var(--friendly); }
.dk-insp-fs .fs-main { flex: 1 1 auto; }
.dk-insp-fs .fs-in { flex: 0 0 auto; font-family: var(--mono); font-size: 10px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.08em; }
.dk-insp-fs.ef-pivot { cursor: pointer; }
@keyframes dk-fs-pulse { 50% { opacity: 0.45; } }
/* Remembered squares (#35): the three relocate anchors a piece carries. Quieter than
   a Foreseen row — this is reference, not a countdown — but it PIVOTS like one, so the
   square you're being shown is one tap from the sheet. Square in mono on the right,
   matching the .fs-in rail so the two sections align down the same edge. */
.dk-insp-mem { display: flex; align-items: center; gap: 9px; padding: 6px 10px; border-radius: 5px; margin-top: 6px;
  font-family: var(--serif); font-size: 13px; color: var(--ink-muted); border: 1px solid var(--hair); background: var(--panel-2); }
.dk-insp-mem .mem-main { flex: 1 1 auto; display: flex; flex-direction: column; gap: 1px; }
.dk-insp-mem .mem-via { font-size: 11px; color: var(--ink-faint); }
.dk-insp-mem .mem-sq { flex: 0 0 auto; font-family: var(--mono); font-size: 11px; color: var(--gold); letter-spacing: 0.08em; }
/* `.ef-pivot` alone buys nothing — the existing rule is scoped `.dk-insp-fs.ef-pivot`,
   so a mem row wearing the class read `cursor: auto` and looked inert. Caught by a
   computed-style probe, which is the only thing that would have caught it. */
.dk-insp-mem.ef-pivot { cursor: pointer; }
.dk-insp-mem.ef-pivot:hover { border-color: var(--hair-2); }
/* back button (bucket F): pops one stack layer; Dismiss below closes the whole stack */
.dk-insp-back { flex: 0 0 auto; width: 34px; height: 34px; border-radius: 5px; border: 1px solid var(--hair-2); background: var(--panel-2); color: var(--gold); font-size: 20px; line-height: 1; cursor: pointer; }
.dk-insp-foot { flex: 0 0 auto; padding: 10px 14px 14px; background: linear-gradient(0deg, var(--bg) 40%, transparent); }
.dk-dismiss {
  width: 100%; padding: 12px; border-radius: 6px; cursor: pointer; border: 1px solid var(--bronze-hi);
  background: linear-gradient(180deg, rgba(var(--bronze-hi-rgb),0.18), rgba(var(--bronze-rgb),0.12)); color: var(--gold);
  font-family: var(--display); font-weight: 700; font-size: 13px; letter-spacing: 0.28em; text-transform: uppercase;
}

/* ---- full-deck view: a readable spread of YOUR whole deck (your deck only) --
   Opened by clicking your deck pile; the corner cascade only shows card edges.
   A centred modal over the shared inspect backdrop. */
#dk-deckview:empty { display: none; }
.deck-clickable { cursor: pointer; }

/* ---- mobile event-log overlay (#10): the event log lives in the dev sidebar (hidden in
   the product UI); a status-bar button opens it as a sheet so it's reachable on mobile. */
#dk-logview:empty { display: none; }
.dk-logview-back { position: absolute; inset: 0; z-index: 52; background: rgba(var(--scrim-rgb),0.66); }
.dk-logview-panel {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 53;
  width: min(94%, 520px); max-height: 80%; display: flex; flex-direction: column;
  background: linear-gradient(180deg, var(--panel) 0%, var(--bg) 70%);
  border: 1px solid var(--bronze); border-radius: 12px;
  box-shadow: 0 18px 44px rgba(0,0,0,0.6); animation: dk-sheet-up 0.2s ease;
}
.dk-logview-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  padding: 11px 14px 9px; border-bottom: 1px solid var(--hair); }
.dk-logview-title { font-family: var(--display); font-weight: 800; font-size: 18px; letter-spacing: 0.04em; color: var(--gold); }
.dk-logview-actions { display: flex; align-items: center; gap: 10px; }
.dk-logview-close { background: none; border: none; color: var(--ink-faint); font-size: 17px; line-height: 1; cursor: pointer; padding: 2px 4px; }
/* `--ink` is not a token this page defines — these two hovers were silently inert
   (an undefined custom property makes `color` invalid-at-computed-value, i.e. it just
   inherits). `--ink-muted` is the body-ink token, themed in both palettes, and reads
   brighter than the resting `--ink-faint` — which is what a hover wanted. */
.dk-logview-close:hover { color: var(--ink-muted); }
.dk-logview-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 8px 12px; background: none; }
.dk-log-btn { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-faint); background: rgba(255,255,255,0.05); border: 1px solid var(--hair);
  border-radius: 5px; padding: 4px 8px; cursor: pointer; line-height: 1; }
.dk-log-btn:hover { color: var(--ink-muted); border-color: var(--bronze); }
.dk-deckview-panel {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 53;
  width: min(94%, 580px); max-height: 84%;
  display: flex; flex-direction: column;
  background: linear-gradient(180deg, var(--panel) 0%, var(--bg) 70%);
  border: 1px solid var(--bronze); border-radius: 12px;
  box-shadow: 0 18px 44px rgba(0,0,0,0.6); animation: dk-sheet-up 0.2s ease;
}
.dk-deckview-head { flex: 0 0 auto; padding: 14px 16px 9px; text-align: center; border-bottom: 1px solid var(--hair); }
.dk-deckview-title { font-family: var(--display); font-weight: 800; font-size: 19px; letter-spacing: 0.04em; color: var(--gold); }
.dk-deckview-sub { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-faint); margin-top: 3px; }
.dk-deckview-grid {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; align-content: flex-start;
  padding: 16px;
}
.dk-deckview-cell { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.dk-deckview-avail { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--gold-dim); text-align: center; max-width: 150px; line-height: 1.35; overflow-wrap: anywhere; } /* full Lane-1 condition may wrap (#17); anywhere = an unbreakable token can never paint over the neighbor cell */
.dk-deckview-avail.soon { color: var(--gold); }
/* per-condition rows (#83): one line per Lane-1 gate, verdict-tinted — a met
   clause settles to verdigris, an unmet one stays the dim base so the eye goes
   to what still holds the card back */
.dk-deckview-avail .dk-avail-cond { margin-bottom: 2px; }
.dk-deckview-avail .dk-avail-cond:last-child { margin-bottom: 0; }
.dk-deckview-avail .dk-avail-cond.met { color: var(--verdigris); }
/* #16: the "view all played" second-level reveal button on the played-card sheet */
.dk-viewall { display: block; margin: 10px auto 4px; }
.dk-deckview-card { flex: 0 0 auto; border-radius: 4px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.55)); }
.dk-deckview-card.admit-next { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 5px; }
.dk-deckview-panel .dk-insp-foot { background: none; border-top: 1px solid var(--hair); }

/* ---- event stepper (Slice 5): a bottom transport bar + "why" panel ----- */
#dk-stepper:empty { display: none; }
#dk-stepper {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 54;
  display: flex; flex-direction: column; gap: 6px;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, rgba(var(--bg-rgb),0.94), var(--bg));
  border-top: 1px solid var(--bronze); box-shadow: 0 -10px 26px rgba(0,0,0,0.55);
}
.dk-step-why .sw-line { font-family: var(--mono); font-size: 11px; color: var(--gold); word-break: break-word; }
.dk-step-why .sw-meta { font-family: var(--mono); font-size: 8px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-faint); margin-top: 2px; }
.dk-step-tx { display: flex; gap: 6px; align-items: center; }
.dk-step-tx button {
  flex: 0 0 auto; min-width: 38px; height: 34px; border-radius: 3px; cursor: pointer;
  font-family: var(--display); font-weight: 700; font-size: 13px; letter-spacing: 0.06em;
  border: 1px solid var(--rim); background: var(--panel-2); color: var(--gold-dim);
}
.dk-step-tx button.play { background: linear-gradient(180deg, var(--bronze-hi), var(--bronze)); color: var(--bronze-ink); border-color: var(--bronze-hi); }
.dk-step-tx button.exit { margin-left: auto; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; padding: 0 12px; }

/* ---- card surface (plain-click read view): big upright card over a backdrop -- */
#dk-surface:empty { display: none; }
.dk-surface-back {
  position: absolute; inset: 0; z-index: 55; display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 8px;
  background: rgba(var(--bg-void-rgb),0.72); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  animation: dk-sheet-up 0.16s ease;
}
.dk-surface-card { filter: drop-shadow(0 14px 34px rgba(0,0,0,0.7)); }

/* ---- dev side panel (controls + log live here; hotseat is also the testbed) */
.dk-side { color: var(--ink-muted); }
/* Dev chrome (old hotseat header + side log/controls) is hidden by default so the
   new layout reads cleanly; surface it with backtick (`) or window.dk.dev(). */
body.dev-hidden header { display: none; }
body.dev-hidden .dk-side { display: none; }
body.dev-hidden main { justify-content: center; padding: 0; }
/* the skip/pass/resolve/auto-resolve controls + lock-in readout live in the dev zone
   (not in the product mockup) — hide the whole zone with the rest of the dev chrome. */
body.dev-hidden .dk-screen .dk-devzone { display: none; }
.dk-devzone { background: linear-gradient(0deg, rgba(0,0,0,0.35), transparent); padding: 4px 8px; }

/* the move-confirm prompt — a centred overlay (it's a real gameplay gate, not dev).
   width: max-content, or left:50% would shrink-to-fit against the right half only,
   squeezing long warnings into a tall sliver. */
#confirm.confirm { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);
  z-index: 50; width: max-content; max-width: min(340px, 88%); }
#confirm.confirm.hidden { display: none; }
/* "Show me" (#37) — the answer-the-warning control. Deliberately quieter than the
   two decisions either side of it: it is an aside, not a third choice about the
   move. There is NO global .hidden in this codebase (see the note further down),
   so it needs its own rule or it would show on every prompt. */
#confirm-show { margin-right: auto; background: transparent; border: 1px solid #ff6b6b;
  color: #ffc9c9; letter-spacing: 0.04em; }
#confirm-show:hover { background: rgba(255,107,107,0.16); color: #ffe2e2; }
#confirm-show.hidden { display: none; }
/* A forecast must not look like a record: the transport it shares with the replay
   button takes the warning's own red rim while it is playing a CLAIM about a turn
   that has not happened (plan/warning-preview § the honesty problem). */
#dk-stepper.preview { border-top-color: #ff6b6b; }
#dk-stepper.preview .dk-step-why .sw-line { color: #ffd9d9; }
#dk-stepper.preview .dk-step-why .sw-meta { color: #ff9a9a; }
/* …and past the split it has left the turn you staged and is playing the worst
   case the warning names — the opponent's reply, or the coming bite. Brighter,
   because that tail is the thing you pressed the button to see. */
#dk-stepper.preview.tail { border-top-color: #ff2f2f; box-shadow: 0 -10px 26px rgba(255,47,47,0.22); }
#dk-stepper.preview.tail .dk-step-why .sw-meta { color: #ff6b6b; }

/* While the transport is up, the board is a DERIVED display board — a past turn
   or a forecast — and every click on it is swallowed. It must therefore not wear
   the live board's invitations: a staged move's legal dots and selection ring
   survive a preview (nothing clears them — the move is still staged, which is the
   point), and painted over a forecast they read as "this board is yours to act
   on". They are not. (Maxx 2026-08-15 — the swallowed-taps report.) */
body.dk-stepping .sq.legal::before,
body.dk-stepping .sq.target::after,
body.dk-stepping .sq.target-picked::after { display: none; }
body.dk-stepping .sq.selected { box-shadow: none; }
body.dk-stepping .sq.legal,
body.dk-stepping .sq.target,
body.dk-stepping .sq.target-picked,
body.dk-stepping .sq.selectable { cursor: default; }

/* ---- networked match lobby (Stream B step 4) -------------------------------
   Full-screen overlay shown on ?net: pick a deck, then create (mint a code) or
   join by a friend's code. Hidden once match_start lands. */
.dk-lobby {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 120% at 50% 0%, var(--bg-2) 0%, var(--bg) 70%);
  font-family: var(--serif); color: var(--gold);
}
.dk-lobby.hidden { display: none; }
.dk-lobby-card {
  width: min(86vw, 360px);
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--hair-2); border-radius: 14px;
  padding: 22px 22px 26px; text-align: center;
  box-shadow: 0 18px 48px rgba(0,0,0,0.72);
}
.dk-lobby-card h2 {
  font-family: var(--display); font-weight: 700; letter-spacing: 0.04em;
  color: var(--gold); margin: 2px 0 18px; font-size: 1.25rem;
}
.dk-lobby-field { display: block; font-size: 0.95rem; color: var(--gold-dim); margin-bottom: 16px; text-align: left; }
.dk-lobby-field select {
  display: block; width: 100%; margin-top: 6px; padding: 8px 10px;
  background: var(--bg-2); color: var(--gold); border: 1px solid var(--hair-2);
  border-radius: 8px; font-family: var(--mono); font-size: 0.95rem; text-transform: capitalize;
}
.dk-lobby-preview {
  display: inline-block; margin: -8px 0 16px; font-size: 0.82rem;
  color: var(--gold-dim); text-decoration: none; border-bottom: 1px solid transparent;
}
.dk-lobby-preview:hover { color: var(--gold); border-bottom-color: var(--bronze); }
/* Changelog link (plan/changelog) — a quiet footer line on the lobby card, visible
   in all three modes (entry/joiner/created), never mode-toggled. */
.dk-lobby-changelog {
  display: block; margin-top: 16px; font-size: 0.78rem;
  color: var(--ink-faint); text-decoration: none; border-bottom: none;
}
.dk-lobby-changelog:hover { color: var(--gold); }
.dk-lobby-btn {
  font-family: var(--display); letter-spacing: 0.03em; cursor: pointer;
  padding: 9px 16px; border-radius: 8px; font-size: 0.95rem;
  background: var(--bg-2); color: var(--gold); border: 1px solid var(--bronze);
  transition: filter 0.12s ease;
}
.dk-lobby-btn:hover { filter: brightness(1.2); }
.dk-lobby-primary {
  width: 100%; background: linear-gradient(180deg, var(--bronze-hi), var(--bronze));
  color: var(--bronze-ink); border-color: var(--bronze-hi); font-weight: 700;
}
/* VS CPU (plan/ai-opponent): the instant single-player entry, stacked under
   Create as a full-width secondary action on the main (entry) lobby surface. */
.dk-lobby-vscpu { width: 100%; margin-top: 8px; }
.dk-lobby-or { margin: 18px 0 12px; color: var(--ink-faint); font-size: 0.85rem; letter-spacing: 0.05em; }
.dk-lobby-joinrow { display: flex; gap: 8px; }
.dk-lobby-joinrow input {
  flex: 1; min-width: 0; padding: 8px 10px; text-align: center;
  background: var(--bg-2); color: var(--gold); border: 1px solid var(--hair-2);
  border-radius: 8px; font-family: var(--mono); font-size: 1.3rem; letter-spacing: 0.32em;
  text-transform: uppercase;
}
/* Inspectable elements (squares / pieces-on-squares / cards) — attachInspect adds
   .dk-press. touch-action:none keeps a touch-and-hold from being claimed by the
   browser as scroll/zoom (which would fire pointercancel and kill the long-press);
   the callout/selection/tap-highlight suppressions stop the native long-press menu
   + text selection from hijacking the gesture on Android Chrome. (Long-press inspect
   fix — see attachInspect.) */
.dk-press {
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.dk-lobby-joincode { margin: 6px 0 14px; color: var(--gold-dim); font-family: var(--mono); font-size: 0.95rem; letter-spacing: 0.12em; }
/* Per-element hide rules for the state-driven lobby (setLobbyMode toggles .hidden).
   There is NO global `.hidden{display:none}` in this codebase — hiding is per
   component — and these elements set their own `display`, so the .X.hidden selector
   (higher specificity) is what actually removes them in the joiner/created states. */
.dk-lobby-field.hidden,
.dk-lobby-preview.hidden,
.dk-lobby-joincode.hidden,
.dk-lobby-btn.hidden,
.dk-lobby-or.hidden,
.dk-lobby-joinrow.hidden,
.dk-lobby-exp.hidden,
.dk-lobby-expsize.hidden,
.dk-lobby-joinrow input.hidden { display: none; }
/* Experimental-mode lobby block: the host size row + the shared formation preview. */
.dk-lobby-exp { margin-bottom: 16px; }
.dk-lobby-expsize { display: flex; gap: 8px; margin-bottom: 12px; }
.dk-lobby-expsize label { flex: 1; font-size: 0.82rem; color: var(--gold-dim); text-align: left; }
.dk-lobby-expsize input, .dk-lobby-expsize select {
  display: block; width: 100%; margin-top: 4px; padding: 6px 8px;
  background: var(--bg-2); color: var(--gold); border: 1px solid var(--hair-2);
  border-radius: 6px; font-family: var(--mono); font-size: 0.9rem;
}
.dk-lobby-warn { min-height: 1em; margin-top: 6px; font-size: 0.82rem; color: var(--gold-dim); text-align: left; }
.dk-lobby-warn.err { color: var(--enemy-hi); }
/* "Both sides use my formation" — a host-only checkbox living in the size row.
   `flex-wrap` on the parent is what drops it onto its own line: the three sizing
   fields still fit one row, so wrapping changes nothing until this 4th item
   forces it. It must also opt OUT of the block/full-width input styling above,
   which is written for the text and select controls beside it. */
.dk-lobby-expsize { flex-wrap: wrap; }
/* Scoped to the parent on purpose: `.dk-lobby-expsize label` above is (0,1,1) and
   would out-specify a bare `.dk-lobby-mirror` (0,1,0), leaving `flex: 1` in force
   and squeezing this into a 4th column beside Alignment. */
.dk-lobby-expsize .dk-lobby-mirror {
  flex: 0 0 100%; display: flex; align-items: center; gap: 8px;
  margin-top: 4px; cursor: pointer;
}
.dk-lobby-expsize .dk-lobby-mirror input[type="checkbox"] {
  display: inline-block; width: auto; margin-top: 0; padding: 0; flex: none;
  accent-color: var(--gold); cursor: pointer;
}
/* Shown to a joiner in place of the picker the host's mirror took away. */
.dk-lobby-mirrornote {
  margin: 2px 0 6px; padding: 8px 10px; text-align: left;
  background: var(--bg-2); border: 1px solid var(--hair-2); border-radius: 6px;
  font-size: 0.82rem; color: var(--gold-dim);
}
.dk-lobby-mirrornote.hidden { display: none; }
/* 3×3 formation preview — front row on top (toward the enemy), owner tint. */
.dk-formation-preview { display: flex; justify-content: center; margin: 4px 0 2px; }
.dk-formation-preview.hidden { display: none; }
.dk-fpreview-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px;
  padding: 6px; background: var(--bg-2); border: 1px solid var(--hair-2); border-radius: 8px;
}
.dk-fpreview-cell {
  width: 26px; height: 26px; display: flex; align-items: center; justify-content: center;
  font-size: 1.25rem; line-height: 1; color: var(--gold);
  background: color-mix(in srgb, var(--bg-1) 60%, transparent); border-radius: 4px;
}
.dk-lobby-status { min-height: 1.2em; margin-top: 14px; color: var(--gold-dim); font-size: 0.9rem; }
.dk-lobby-status.err { color: var(--enemy-hi); }

/* Account chip (plan/accounts Slice 1) — sits above the lobby title in all
   three lobby modes. The GIS button renders into #gis-button; the handle row
   is the pre-INSERT name prompt shared by guest mint and first sign-in. */
.dk-lobby-account { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-bottom: 14px; }
.dk-account-status { display: flex; align-items: center; gap: 10px; font-size: 0.9rem; color: var(--gold-dim); }
.dk-account-status #account-name { color: var(--gold); }
.dk-account-signout {
  padding: 3px 10px; font-size: 0.78rem; color: var(--gold-dim);
  background: var(--bg-2); border: 1px solid var(--hair-2); border-radius: 999px; cursor: pointer;
}
.dk-account-signout:hover { color: var(--gold); border-color: var(--bronze); }
.dk-account-handlerow { width: 100%; text-align: left; }
.dk-account-handlerow input {
  display: block; width: 100%; margin-top: 6px; padding: 8px 10px;
  background: var(--bg-2); color: var(--gold); border: 1px solid var(--hair-2);
  border-radius: 8px; font-family: var(--mono); font-size: 0.95rem;
}
.dk-account-handlebtns { display: flex; gap: 8px; margin-bottom: 10px; }
.dk-account-status.hidden, .dk-account-gis.hidden, .dk-account-handlerow.hidden { display: none; }

/* Entry gate (Slice 4) — the sign-in / play-as-guest fork shown until an
   account exists. Everything else in the card hides behind it. */
.dk-lobby-gate { display: flex; flex-direction: column; align-items: center; gap: 12px; margin: 6px 0 18px; }
.dk-lobby-gate.hidden { display: none; }
.dk-lobby-gatehead { color: var(--gold-dim); font-size: 0.92rem; }
.dk-lobby-gatenote { color: var(--ink-faint); font-size: 0.76rem; }
.dk-lobby-gate .dk-lobby-or { margin: 2px 0; }
#lobby-guest { min-width: 200px; }

/* Slice 4 action states: a dimmed (guest-locked) Create, disabled buttons when
   no valid deck exists, and the build-your-deck CTA that replaces the picker. */
.dk-lobby-locked { opacity: 0.55; filter: saturate(0.5); }
.dk-lobby-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.dk-lobby-cta { display: block; text-align: center; text-decoration: none; margin: -6px 0 16px; }
.dk-lobby-cta.hidden { display: none; }
#lobby-deck.hidden { display: none; }
/* Under-budget deck row (backlog #74): a deck at the cap prints no footprint at
   all, so the ⬢N marker's PRESENCE is already the warning — this is the colour
   half of Maxx's "warn (orange)", and it is deliberately additive. Browsers
   honour colour on a native <option> unevenly (desktop mostly, native mobile
   pickers not at all), so nothing may rely on it: strip the rule and the label
   still says the same thing. Anything that must be seen belongs in the text. */
/* Both deck pickers, one rule — the lobby's and the rematch panel's (#75). Keyed
   on the CLASS rather than #lobby-deck so the second surface cannot quietly
   lose the colour half the way it would have under an id-scoped selector. */
option.deck-under-budget { color: var(--warned); }
.dk-lobby-created { margin-top: 18px; border-top: 1px solid var(--hair); padding-top: 16px; }
.dk-lobby-created.hidden { display: none; }
.dk-lobby-codelabel, .dk-lobby-sharelabel { color: var(--ink-faint); font-size: 0.8rem; letter-spacing: 0.06em; }
.dk-lobby-codebig {
  font-family: var(--mono); font-size: 2.6rem; font-weight: 600; letter-spacing: 0.3em;
  color: var(--gold); margin: 4px 0 14px; text-indent: 0.3em;
}
.dk-lobby-link {
  width: 100%; margin: 6px 0 10px; padding: 7px 9px;
  background: var(--bg); color: var(--gold-dim); border: 1px solid var(--hair-2);
  border-radius: 7px; font-family: var(--mono); font-size: 0.78rem; text-align: center;
}
.dk-lobby-waiting { margin-top: 14px; color: var(--gold-dim); font-size: 0.9rem; animation: dk-lobby-pulse 1.6s ease-in-out infinite; }
/* Created/waiting state: the "Waiting for opponent…" line moves up to fill the
   blank area under the header (where the deck/create controls were), and is larger
   so the screen reads as a waiting screen rather than a half-loaded form. */
/* Resume banner (2026-08-01): the way back into a match already in progress.
   Deliberately the most prominent thing on the lobby when present — a player who
   cannot get back into their own game is watching it play itself out. Uses rgba(var(--x-rgb), A),
   never rgba(var(--x-rgb) / A): the comma-less form silently drops the whole
   declaration for these comma-separated vars (the carry-tray bug). */
.dk-lobby-resume { margin: 18px 0 20px; padding: 14px 16px; border: 1px solid rgba(var(--gold-rgb), 0.55); border-radius: 10px; background: rgba(var(--gold-rgb), 0.08); box-shadow: 0 0 18px rgba(var(--gold-rgb), 0.12); }
.dk-lobby-resume.hidden { display: none; }
.dk-lobby-resumehead { color: var(--gold); font-size: 1.05rem; letter-spacing: 0.02em; margin-bottom: 2px; }
.dk-lobby-resumemeta { opacity: 0.72; font-size: 0.86rem; margin-bottom: 10px; }
/* Full-width, generous tap target: this is the one control on the lobby a player
   reaches for when something has already gone wrong. */
.dk-lobby-resumebtn { display: block; width: 100%; min-height: 44px; }
.dk-lobby-resumestatus { margin-top: 8px; font-size: 0.86rem; min-height: 1.1em; color: var(--gold); }
.dk-lobby-resumestatus.err { color: var(--enemy-hi); }
.dk-lobby-waiting-top { margin: 22px 0 24px; padding: 4px 0; color: var(--gold); font-size: 1.2rem; letter-spacing: 0.02em; animation: dk-lobby-pulse 1.6s ease-in-out infinite; }
.dk-lobby-waiting-top.hidden { display: none; }
@keyframes dk-lobby-pulse { 0%,100% { opacity: 0.5; } 50% { opacity: 1; } }

/* Board-wide target — a player-scoped card (every effect `scope: player`: TRAPPER
   and the play-modifier metas) aims at the PLAYER, not a square, so the whole board lights
   as one target and any square confirms it (Maxx, 2026-07-27). Deliberately NOT the
   per-square .target dot repeated 64 times: that reads as "pick one of these", which
   is the opposite of what this card wants to say. A breathing gold frame + a faint
   inward wash instead — the board as a single object. */
#board.board-wide-target {
  cursor: pointer;
  box-shadow:
    inset 0 0 0 2px rgba(var(--gold-rgb), 0.85),
    inset 0 0 34px rgba(var(--glow-amber-rgb), 0.30),
    0 0 22px rgba(var(--glow-amber-rgb), 0.34);
  animation: dk-boardwide 1.9s ease-in-out infinite;
}
@keyframes dk-boardwide {
  0%, 100% { box-shadow:
    inset 0 0 0 2px rgba(var(--gold-rgb), 0.60),
    inset 0 0 26px rgba(var(--glow-amber-rgb), 0.20),
    0 0 16px rgba(var(--glow-amber-rgb), 0.24); }
  50%      { box-shadow:
    inset 0 0 0 2px rgba(var(--gold-rgb), 0.95),
    inset 0 0 40px rgba(var(--glow-amber-rgb), 0.38),
    0 0 28px rgba(var(--glow-amber-rgb), 0.42); }
}
@media (prefers-reduced-motion: reduce) {
  #board.board-wide-target { animation: none; }
}

/* Carry flight — a player-scoped card travels from the hand to its seat's carry
   tray on commit, so the card visibly BECOMES the standing carry rather than just
   vanishing from hand. A cloned mini face tweened between the two rects. */
.dk-carry-flight {
  position: fixed; z-index: 60; pointer-events: none;
  border-radius: 4px; overflow: hidden;
  box-shadow: 0 4px 18px rgba(0 0 0 / 0.55);
  transition: transform 460ms cubic-bezier(.3,.7,.3,1), opacity 460ms ease-in;
}
@media (prefers-reduced-motion: reduce) {
  .dk-carry-flight { transition-duration: 1ms; }
}
/* A STANDING rule-mod token reads differently from a pending consumable: it is not
   waiting to be spent, it is simply in force for the rest of the match. */
.dk-carry .tray.standing { border-color: rgba(var(--gold-rgb), 0.85); }
.dk-carry .tray.standing .badge { color: var(--gold, #c6a04a); }
