/* Gameboard v2 — token palette + utility classes (Phase 0)
   Ported verbatim from docs/design-handoffs/2026-04-30-gameboard/project/Gameboard.html lines 11–22 + 51–55 + 57.
   Phase A introduces component-level styles (cards, board, etc.). */

/* Uniquely-named faces, loaded via WKWebView's NATIVE resource loader (reliable over
   capacitor:// — the JS FontFace-API fetch of these woff2 failed on iOS, same class as the
   sound bug, leaving canvas text on fallback metrics → overlapping text). The family names
   are UNIQUE (not 'Cinzel'/'Crimson Text') so declaring them can't restyle other board UI
   that references the real families (e.g. bja-tip tooltips, the hero verdict).
   Two consumers: (1) share-card.js canvas (post-match share card — needs the face in
   document.fonts before drawing); (2) the DESKTOP 1v1 redesign chrome (header ornament,
   WinCounter, HOW TO PLAY — the desktop block at the end of this file), so BJACardCinzel
   fetches on desktop board loads. BJACardCrimson is still canvas-only/lazy. */
@font-face { font-family: 'BJACardCinzel'; src: url('/fonts/cinzel-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'BJACardCinzel'; src: url('/fonts/cinzel-900.woff2') format('woff2'); font-weight: 900; font-display: swap; }
@font-face { font-family: 'BJACardCrimson'; src: url('/fonts/crimson-text-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'BJACardCrimson'; src: url('/fonts/crimson-text-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
/* Desktop redesign PR 2: the rules-line face ("Highest Score Wins!"). Same
   unique-name/self-hosted pattern — the mockup's IM Fell English italic,
   fetched only where the desktop rules restyle references it. */
@font-face { font-family: 'BJAFellItalic'; src: url('/fonts/im-fell-english-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }

:root {
  --ink: #09090b; --ink-2: #121216; --ink-3: #17131a;
  --vellum: #f4f0e8; --vellum-dim: #cfc7b8; --parchment: #d8ccb0;
  --brass: #b7a27a; --brass-hi: #e0c489; --brass-lo: #8a6a3c;
  --oxblood: #6f52be; --oxblood-hi: #8f74d0;
  --steel: #5a6b7a; --steel-hi: #8ea3b4;
  --iron: #1c1720; --iron-edge: #2a2430; --bone: #d8d0c2; --ember: #d9884f;
  --purple: #7b4ba8; --purple-hi: #a07ec0;
  --orange: #c87020; --orange-hi: #e89244;
}

/* Page-shell rules. Body is solid ink — no image — so cold loads of /game
   never flash the brand medallion in the pre-mount window (2026-05-15 fix).
   Intentional gameboard backdrops are owned by .gv2-page or per-mode body
   classes (campaign uses --gv2-c-boss-bg; arena/1v1 default to solid ink
   until follow-up backdrop work lands). */
html, body { color: var(--vellum); margin: 0; padding: 0; min-height: 100vh; min-height: 100dvh; font-family: 'Inter', sans-serif; }
body {
  background-color: var(--ink);
}
* { box-sizing: border-box; }

/* Phase D PR 13 §3 — kill iOS Safari grey tap-highlight (paired with §7 :active). */
html { -webkit-tap-highlight-color: transparent; touch-action: manipulation; /* manipulation kills iOS double-tap-zoom + 300ms tap delay; keeps pan + pinch */ }

/* Phase D PR 13 §2 — touch-action: manipulation. Kills iOS Safari 300ms
   double-tap-zoom delay on every interactive control. Includes .gv2-mf-overlay
   (no cursor:pointer but has tap-to-skip click handler in match-found-overlay.js). */
.gv2-ac, .gv2-cb, .gv2-bc-forfeit, .gv2-page .gv2-ar-card,
.gv2-overlay-chooser-tile, .gv2-overlay-chooser-btn, .gv2-overlay-btn,
.gv2-qr-close, .gv2-ca-help-link, .gv2-dctl-btn,
.gv2-me-cta-btn,
.gv2-me-xp-section[data-xp-animating="1"],
.gv2-mf-overlay {
  touch-action: manipulation;
}

/* Phase D PR 13 §7 — Brass/oxblood :active tap feedback (paired with §3).
   Subtle press-down (1px) + warm shimmer (8% brightness). Disabled-state
   guards prevent feedback firing on non-actionable controls. */
.gv2-ac:not(.gv2-ac-state-disabled):not(.gv2-ac-state-used):not(.gv2-ac-state-ghost):active,
.gv2-cb:not(.gv2-cb-disabled):active,
.gv2-bc-forfeit:active,
.gv2-page .gv2-ar-card:not(.gv2-ar-card-disabled):active,
.gv2-overlay-chooser-tile:active,
.gv2-overlay-chooser-btn:not(:disabled):active,
.gv2-overlay-btn:not(:disabled):active,
.gv2-qr-close:active,
.gv2-ca-help-link:active,
.gv2-dctl-btn:active,
.gv2-me-cta-btn:not(:disabled):active {
  transform: translateY(1px);
  filter: brightness(1.08);
}

/* Phase D follow-up — keyboard-side parity with the :active block above.
   Brass-hi outline on `:focus-visible` for v2 button controls that previously
   relied on the browser default focus ring. Mirrors the :active selector list,
   minus .gv2-qr-close + .gv2-ca-help-link (which already have their own
   :focus-visible rules elsewhere) and minus 3 div-rendered targets (.gv2-ac,
   .gv2-ar-card, .gv2-overlay-chooser-tile — those are <div>s without tabindex
   today, tracked as a separate a11y follow-up). 2px offset matches the
   .gv2-ca-help-link convention. */
.gv2-cb:not(.gv2-cb-disabled):focus-visible,
.gv2-bc-forfeit:focus-visible,
.gv2-overlay-chooser-btn:not(:disabled):focus-visible,
.gv2-overlay-btn:not(:disabled):focus-visible,
.gv2-dctl-btn:focus-visible,
.gv2-me-cta-btn:not(:disabled):focus-visible {
  outline: 2px solid var(--brass-hi);
  outline-offset: 2px;
}
.placeholder-card { max-width: 600px; margin: 80px auto; padding: 32px; background: rgba(14,14,18,.94); color: var(--vellum); border: 1px solid var(--brass-lo); position: relative; }
code { font-family: 'JetBrains Mono', monospace; }

/* Utility classes */
.mono { font-family: 'JetBrains Mono', monospace; font-feature-settings: "tnum"; }
.serif { font-family: 'Cormorant Garamond', serif; }
.frak { font-family: 'UnifrakturMaguntia', serif; }
.ucase { text-transform: uppercase; letter-spacing: .14em; }

.corner-brackets { position: relative; }
.corner-brackets::before, .corner-brackets::after,
.corner-brackets > .cb-bl, .corner-brackets > .cb-br {
  content: ""; position: absolute; width: 12px; height: 12px;
  border-color: var(--brass-hi); border-style: solid; opacity: .7; pointer-events: none;
}
.corner-brackets::before  { top: 6px; left: 6px;  border-width: 1px 0 0 1px; }
.corner-brackets::after   { top: 6px; right: 6px; border-width: 1px 1px 0 0; }
.corner-brackets > .cb-bl { bottom: 6px; left: 6px;  border-width: 0 0 1px 1px; }
.corner-brackets > .cb-br { bottom: 6px; right: 6px; border-width: 0 1px 1px 0; }

@keyframes emberpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(217,136,79,.4), 0 0 12px 0 rgba(217,136,79,.25); }
  50%      { box-shadow: 0 0 0 4px rgba(217,136,79,0), 0 0 24px 2px rgba(217,136,79,.5); }
}

/* ─────────────────────────────────────────────────────────────────
   Phase A — Component primitives
   All rules below are scoped to .gv2-* selectors per plan decision.
   No bare-element selectors (body, *, h1, etc.) — those stay in the
   page-shell block above and are owned by Phase 0.
   ───────────────────────────────────────────────────────────────── */

/* PlayingCard — vellum playing card with rank/suit corners + center pip. */
.gv2-pc {
  width: var(--pc-w, 80px); height: var(--pc-h, 110px); position: relative;
  /* QA item 2 (2026-07-17): neutralized from #f4f0e8→#d8d0c2 to match the painted
     face art's paper (sampled ~#f4f4f4). The face PNGs have alpha:0 rounded corners
     (r≈6.65% of width), so the card background genuinely SHOWS THROUGH all four
     corners of every A/J/Q/K — a warm-tan card under neutral-white art puts visible
     tan nubs on the corners. The board + deal-flight rules carry their own
     neutralized copy (~:4881/:4891/:8749/:8932), but this base is what actually
     reaches 1v1 short-landscape (~:4650, size-only) and BOTH choosers, which mount
     OUTSIDE .gv2-page (chooser-fc.js:131, chooser-sw.js:187) where no board-scoped
     rule can touch them. Campaign lands here too and is fixed by the same stroke. */
  background: linear-gradient(180deg, #fbfbfb 0%, #eeeeee 100%);
  border: 1px solid #5a4418;
  box-shadow: 0 6px 14px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,240,200,.45);
  display: flex; align-items: center; justify-content: center; flex-direction: column;
  flex-shrink: 0;
}
.gv2-pc-dim {
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.15);
  filter: saturate(.6) brightness(.7);
  opacity: .55;
}
.gv2-pc-corner {
  position: absolute; font-family: 'Cormorant Garamond', serif; font-style: italic;
  /* C1 PR 3 item 2: weight 700 (heaviest in our import) gives the J a more
     pronounced terminal hook so it's not mistaken for a "1" at corner sizes. */
  font-weight: 700;
  font-size: 20px; line-height: 1; color: var(--pc-color, #1a1a14); text-align: center;
  z-index: 2;
}
.gv2-pc-corner-tl { top: 4px; left: 5px; }
.gv2-pc-corner-br { bottom: 4px; right: 5px; transform: rotate(180deg); }
.gv2-pc-pip {
  font-family: 'Cormorant Garamond', serif;
  font-size: var(--pc-pip, 48px); line-height: 1;
  color: var(--pc-color, #1a1a14); opacity: .92;
}
/* Cards 2-10 keep both procedural corner numbers and replace only the large
   center glyph with one shared illustration per suit. The source JPEGs have a
   white canvas; multiply blends that canvas into the card's neutral paper while
   preserving the colored design. Keep this below .gv2-pc-corner (z:2) and below
   the full-card A/J/Q/K art (z:3). */
.gv2-pc-number-art {
  position: absolute; left: 50%; top: 50%; z-index: 1;
  width: 76%; max-height: 64%; object-fit: contain;
  transform: translate(-50%, -50%); mix-blend-mode: multiply;
  display: block; pointer-events: none;
  -webkit-touch-callout: none; -webkit-user-drag: none; user-select: none;
}
/* QA item 2: painted face art (A/J/Q/K), emitted by PlayingCard.js OVER the
   procedural face. inset:0 covers the padding box, so .gv2-pc's border still
   frames the art.
   `fill`, NOT `contain` — and this is deliberate. The card box is NOT one ratio:
   the art is 512x704 (.7273), the default box 80x110 (.7273) and portrait 72x99
   (~:4873) match it, but the DESKTOP 1v1 board sets --pc-h to w*1.44928 (~:8756)
   = .690, and landscape/campaign squeeze to 50x66 = .757. Under `contain` the
   desktop board — the surface this art was drawn for — letterboxes ~5px of dead
   band above and below EVERY face card, against a parchment fill, while the 2-10s
   beside them run full-bleed. `cover` is worse: it crops toward the edges and the
   art's corner index (x .055-.146) is the only part of a buried fan card that
   reads. `fill` stretches by 5.4% at the desktop extreme, which on a painted
   portrait is imperceptible, and keeps every card edge-to-edge at every size.
   draggable=false + touch-callout:none stop a long-press "Save Image" mid-match
   (same reason as ActionCard). No !important needed: nothing else targets this
   class, and it reads no inline var — width/height are 100% of the parent, so
   PlayingCard's inline --pc-w/--pc-h drive it for free. */
.gv2-pc-img {
  position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%;
  object-fit: fill; display: block; pointer-events: none;
  -webkit-touch-callout: none; -webkit-user-drag: none; user-select: none;
}

/* ActionCard — base shell. Per-kind values flow via CSS custom properties. */
.gv2-ac {
  width: var(--gv2-ac-w, 80px); height: var(--gv2-ac-h, 110px); position: relative;
  background: var(--gv2-ac-bg, #1a1a1f);
  border: 1px solid var(--gv2-ac-border, #555);
  border-top: 2px solid var(--gv2-ac-border, #555);
  box-shadow: 0 6px 16px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.14),
              0 0 0 1px var(--gv2-ac-border, #555);
  color: var(--gv2-ac-fg, #fff);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; flex-shrink: 0; cursor: pointer;
  transition: transform .15s, box-shadow .15s; overflow: hidden;
}
.gv2-ac-fluid { width: 100%; max-width: 80px; min-width: 0; }
.gv2-ac-state-disabled { filter: saturate(.7) brightness(.85); opacity: .85; cursor: default;
  box-shadow: inset 0 0 12px rgba(0,0,0,.5); }
.gv2-ac-state-used { filter: saturate(.55) brightness(.7); opacity: .65; cursor: default;
  box-shadow: inset 0 0 12px rgba(0,0,0,.5); }
.gv2-ac-state-ghost { filter: saturate(.4) brightness(.6); opacity: .5; cursor: default;
  box-shadow: inset 0 0 12px rgba(0,0,0,.5); }

/* --- Painted art face (Batch B: docs/NUTS/2026-07-16-batch-b-power-cards/) ---
   ORDER IS LOAD-BEARING. This block sits AFTER the state rules on purpose:
   .gv2-ac-art is (0,1,0) — an exact specificity tie with .gv2-ac above and with
   .gv2-ac-state-* — so source order is the ONLY thing deciding border/background/
   box-shadow. Move this block above them and every painted card silently regains
   the procedural border it is meant to drop (the painting has its own baked in).

   aspect-ratio shapes the BOX to the art so the art never crops, bars, or stretches.
   It has to: the trays are fixed-height / fluid-width and the grid SHRINKS tracks
   (see actionTrackMetrics in render-geometry.js — "~80 on desktop, 52 on portrait
   1v1, ~50 in a narrow landscape column"), so a real box ranges ~0.44–1.6 while the art is fixed 0.727.
   No object-fit value absorbs that. Deriving height from width deletes the mismatch
   rather than hiding it. --gv2-ac-h is untouched and still owns the CSS face.
   --------------------------------------------------------------------------- */
.gv2-ac-art {
  height: auto;
  aspect-ratio: 512 / 704;
  border: 0;
  /* The card's own edge colour, so it never flashes the procedural gradient
     underneath while the <img> decodes — that gradient's palette diverged from the
     painting (+1 is brass here, green in the art). */
  background: var(--gv2-ac-art-bg, #14141a);
  box-shadow: 0 6px 16px rgba(0,0,0,.55);
}
/* iOS/WebKit collapses aspect-ratio height when a flex box has NO in-flow content.
   .gv2-ac-art has none — its <img> is position:absolute — so on iOS the card
   collapses (in the tray it crushes to the ~15px label band: a squat, half-height
   "sunk into the felt" tile; the FC/overflow/boon chooser cards go near-invisible).
   Chromium honors aspect-ratio, so it's iOS-only. This in-flow padding strut
   re-establishes the 512/704 height regardless of whether aspect-ratio resolves:
   percentage padding resolves against the containing block's WIDTH (the card width),
   so it equals the aspect height and never double-counts on engines that DO honor
   aspect-ratio. Base-scoped on purpose — every .gv2-ac-art surface has the same
   collapse. face:'css' cards (fan, SW chips) carry no .gv2-ac-art and flight cards
   force height:100%, so both are naturally excluded. Depends on block-size staying
   free: never put container-type:size on an art-card ancestor (see :4559). 512/704
   is also duplicated in ActionCard.js ART_W/H + render-geometry trayTileHeightFor —
   keep in sync. Batch B regression; verified in a headless render (tray 52×72 +
   FC-size 120×165) with aspect-ratio honored AND disabled. */
.gv2-ac-art::before {
  content: ""; display: block; width: 0;
  padding-top: calc(704 / 512 * 100%); /* 137.5% = 704/512 */
}
.gv2-ac-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; display: block;
  /* An <img> is natively drag- and long-press-able. -webkit-touch-callout:none at
     .gv2-ar-card covers the tray only — without this a long-press on a chooser/overlay card
     raises "Save Image" mid-match. */
  -webkit-touch-callout: none; -webkit-user-drag: none; user-select: none;
}
/* (0,2,0) — beats .gv2-ac-state-* (0,1,0) for `filter`, while their opacity/cursor
   still apply. Deliberately LOSES to the desktop foe-tray dim (0,7,0) in the desktop
   block:
   today's state filter loses to it too, so that is parity, not a regression.
   Dimmed harder than the CSS face because the painting is full-bleed — the
   procedural face drops its pattern overlays when dim, so it goes barer as well
   as darker, and art has no equivalent to shed. */
.gv2-ac-art.gv2-ac-state-disabled { filter: saturate(.6) brightness(.82); }
.gv2-ac-art.gv2-ac-state-used     { filter: grayscale(.65) saturate(.4) brightness(.66); }
.gv2-ac-art.gv2-ac-state-ghost    { filter: grayscale(.85) saturate(.25) brightness(.55); }

.gv2-ac-tick {
  position: absolute; width: 5px; height: 5px; opacity: .7; z-index: 1;
}
.gv2-ac-tick-tl { top: 2px; left: 2px;
  border-top: 1px solid var(--gv2-ac-border, #555); border-left: 1px solid var(--gv2-ac-border, #555); }
.gv2-ac-tick-tr { top: 2px; right: 2px;
  border-top: 1px solid var(--gv2-ac-border, #555); border-right: 1px solid var(--gv2-ac-border, #555); }
.gv2-ac-tick-bl { bottom: 2px; left: 2px;
  border-bottom: 1px solid var(--gv2-ac-border, #555); border-left: 1px solid var(--gv2-ac-border, #555); }
.gv2-ac-tick-br { bottom: 2px; right: 2px;
  border-bottom: 1px solid var(--gv2-ac-border, #555); border-right: 1px solid var(--gv2-ac-border, #555); }

.gv2-ac-glyph {
  position: relative; z-index: 2;
  font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 700;
  line-height: 1; letter-spacing: .02em;
}

/* Top-left corner indicator for score-mod ActionCards (+1/+2/-5).
   Mirrors PlayingCard's .gv2-pc-corner-tl so the value reads when
   adjacent fan cards overlap and hide the centered glyph. */
.gv2-ac-corner-tl {
  position: absolute; top: 4px; left: 5px; z-index: 3;
  font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 700;
  font-size: 20px; line-height: 1; letter-spacing: .02em;
  text-shadow: 0 1px 2px rgba(0,0,0,.7);
}

/* Sigil (ST/FC/SW/FH/FF). Those kinds render as painted art on every surface — so
   the ONLY thing that draws these is the art 404 fallback in ActionCard.js, which
   re-renders the procedural face when an image fails. That makes this the failure
   path's identity: without it a broken card shows an unstyled, unpositioned glyph
   and the player cannot tell Stall from Swap from Free Card. Batch B deleted the rest
   of the sigil styling with the art-only kinds and very nearly took these with it. */
.gv2-ac-sigil {
  position: relative; z-index: 2;
  line-height: 1; font-family: serif;
}
.gv2-ac-sigil-hazard { font-weight: 700; transform: rotate(-8deg) translateY(-2px);
  text-shadow: 0 0 10px #ffcc33, 0 0 20px rgba(255,80,40,.8), 0 2px 0 #1a0203; }
.gv2-ac-sigil-frost {
  text-shadow: 0 0 10px #dfefff, 0 0 18px rgba(127,205,255,.6), 0 1px 0 #0a1e36;
}
.gv2-ac-sigil:not(.gv2-ac-sigil-hazard):not(.gv2-ac-sigil-frost) {
  text-shadow: 0 0 10px var(--gv2-ac-accent-glow, #ffffffaa), 0 1px 4px rgba(0,0,0,.6);
}
/* FH-only sub-label tuning. Reachable via the art 404 fallback: cssFaceInner emits
   .gv2-ac-sub-fh for FH, and the fallback calls it. (Its sibling .gv2-ac-fh is NOT
   restored — FH always resolves to art, so ActionCard never emits that class, and the
   rule only re-stated the centring base .gv2-ac already does.) */
.gv2-ac-sub-fh {
  font-size: 6.5px; letter-spacing: .18em; padding: 0 1px;
}
.gv2-ac-sub {
  position: relative; z-index: 2;
  font-size: 7.5px; letter-spacing: .22em; text-transform: uppercase;
  opacity: .9; text-shadow: 0 1px 2px rgba(0,0,0,.6);
  margin-top: 2px; text-align: center; max-width: 100%; padding: 0 2px;
}
/* CSS custom-property shadow vars per plan (boon/bane/default text shadows) */
.gv2-ac { --gv2-shadow-default: 0 1px 2px rgba(0,0,0,.5);
  --gv2-shadow-boon: 0 0 10px var(--gv2-ac-accent-glow, #ffffffaa), 0 2px 0 #082408, 0 1px 4px rgba(0,0,0,.6);
  --gv2-shadow-bane: 0 0 10px var(--gv2-ac-accent-glow, #ffffffaa), 0 2px 0 #2a0506, 0 1px 4px rgba(0,0,0,.6); }

/* ActionCard pattern overlays — score-mods only (+1/+2/-5 => laurel/laurel-double/drip).
   The hazard/frost/veil/burst/weave overlays went with Batch B: their kinds
   (FH/FF/ST/FC/SW) are painted art now and never emit a pattern node. */
.gv2-ac-pattern { position: absolute; inset: 0; pointer-events: none; }
.gv2-ac-pattern-laurel-glow {
  background: radial-gradient(ellipse 80% 60% at 50% 110%, rgba(255,255,180,.18), transparent 70%);
}
.gv2-ac-pattern-laurel-line { left: 6%; right: 6%; height: 1px; opacity: .8; inset: auto;
  background: linear-gradient(90deg, transparent, var(--gv2-ac-accent, var(--brass-hi)), transparent); }
.gv2-ac-pattern-laurel-top { top: 6px; }
.gv2-ac-pattern-laurel-bottom { bottom: 6px; }
.gv2-ac-pattern-laurel-line-2 { left: 10%; right: 10%; height: 1px; opacity: .6; inset: auto;
  background: linear-gradient(90deg, transparent, var(--gv2-ac-accent, var(--brass-hi)), transparent); }
.gv2-ac-pattern-laurel-top-2 { top: 9px; }
.gv2-ac-pattern-laurel-bottom-2 { bottom: 9px; }

.gv2-ac-pattern-drip-bands {
  background: repeating-linear-gradient(180deg, transparent 0 8px, rgba(0,0,0,.2) 8px 9px);
  mix-blend-mode: multiply;
}
.gv2-ac-pattern-drip-top { left: 0; right: 0; top: 0; height: 3px; opacity: .7; inset: auto;
  background: linear-gradient(90deg, transparent, var(--gv2-ac-accent, var(--brass-hi)), transparent); }


/* UsedChip — small ~28px square indicator for spent tactical actions. */
.gv2-uc {
  width: var(--gv2-uc-w, 28px); height: var(--gv2-uc-w, 28px); flex-shrink: 0;
  background: var(--gv2-uc-bg, #1a1a1f);
  border: 1px solid var(--gv2-uc-border, #555);
  filter: saturate(.55) brightness(.65); opacity: .75;
  display: flex; align-items: center; justify-content: center;
  color: var(--gv2-uc-accent, var(--brass-hi));
  font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 600;
  font-size: 13px; line-height: 1; position: relative; overflow: hidden;
}
.gv2-uc-mark { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; }
.gv2-uc-mark > svg { width: 60%; height: 60%; display: block; }

/* DeckCard — face-down vellum cross-hatch + sword glyph. */
.gv2-dc {
  position: absolute;
  left: calc(50% - var(--gv2-dc-w, 70px) / 2 + var(--gv2-dc-x, 0px));
  top: var(--gv2-dc-y, 0px);
  width: var(--gv2-dc-w, 70px); height: var(--gv2-dc-h, 94px);
  transform: rotate(var(--gv2-dc-rot, 0deg));
  background:
    repeating-linear-gradient(45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(-45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    linear-gradient(180deg, #181221, #0c0a11);
  border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass);
  box-shadow: 0 6px 14px rgba(0,0,0,.6), inset 0 0 14px rgba(0,0,0,.5);
  display: flex; align-items: center; justify-content: center;
  color: var(--brass-hi);
}
.gv2-dc-glyph { font-family: 'UnifrakturMaguntia', serif; font-size: 28px; }

/* DeckCard fan-card variant — used as face-down playing-card replacement in
   the fan. Resets the absolute-stacking layout (designed for CenterAxis deck
   stack) so the card flows naturally inside .gv2-fan-card. */
.gv2-dc[data-size="fan"] {
  position: relative;
  left: auto;
  top: auto;
}
.gv2-dc[data-size="fan"] .gv2-dc-glyph { font-size: 36px; }

/* ScorePlaque — 150px-wide score block (no gauge per master plan addendum).
   z-index:51 lifts the plaque above the FF crack layer (.gv2-id-root z-index:50)
   and the vellum frost (.gv2-fo z-index:1) so the frozen score stays readable —
   notably on mobile, where the compressed layout puts the cracks over the smaller
   numeral. Shared 1v1 + campaign. 2026-06-06. */
.gv2-sp {
  position: relative; z-index: 51; padding: 12px 14px 14px; width: 150px; flex-shrink: 0;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 60%, #050306 100%);
  border: 1px solid var(--brass-lo);
  border-top: 3px solid var(--brass-hi);
  border-bottom: 2px solid var(--brass-lo);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06), 0 4px 12px rgba(0,0,0,.5);
  text-align: center; display: flex; flex-direction: column; overflow: hidden;
}
.gv2-sp-bust { border-color: var(--oxblood-hi); border-top-color: var(--oxblood-hi);
  box-shadow: 0 0 0 1px var(--oxblood-hi), 0 0 18px rgba(143,116,208,.45), inset 0 1px 0 rgba(255,255,255,.05); }
.gv2-sp-blackjack { box-shadow: 0 0 0 1px var(--brass-hi), 0 0 22px rgba(224,196,137,.45), inset 0 1px 0 rgba(255,255,255,.08);
  animation: emberpulse 2.2s infinite; }

.gv2-sp-band { position: absolute; left: 0; right: 0; top: 3px; height: 1px; opacity: .5;
  background: linear-gradient(90deg, transparent, var(--brass-hi), transparent); }
.gv2-sp-bust .gv2-sp-band { background: linear-gradient(90deg, transparent, var(--oxblood-hi), transparent); }
.gv2-sp-tick { position: absolute; width: 6px; height: 6px; opacity: .7; }
.gv2-sp-tick-tl { top: 5px; left: 5px;
  border-top: 1px solid var(--brass-hi); border-left: 1px solid var(--brass-hi); }
.gv2-sp-tick-tr { top: 5px; right: 5px;
  border-top: 1px solid var(--brass-hi); border-right: 1px solid var(--brass-hi); }
.gv2-sp-tick-bl { bottom: 5px; left: 5px;
  border-bottom: 1px solid var(--brass-lo); border-left: 1px solid var(--brass-lo); }
.gv2-sp-tick-br { bottom: 5px; right: 5px;
  border-bottom: 1px solid var(--brass-lo); border-right: 1px solid var(--brass-lo); }
.gv2-sp-bust .gv2-sp-tick-tl, .gv2-sp-bust .gv2-sp-tick-tr {
  border-top-color: var(--oxblood-hi); border-left-color: var(--oxblood-hi); border-right-color: var(--oxblood-hi); }
.gv2-sp-bust .gv2-sp-tick-bl, .gv2-sp-bust .gv2-sp-tick-br {
  border-bottom-color: var(--oxblood-hi); border-left-color: var(--oxblood-hi); border-right-color: var(--oxblood-hi); }

.gv2-sp-caption { font-size: 8px; letter-spacing: .4em; color: var(--vellum-dim);
  position: relative; z-index: 1; }
.gv2-sp-numeral-wrap { position: relative; margin-top: 2px; }
.gv2-sp-halo { position: absolute; inset: -8px -10px 0 -10px; pointer-events: none;
  background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(224,196,137,.10), transparent 70%); }
.gv2-sp-bust .gv2-sp-halo {
  background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(143,116,208,.35), transparent 70%); }
.gv2-sp-blackjack .gv2-sp-halo {
  background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(224,196,137,.32), transparent 70%); }
.gv2-sp-numeral {
  position: relative; font-size: 64px; line-height: 1; font-style: italic; font-weight: 600;
  color: var(--brass-hi);
  text-shadow: 0 0 14px rgba(224,196,137,.4), 0 2px 0 #0e0b14, 0 2px 12px rgba(0,0,0,.7);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.gv2-sp-bust .gv2-sp-numeral { color: var(--oxblood-hi); text-decoration: line-through;
  text-shadow: 0 0 14px rgba(143,116,208,.7), 0 2px 0 #1a0203, 0 2px 12px rgba(0,0,0,.7); }
.gv2-sp-subrow { font-size: 8px; letter-spacing: .3em; color: var(--vellum-dim);
  margin-top: 0; height: 10px; position: relative; z-index: 1; }
.gv2-sp-bust .gv2-sp-subrow { color: var(--oxblood-hi); }
.gv2-sp-blackjack .gv2-sp-subrow { color: var(--brass-hi); }
.gv2-sp-status { font-size: 7px; letter-spacing: .3em; color: var(--vellum-dim); margin-top: 6px; }
.gv2-sp-bust .gv2-sp-status { color: var(--oxblood-hi); }
.gv2-sp-blackjack .gv2-sp-status { color: var(--brass-hi); }

/* BoardChrome + RoundDot — top strip with round counter, dots, forfeit. */
.gv2-bc { max-width: 1320px; margin: 0 auto;
  display: grid; grid-template-columns: 240px 1fr 240px;
  align-items: center; gap: 20px; }
.gv2-bc-counter { display: flex; align-items: center; gap: 14px; }
.gv2-bc-counter-label { font-size: 9px; color: var(--vellum-dim); letter-spacing: .35em; }
.gv2-bc-counter-num { font-size: 30px; font-style: italic; color: var(--brass-hi);
  line-height: 1; letter-spacing: .02em; }
.gv2-bc-center { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.gv2-bc-caption { font-size: 11px; color: var(--vellum-dim); letter-spacing: .4em; }
.gv2-bc-mp { color: var(--oxblood-hi); margin-left: 10px; font-weight: 700;
  display: inline-block; animation: gv2-mp-pulse 1.6s ease-in-out infinite; } /* glow removed 2026-05-31 (P3); QA #8: pulse + weight only — NO font-size/scale here so the inline caption line doesn't reflow */
/* B7 (2026-05-28): on desktop the Match Point indicator moves to CenterAxis,
 * above "Highest Score Wins" (.gv2-ca-mp). Hide the top-strip copy here; it stays
 * inline on mobile / landscape-short (≤500h) where the rules callout is hidden,
 * so exactly one Match Point shows at every viewport. */
@media (min-width: 768px) and (min-height: 501px) {
  .gv2-bc-mp { display: none; }
}
.gv2-bc-dots { display: flex; gap: 8px; }
/* Mobile-only round timeline (dots + scoreline + Match Point) shown next to the
   deck — hidden on desktop, where the timeline lives in the top strip. Shown +
   positioned in the mobile @media block (search [data-gv2-mobile-timeline]). */
.gv2-mt { display: none; }
/* #4 wrapper around .gv2-axis (deck) + the mobile timeline. display:contents keeps
   both as DIRECT grid items on desktop (axis → center column; timeline display:none)
   and in landscape (grid-area c-axis / c-time). The portrait media block flips it to
   flex-row so the timeline sits to the RIGHT of the deck. Only exists in the 1v1
   board (campaign has its own shell), so these rules are inert elsewhere. */
.gv2-axis-mt { display: contents; }
.gv2-bc-right { display: flex; justify-content: flex-end; }
.gv2-bc-forfeit { padding: 8px 14px; background: transparent;
  border: 1px solid var(--brass-lo); color: var(--vellum-dim);
  font-size: 10px; letter-spacing: .3em; text-transform: uppercase;
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  transition: transform .12s ease; }
.gv2-bc-forfeit-icon { font-size: 14px; }

.gv2-rd { width: 14px; height: 14px; transform: rotate(45deg);
  border: 1px solid #8a8a92; background: #8a8a92;
  position: relative; }
.gv2-rd-p1 { background: var(--purple); border-color: var(--purple);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18); }
.gv2-rd-p2 { background: var(--orange); border-color: var(--orange);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18); }
/* PR 2 Bug D: DoN-doubled pip gets a counter-rotated ×2 marker positioned
   outside the rotated 14×14 body (otherwise the marker would inherit the
   45deg rotation and overlap the pip). The marker hangs off the bottom-right;
   adjacent pips have gap:8px so the -6px offset stays clear of neighbors. */
.gv2-rd-doubled::after { content: '×2'; position: absolute;
  bottom: -8px; right: -6px; transform: rotate(-45deg);
  font-size: 8px; font-weight: 700; line-height: 1;
  color: var(--brass-hi); pointer-events: none;
  text-shadow: 0 1px 0 rgba(0,0,0,.6); }

/* StatusRibbon — per-player state pill. Per-state colors. */
.gv2-sr { padding: 6px 10px; font-size: 10px; letter-spacing: .3em; text-align: center;
  border: 1px solid var(--iron-edge); }
.gv2-sr-turn { background: var(--gv2-sr-accent, var(--purple));
  border-color: var(--gv2-sr-accent, var(--purple)); color: #fff;
  animation: emberpulse 2.4s infinite; }
.gv2-sr-waiting { background: transparent; color: var(--vellum-dim); }
.gv2-sr-stood { background: var(--bone); border-color: var(--bone); color: #09090b; }
.gv2-sr-bust { background: var(--oxblood-hi); border-color: var(--oxblood-hi); color: #fff; }
.gv2-sr-frozen { background: #5dade2; border-color: #5dade2; color: #09090b; }
.gv2-sr-must-draw { background: var(--oxblood-hi); border-color: var(--oxblood-hi); color: #fff; }
.gv2-sr-won { background: var(--brass-hi); border-color: var(--brass-hi); color: #09090b; }
.gv2-sr-lost { background: var(--iron-edge); border-color: var(--iron-edge); color: var(--vellum-dim); }

/* TimeBankInline — header time-bank chip. */
.gv2-tb { display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 8px; background: rgba(0,0,0,.4);
  border: 1px solid var(--iron-edge); }
.gv2-tb-active { border-color: var(--brass-lo); }
.gv2-tb-danger { border-color: var(--oxblood-hi); }
.gv2-tb-danger.gv2-tb-active { animation: emberpulse 1s infinite; }
.gv2-tb-label { font-size: 7px; letter-spacing: .3em; color: var(--vellum-dim); }
.gv2-tb-danger .gv2-tb-label { color: var(--oxblood-hi); }
.gv2-tb-time { font-size: 12px; line-height: 1; color: var(--vellum-dim);
  font-variant-numeric: tabular-nums; font-weight: 600; }
.gv2-tb-active .gv2-tb-time { color: var(--brass-hi); }
.gv2-tb-danger .gv2-tb-time { color: var(--oxblood-hi); }
.gv2-tb-idle-dot { width: 4px; height: 4px; background: var(--vellum-dim);
  border-radius: 50%; opacity: .5; }

/* RoundsWonRail — per-player wins plaque under controls. */
.gv2-rwr { flex: 0 0 140px; padding: 10px 12px 8px;
  background: linear-gradient(180deg, rgba(20,16,10,.85), rgba(8,6,10,.9));
  border: 1px solid var(--gv2-rwr-accent, var(--purple));
  border-top: 2px solid var(--gv2-rwr-accent-hi, var(--gv2-rwr-accent, var(--purple)));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06), 0 4px 12px rgba(0,0,0,.4);
  text-align: center; display: flex; flex-direction: column; justify-content: center;
  position: relative; overflow: hidden; }
/* `.gv2-rwr-mp` box treatment removed PR 1, 2026-05-26 (border / box-shadow /
 * emberpulse stripped per Matt's spec). Match-point state is now distinguished
 * by the footer color tint rule below. Header `.gv2-bc-mp` glow removed too (P3, 2026-05-31). */
.gv2-rwr-label { font-size: 8px; color: var(--vellum-dim); letter-spacing: .35em; }
.gv2-rwr-num { font-size: 38px; font-style: italic;
  color: var(--gv2-rwr-accent-hi, var(--gv2-rwr-accent, var(--purple-hi)));
  line-height: 1; font-weight: 500; margin-top: 2px;
  text-shadow: 0 2px 10px rgba(0,0,0,.7); font-variant-numeric: tabular-nums; }
.gv2-rwr-denom { color: var(--vellum-dim); font-size: 18px; margin-left: 4px; }
.gv2-rwr-pips { display: flex; gap: 3px; justify-content: center; margin-top: 6px; }
.gv2-rwr-pip { width: 8px; height: 8px; transform: rotate(45deg);
  border: 1px solid var(--iron-edge); background: transparent; }
.gv2-rwr-pip-lit { background: var(--gv2-rwr-accent, var(--purple));
  border-color: var(--gv2-rwr-accent, var(--purple));
  box-shadow: 0 0 6px var(--gv2-rwr-accent, var(--purple)); }
.gv2-rwr-footer { font-size: 8.5px; letter-spacing: .22em; margin-top: 6px; color: var(--vellum-dim); }
.gv2-rwr-mp .gv2-rwr-footer { color: var(--oxblood-hi); }
.gv2-rwr-won .gv2-rwr-footer { color: var(--brass-hi); }

/* ============================================================
   Device-QA batch A (2026-07-02) — 1v1 board only.
   ============================================================ */
/* Item 3: kill the pulsing box on a 21/blackjack score. emberpulse (lines
   ~110) animates box-shadow, which repaints a breathing ring over the
   portrait-stripped plaque (.gv2-sp box-shadow:none, portrait block). Killing the
   animation removes the box on portrait and the pulse on desktop (the static
   brass ring at ~358 stays on desktop). Campaign keeps its pulse. */
.gv2-page:not([data-mode="campaign"]) .gv2-sp-blackjack { animation: none; }

/* Item 6: score numerals use Inter (upright), not Cormorant Garamond italic —
   the disliked "1" glyph. Inter loads no italic weight, so font-style:normal
   avoids faux-italic. Scoped to the 1v1 board; campaign stays on Cormorant. */
.gv2-page:not([data-mode="campaign"]) .gv2-sp-numeral,
.gv2-page:not([data-mode="campaign"]) .gv2-rwr-num,
.gv2-page:not([data-mode="campaign"]) .gv2-bc-counter-num,
.gv2-page:not([data-mode="campaign"]) .gv2-wc-numeral {
  font-family: 'Inter', sans-serif;
  font-style: normal;
}

/* PlayerControls + ControlBtn — Hit/Stand. */
.gv2-pcl { flex: 1; display: flex; gap: 10px; }
.gv2-cb { flex: 1; min-height: 64px; padding: 10px 12px;
  border: 1px solid #2a2a30; border-top: 2px solid #2a2a30;
  background: linear-gradient(180deg, #1a1a1f, #0e0e12);
  color: var(--vellum-dim); cursor: pointer;
  text-align: center; transition: transform .12s; position: relative; overflow: hidden; }
.gv2-cb-hit { background: linear-gradient(180deg, #c93030 0%, #6e0e0e 60%, #2a0506 100%);
  border-color: #ff4a4a; border-top-color: #ff4a4a; color: #ffe066;
  box-shadow: 0 6px 16px rgba(0,0,0,.5), 0 0 0 1px rgba(255,74,74,.27), inset 0 1px 0 rgba(255,255,255,.14); }
.gv2-cb-stand { background: linear-gradient(180deg, #3a7ab0 0%, #1f4a7a 60%, #0a1e36 100%);
  border-color: #7fcdff; border-top-color: #7fcdff; color: #dfefff;
  box-shadow: 0 6px 16px rgba(0,0,0,.5), 0 0 0 1px rgba(127,205,255,.27), inset 0 1px 0 rgba(255,255,255,.14); }
.gv2-cb-disabled { background: linear-gradient(180deg, #1a1a1f, #0e0e12) !important;
  border-color: #2a2a30 !important; color: var(--vellum-dim) !important;
  /* C1 PR 3 item 3: not-allowed communicates "cannot click" on opponent buttons
     (or own buttons during opponent's turn). !important wins over the base
     `cursor: pointer` on `.gv2-cb`. */
  cursor: not-allowed !important; opacity: .55;
  box-shadow: inset 0 0 8px rgba(0,0,0,.4) !important; }
.gv2-cb-glow { position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse 80% 60% at 50% 110%, rgba(255,255,255,.08), transparent 70%); }
.gv2-cb-disabled .gv2-cb-glow { display: none; }
.gv2-cb-label { position: relative; font-size: 26px; font-style: italic;
  letter-spacing: .08em; line-height: 1; font-weight: 700; }
.gv2-cb-hit:not(.gv2-cb-disabled) .gv2-cb-label {
  text-shadow: 0 0 12px rgba(255,224,102,.53), 0 2px 0 rgba(0,0,0,.6); }
.gv2-cb-stand:not(.gv2-cb-disabled) .gv2-cb-label {
  text-shadow: 0 0 12px rgba(223,239,255,.53), 0 2px 0 rgba(0,0,0,.6); }
.gv2-cb-sub { position: relative; font-size: 8px; letter-spacing: .3em; margin-top: 4px; opacity: .8; }

/* CenterAxis + DeckTotem + DeckControls + CardCountStrip + MatchClock */
.gv2-ca { display: flex; flex-direction: column; align-items: stretch; gap: 12px;
  padding-top: 110px; position: relative; }
/* 2026-06-01 (QA #5): "Highest Score Wins" plaque box RESTORED per Matt's directive
   (bg + brass border). Supersedes B7 2026-05-28, which had removed it to read as plain
   text. Match Point now floats ABOVE this box (moved out of .gv2-ca-rules in CenterAxis.js). */
.gv2-ca-rules { text-align: center; padding: 18px 22px; position: relative;
  align-self: center; /* shrink-wrap the plaque to its content + center it in the
    stretch column (flex-correct; inline-block is blockified on a flex item) */
  background: rgba(0,0,0,.35); border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass-hi); border-radius: 2px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 6px 18px rgba(0,0,0,.45); }
/* Match Point: now a sibling floating in the gap ABOVE the plaque (was inside
   .gv2-ca-rules). 2026-06-01 (QA #5): 2x size (30px) + text-align:center (no longer
   inherits centering from the rules box). The match-point TEXT stays glow-free
   (opacity-only pulse, P3 2026-05-31) — only the plaque box came back, not a chip glow.
   Hidden on mobile via `.gv2-page .gv2-ca-mp` in the mobile media block; the top-strip
   .gv2-bc-mp shows there instead (one Match Point per viewport). */
.gv2-ca-mp { color: var(--oxblood-hi); font-size: 30px; letter-spacing: .3em;
  text-align: center; font-weight: 700; margin: 0 0 18px;
  animation: gv2-mp-pulse 1.6s ease-in-out infinite; }
@keyframes gv2-mp-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
@media (prefers-reduced-motion: reduce) {
  .gv2-ca-mp, .gv2-bc-mp { animation: none; }
}
/* C1 PR 3 item 5: bumped sizes for legibility — title 14→18, sub 8→11. */
.gv2-ca-rules-title { font-size: 18px; font-style: italic; color: var(--brass-hi);
  line-height: 1.2; font-weight: 500; text-shadow: 0 1px 2px rgba(0,0,0,.6); }
.gv2-ca-rules-target { font-size: 18px; font-style: italic; color: var(--brass-hi);
  line-height: 1.2; font-weight: 500; text-shadow: 0 1px 2px rgba(0,0,0,.6); margin-top: 2px; }
.gv2-ca-rules-sub { font-size: 11px; color: var(--vellum-dim); letter-spacing: .3em; margin-top: 4px; }
.gv2-ca-help { text-align: center; margin-top: 4px; }
.gv2-ca-help-link { font-size: 8px; color: var(--vellum-dim); letter-spacing: .3em;
  text-decoration: none; border-bottom: 1px dotted var(--brass-lo); padding-bottom: 2px; }

.gv2-dt { position: relative; height: 130px;
  display: flex; justify-content: center; align-items: flex-start; }

/* Mobile-footer container: rendered as 4th child of .gv2-game-grid.
   Hidden by default; revealed inside the mobile media query (line ~2960)
   where it relocates the toggle row + count strip + how-to-play link to a
   strip below the player areas. Source order matters: this default rule
   MUST stay above the mobile media query block. */
.gv2-mobile-footer { display: none; }

/* Landscape-only center-column nodes (Option B). Default-hidden grid children of
   .gv2-game-grid (1v1 shell); shown + grid-placed only inside the landscape media
   block far below. MUST stay above that block in source order. Inert on
   desktop/portrait (display:none → no grid cell, no layout shift). */
.gv2-ls-round, .gv2-ls-turn, .gv2-ls-concede, .gv2-ls-boss { display: none; }

.gv2-dctl { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px;
  padding: 6px; background: rgba(0,0,0,.35); border: 1px solid #17131a; }
.gv2-dctl-btn { aspect-ratio: 1; background: rgba(20,16,10,.7);
  border: 1px solid #2a2520; color: var(--brass-hi);
  font-size: 16px; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer; }
/* iOS: the voice engine is stripped from the bundle (no mic surface), so hide the
   voice button — a control that no-ops is a tell. html.bja-ios is set by the injector;
   web never has it, so web is byte-identical. */
html.bja-ios .gv2-dctl-btn[data-act="voice"] { display: none; }

.gv2-ccs { padding: 8px 10px; background: rgba(0,0,0,.3); border: 1px solid #17131a;
  display: grid; grid-template-rows: auto auto; gap: 6px; }
.gv2-ccs-row { display: flex; justify-content: space-between; align-items: baseline; }
.gv2-ccs-label { font-size: 8px; color: var(--vellum-dim); letter-spacing: .3em; }
.gv2-ccs-count { font-size: 18px; font-style: italic; color: var(--brass-hi);
  font-variant-numeric: tabular-nums; }
.gv2-ccs-deck { font-size: 14px; color: var(--vellum); font-variant-numeric: tabular-nums; }

.gv2-mc { padding: 8px 10px; background: rgba(0,0,0,.4);
  border: 1px solid var(--brass-lo); text-align: center; }
.gv2-mc-danger { border-color: var(--oxblood-hi); animation: emberpulse 1s infinite; }
.gv2-mc-label { font-size: 8px; color: var(--vellum-dim); letter-spacing: .3em; }
.gv2-mc-danger .gv2-mc-label { color: var(--oxblood-hi); }
.gv2-mc-time { font-size: 22px; font-style: italic; line-height: 1.1;
  color: var(--brass-hi); font-variant-numeric: tabular-nums; margin-top: 2px; }
.gv2-mc-danger .gv2-mc-time { color: var(--oxblood-hi); }

/* FootStrip — message panel only (actions-remaining tracker removed). */
.gv2-fs { display: block; }
.gv2-fs-msg { padding: 16px 20px; background: rgba(0,0,0,.4);
  border: 1px solid var(--brass-lo); text-align: center; position: relative; }
.gv2-fs-msg-title { font-size: 26px; font-style: italic; color: var(--brass-hi); line-height: 1; }
.gv2-fs-msg-body { font-size: 14px; font-style: italic; color: var(--vellum-dim); margin-top: 6px; }
.gv2-fs-msg-rule { position: absolute; left: 20px; right: 20px; bottom: 6px; height: 1px; opacity: .5;
  background: repeating-linear-gradient(90deg, var(--brass-lo) 0 3px, transparent 3px 6px); }

/* FrostOverlay — frozen-state decorative wash.
   Mounted unconditionally as a child of .gv2-pa; visibility gated by
   data-state="frozen". mix-blend-mode: screen keeps cards readable.
   Redesigned for vellum/brass aesthetic: ice-tinted vellum washes + thin
   brass-low ice edge + faint diagonal "ice crack" lines + soft pulse.
   z-index: 1 keeps boon-trigger / chooser overlays cleanly above. */
.gv2-fo {
  position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen;
  display: none; z-index: 1;
  box-shadow: inset 0 0 0 1px rgba(140, 110, 60, .35),
              inset 0 0 24px rgba(93, 173, 226, .18);
  background:
    radial-gradient(ellipse at 22% 28%, rgba(200, 215, 195, .14), transparent 55%),
    radial-gradient(ellipse at 78% 72%, rgba(180, 200, 215, .14), transparent 55%),
    repeating-linear-gradient(118deg, rgba(220, 230, 235, .05) 0 1px, transparent 1px 14px),
    repeating-linear-gradient(62deg, rgba(220, 230, 235, .04) 0 1px, transparent 1px 22px);
  animation: gv2-fo-pulse 2.4s ease-in-out infinite alternate;
}
.gv2-pa[data-state="frozen"] .gv2-fo { display: block; }
@keyframes gv2-fo-pulse {
  from { opacity: .85; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .gv2-fo { animation: none; opacity: .92; }
}

/* Demo page sample-row styling (Phase A demo only). */
.gv2-demo-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
.gv2-demo-section { margin: 32px 0; }
.gv2-demo-section h2 { font-family: 'Cormorant Garamond', serif; font-style: italic;
  color: var(--brass-hi); font-size: 24px; margin: 0 0 12px; }
.gv2-demo-section .gv2-demo-eyebrow { font-family: 'JetBrains Mono', monospace;
  font-size: 10px; letter-spacing: .3em; text-transform: uppercase;
  color: var(--brass); margin-bottom: 8px; }
.gv2-demo-page { color: var(--vellum); background: var(--ink);
  min-height: 100vh; min-height: 100dvh; padding: 32px 48px; }
.gv2-demo-page h1 { font-family: 'Cormorant Garamond', serif; font-style: italic;
  color: var(--brass-hi); font-size: 32px; margin: 0; }
.gv2-demo-page .gv2-demo-pageheader { margin-bottom: 32px; }
.gv2-demo-label { font-family: 'JetBrains Mono', monospace; font-size: 10px;
  color: var(--vellum-dim); margin-bottom: 4px; letter-spacing: .15em; }
.gv2-demo-stage { padding: 16px; background: rgba(20,16,10,.5);
  border: 1px solid var(--iron-edge); margin-bottom: 12px; }

/* ============================================
   PHASE B — fx-* CSS port from public/css/game.css
   These are duplicated here so /game-v2 has the EffectsManager
   visual rules without loading game.css. game.css is locked-out
   for Phase B; Phase F deletes it. Keep these in sync if game.css
   fx-* rules are tweaked during overlap window (unlikely — v1
   path is frozen during the v2 rebuild).
   ============================================ */

/* Floating text — rises and fades from a target */
.fx-float {
  position: fixed; pointer-events: none; z-index: 9999;
  font-family: var(--gv2-font-display, 'Cormorant Garamond', serif); font-weight: 900; font-size: 1.4rem;
  transform: translateX(-50%);
  text-shadow: 0 1px 6px rgba(0,0,0,0.7);
  animation: gv2-fx-float-up 1s ease-out forwards;
}
.fx-float.fx-positive { color: #3a9e50; font-size: 2rem; }
.fx-float.fx-negative { color: #cc3333; font-size: 2rem; }
.fx-float.fx-win { color: #3a9e50; font-size: 5rem; letter-spacing: 6px; font-weight: 900;
  text-shadow: 0 3px 12px rgba(0,0,0,0.8), 0 0 30px rgba(58,158,80,0.4);
  animation: gv2-fx-win-slam 2.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
.fx-float.fx-lose { color: #cc3333; font-size: 5rem; letter-spacing: 6px; font-weight: 900;
  text-shadow: 0 3px 12px rgba(0,0,0,0.8), 0 0 30px rgba(204,51,51,0.4);
  animation: gv2-fx-lose-slam 2.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
.fx-float.fx-bust { color: #cc3333; font-size: 7rem; letter-spacing: 8px; font-weight: 900;
  text-shadow: 0 3px 12px rgba(0,0,0,0.8), 0 0 40px rgba(204,51,51,0.4);
  animation: gv2-fx-bust-slam 2.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
.fx-float.fx-push { color: #8a8070; font-size: 5rem; letter-spacing: 6px; font-weight: 900;
  text-shadow: 0 3px 12px rgba(0,0,0,0.8), 0 0 20px rgba(138,128,112,0.3);
  animation: gv2-fx-push-slam 2.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
.fx-float.fx-twenty-one { color: var(--brass-hi, #c9b07a); font-size: 2.6rem; letter-spacing: 2px;
  text-shadow: 0 2px 8px rgba(0,0,0,0.7); }
.fx-float.fx-info { color: #3366aa; }

@keyframes gv2-fx-float-up {
  0%   { opacity: 1; transform: translateX(-50%) translateY(0)    scale(0.8); }
  20%  { opacity: 1; transform: translateX(-50%) translateY(-10px) scale(1.1); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-50px) scale(0.9); }
}
@keyframes gv2-fx-win-slam {
  0%   { opacity: 0; transform: translateX(-50%) translateY(0)    scale(3.5) rotate(5deg); filter: brightness(2.5); }
  10%  { opacity: 1; transform: translateX(-50%) translateY(-5px) scale(1.2) rotate(5deg); filter: brightness(1.5); }
  15%  {             transform: translateX(-50%) translateY(-5px) scale(1.0) rotate(5deg); filter: brightness(1); }
  80%  { opacity: 1; transform: translateX(-50%) translateY(-10px) scale(1.0) rotate(5deg); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-30px) scale(0.95) rotate(5deg); }
}
@keyframes gv2-fx-lose-slam {
  0%   { opacity: 0; transform: translateX(-50%) translateY(0)    scale(3.5) rotate(-5deg); filter: brightness(2.5); }
  10%  { opacity: 1; transform: translateX(-50%) translateY(-5px) scale(1.2) rotate(-5deg); filter: brightness(1.5); }
  15%  {             transform: translateX(-50%) translateY(-5px) scale(1.0) rotate(-5deg); filter: brightness(1); }
  80%  { opacity: 1; transform: translateX(-50%) translateY(-10px) scale(1.0) rotate(-5deg); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-30px) scale(0.95) rotate(-5deg); }
}
@keyframes gv2-fx-bust-slam {
  0%   { opacity: 0; transform: translateX(-50%) translateY(0)    scale(3.5) rotate(-8deg); filter: brightness(2.5); }
  10%  { opacity: 1; transform: translateX(-50%) translateY(-5px) scale(1.2) rotate(-8deg); filter: brightness(1.5); }
  18%  {             transform: translateX(-50%) translateY(-8px) scale(1.0) rotate(-8deg); filter: brightness(1); }
  75%  { opacity: 1; transform: translateX(-50%) translateY(-10px) scale(1.0) rotate(-8deg); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-30px) scale(0.95) rotate(-8deg); }
}
@keyframes gv2-fx-push-slam {
  0%   { opacity: 0; transform: translateX(-50%) translateY(0)    scale(3.5); filter: brightness(2); }
  10%  { opacity: 1; transform: translateX(-50%) translateY(-5px) scale(1.2); filter: brightness(1.3); }
  15%  {             transform: translateX(-50%) translateY(-5px) scale(1.0); filter: brightness(1); }
  80%  { opacity: 1; transform: translateX(-50%) translateY(-10px) scale(1.0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-30px) scale(0.95); }
}

/* v2 screen shake — applied to .gv2-bc (BoardChrome root). state-driver.js
   uses screenShakeV2 which writes the .fx-shaking class on .gv2-bc.
   Matches v1 .fx-shaking semantics; renamed wrapper to avoid v1 selector clash.
   FU2: .gv2-me-card is the match-end modal card — screenShakeV2 can target it for a
   VISIBLE modal-entrance shake (the keyframe is a pure translate, safe on the
   margin-centered card). Same animation, two valid hosts. */
.gv2-bc.fx-shaking,
.gv2-me-card.fx-shaking,
.gv2-game-grid.fx-shaking,
/* Portrait redesign PR 3: screenShakeV2 retargets to .gv2-page at portrait
   (the grid contains the fixed HIT/STAND bar — its transform would re-scope
   it; the viewport-sized page shakes everything uniformly). */
.gv2-page.fx-shaking,
/* Desktop redesign PR 3: on a SCROLLING desktop page (sub-900px heights)
   screenShakeV2 falls back to shaking the fans — a taller-than-viewport
   page transform would re-scope the fixed bar/log to the page bottom. */
.gv2-pa-fan.fx-shaking {
  animation: gv2-fx-shake 0.3s ease-out !important;
}
@keyframes gv2-fx-shake {
  0%, 100% { transform: translate(0, 0); }
  10% { transform: translate(var(--shake-amp, 4px), calc(var(--shake-amp, 4px) * -0.5)); }
  20% { transform: translate(calc(var(--shake-amp, 4px) * -0.8), var(--shake-amp, 4px)); }
  30% { transform: translate(var(--shake-amp, 4px), calc(var(--shake-amp, 4px) * -0.3)); }
  40% { transform: translate(calc(var(--shake-amp, 4px) * -0.6), calc(var(--shake-amp, 4px) * 0.7)); }
  50% { transform: translate(calc(var(--shake-amp, 4px) * 0.4), calc(var(--shake-amp, 4px) * -0.5)); }
  60% { transform: translate(calc(var(--shake-amp, 4px) * -0.3), calc(var(--shake-amp, 4px) * 0.3)); }
  70% { transform: translate(calc(var(--shake-amp, 4px) * 0.2), calc(var(--shake-amp, 4px) * -0.2)); }
  80%, 100% { transform: translate(0, 0); }
}

/* Particle burst — radiates from a point */
.fx-particles { position: fixed; pointer-events: none; z-index: 9998; }
.fx-particle {
  position: absolute; width: 5px; height: 5px; border-radius: 50%;
  animation: gv2-fx-particle-fly 0.5s ease-out forwards;
}
@keyframes gv2-fx-particle-fly {
  0%   { opacity: 1; transform: translate(0, 0)              scale(1); }
  100% { opacity: 0; transform: translate(var(--px), var(--py)) scale(0.2); }
}

/* Confetti rain — falls from top of screen */
.fx-confetti-box {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 9997; overflow: hidden;
}
.fx-confetti {
  position: absolute; top: -12px;
  width: 10px; height: 6px; border-radius: 2px;
  animation: gv2-fx-confetti-fall 3s ease-in forwards;
}
@keyframes gv2-fx-confetti-fall {
  0%   { opacity: 1; transform: translateY(0)     rotate(0deg)   scale(1); }
  70%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(100vh) rotate(720deg) scale(0.5); }
}

/* Area shake — short jitter on a target element */
.fx-area-shake { animation: gv2-fx-area-shake 150ms ease-out; }
@keyframes gv2-fx-area-shake {
  0%, 100% { translate: 0 0; }
  25%      { translate: 2px -1px; }
  50%      { translate: -2px 1px; }
  75%      { translate: 1px -2px; }
}

/* Bust flash — reused on .gv2-pa for v2 bust visual */
.gv2-pa.fx-bust-flash {
  animation: gv2-fx-area-bust-flash 0.6s ease-out forwards;
}
@keyframes gv2-fx-area-bust-flash {
  0%   { box-shadow: 0 0 30px rgba(204, 51, 51, 0.5), inset 0 0 20px rgba(204, 51, 51, 0.1); }
  100% { box-shadow: none; }
}

/* ============================================
   PHASE B PR 6a — Page shell + 3-col grid + .gv2-pa wrapper
   + .gv2-pa[data-state] rules + overlays + toast.
   All selectors scoped under .gv2-pa or .gv2-overlay-* — no bare
   [data-state] selectors (they would leak to /demo-v2 demo containers).
   Overlay z-index ladder: 8900 (banner) < 9100 (match-end)
                         < 9150 (choosers) < 9200 (forfeit) < 9300 (countdown)
                         < 9500 (boon-trigger) < 9550 (consumable-prompt)
                         < 9600 (share) < 9700 (match-closed notice, PR 12)
                         < 9997-9999 (fx-particles) < 10000 (toast).
   z-index 9000 (game-start overlay) freed PR 1, 2026-05-26.
   ============================================ */

/* Page shell — replaces Phase 0 placeholder for /game-v2. */
.gv2-page {
  max-width: 1440px; margin: 0 auto; padding: 28px 40px;
  display: flex; flex-direction: column; gap: 22px;
  background: var(--ink); color: var(--vellum);
  min-height: 100vh;
  min-height: 100dvh;
}

/* 3-col grid: P1 (left) | CenterAxis (mid) | P2 (right) */
.gv2-game-grid {
  display: grid;
  grid-template-columns: 1fr 240px 1fr;
  gap: 28px; align-items: start;
}

/* POV (2026-06-18): the LOCAL player is ALWAYS the left column on desktop/tablet.
   DOM order is fixed pa-0 | axis | pa-1. The desktop grid auto-places, and pinning
   ONLY the two player areas leaves the auto-placed axis to drop to its own row
   (vertical stack), so ALL THREE items are explicitly placed (col + row 1).
   Scoped to the desktop/tablet viewport (min-width:768 AND min-height:501) so it
   cannot overlap the mobile order-swap (≤767px) or the landscape grid-area swap
   ((landscape) and (max-height:500px)) — no cascade fight at any width. Campaign
   is always data-my-idx=0 + has its own layout, so :not(campaign) leaves it alone. */
@media (min-width: 768px) and (min-height: 501px) {
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) [data-gv2-player-area="1"] { grid-column: 1; grid-row: 1; }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-axis { grid-column: 2; grid-row: 1; }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) [data-gv2-player-area="0"] { grid-column: 3; grid-row: 1; }
}

/* Player area wrapper — receives data-state from setPlayerState. */
.gv2-pa {
  display: flex; flex-direction: column; gap: 12px;
  padding: 18px 20px;
  background: rgba(20,16,10,.5);
  border: 1px solid var(--iron-edge);
  position: relative;
  /* Fade the turn-glow box-shadow in/out so handoff is perceptible.
     border-color intentionally NOT transitioned — the [data-state="frozen"]
     blue border at line 812 would also fade, which is out of scope. */
  transition: box-shadow 250ms ease;
}
.gv2-pa-row { display: flex; gap: 14px; align-items: flex-end; justify-content: center; }
.gv2-pa[data-gv2-player-area="1"] .gv2-pa-row { flex-direction: row; }
.gv2-pa-controls { display: flex; }
.gv2-pa-rail { display: flex; justify-content: center; }

/* .gv2-pa[data-state="X"] rules — 14 states.
   Selectors scoped to .gv2-pa parent so they never leak to /demo-v2. */
.gv2-pa[data-state="idle"] .gv2-sr { visibility: hidden; }
.gv2-pa[data-state="waitingToStart"] .gv2-sr { background: rgba(224,196,137,.15); border-color: var(--brass-lo); color: var(--brass-hi); }
.gv2-pa[data-state="dealing"] .gv2-sr { background: rgba(224,196,137,.10); color: var(--vellum-dim); }
.gv2-pa[data-state="turn"] .gv2-sr { /* uses --gv2-sr-accent (per-player accent) — already styled by .gv2-sr-turn */ }
.gv2-pa[data-state="waiting"] .gv2-sr { opacity: .65; }
.gv2-pa[data-state="targeted"] .gv2-sr { background: var(--oxblood); border-color: var(--oxblood-hi); color: #fff; }
.gv2-pa[data-state="mustDraw"] .gv2-sr { background: var(--oxblood-hi); border-color: var(--oxblood-hi); color: #fff; animation: emberpulse 1.4s infinite; }
.gv2-pa[data-state="frozen"] .gv2-sr { background: #5dade2; border-color: #5dade2; color: #09090b; }
.gv2-pa[data-state="stood"] .gv2-sr { background: var(--bone); border-color: var(--bone); color: #09090b; }
.gv2-pa[data-state="bust"] .gv2-sr { background: var(--oxblood-hi); border-color: var(--oxblood-hi); color: #fff; }
.gv2-pa[data-state="disconnected"] .gv2-sr { background: var(--iron); border-color: var(--iron-edge); color: var(--vellum-dim); opacity: .8; }
.gv2-pa[data-state="won"] .gv2-sr { background: var(--brass-hi); border-color: var(--brass-hi); color: #09090b; }
.gv2-pa[data-state="lost"] .gv2-sr { background: var(--iron-edge); border-color: var(--iron-edge); color: var(--vellum-dim); }
.gv2-pa[data-state="push"] .gv2-sr { background: var(--steel); border-color: var(--steel-hi); color: var(--vellum); }

/* Batch C: wrapper-level treatments to make whose-turn-it-is obvious at a glance.
   Layered on top of the StatusRibbon-only styling above.
   The `:not(.fx-bust-flash)` guard prevents this static box-shadow from overriding
   the bust-flash keyframe's `box-shadow: none` end-state when the class is added
   mid-flash. The keyframe (line ~720) is documented as reused on `.gv2-pa` for
   the v2 bust visual; no v2 caller adds the class today, but the guard keeps the
   visual clean if/when one does. */
.gv2-pa[data-state="turn"]:not(.fx-bust-flash) {
  border-color: var(--gv2-sr-accent, var(--brass-hi));
  box-shadow: 0 0 0 1px var(--gv2-sr-accent, var(--brass-hi)),
              0 0 24px -6px var(--gv2-sr-accent, var(--brass-hi));
}
.gv2-pa[data-state="stood"] { opacity: .78; }
.gv2-pa[data-state="stood"] .gv2-pa-controls { opacity: .55; }
.gv2-pa[data-state="waiting"] { opacity: .9; }
.gv2-pa[data-state="frozen"] { border-color: rgba(93,173,226,.5); }

/* Top + bottom strips (BoardChrome / FootStrip) */
.gv2-board-top, .gv2-board-bottom { width: 100%; }
.gv2-axis { padding-top: 0; }

/* Overlay base */
.gv2-overlay {
  position: fixed; inset: 0;
  display: none; align-items: center; justify-content: center;
  background: rgba(11,11,13,.85);
  pointer-events: auto;
}
.gv2-overlay-active { display: flex; }
.gv2-overlay-card {
  max-width: 480px; padding: 28px 32px;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 100%);
  border: 1px solid var(--brass-lo);
  border-top: 3px solid var(--brass-hi);
  box-shadow: 0 12px 40px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.06);
  text-align: center; color: var(--vellum);
}
.gv2-overlay-eyebrow { font-size: 9px; color: var(--brass); margin-bottom: 12px; }
.gv2-overlay-title { font-size: 32px; font-style: italic; color: var(--brass-hi); line-height: 1.2; }
.gv2-overlay-vs { color: var(--vellum-dim); font-size: 18px; margin: 0 8px; }
.gv2-overlay-sub { font-size: 11px; color: var(--vellum-dim); margin-top: 12px; letter-spacing: .25em; }
.gv2-overlay-actions { display: flex; gap: 12px; justify-content: center; margin-top: 24px; }
.gv2-overlay-btn {
  padding: 10px 20px; font-size: 11px; letter-spacing: .25em; text-transform: uppercase;
  border: 1px solid var(--brass-lo); background: transparent; color: var(--vellum);
  cursor: pointer; font-family: 'JetBrains Mono', monospace;
  transition: transform .12s ease;
}
.gv2-overlay-btn:disabled { opacity: .5; cursor: default; }
.gv2-overlay-btn-primary { background: var(--brass-lo); border-color: var(--brass-hi); color: #09090b; }
.gv2-overlay-btn-secondary { /* default */ }
.gv2-overlay-btn-danger { background: var(--oxblood); border-color: var(--oxblood-hi); color: #fff; }

/* Phase 4 medieval audit (2026-05-17) — campaign matchEnd "Return to Crusade"
   button. The shared .gv2-overlay-btn-primary uses JetBrains Mono + brass-lo
   solid background — a utility/tactical look that clashes with the medieval
   headline above it ("VICTORIOUS / DEFEATED" in Cormorant italic on brass-hi).
   Scoped restyle to keep this seam medieval-coherent. Reuses the modal-CTA
   recipe from PR 7 (.campaign-modal-overlay.medieval .campaign-ctrl-btn). */
.gv2-page[data-mode="campaign"] .gv2-c-matchend .gv2-overlay-btn-primary {
  background: linear-gradient(180deg, var(--oxblood-hi), var(--oxblood));
  border: 1px solid var(--brass);
  color: var(--vellum);
  padding: 10px 22px;
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 16px;
  font-weight: 600;
  font-style: italic;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: 0;
}
.gv2-page[data-mode="campaign"] .gv2-c-matchend .gv2-overlay-btn-primary:hover {
  background: linear-gradient(180deg, #b8323b, var(--oxblood-hi));
}
/* B10 (2026-05-28): the win overlay's secondary "Return to Campaign Home" button
   sits under the medieval primary. Give it the same serif/size but keep it
   outlined (transparent) so the primary→secondary hierarchy reads and the pair
   stays visually coherent (base .gv2-overlay-btn would be JetBrains-Mono 11px —
   a jarring mismatch next to the Cormorant-italic primary). */
.gv2-page[data-mode="campaign"] .gv2-c-matchend .gv2-overlay-btn-secondary {
  background: transparent;
  border: 1px solid var(--brass-lo);
  color: var(--vellum);
  padding: 9px 22px;
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 15px;
  font-weight: 600;
  font-style: italic;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: 0;
}
.gv2-page[data-mode="campaign"] .gv2-c-matchend .gv2-overlay-btn-secondary:hover {
  border-color: var(--brass-hi);
  color: var(--brass-hi);
}

.gv2-result-win { color: var(--brass-hi); }
.gv2-result-loss { color: var(--oxblood-hi); }

/* z-index 9000 (.gv2-overlay-game-start) freed PR 1, 2026-05-26 — "Match Begins" overlay deleted. */
.gv2-overlay-match-end { z-index: 9100; }
.gv2-overlay-forfeit { z-index: 9200; }

/* Disconnect notice — center-screen modal (was a foot-of-board bar, too small to
   notice; a user got confused why they couldn't act). Full-screen + pointer-events
   none: informational only, must NOT block the board (user can still leave via
   board controls). Entire base block replaced — no leftover bottom/left/transform. */
.gv2-overlay-banner {
  position: fixed; inset: 0;
  display: none; align-items: center; justify-content: center;
  pointer-events: none;
  color: var(--vellum);
}
.gv2-overlay-banner.gv2-overlay-active { display: flex; }
.gv2-overlay-disconnect-banner { z-index: 8900; }
.gv2-db-scrim { position: absolute; inset: 0; background: rgba(10,8,6,.55); }
.gv2-db-card {
  position: relative; /* lift above the absolutely-positioned scrim */
  max-width: 360px; padding: 28px 32px; text-align: center;
  /* No max-height on purpose. The parent .gv2-overlay-banner is pointer-events:none,
     so a capped card could not be scrolled — and `max-height` with the default
     `overflow: visible` caps only the BORDER BOX, so the background and border would
     stop mid-card while the text kept flowing onto bare board. Worse than no rule.
     The card is ~245px tall at its largest (compact + hint); if it ever does need to
     fit a short landscape viewport, shrink .gv2-db-icon / .gv2-db-time-visible in the
     existing compact media block instead of clipping here. */
  background: rgba(20,16,10,.97);
  border: 1px solid var(--oxblood); border-radius: 4px; /* brightens to --oxblood-hi when urgent */
  box-shadow: 0 12px 40px rgba(0,0,0,.6);
  font-family: 'JetBrains Mono', monospace;
}
.gv2-db-icon { font-size: 40px; line-height: 1; color: var(--oxblood-hi); }
.gv2-db-title {
  margin-top: 10px; font-family: 'Cormorant Garamond', serif;
  font-size: 26px; font-weight: 600; letter-spacing: .02em; color: var(--vellum);
}
.gv2-db-sub {
  margin-top: 6px; font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--vellum-dim, #b8a98c);
}
.gv2-db-time-label {
  margin-top: 4px; font-size: 10px; letter-spacing: .25em; text-transform: uppercase;
  color: var(--vellum-dim, #b8a98c);
}
/* Escape-hatch hint. Quieter than .gv2-db-sub — it is a fallback, not the message. */
.gv2-db-hint {
  margin-top: 14px; padding-top: 12px;
  border-top: 1px solid rgba(184,169,140,.15);
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--vellum-dim, #b8a98c);
}

/* ============================================
   PR 12 (G4) — "match wrapped up" grace notice.
   Body-mounted (NOT inside the render root, which renderFromState wipes). z-index 9700
   sits above the match-end overlay (9100) + share sheet (9600), below the toast (10000).
   Top-anchored + width-bounded so the panel clears the match-end overlay's corner buttons
   (top-left web Share .gv2-me-share-btn; top-left iOS-landscape Share/Home) which stay
   clickable after cleanup. The wrapper is pointer-events:none so it never eats a click on
   those buttons; only the panel itself is interactive.
   ============================================ */
.gv2-match-closed {
  position: fixed; top: 0; left: 0; right: 0; z-index: 9700;
  display: flex; justify-content: center;
  padding: calc(env(safe-area-inset-top, 0px) + 12px) 12px 0;
  pointer-events: none;
}
.gv2-match-closed-panel {
  pointer-events: auto;
  max-width: min(420px, calc(100vw - 140px)); /* leave the corner buttons uncovered */
  width: 100%;
  padding: 14px 18px; text-align: center;
  background: rgba(20,16,10,.97);
  border: 1px solid var(--brass-lo); border-radius: 4px;
  box-shadow: 0 12px 40px rgba(0,0,0,.6);
  font-family: 'JetBrains Mono', monospace;
}
.gv2-match-closed-title {
  font-family: 'Cormorant Garamond', serif;
  font-size: 20px; font-weight: 600; letter-spacing: .02em; color: var(--vellum);
  min-height: 1em; /* reserve space so the next-frame fill doesn't jump the layout */
}
.gv2-match-closed-count {
  margin-top: 4px; font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--vellum-dim, #b8a98c);
}
.gv2-match-closed-btns {
  margin-top: 12px; display: flex; gap: 10px; justify-content: center;
}
.gv2-match-closed-btns button {
  font-family: 'JetBrains Mono', monospace; font-size: 12px; letter-spacing: .06em;
  padding: 8px 16px; border-radius: 3px; cursor: pointer;
  text-transform: uppercase; transition: background .15s ease, border-color .15s ease;
}
.gv2-match-closed-return {
  background: var(--brass-lo); border: 1px solid var(--brass); color: var(--ink);
  font-weight: 700;
}
.gv2-match-closed-return:hover { background: var(--brass); }
.gv2-match-closed-stay {
  background: transparent; border: 1px solid var(--brass-lo); color: var(--vellum-dim, #b8a98c);
}
.gv2-match-closed-stay:hover { border-color: var(--brass); color: var(--vellum); }

/* ============================================
   PHASE B PR 8 — Layout reflow per design source
   docs/design-handoffs/2026-04-30-gameboard/project/gameboard.jsx:247-396
   ScorePlaque moves to hand row, TimeBank to header row,
   CardSection labels above each row, fluid action grid.
   ============================================ */

.gv2-page .gv2-pa {
  /* Stack rows with consistent spacing — overrides earlier rule's gap. */
  gap: 8px;
}
.gv2-page .gv2-pa-row {
  /* Header row: spacer | bank | status ribbon (mirrored for P2). */
  display: flex; align-items: center; gap: 10px;
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-row {
  flex-direction: row-reverse;
}
.gv2-page .gv2-pa-header-spacer { flex: 1; }
.gv2-page .gv2-pa-bank { display: inline-flex; align-items: center; }

/* PlayerHeader — wax seal + italic-serif name. PR-10 (2026-05-15):
   eyebrow "You"/"Opponent" label dropped (StatusRibbon localizes turn state
   so it was redundant), name font-size bumped, anchored top-left for both
   seats (mirror flip removed — both areas now read self/opp identically). */
.gv2-page .gv2-ph {
  flex: 1; min-width: 0;
  display: flex; flex-direction: row; align-items: center; gap: 10px;
}
.gv2-page .gv2-ph-seal {
  width: 36px; height: 36px; flex-shrink: 0;
  border-radius: 50%;
  border: 2px solid var(--brass-lo, #8a6a3c);
  background: rgba(183,162,122,.08);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  box-sizing: border-box;
}
.gv2-page .gv2-ph-seal-img { width: 100%; height: 100%; object-fit: cover; }
/* Boss avatars are 256×146 landscape with faces upper-center — naive
   center-crop leaves the chest in the circle. Pull the focal point up. */
.gv2-page .gv2-ph-seal-img[src*="/avatars/boss-"],
.gv2-me-crest-avatar[src*="/avatars/boss-"] { object-position: center 28%; }
.gv2-page .gv2-ph-seal-initial {
  font-size: 18px; font-style: italic;
  color: var(--brass-hi, #e0c489);
  line-height: 1;
}
.gv2-page .gv2-ph-text { min-width: 0; text-align: left; }
.gv2-page .gv2-ph-name {
  font-size: 28px; font-style: italic; line-height: 1; letter-spacing: 0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.gv2-page .gv2-pa-section-label {
  font-size: 9px; color: var(--vellum-dim); letter-spacing: .35em;
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 4px;
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-section-label {
  flex-direction: row-reverse;
}
.gv2-page .gv2-pa-label-counter {
  font-size: 8px; color: var(--vellum-dim); letter-spacing: .25em;
  font-family: 'JetBrains Mono', monospace;
}

.gv2-page .gv2-pa-hand {
  /* Hand row: ScorePlaque sits next to the fan. P2 mirrors. */
  display: flex; gap: 14px; align-items: stretch; min-height: 122px;
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-hand {
  flex-direction: row-reverse;
}
.gv2-page .gv2-pa-hand .gv2-pa-fan {
  flex: 1; min-width: 0;
}

/* Actions row: pre-mounted .gv2-ar-playable inside.
   Switch grid to fluid 1fr (count-aware), but cap each cell at 80px so a
   solo card doesn't stretch to the full row width. */
.gv2-page .gv2-pa-actions {
  /* Container is now a thin wrapper around .gv2-ar-playable. No more
     bank chip / used chips inside. */
  min-height: 110px;
}
.gv2-page .gv2-ar-playable {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(0, 1fr));
  gap: 6px; min-height: 110px;
  /* Per-spec §1d: cells fluid down from 80px max-width (post-Phase-G upsize). */
  grid-auto-columns: minmax(0, 80px);
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable {
  direction: rtl;
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable > .gv2-ar-card {
  direction: ltr;
}
.gv2-page .gv2-ar-card { max-width: 80px; }

/* Tray action-card label alignment (1v1 AND campaign — both render the tray via
   the shared renderActionsRow into .gv2-ar-playable, all viewports). Reserve a
   fixed bottom band and pin the label into it so SCORE/SWAP/STALL/FREEZE/etc. all
   sit on one baseline regardless of the glyph/sigil height centered above. nowrap
   keeps each label line on one line; the explicit <br> in FREE CARD / FORCED HIT
   (ActionCard.js) still stacks those two words. Border-box (game-v2.css:25) keeps
   the padding-bottom inside --gv2-ac-h, so the card never grows. The played-card
   fan (not .gv2-ar-playable, renders label:false) is untouched. Bug fix:
   action-card label alignment. */
.gv2-page .gv2-ar-playable .gv2-ac {
  padding-bottom: 15px;
}
/* The tray reserves a 15px sub-label band on .gv2-ac (above), but the art path bakes
   its label into the PNG and renders no .gv2-ac-sub, so on an art tile that band is
   dead space that — under border-box + the base .gv2-ac-art::before strut (:246) —
   would add 15px of height overshoot. Zero it for art tiles only; the CSS-face
   fallback (which loses .gv2-ac-art on img load-failure) keeps the band for its DOM
   sub-label. */
.gv2-page .gv2-ar-playable .gv2-ac.gv2-ac-art {
  padding-bottom: 0;
}
.gv2-page .gv2-ar-playable .gv2-ac-sub {
  position: absolute; left: 0; right: 0; bottom: 3px;
  margin-top: 0; white-space: nowrap; line-height: 1.05;
}
/* FREE CARD / FORCED HIT carry an explicit <br> in their sub label (ActionCard.js)
   so the two words stack vertically instead of clipping on a narrow tray card.
   The <br> wins under white-space:nowrap (nowrap only suppresses auto-wrap, not a
   forced break), so each word stays on its own single line. */

.gv2-page .gv2-pa-spent {
  display: flex; gap: 4px; flex-wrap: wrap; min-height: 28px;
  padding: 4px 0;
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-spent {
  justify-content: flex-end;
}

.gv2-page .gv2-pa-bottom {
  display: flex; gap: 12px; align-items: stretch; margin-top: 8px;
}
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-bottom {
  flex-direction: row-reverse;
}
.gv2-page .gv2-pa-controls { flex: 1; }

/* Choosers — SW (full-screen 2-column) + FC (small modal), z-index 9150 ladder. */
.gv2-overlay-chooser {
  position: fixed; inset: 0;
  display: none; align-items: center; justify-content: center;
  background: rgba(11,11,13,.92);
  z-index: 9150;
}
.gv2-overlay-chooser.gv2-overlay-active { display: flex; }
.gv2-overlay-chooser-fc { background: rgba(11,11,13,.55); }
.gv2-overlay-chooser-card {
  max-width: 720px; padding: 24px 32px;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 100%);
  border: 1px solid var(--brass-lo);
  border-top: 3px solid var(--brass-hi);
  box-shadow: 0 12px 40px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.06);
  text-align: center; color: var(--vellum);
}
.gv2-overlay-chooser-eyebrow {
  font-size: 9px; color: var(--brass); letter-spacing: .3em; text-transform: uppercase;
  margin-bottom: 12px; font-family: 'JetBrains Mono', monospace;
}
.gv2-overlay-chooser-title {
  font-size: 28px; font-style: italic; color: var(--brass-hi); line-height: 1.2;
  font-family: 'Cormorant Garamond', serif;
}
.gv2-overlay-chooser-sub {
  font-size: 11px; color: var(--vellum-dim); margin-top: 8px;
  letter-spacing: .25em; font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
}
.gv2-overlay-chooser-banner {
  font-size: 14px; color: var(--vellum-dim); margin-top: 20px; font-style: italic;
}
.gv2-overlay-chooser-cols {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 32px; margin-top: 24px;
}
.gv2-overlay-chooser-col-label {
  font-size: 9px; color: var(--vellum-dim); letter-spacing: .3em;
  text-transform: uppercase; margin-bottom: 8px;
  font-family: 'JetBrains Mono', monospace;
}
/* PR-A #4 — SW chooser selection-time labels: bigger + brighter so
   "Your Hand" vs "Opponent's Hand" reads at a glance. Scoped under
   .gv2-overlay-chooser-cols so post-swap outcome labels (chooser-sw-row /
   chooser-sw-scores parents) keep the original subtle treatment. */
.gv2-overlay-chooser-cols .gv2-overlay-chooser-col-label {
  font-size: 13px; color: var(--brass); letter-spacing: .25em;
  margin-bottom: 12px;
}
.gv2-overlay-chooser-col-cards {
  display: flex; gap: 6px; justify-content: center; flex-wrap: wrap;
  min-height: 96px;
}
/* SW swap modal: 3-child cols [Your Hand | middle | Opponent's Hand]. The middle
   holds the score preview + Swap button (active) or the waiting banner (passive).
   Desktop keeps the familiar look via grid areas — hands side by side, middle
   spanning the row below. Landscape (see the (orientation:landscape) block)
   reflows to "yh mid oh" so the whole modal fits with no scroll. SW-scoped so the
   FC modal + other choosers are untouched. */
.gv2-overlay-chooser-sw .gv2-overlay-chooser-cols {
  grid-template-areas: "yh oh" "mid mid";
  align-items: start;
}
.gv2-overlay-chooser-sw .gv2-overlay-chooser-cols > .gv2-overlay-chooser-col:first-child { grid-area: yh; }
.gv2-overlay-chooser-sw .gv2-overlay-chooser-cols > .gv2-overlay-chooser-mid { grid-area: mid; }
.gv2-overlay-chooser-sw .gv2-overlay-chooser-cols > .gv2-overlay-chooser-col:last-child { grid-area: oh; }
.gv2-overlay-chooser-sw .gv2-overlay-chooser-mid {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px;
}
.gv2-overlay-chooser-tile {
  cursor: pointer; padding: 4px;
  border: 2px solid transparent; transition: border-color .12s, transform .12s;
}
@media (hover: hover) {
  .gv2-overlay-chooser-tile:hover { transform: translateY(-2px); }
}
/* PR 13 §7 cascade fix: re-declare :active after :hover so desktop mouse
   gets the press-down feedback when clicking (otherwise :hover's translateY
   wins on source order). filter: brightness(1.08) from §7 already wins. */
.gv2-overlay-chooser-tile:active { transform: translateY(1px); }
.gv2-overlay-chooser-tile.gv2-chooser-selected {
  border-color: var(--brass-hi);
  box-shadow: 0 0 12px rgba(224,196,137,.4);
}
.gv2-overlay-chooser-actions {
  display: flex; gap: 12px; justify-content: center; margin-top: 24px;
}
.gv2-overlay-chooser-btn {
  padding: 10px 24px; font-size: 12px; letter-spacing: .25em; text-transform: uppercase;
  border: 1px solid var(--brass-lo); background: transparent; color: var(--vellum);
  cursor: pointer; font-family: 'JetBrains Mono', monospace;
}
.gv2-overlay-chooser-btn:disabled { opacity: .4; cursor: default; }
/* SW Discard button (2026-07-17): top-right of the SW chooser card. position:relative
   is SCOPED to the SW chooser so the shared .gv2-overlay-chooser-card rule (also used
   by overflow-alert + FC chooser) is untouched. Uses the --oxblood-hi accent (violet
   post-rebrand) so it reads as a distinct secondary action, not a neutral close-X.
   Positive offsets keep it inside the padding box so it isn't clipped where the card
   becomes a scroll container on short viewports. */
.gv2-overlay-chooser-sw .gv2-overlay-chooser-card { position: relative; }
.gv2-overlay-chooser-sw-discard {
  position: absolute; top: 12px; right: 12px; z-index: 1;
  padding: 6px 14px; font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
  border: 1px solid var(--oxblood-hi); background: transparent; color: var(--oxblood-hi);
  cursor: pointer; font-family: 'JetBrains Mono', monospace;
}
.gv2-overlay-chooser-sw-discard:disabled { opacity: .4; cursor: default; }
.gv2-overlay-chooser-btn-primary {
  background: var(--brass-lo); border-color: var(--brass-hi); color: #09090b;
}
.gv2-overlay-chooser-btn-primary:disabled {
  background: transparent; color: var(--vellum-dim);
}
/* PR-9 (2026-05-15): client-side BUST guard on SW Confirm button.
   Overrides the disabled-fade so the warning reads oxblood, not muted brass. */
.gv2-overlay-chooser-btn-would-bust:disabled {
  background: transparent;
  border-color: var(--oxblood-hi);
  color: var(--oxblood-hi);
  opacity: 1;
  cursor: not-allowed;
}

/* Medieval chooser buttons (FC, SW, overflow-alert) on /campaign.
   Extends the matchEnd recipe from PR #623 to all three chooser-button
   sites. Shared .gv2-overlay-chooser-btn(-primary) classes used in
   chooser-fc.js:128, chooser-sw.js:179, overflow-alert.js:61.

   Scope note: chooser modals are appended directly to document.body
   (NOT inside .gv2-page), so we scope on body.gv2-campaign-bg-active
   instead of .gv2-page[data-mode="campaign"]. The body class is set
   statically in campaign-v2.html:16 and defensively in
   campaign/game-ui.js:63. It is never present on /game or /.

   Placement note: appended after the chooser-btn :disabled rules above
   so the medieval gradient survives the :disabled case — specifically,
   chooser-sw.js renders the would-bust button with submitDisabled=true
   AND .gv2-overlay-chooser-btn-would-bust (chooser-sw.js:251,259). The
   new selectors are specificity (0,2,1) — `body` type + two classes —
   which strictly beats the :disabled block above at (0,2,0), so they
   win on the cascade. Placement after the :disabled block is defense-
   in-depth: if a future refactor drops `body` from the scope (e.g.,
   moves to a class-only selector), tied (0,2,0) specificity falls back
   to source order. DO NOT move this block above the :disabled rules. */
body.gv2-campaign-bg-active .gv2-overlay-chooser-btn-primary {
  background: linear-gradient(180deg, var(--oxblood-hi), var(--oxblood));
  border: 1px solid var(--brass);
  color: var(--vellum);
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 16px;
  font-weight: 600;
  font-style: italic;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: 0;
}
body.gv2-campaign-bg-active .gv2-overlay-chooser-btn-primary:hover {
  background: linear-gradient(180deg, #b8323b, var(--oxblood-hi));
}
body.gv2-campaign-bg-active .gv2-overlay-chooser-btn {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 16px;
  font-style: italic;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: 0;
}
body.gv2-campaign-bg-active .gv2-overlay-chooser-btn-would-bust {
  background: linear-gradient(180deg, #c4323b, #8a1e26);
  border: 1px solid var(--brass);
  color: var(--vellum);
}

.gv2-overlay-chooser-fc-card {
  display: flex; justify-content: center; margin: 16px 0;
}
/* 1d: FC preview shows only the centered glyph + sub-label — suppress the top-left
   corner copy (ActionCard always emits it for score-mods). Mirrors the SW/boost-chip
   precedent (.gv2-chooser-boost-chip .gv2-ac-corner-tl). Bold the sub so "SCORE" reads. */
.gv2-overlay-chooser-fc-card .gv2-ac-corner-tl { display: none; }
.gv2-overlay-chooser-fc-card .gv2-ac-sub { font-weight: 600; } /* 600, not 700: game-v2.html loads JetBrains Mono up to wght@600 only — 700 would fall back / synthesize */

/* 2026-05-06 FC modal redesign — score preview row + bigger modal.
   Sized bumps scoped under .gv2-overlay-chooser-fc to avoid affecting
   the SW chooser (which shares .gv2-overlay-chooser-card / -title). */
.gv2-overlay-chooser-fc .gv2-overlay-chooser-card {
  max-width: min(1280px, 95vw); padding: 40px 64px;
}
.gv2-overlay-chooser-fc .gv2-overlay-chooser-title {
  font-size: 36px;
}

.gv2-overlay-chooser-fc-scores {
  display: grid; grid-template-columns: 1fr 1fr; gap: 32px;
  margin-top: 16px; padding: 16px 0;
  border-top: 1px solid var(--brass-lo);
  border-bottom: 1px solid var(--brass-lo);
}
.gv2-overlay-chooser-fc-score-col {
  text-align: center; padding: 0 8px;
}
.gv2-overlay-chooser-fc-score-label {
  font-size: 9px; color: var(--vellum-dim); letter-spacing: .3em;
  text-transform: uppercase; margin-bottom: 8px;
  font-family: 'JetBrains Mono', monospace;
}
.gv2-overlay-chooser-fc-score-num {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic; font-weight: 600; font-size: 32px;
  color: var(--brass-hi); line-height: 1.1;
}
.gv2-overlay-chooser-fc-score-num.bust {
  color: var(--oxblood-hi);
}
.gv2-overlay-chooser-fc-score-text {
  font-family: 'Cormorant Garamond', serif;
  font-size: 14px; color: var(--vellum); line-height: 1.3;
}
.gv2-overlay-chooser-fc-confirm-body {
  font-family: 'Cormorant Garamond', serif; font-size: 16px;
  color: var(--vellum); margin: 16px 0; font-style: italic;
}

/* Phase 2 outcome stamp on offered card (chooser-fc opponent-POV).
   Container becomes positioning context; stamp overlays the card. */
.gv2-overlay-chooser-fc-card-stamped { position: relative; }
.gv2-overlay-chooser-stamp-kept,
.gv2-overlay-chooser-stamp-rejected {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) rotate(-12deg);
  padding: 6px 16px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: .15em;
  text-transform: uppercase;
  border: 3px solid currentColor;
  border-radius: 4px;
  pointer-events: none;
  text-shadow: 0 1px 0 rgba(0,0,0,.4);
  background: rgba(11,11,13,.7);
}
.gv2-overlay-chooser-stamp-kept {
  color: #5fb674;
  box-shadow: 0 0 12px rgba(95,182,116,.4);
}
.gv2-overlay-chooser-stamp-rejected {
  color: #c95757;
  box-shadow: 0 0 12px rgba(201,87,87,.4);
}

/* Phase 2 outcome layout (chooser-sw opponent-POV).
   Two cards with arrow between, plus prev→new score plaques per player. */
.gv2-overlay-chooser-sw-row {
  display: flex; align-items: center; justify-content: center;
  gap: 24px; margin-top: 16px;
}
.gv2-overlay-chooser-sw-side { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.gv2-overlay-chooser-sw-arrow {
  font-size: 32px; color: var(--brass-hi);
  font-family: 'Cormorant Garamond', serif;
  text-shadow: 0 0 8px rgba(224,196,137,.4);
}
.gv2-overlay-chooser-sw-scores {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 32px; margin-top: 20px;
}

/* PR-A #2 — pre-submit SW score preview (selection-time, before server). */
.gv2-overlay-chooser-sw-preview {
  display: grid; grid-template-columns: 1fr 1fr; gap: 32px;
  margin-top: 16px; padding: 12px 0;
  border-top: 1px solid var(--brass-lo);
  border-bottom: 1px solid var(--brass-lo);
}
.gv2-overlay-chooser-sw-preview-row {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  font-family: 'JetBrains Mono', monospace;
}
.gv2-overlay-chooser-sw-preview-label {
  font-size: 10px; color: var(--vellum-dim); letter-spacing: .25em;
  text-transform: uppercase; margin-right: 4px;
}
.gv2-overlay-chooser-sw-preview-prev {
  color: var(--vellum-dim); text-decoration: line-through; font-size: 16px;
}
.gv2-overlay-chooser-sw-preview-arrow { color: var(--brass); }
.gv2-overlay-chooser-sw-preview-new {
  color: var(--brass-hi); font-weight: 700; font-size: 20px;
  font-family: 'Cormorant Garamond', serif; font-style: italic;
}
/* QA #7 (2026-05-31): red ⚠ next to whichever resulting score is illegal (busts >21).
   Carries the same .bja-tip tooltip as the disabled Swap button. */
.gv2-overlay-chooser-sw-preview-warn {
  color: #e0524d; font-size: 16px; line-height: 1;
  margin-left: 6px; cursor: help;
}
.gv2-overlay-chooser-sw-score {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  font-family: 'JetBrains Mono', monospace; font-size: 14px;
}
.gv2-overlay-chooser-sw-score-prev {
  color: var(--vellum-dim);
  text-decoration: line-through;
  font-size: 16px;
  margin-right: 6px;
}
.gv2-overlay-chooser-sw-score-arrow { color: var(--brass); margin: 0 4px; }
.gv2-overlay-chooser-sw-score-new {
  color: var(--brass-hi); font-weight: 700; font-size: 20px;
}

/* Inline toast (used by game-ui.js for switchError, recovery, forfeit-win, etc.) */
.gv2-toast {
  position: fixed; top: 24px; left: 50%; transform: translateX(-50%);
  padding: 10px 18px; font-family: 'JetBrains Mono', monospace; font-size: 11px;
  letter-spacing: .15em; color: var(--vellum);
  background: rgba(20,16,10,.95); border: 1px solid var(--brass-lo);
  z-index: 9500; transition: opacity .8s ease;
}
.gv2-toast-error { border-color: var(--oxblood-hi); color: var(--oxblood-hi); }
.gv2-toast-info { border-color: var(--brass-lo); }

/* ============================================
   PHASE B PR 7 — Deal animation + SK/SW/FC splashes.
   .gv2-fly / .gv2-flight z-index ladder reference:
     .gv2-overlay-disconnect-banner  8900
     .gv2-overlay-match-end          9100
     .gv2-overlay-forfeit            9200
     .gv2-fly / .gv2-flight          9400  (these rules)
     .gv2-toast                      9500
     .gv2-share-overlay              9600
     .gv2-match-closed               9700  (PR 12 — above the overlay + share sheet)
     .fx-confetti-box                9997
     .fx-particles                   9998
     .fx-float                       9999
   Flight cards sit above interactive overlays (so cards are visible flying
   past them) but BELOW slam text + toasts (which must remain readable
   mid-deal). .gv2-flight is the rebuild WAAPI flight (deal + hit; transform
   keyframes, body-mounted, sized inline from render-geometry.js);
   .gv2-fly survives solely for the byte-frozen FH slam.
   ============================================ */

/* .gv2-fly + splash classes are mounted on document.body (position: fixed),
   so scoping under .gv2-page would hide them. Keep them at top level. They're
   only ADDED to the DOM by /game's JS — /demo-v2 never creates these elements,
   so the rules are inert there. (No CSS leak risk; just dead rule definitions.) */
.gv2-fly {
  position: fixed;
  width: 80px; height: 110px;
  z-index: 9400;
  pointer-events: none;
  border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass);
  background:
    repeating-linear-gradient(45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(-45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    linear-gradient(180deg, #181221, #0c0a11);
  box-shadow: 0 6px 14px rgba(0,0,0,.6), inset 0 0 14px rgba(0,0,0,.5);
  /* Rebuild PR 3: the left/top/opacity transition is deleted — deal + hit
     flights ride .gv2-flight transform keyframes; FH's movement transitions
     are declared on its own slam/slide classes (game-v2.css FH section). The
     rest of this rule survives permanently as an FH dependency (position,
     size, z-index, pointer-events, card-back paint). */
}
/* Breakpoint mirrors: the (orientation: landscape) and (max-height: 500px) clause
   (and the 767px mobile width) are canonically defined in public/js/game-v2/mq.js
   (window.GameV2MQ). CSS @media can't read JS/vars — if you change the values
   there, grep this file for the old literals and update every mirror. */
/* ── Rebuild PR 2: WAAPI deal flight (.gv2-flight) ──────────────────────
   Body-mounted like .gv2-fly; positioned once at 0,0 and moved via transform
   keyframes only (GPU-composited, no per-frame layout). Width/height are set
   INLINE from render-geometry.js card dims — the flip faces are inset:0
   absolutes and would collapse on an unsized outer. The 3-node flip stack
   mirrors the documented FH contract (.gv2-fly-fh-card3d below): outer
   (transform keyframes) → flipper (rotateY keyframes, preserve-3d,
   perspective inside its OWN transform values — a perspective property on
   the transforming outer would flatten its children) → two backface-hidden
   faces. A single flipping node cannot do a back→front reveal. */
.gv2-flight {
  position: fixed;
  left: 0; top: 0;
  z-index: 9400;
  pointer-events: none;
  will-change: transform;
}
.gv2-flight-flipper {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}
.gv2-flight-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
/* Back face carries the card-back look itself (same recipe as .gv2-fly) so a
   back-only flight (hidden boss cards / no-deps fallback) needs no component. */
.gv2-flight-face-back {
  transform: rotateY(180deg);
  border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass);
  background:
    repeating-linear-gradient(45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(-45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    linear-gradient(180deg, #181221, #0c0a11);
  box-shadow: 0 6px 14px rgba(0,0,0,.6), inset 0 0 14px rgba(0,0,0,.5);
}
/* Static (no-flip) back: not rotated — it's the only face. */
.gv2-flight-face-static { transform: none; }
/* Component markup fills the face regardless of its intrinsic w/h props. */
.gv2-flight-face > * { width: 100% !important; height: 100% !important; }

/* (Rebuild PR 3: the landscape .gv2-fly:not(FH) 50x66 sizing rule is deleted —
   it became inert when the last non-FH .gv2-fly flight was removed; the FH
   card deliberately stays full-size at every breakpoint. .gv2-flight sizes
   inline from render-geometry.js.) */

/* Splash text variants — extend .fx-float (port from PR 5) with PR 7 colors. */
.fx-float.gv2-splash-sk {
  color: var(--purple-hi);
  font-size: 4rem;
  letter-spacing: .12em;
  text-shadow: 0 0 24px rgba(160,124,192,.6), 0 2px 0 #1a0e26;
}
.fx-float.gv2-splash-sw {
  color: var(--brass-hi);
  font-size: 3.5rem;
  letter-spacing: .12em;
  text-shadow: 0 0 24px rgba(224,196,137,.6), 0 2px 0 #1a140e;
}
.fx-float.gv2-splash-fc {
  color: #ffd870;
  font-size: 3rem;
  letter-spacing: .15em;
  text-shadow: 0 0 24px rgba(255,216,112,.7), 0 0 12px rgba(224,196,137,.5), 0 2px 0 #1a140e;
}

/* ============================================
   PHASE B PR 6b — Card fan + actions row + BANK chip.
   All rules scoped under .gv2-page so nothing leaks to /demo-v2.
   ============================================ */

/* Fan container — relative anchor; absolute children stack horizontally. */
.gv2-page .gv2-pa-fan {
  position: relative; min-height: 122px; height: 122px;
  margin: 8px 0;
}

/* ============================================================
   SIZE PROBE — the CSS/JS geometry contract.
   game-ui.js#mountShell emits <i class="gv2-sizeprobe"> as the first child of
   .gv2-page; render-geometry.js#readProbe reads its COMPUTED width / height /
   padding-left to learn the card + tray tile sizes CSS actually resolved.
   Why not read the custom properties directly: an UNREGISTERED custom property
   computes to its raw token text ("clamp(48px, ...)"), not a length, so
   parseFloat yields NaN. Real properties do resolve to used px.
   Why not mirror the clamps in JS: CSS svh and window.innerHeight are different
   viewports (~60-100px apart on iOS/iPad Safari), so an arithmetic mirror pops
   flights at the landing handoff on exactly the devices desktop can't reproduce.
   box-sizing MUST be content-box — the global * { border-box } at the top of
   this file would otherwise subtract padding-left from width.
   Each redesigned breakpoint RE-POINTS the three dimensions at its own tokens.
   Leaving a breakpoint unpointed makes width `auto` -> 0 on an abs-positioned
   empty box, which is why readProbe guards > 0 and falls back.
   ============================================================ */
.gv2-page .gv2-sizeprobe {
  position: absolute; top: 0; left: 0;
  visibility: hidden; pointer-events: none;
  box-sizing: content-box;
  width: 80px; height: 110px; padding-left: 80px;
}
.gv2-page .gv2-fan-card {
  position: absolute; top: 0; left: 0;
  transition: left .18s ease;
}
/* 1v1 used score modifiers use the same painted skin as playable action cards.
   Fan geometry writes the live card footprint and overlap reveal onto each
   wrapper, so portrait, short-landscape and desktop keep their existing layout.
   A buried art card owns an opaque identity strip: this prevents the next card's
   overlap from leaving only an unreadable slice of the painted value. */
.gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-art {
  width: var(--gv2-fan-card-w) !important;
  height: var(--gv2-fan-card-h) !important;
  aspect-ratio: auto;
}
.gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-art .gv2-ac-img {
  object-fit: cover;
}
.gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-art-corner {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 2;
  display: flex;
  width: var(--gv2-fan-reveal);
  align-items: flex-start;
  justify-content: center;
  overflow: hidden;
  padding-top: 7%;
  box-sizing: border-box;
  color: var(--gv2-ac-accent);
  background: var(--gv2-ac-art-bg, #14141a);
  border-right: 1px solid var(--gv2-ac-border);
  font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
  font-size: clamp(8px, calc(var(--gv2-fan-reveal) * .38), 18px);
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 1px 2px #000;
  pointer-events: none;
}
.gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:last-child .gv2-ac-art-corner {
  display: none;
}
/* P2 mirror: anchor right, lay out toward the left.
   We invert each card's offset by computing right:offset via JS would be cleaner,
   but a CSS-only approach uses scaleX on the container — keeps the JS simple.
   For PR 6b we accept LTR layout on both sides; mirror polish is PR 7+. */
.gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-fan { /* same orientation for now */ }
.gv2-page .gv2-fan-truncated::after {
  content: '⋯'; position: absolute; right: 0; top: 50%;
  transform: translateY(-50%); color: var(--brass-hi);
  font-size: 24px; pointer-events: none;
}

/* PR 8: action-card hover/disabled states. The grid layout itself + cell
   max-width are defined higher up (search "PHASE B PR 8 — Layout reflow").
   PR 6b's old .gv2-ar-bank / .gv2-ar-used / .gv2-ar-used-* rules removed —
   those classes no longer render (BANK chip now in header, used chips in
   .gv2-pa-spent). */
.gv2-page .gv2-ar-card {
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  position: relative;
  transition: transform .12s ease, box-shadow .12s ease;
  /* Touch: a long-press shows the card's tooltip (single tap plays). Suppress
     the iOS text-selection + callout menu so the long-press is clean. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
@media (hover: hover) {
  .gv2-page .gv2-ar-card:not(.gv2-ar-card-disabled):hover {
    transform: translateY(-2px);
  }
}
/* PR 13 §7 cascade fix: re-declare :active after :hover so desktop mouse
   gets the press-down feedback when clicking. */
.gv2-page .gv2-ar-card:not(.gv2-ar-card-disabled):active { transform: translateY(1px); }
.gv2-page .gv2-ar-card-disabled {
  cursor: default;
  pointer-events: none;
}
/* A disabled card carrying a "why you can't play this" reason tip (currently
   Stall/Swap blocked by a stood/frozen opponent) must re-enable pointer-events so its
   bja-tip fires on hover/tap — the disabled wrapper's pointer-events:none above
   otherwise makes the whole subtree inert. Higher specificity (two classes) wins
   over the rule above. The card stays UNPLAYABLE: the click handler bails on
   .gv2-ar-card-disabled and canPlayCard still returns false. cursor:help hints
   there's an explanation. */
.gv2-page .gv2-ar-card-disabled.gv2-ar-card-tip {
  pointer-events: auto;
  cursor: help;
}
.gv2-page .gv2-ar-card-stood-blocked > .gv2-ac {
  filter: grayscale(1) !important;
}
.gv2-page .gv2-ar-first-match-tip {
  width: 100%;
  margin-top: 5px;
  color: #cfc7b8;
  font-family: Inter, sans-serif;
  font-size: clamp(8px, 1.8vw, 10px);
  font-weight: 600;
  line-height: 1.15;
  text-align: center;
  overflow-wrap: anywhere;
  cursor: default;
}

/* Persistent teaching adds a text band to the local action tray. On portrait
   boards the panels are content-sized and the foe panel is the clip zone, so
   reclaim that height from both seats' existing card tokens during onboarding. */
@media (max-width: 767px) and (orientation: portrait) {
  .gv2-page:not([data-mode="campaign"])[data-first-match-action-tips] {
    --gv2-portcard-w: clamp(38px, calc(100svh * 58 / 750), 58px);
    --gv2-portcard-h: clamp(52px, calc(100svh * 80 / 750), 80px);
    --gv2-porttray-w: clamp(32px, calc(100svh * 44 / 750), 44px);
  }
}
@media (max-width: 340px) and (max-height: 590px) and (orientation: portrait) {
  .gv2-page[data-first-match-action-tips] .gv2-ar-first-match-tip {
    margin-top: 3px;
  }
}
/* ============================================
   PHASE C PR 9 — FH/FF CountdownOverlay
   Z-index 9300 (between forfeit 9200 and fly 9400 in the locked ladder).
   Four new keyframes, all gv2-co-* namespaced to avoid collision with
   the legacy `emberpulse` keyframe (which animates brass colors used by
   [data-state="mustDraw"] and Phase A primitives):
     - gv2-co-emberpulse  (1s, opacity-only, footer bar)
     - gv2-co-digit-pulse (0.4s, single-play, digit transitions)
     - gv2-co-card-pulse  (1.6s, accent-aware box-shadow, FH only)
     - gv2-co-dot-pulse   (1.2s, opacity+scale, status dot)
   Source: docs/design-handoffs/2026-04-30-gameboard/project/gameboard.jsx:1096-1288
   ============================================ */

@keyframes gv2-co-emberpulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.65; }
}
@keyframes gv2-co-digit-pulse {
  0% { transform: scale(1); }
  35% { transform: scale(1.18); }
  100% { transform: scale(1); }
}
/* Card pulse — accent-aware so FH oxblood box-shadow doesn't get clobbered
   by the legacy brass `emberpulse` keyframe (review pass 1 #M1). Reads the
   per-kind CSS vars set by .gv2-co-fh-root. FF is static (no pulse). */
@keyframes gv2-co-card-pulse {
  0%, 100% {
    box-shadow:
      0 24px 60px rgba(0,0,0,.75),
      0 0 50px var(--gv2-co-soft),
      inset 0 1px 0 rgba(255,255,255,.06);
  }
  50% {
    box-shadow:
      0 24px 60px rgba(0,0,0,.75),
      0 0 70px var(--gv2-co-accent),
      inset 0 1px 0 rgba(255,255,255,.06);
  }
}
/* Status-dot pulse — accent-aware (review pass 1 #L1). The dot already
   gets its color from var(--gv2-co-accent); we pulse opacity + scale so
   FF's frost-blue dot doesn't get tinted brass by legacy `emberpulse`. */
@keyframes gv2-co-dot-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.6; transform: scale(1.15); }
}

/* Attacker-area anchored modal — positioning set inline by countdown-overlay.js
   via anchorToAttacker(). Desktop: position:absolute over the attacker pa.
   Mobile + landscape-phone: @media block below restores fixed-centered. */
.gv2-co {
  position: absolute;
  z-index: 9300; pointer-events: none;
  width: 480px;
  font-family: 'Inter', sans-serif;
  animation: gv2-co-modal-punch 520ms cubic-bezier(.22,1.4,.36,1) both;
}
/* Suppress entry animation on baseline events (rejoin) — set by JS */
.gv2-co.gv2-co-no-entry { animation: none; }

/* Mobile + landscape phone: hard-centered fixed-position fallback.
   Matches the compound media query used at render-deal-animation.js:334-335
   and 10+ other places in this file. */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-co {
    position: fixed !important;
    left: 50% !important; top: 44% !important;
    transform: translate(-50%, -50%) !important;
    width: min(95vw, 360px) !important;
  }
}

/* Modal punch entry — adapted from design source instants.jsx keyframe modalPunch */
@keyframes gv2-co-modal-punch {
  0%   { transform: translate(0, 8px) scale(.78) rotate(-1.4deg); filter: blur(10px) brightness(2); opacity: 0; }
  55%  { transform: translate(0, 0) scale(1.05) rotate(.6deg); filter: blur(0) brightness(1.4); opacity: 1; }
  78%  { transform: translate(0, 0) scale(.985) rotate(-.25deg); }
  100% { transform: translate(0, 0) scale(1) rotate(0); filter: blur(0) brightness(1); opacity: 1; }
}
/* Card breathing — slow inner glow pulse during the 3s countdown */
@keyframes gv2-co-modal-breathe-fh {
  0%,100% { box-shadow: 0 30px 80px rgba(0,0,0,.85), 0 0 50px rgba(201,57,69,.45), inset 0 1px 0 rgba(255,255,255,.06); }
  50%     { box-shadow: 0 36px 100px rgba(0,0,0,.9),  0 0 90px rgba(201,57,69,.75), inset 0 1px 0 rgba(255,255,255,.08); }
}
@keyframes gv2-co-modal-breathe-ff {
  0%,100% { box-shadow: 0 30px 80px rgba(0,0,0,.85), 0 0 50px rgba(127,205,255,.4),  inset 0 1px 0 rgba(255,255,255,.06); }
  50%     { box-shadow: 0 36px 100px rgba(0,0,0,.9),  0 0 100px rgba(127,205,255,.7), inset 0 1px 0 rgba(255,255,255,.08); }
}
/* Glyph ignite — fires on mount + every digit tick */
@keyframes gv2-co-glyph-ignite {
  0%   { opacity: 0; transform: scale(.4) rotate(-12deg); filter: blur(8px) brightness(2.4); }
  55%  { opacity: 1; transform: scale(1.18) rotate(2deg);  filter: blur(0) brightness(1.6); }
  100% { opacity: 1; transform: scale(1) rotate(0); filter: blur(0) brightness(1); }
}
/* Big-digit count pulse — fires when digit changes */
@keyframes gv2-co-number-count {
  0%,100% { transform: scale(1); }
  20%     { transform: scale(1.18); }
}
/* Cast beam — shoots from modal edge toward defender, then holds */
@keyframes gv2-co-cast-beam {
  0%   { transform: scaleX(0); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: scaleX(1); opacity: .85; }
}
/* Status-dot wick flicker — replaces dot-pulse for the casting strip */
@keyframes gv2-co-wick-flicker {
  0%,100% { opacity: 1; filter: brightness(1); }
  35%     { opacity: .82; filter: brightness(.85); }
  65%     { opacity: 1; filter: brightness(1.25); }
}
.gv2-co-halo {
  position: absolute; inset: -40px;
  filter: blur(8px);
  pointer-events: none;
}
.gv2-co-card {
  position: relative;
  background: linear-gradient(180deg, #1a0306 0%, #0a0205 65%, #050103 100%);
  box-shadow:
    0 24px 60px rgba(0,0,0,.75),
    0 0 50px var(--gv2-co-soft, rgba(201,57,69,.55)),
    inset 0 1px 0 rgba(255,255,255,.06);
  border: 1px solid var(--gv2-co-accent, var(--oxblood-hi));
}
/* Note: .gv2-co-fh-root .gv2-co-card rule below (post-PR-1) replaces this
   for the new design (breathing glow via box-shadow). Kept the keyframe
   `gv2-co-card-pulse` defined for now in case a future PR re-enables it. */
.gv2-co-card.gv2-co-fh:not(.gv2-co-fh-root *) {
  animation: gv2-co-card-pulse 1.6s infinite;
}
.gv2-co-hairline-top, .gv2-co-hairline-bot {
  position: absolute; left: 8px; right: 8px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gv2-co-accent), transparent);
}
.gv2-co-hairline-top { top: -1px; }
.gv2-co-hairline-bot { bottom: -1px; opacity: .7; }
.gv2-co-corner {
  position: absolute; width: 12px; height: 12px;
}
.gv2-co-corner-tl { top: 6px; left: 6px;
  border-top: 1px solid var(--gv2-co-accent); border-left: 1px solid var(--gv2-co-accent); }
.gv2-co-corner-tr { top: 6px; right: 6px;
  border-top: 1px solid var(--gv2-co-accent); border-right: 1px solid var(--gv2-co-accent); }
.gv2-co-corner-bl { bottom: 6px; left: 6px;
  border-bottom: 1px solid var(--gv2-co-accent); border-left: 1px solid var(--gv2-co-accent); }
.gv2-co-corner-br { bottom: 6px; right: 6px;
  border-bottom: 1px solid var(--gv2-co-accent); border-right: 1px solid var(--gv2-co-accent); }

.gv2-co-eyebrow {
  padding: 10px 18px 0;
  display: flex; align-items: center; justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px; letter-spacing: .4em; text-transform: uppercase;
}
.gv2-co-eyebrow-target { color: var(--vellum-dim); }
.gv2-co-eyebrow-status { display: flex; align-items: center; gap: 6px; color: var(--gv2-co-accent); }
.gv2-co-status-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--gv2-co-accent);
  box-shadow: 0 0 8px var(--gv2-co-accent);
  animation: gv2-co-dot-pulse 1.2s infinite;
}

.gv2-co-body {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: 18px;
  padding: 14px 22px 10px;
}
.gv2-co-glyph {
  position: relative; width: 56px; height: 56px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.gv2-co-glyph svg { display: block; }
.gv2-co-glyph-frame { position: absolute; inset: 0; }
.gv2-co-glyph-icon {
  position: relative;
  filter: drop-shadow(0 0 6px var(--gv2-co-accent));
}

.gv2-co-ribbon-col { /* middle column */ }
.gv2-co-ribbon {
  font-family: 'Cormorant Garamond', serif;
  font-size: 40px; font-style: italic; font-weight: 600;
  color: var(--gv2-co-accent); line-height: 1; letter-spacing: .02em;
  text-shadow: 0 0 18px var(--gv2-co-soft), 0 2px 0 #000;
}
.gv2-co-ribbon-body {
  font-size: 12px; color: var(--vellum); margin-top: 6px;
  letter-spacing: .05em;
}

.gv2-co-dial {
  position: relative; width: 70px; height: 70px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.gv2-co-dial-frame { position: absolute; inset: 0; }
.gv2-co-digit {
  position: relative;
  font-family: 'Cormorant Garamond', serif;
  font-size: 38px; font-style: italic; font-weight: 600;
  color: var(--gv2-co-accent); line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 14px var(--gv2-co-accent), 0 2px 0 #000;
}
.gv2-co-digit-pulse {
  animation: gv2-co-digit-pulse 0.4s ease-out;
}

.gv2-co-footer { padding: 0 18px 12px; }
.gv2-co-footer-head {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 6px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 8px; letter-spacing: .35em; text-transform: uppercase;
}
.gv2-co-footer-label { color: var(--vellum-dim); }
.gv2-co-footer-divider {
  flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--gv2-co-soft), transparent);
}
.gv2-co-footer-time { color: var(--gv2-co-accent); }
.gv2-co-bar {
  height: 4px; position: relative;
  background: rgba(0,0,0,.6);
  border: 1px solid var(--gv2-co-deep, #6e0e0e);
  overflow: hidden;
}
.gv2-co-bar-fill {
  position: absolute; inset: 0;
  background: linear-gradient(90deg, var(--gv2-co-accent), var(--gv2-co-deep));
  box-shadow: 0 0 8px var(--gv2-co-accent), inset 0 1px 0 rgba(255,255,255,.3);
  animation: gv2-co-emberpulse 1s infinite;
  /* width is set inline per tick */
}
.gv2-co-bar-tick {
  position: absolute; top: -1px; bottom: -1px; width: 1px;
  background: rgba(0,0,0,.7);
}

/* FF (frost) palette overrides — set as CSS vars on the .gv2-co root. */
.gv2-co-ff {
  --gv2-co-accent: #7fcdff;
  --gv2-co-soft: rgba(127,205,255,.55);
  --gv2-co-deep: #1f4a7a;
}
.gv2-co-ff .gv2-co-card {
  background: linear-gradient(180deg, #07131e 0%, #04080f 65%, #02050a 100%);
}
/* FH (default) palette — explicit for symmetry. */
.gv2-co-fh-root {
  --gv2-co-accent: var(--oxblood-hi);
  --gv2-co-soft: rgba(201,57,69,.55);
  --gv2-co-deep: #6e0e0e;
}

/* PR — Attacker-area modal additions (huge title, big hex dial, cast beam).
   These are NEW styles layered on top of the existing chrome above; the
   old .gv2-co-ribbon / .gv2-co-dial / .gv2-co-digit remain for the
   existing render path but the refactor uses these new -big variants. */
.gv2-co-fh-root .gv2-co-card { animation: gv2-co-modal-breathe-fh 2s ease-in-out infinite; }
.gv2-co-ff .gv2-co-card      { animation: gv2-co-modal-breathe-ff 2s ease-in-out infinite; }
/* Suppress breathing on baseline (rejoin) re-mount so it doesn't re-fire */
.gv2-co-no-entry .gv2-co-card { animation: none; }

/* Cast beam — from modal toward defender side */
.gv2-co-beam {
  position: absolute; top: 50%; height: 3px;
  width: 120px;
  transform: translateY(-50%) scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--gv2-co-accent), transparent);
  box-shadow: 0 0 18px var(--gv2-co-accent), 0 0 36px var(--gv2-co-accent);
  opacity: 0;
  animation: gv2-co-cast-beam 1.4s ease-out 260ms forwards;
  pointer-events: none;
}
.gv2-co-beam-left { left: 100%; }
.gv2-co-beam-right { right: 100%; transform: translateY(-50%) scaleX(0); transform-origin: right center; background: linear-gradient(270deg, var(--gv2-co-accent), transparent); }
.gv2-co-no-entry .gv2-co-beam { animation: none; opacity: .85; transform: translateY(-50%) scaleX(1); }

/* Huge serif title — replaces the body ribbon for the new design. Two-line
   stacked "Forced / Hit" or "Forced / Freeze." */
.gv2-co-title {
  font-family: 'Cormorant Garamond', serif;
  font-size: 64px; font-style: italic; font-weight: 600;
  color: var(--gv2-co-accent); line-height: .85; letter-spacing: .005em;
  text-shadow: 0 0 30px var(--gv2-co-soft), 0 0 60px var(--gv2-co-soft), 0 4px 0 #000;
  white-space: nowrap;
  margin: 0;
}
.gv2-co-title-line { display: block; }
.gv2-co-subtitle {
  font-family: 'Cormorant Garamond', serif;
  font-size: 14px; color: var(--vellum-dim);
  margin-top: 8px; line-height: 1.35; font-style: italic;
  max-width: 280px;
}

/* Big hex-frame dial (replaces .gv2-co-dial for the new layout) */
.gv2-co-bigdial {
  position: relative; width: 132px; height: 132px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.gv2-co-bigdial svg { position: absolute; inset: 0; }
.gv2-co-bigdial-digit {
  position: relative;
  font-family: 'Cormorant Garamond', serif;
  font-size: 78px; font-style: italic; font-weight: 600;
  color: var(--gv2-co-accent); line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 28px var(--gv2-co-accent), 0 0 10px rgba(255,255,255,.4);
}
.gv2-co-bigdial-digit-pulse {
  animation: gv2-co-number-count .9s ease-out;
}

/* Body layout — title on left, big dial on right */
.gv2-co-body-big {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 14px; align-items: center;
  padding: 14px 22px 12px;
}

/* Wick flicker on status dot — replaces dot-pulse for the new modal */
.gv2-co .gv2-co-status-dot-wick {
  animation: gv2-co-wick-flicker .9s ease-in-out infinite;
}

/* (The old `.gv2-co-glyph-ignite` class rule was removed 2026-07-09 — the glyph it
   drove is replaced by the ActionCard slot below, which reuses the keyframe via
   `.gv2-co-cardart .gv2-ac`. The @keyframes gv2-co-glyph-ignite itself stays.) */

/* ============================================
   FH/FF DECK REVEAL (2026-07-09) — "Interrupted Hit"
   The countdown modal now shows the ACTUAL FH/FF card art (via ActionCard)
   in place of the abstract diamond glyph, with a drawer-attribution caption
   underneath. On 1v1 deck draws a flying card flips off the deck and docks
   onto this slot; skip paths show it directly. See instant-reveal.js.
   ============================================ */
/* Pre-reveal hide: modal mounts hidden while the flight is airborne, then the
   dock (or a skip/fallback) removes this class. !important so it wins over the
   inline visibility:'' that anchorToAttacker writes on the mobile path. */
.gv2-co.gv2-co-prereveal { visibility: hidden !important; }
/* Reveal-only animation — opacity/glow ONLY (no transform), so the just-docked
   flight card is never jerked by a scale/translate as it hands off. */
.gv2-co.gv2-co-reveal .gv2-co-card { animation: gv2-co-reveal-glow 420ms ease-out both; }
@keyframes gv2-co-reveal-glow {
  0%   { filter: brightness(1.5); }
  100% { filter: brightness(1); }
}

/* Card-art slot — replaces the 56px glyph box with a real (small) card + caption. */
.gv2-co-cardart {
  position: relative; flex-shrink: 0;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
}
.gv2-co-cardart .gv2-ac { animation: gv2-co-glyph-ignite 620ms ease-out 80ms both; }
/* While the modal is quiet (mounted-hidden for a flight, or a baseline snapshot)
   the card-art ignite must NOT run: its 0% keyframe is scale(.4), so a
   getBoundingClientRect during it returns a SHRUNK box and the flight would size
   itself ~half-scale then pop at the dock. Mirrors .gv2-co-card / .gv2-co-beam. */
.gv2-co-no-entry .gv2-co-cardart .gv2-ac { animation: none; }
.gv2-co-drew {
  font-family: 'Inter', sans-serif; font-weight: 800;
  font-size: 8.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--gv2-co-accent, var(--oxblood-hi));
  text-shadow: 0 0 8px var(--gv2-co-soft, rgba(201,57,69,.5));
  white-space: nowrap;
}
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-co-drew { font-size: 8px; }
}

/* The flying reveal card — body-mounted, ABOVE the 9300 modal so it docks
   visibly onto the slot. Sits in the deal-flight tier (9400). left/top:0 with
   JS-driven transform:translate() positions it in viewport coords. */
/* NOTE: NO `perspective` property here. This outer element is the one being
   transform-animated (translate/scale/rotate), and a perspective PROPERTY on a
   transforming element flattens its preserve-3d children on WebKit (documented
   at .gv2-flight ~L1560 + render-deal-animation.js). The flip carries its
   perspective INSIDE its transform values instead (perspective(1000px) rotateY…),
   matching the two iOS-verified flights. */
.gv2-ir-fly {
  position: fixed; left: 0; top: 0; z-index: 9400;
  pointer-events: none;
  transform-origin: center center;
}
.gv2-ir-fly-inner {
  position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d;
}
.gv2-ir-face {
  position: absolute; inset: 0;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  border-radius: 10px; overflow: hidden;
}
/* Face-down back — ornamented card back, brass on oxblood felt. */
.gv2-ir-back {
  transform: rotateY(0deg);
  background:
    radial-gradient(60% 45% at 50% 50%, rgba(183,162,122,.28), transparent 70%),
    repeating-linear-gradient(45deg, #20150a 0 8px, #241a0e 8px 16px),
    linear-gradient(180deg, #241a0e, #120b04);
  border: 1px solid var(--brass-lo);
  box-shadow: inset 0 0 0 3px #0c0703, inset 0 0 0 4px rgba(183,162,122,.35), 0 14px 34px rgba(0,0,0,.7);
}
.gv2-ir-back::after {
  content: "❖"; position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 30px; color: rgba(183,162,122,.55); text-shadow: 0 0 12px rgba(183,162,122,.35);
}
/* Front face — holds the real ActionCard art (fills the face). */
.gv2-ir-front { transform: rotateY(180deg); }
.gv2-ir-front .gv2-ac { width: 100% !important; height: 100%; }
/* ============================================
   PHASE C PR 11 — Vellum overlay restyles + new overlays
   match-found, game-start vellum, round slam, forfeit pulse,
   quick-rules slide-out panel, disconnect banner with countdown
   All keyframes namespaced gv2-pr11-*. All selectors prefixed gv2-*.
   ============================================ */

/* ---------- Screen-reader only utility ---------- */
.gv2-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ---------- Shared corner-bracket SVG (used by round-slam + forfeit) ----------
 * Legacy `.gv2-gs-card / .gv2-gs-eyebrow / .gv2-gs-vs / .gv2-gs-name /
 * .gv2-gs-vs-mark / .gv2-gs-sub` rules deleted PR 1, 2026-05-26 alongside the
 * "Match Begins" overlay. `.gv2-gs-brackets` retained — used by round-slam
 * (overlays.js cornerBracketsSVG → openRoundSlam) AND forfeit modal
 * (selector `.gv2-fm-card .gv2-gs-brackets` below). */
.gv2-gs-brackets {
  position: absolute; inset: 4px; width: calc(100% - 8px); height: calc(100% - 8px);
  color: var(--brass);
  pointer-events: none; opacity: .9;
}

/* ---------- Forfeit modal vellum restyle + pulse CTA ---------- */
.gv2-fm-card {
  position: relative;
  padding: 36px 44px 30px;
  border-top-color: var(--oxblood-hi);
}
.gv2-fm-card .gv2-gs-brackets,
.gv2-fm-card > svg.gv2-gs-brackets,
.gv2-fm-card > .gv2-gs-brackets { color: var(--oxblood); }
.gv2-fm-eyebrow {
  font-size: 9px; letter-spacing: .35em; color: var(--oxblood-hi);
  margin-bottom: 14px;
}
.gv2-fm-title {
  font-size: 30px; font-style: italic; color: var(--vellum); line-height: 1.2;
  font-family: 'Cormorant Garamond', serif;
}
.gv2-fm-sub {
  margin-top: 14px; font-size: 11px; color: var(--vellum-dim);
  letter-spacing: .12em;
}
.gv2-fm-confirm {
  position: relative;
  animation: gv2-pr11-forfeit-pulse 2s ease-in-out infinite;
}
@keyframes gv2-pr11-forfeit-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(143,116,208,0); }
  50%      { box-shadow: 0 0 0 6px rgba(143,116,208,.18); }
}

/* ---------- Round slam (rounds 2+) ---------- */
.gv2-overlay-round-slam {
  z-index: 9060;
  background: transparent;
  pointer-events: none;
}
.gv2-overlay-round-slam.gv2-overlay-active { display: flex; }
/* Vellum/brass card frame for the round-slam — gradient bg + brass border
   inherited from `.gv2-overlay-card`. Small padding (round-slam is a 1.5s
   punctuation between rounds). Carries flex centering + text-align from the
   old .gv2-rs-stack rule so the layout stays identical. */
.gv2-rs-card {
  position: relative;
  padding: 22px 40px 20px;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 100%);
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center;
}
.gv2-rs-eyebrow {
  font-size: 14px; letter-spacing: .5em; color: var(--brass);
}
.gv2-rs-num {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic; font-weight: 600;
  font-size: clamp(48px, 12vw, 128px);
  color: var(--brass-hi); line-height: 1;
  /* Cormorant Garamond italic has a tall internal ascent: with line-height:1 in a
     centered flex column the glyph sits low in its line box and visually hugs the
     card's bottom padding. Nudge up with an EM-relative transform so it scales with
     the clamped font-size (a fixed px would be wrong at the 48px/128px extremes).
     QA 2026-06-08. Other serif-italic numbers (.gv2-bc-counter-num, .gv2-rwr-num)
     were evaluated and left: they set no font-family (not Cormorant, no tall ascent).
     No blanket .serif transform. */
  transform: translateY(-0.06em);
  /* Brass-halo lift carried over from the old .gv2-rs-stack rule. */
  text-shadow: 0 4px 18px rgba(0,0,0,.85), 0 0 24px rgba(183,162,122,.25);
}
.gv2-overlay-round-slam.gv2-rs-in .gv2-rs-card {
  animation: gv2-pr11-rs-in 400ms ease-out forwards;
}
.gv2-overlay-round-slam.gv2-rs-out .gv2-rs-card {
  animation: gv2-pr11-rs-out 400ms ease-in forwards;
}
@keyframes gv2-pr11-rs-in {
  from { opacity: 0; transform: scale(0.85); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes gv2-pr11-rs-out {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(1.08); }
}

/* ---------- Quick-rules slide-out panel ---------- */
.gv2-qr-panel {
  position: fixed; top: 0; right: 0; bottom: 0;
  width: 380px; max-width: 100vw;
  z-index: 9050;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 100%);
  border-left: 1px solid var(--brass-lo);
  box-shadow: -10px 0 30px rgba(0,0,0,.6);
  color: var(--vellum);
  transform: translateX(100%);
  transition: transform 320ms cubic-bezier(.2,.8,.3,1);
  display: flex; flex-direction: column;
  pointer-events: auto;
  overflow: hidden;
}
.gv2-qr-panel.gv2-qr-open { transform: translateX(0); }
.gv2-qr-header {
  padding: 22px 24px 14px;
  border-bottom: 1px solid var(--brass-lo);
  position: relative;
}
.gv2-qr-eyebrow {
  font-size: 9px; letter-spacing: .35em; color: var(--brass);
  margin-bottom: 6px;
}
.gv2-qr-title {
  font-family: 'Cormorant Garamond', serif;
  font-size: 24px; font-style: italic; color: var(--brass-hi);
  margin: 0; line-height: 1.1;
}
.gv2-qr-close {
  position: absolute; top: 14px; right: 14px;
  width: 32px; height: 32px;
  background: transparent; color: var(--vellum-dim);
  border: 1px solid var(--brass-lo); cursor: pointer;
  font-size: 20px; line-height: 1;
  font-family: 'Cormorant Garamond', serif;
}
@media (hover: hover) {
  .gv2-qr-close:hover { color: var(--vellum); border-color: var(--brass); }
}
.gv2-qr-close:focus-visible { outline: 2px solid var(--brass-hi); outline-offset: 1px; }
.gv2-qr-body {
  flex: 1; overflow-y: auto;
  padding: 18px 24px 28px;
}
.gv2-qr-section { margin-bottom: 22px; }
.gv2-qr-section-title {
  font-size: 9px; letter-spacing: .3em; color: var(--brass);
  margin: 0 0 8px;
}
.gv2-qr-p {
  font-size: 13px; line-height: 1.5; color: var(--vellum); margin: 0;
  font-family: 'JetBrains Mono', monospace;
}
.gv2-qr-p strong { color: var(--brass-hi); font-weight: 600; }
.gv2-qr-table {
  width: 100%; border-collapse: collapse;
  font-family: 'JetBrains Mono', monospace; font-size: 12px;
}
.gv2-qr-table td {
  padding: 6px 8px;
  border-bottom: 1px solid rgba(183,162,122,.15);
  vertical-align: middle;
  color: var(--vellum);
}
.gv2-qr-table td:first-child { width: 40%; color: var(--vellum-dim); }
.gv2-qr-badge {
  display: inline-block; padding: 2px 6px;
  background: rgba(183,162,122,.12);
  border: 1px solid var(--brass-lo);
  font-size: 10px; letter-spacing: .1em; color: var(--vellum);
}
.gv2-qr-badge-plus { background: rgba(183,162,122,.18); color: var(--brass-hi); }
.gv2-qr-badge-minus { background: rgba(143,116,208,.2); color: var(--oxblood-hi); border-color: var(--oxblood); }
.gv2-qr-badge-instant { background: rgba(28,84,140,.18); color: #9ec3e6; border-color: #2c4f78; }

/* Hover hint for the help link (subtle brass underline pulse) */
.gv2-ca-help-link { cursor: pointer; }
@media (hover: hover) {
  .gv2-ca-help-link:hover { color: var(--brass-hi); }
}
.gv2-ca-help-link:focus-visible { outline: 2px solid var(--brass-hi); outline-offset: 2px; }

/* Mobile: full-screen sheet on small viewports.
   Comma-OR with phone-landscape gate so the QR panel goes full-screen
   when the gameboard does (Bundle C, 2026-05-14). */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-qr-panel {
    width: 100vw; max-width: 100vw;
    border-left: none;
  }
}

/* ---------- Disconnect modal countdown ---------- */
.gv2-overlay-disconnect-banner.gv2-overlay-active { display: flex; }
.gv2-db-time-visible {
  display: inline-block; margin-top: 16px;
  font-size: 40px; letter-spacing: .08em;
  color: var(--vellum);
  font-variant-numeric: tabular-nums;
}
/* Urgent (≤30s): pulse the card + recolor the countdown. gv2-db-urgent is on the
   container; the card is its descendant. */
.gv2-db-urgent .gv2-db-card {
  border-color: var(--oxblood-hi);
  animation: gv2-pr11-db-pulse 1s ease-in-out infinite;
}
.gv2-db-urgent .gv2-db-time-visible { color: var(--oxblood-hi); }
@keyframes gv2-pr11-db-pulse {
  0%, 100% { box-shadow: 0 12px 40px rgba(0,0,0,.6), 0 0 0 0 rgba(143,116,208,0); }
  50%      { box-shadow: 0 12px 40px rgba(0,0,0,.6), 0 0 0 6px rgba(143,116,208,.35); }
}

/* Match-found overlay live styles live in public/css/match-found-overlay.css —
   loaded by public/index.html (homescreen). All `.gv2-mf-*` selectors were
   removed from this file in PR 2 (2026-05-16) after the Lists overlay rebuild
   replaced the prior vertical-card design. */

/* ---------- Reduced motion respect ---------- */
@media (prefers-reduced-motion: reduce) {
  .gv2-overlay-round-slam.gv2-rs-in .gv2-rs-card,
  .gv2-overlay-round-slam.gv2-rs-out .gv2-rs-card { animation: none; }
  .gv2-fm-confirm { animation: none; }
  .gv2-db-urgent .gv2-db-card { animation: none; }
  .gv2-qr-panel { transition: none; }
  /* Slam text — neutralize aggressive rotate/scale/brightness keyframes from
     lines 637-664 with an opacity-only fade. JS-side guards in state-driver.js
     skip particle bursts + screen shake + area shake under reduced-motion. */
  .fx-float.fx-win,
  .fx-float.fx-lose,
  .fx-float.fx-bust,
  .fx-float.fx-push {
    animation: gv2-fx-reduced-fade 1.5s ease-out forwards;
  }
  /* Bust flash glow — disabled (pulsing red box-shadow). */
  .gv2-pa.fx-bust-flash { animation: none; }
  /* Turn-handoff glow fade — snap instead of fade under reduced-motion.
     Matches today's pre-handoff-feature behavior. Sound still plays. */
  .gv2-pa { transition: none; }
}

/* Opacity-only fade for slam text under prefers-reduced-motion (overrides
   the rotate/scale/brightness keyframes via @media specificity). */
@keyframes gv2-fx-reduced-fade {
  0%   { opacity: 0; }
  20%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ============================================
   POST-MATCH V5 — Heraldic Banner overlay (PR 1, 2026-05-06)
   Plan: docs/plans/2026-05-06-postmatch-v5-pr1-plan.md
   All selectors prefixed gv2-me-*. Class names .gv2-me-cta-btn / .gv2-me-soc-btn /
   .gv2-me-xp-section preserved from PR 10 to keep external rules at lines 38-39 / 57-58 / 76-77 active.
   ============================================ */

.gv2-overlay-match-end {
  z-index: 9100;
  position: fixed; inset: 0;
  /* Lucky Jack comp #mo: neutral near-black backdrop; the outcome-tinted radial + dot
     texture live on the themed .gv2-me-banner card (they need the win/loss theme class,
     which sits on .gv2-me-card, not on this outcome-agnostic overlay). */
  background: radial-gradient(ellipse 90% 70% at 50% 30%, #12080c 0%, #0a0409 62%, #060207 100%);
  display: none;
  overflow-y: auto;
  /* device-QA item 11: pin the modal — kill the iOS rubber-band overscroll on
     this fixed scroll container that let the user "drag" the whole modal
     around. Legit in-bounds scroll (tall content) still works. */
  overscroll-behavior: none;
}
.gv2-overlay-match-end.gv2-overlay-active { display: block; }

/* Card shell — the V5 standard hangs centered, not bound to a card box */
.gv2-me-card {
  position: relative;
  width: 100%; max-width: 1080px;
  margin: 0 auto;
  padding: 56px 24px 32px;
  color: var(--vellum);
  display: flex; flex-direction: column; align-items: center;
  font-family: 'Inter', sans-serif;
}
.gv2-me-card-fallback {
  max-width: 600px;
  padding: 48px 32px;
  text-align: center;
}
.gv2-me-card-fallback .gv2-me-cta { margin-top: 24px; }

/* Backdrop weave (subtle horizontal stripes + vignette) */
.gv2-overlay-match-end::before {
  content: ''; position: absolute; inset: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(0deg, rgba(0,0,0,.10) 0 1px, transparent 1px 3px);
  opacity: .6;
  z-index: 0;
}
.gv2-overlay-match-end::after {
  content: ''; position: absolute; inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 100% 70% at 50% 50%, transparent 40%, rgba(0,0,0,.55) 100%);
  z-index: 0;
}
.gv2-me-card { position: relative; z-index: 1; }

/* ===== NEON-FRAMED CARD (comp #mo) ===== */
.gv2-me-banner {
  position: relative;
  width: 100%; max-width: 1080px;
  /* outcome-tinted radial, verbatim from #mo (var(--bg-mid/1/2)) */
  background: radial-gradient(120% 100% at 50% 30%, var(--lj-bg-mid) 0%, var(--lj-bg-1) 45%, var(--lj-bg-2) 88%);
  padding: 40px 56px 40px;
  border-radius: 26px;
  animation: gv2-me-v5-fade-in 380ms ease-out;
}
@keyframes gv2-me-v5-fade-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Masked dot texture inside the card (#mo .dots) */
.gv2-me-banner-dots {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; border-radius: 26px;
  background-image: radial-gradient(rgba(255,255,255,.06) 1px, transparent 1.5px);
  background-size: 22px 22px;
  -webkit-mask-image: radial-gradient(ellipse 74% 66% at 50% 42%, #000 22%, transparent 80%);
  mask-image: radial-gradient(ellipse 74% 66% at 50% 42%, #000 22%, transparent 80%);
}

/* Neon frame + inner white hairline (#mo .frame), with four neon corner diamonds
   (#mo .ncorner i). All decorative (aria-hidden); z-index 1 sits under banner-content
   (z-index 2), share (5) and the H2H ribbon (4). */
.gv2-me-frame {
  position: absolute; inset: 12px; z-index: 1; pointer-events: none;
  border: 2px solid var(--lj-neon); border-radius: 20px;
  box-shadow: 0 0 22px var(--lj-neon-glow), 0 0 60px var(--lj-neon-glow), inset 0 0 34px var(--lj-neon-glow);
}
.gv2-me-frame::before {
  content: ''; position: absolute; inset: 8px;
  border: 1px solid rgba(255,255,255,.14); border-radius: 14px;
}
.gv2-me-frame-corner {
  position: absolute; width: 12px; height: 12px;
  background: var(--lj-neon); transform: rotate(45deg);
  box-shadow: 0 0 10px var(--lj-neon-glow);
}
.gv2-me-frame-corner-tl { top: -7px; left: -7px; }
.gv2-me-frame-corner-tr { top: -7px; right: -7px; }
.gv2-me-frame-corner-bl { bottom: -7px; left: -7px; }
.gv2-me-frame-corner-br { bottom: -7px; right: -7px; }
/* Tighten the frame + verdict diamond flanks on small screens so they don't crowd the
   reduced banner padding (24px @900, 16px @600). */
@media (max-width: 600px) {
  .gv2-me-banner { border-radius: 18px; }
  .gv2-me-banner-dots { border-radius: 18px; }
  .gv2-me-frame { inset: 8px; border-radius: 13px; }
  .gv2-me-crown-verdict { gap: 12px; }
  .gv2-me-theme-win .gv2-me-crown-verdict::before,
  .gv2-me-theme-win .gv2-me-crown-verdict::after,
  .gv2-me-theme-loss .gv2-me-crown-verdict::before,
  .gv2-me-theme-loss .gv2-me-crown-verdict::after { width: 9px; height: 9px; }
}

.gv2-me-banner-content {
  position: relative;
  z-index: 2;
}

/* PR-2: RoH + CTA + async-fallback all live inside the banner now. The banner
   provides side padding, so override the original card-relative width constraints
   (`calc(100% - 88px); max-width: 880px;`) — let them fill the banner-content width. */
.gv2-me-banner-content > .gv2-me-roll-of-honour,
.gv2-me-banner-content > .gv2-me-cta,
.gv2-me-banner-content > .gv2-me-async-fallback {
  width: 100%;
  max-width: none;
}

/* ===== SHARE BUTTON (top-LEFT of the card; 1v1 only) — labeled gold-ring pill (#mo .share).
   Top-left so it never clashes with the top-right H2H "vs opponent" ribbon (>767px). ===== */
.gv2-me-share-btn {
  position: absolute;
  top: 14px; left: 16px; right: auto;
  z-index: 5;
  height: 34px;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 0 14px;
  border-radius: 999px;
  border: 1.5px solid var(--lj-gold-lo);
  background: rgba(10,4,7,.55);
  color: var(--lj-gold);
  cursor: pointer;
  transition: border-color .15s ease, color .15s ease, background .15s ease, transform .1s ease;
}
.gv2-me-share-btn:hover { background: rgba(10,4,7,.82); border-color: var(--lj-gold); transform: translateY(-1px); }
.gv2-me-share-btn:active { transform: translateY(0); }
.gv2-me-share-btn:focus-visible { outline: 2px solid var(--lj-gold-hi); outline-offset: 2px; }
.gv2-me-share-glyph { width: 16px; height: 16px; color: inherit; flex: 0 0 auto; }
.gv2-me-share-glyph circle { fill: currentColor; }
.gv2-me-share-glyph path { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.gv2-me-share-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  white-space: nowrap; color: inherit;
}

/* ===== H2H CORNER RIBBON ===== */
.gv2-me-h2h-corner {
  position: absolute;
  top: 14px; right: 56px;
  z-index: 4;
  width: 160px;
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 4px;
}
.gv2-me-h2h-corner-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 8px; letter-spacing: .4em;
  color: rgba(236,227,207,.5);
  white-space: nowrap;
  text-transform: uppercase;
}
.gv2-me-theme-win .gv2-me-h2h-corner-label { color: rgba(236,227,207,.62); }
.gv2-me-h2h-corner-numbers {
  display: flex; align-items: baseline; gap: 8px;
}
.gv2-me-h2h-corner-num {
  font-family: 'Cormorant Garamond', serif;
  font-size: 16px; font-style: italic; font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.gv2-me-h2h-corner-num-w { color: var(--lj-neon-hi); }
.gv2-me-h2h-corner-num-l { color: rgba(236,227,207,.5); }
.gv2-me-theme-win .gv2-me-h2h-corner-num-l { color: rgba(236,227,207,.62); }
.gv2-me-h2h-corner-sep {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px; letter-spacing: .2em;
  color: rgba(236,227,207,.5);
}
.gv2-me-theme-win .gv2-me-h2h-corner-sep { color: rgba(236,227,207,.62); }
.gv2-me-h2h-corner-bar {
  position: relative; width: 100%; height: 2px;
  background: rgba(0,0,0,.45);
  margin-top: 2px;
}
.gv2-me-h2h-corner-bar-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  background: var(--lj-neon);
}

/* ===== CROWN (eyebrow + verdict word) ===== */
/* Lucky Jack casino-noir (locked comp #mo). Full outcome-neon + gold token API, scoped to
   the post-match card ONLY — repointing :root would recolour the whole in-match board
   (300+ var(--brass|--oxblood) uses in this file). Base = LOSS (red neon); .gv2-me-theme-win
   re-points the whole neon set to jade. GOLD is a CONSTANT accent across both outcomes, so it
   lives on the base only. Hung on .gv2-me-card / .gv2-me-hero-card because those carry the
   theme class (match-end-overlay.js emits `gv2-me-card <themeClass>`). The win rule is
   qualified (0,2,0) so it beats the base by SPECIFICITY, not source order. Exact hexes are
   pulled verbatim from docs/modal-design/lucky-jack-screens.html #mo. */
.gv2-me-card, .gv2-me-hero-card {
  --lj-neon: #ff2f2f; --lj-neon-hi: #ff7a6e; --lj-neon-glow: rgba(255,48,48,.55);
  --lj-script: linear-gradient(180deg,#ff9a90,#e21f24 60%,#8f0d12);
  --lj-bg-mid: #3a0810; --lj-bg-1: #1c0407; --lj-bg-2: #0a0203;
  --lj-panel: #280610; --lj-panel-edge: #5a1018;
  --lj-arow-bg: rgba(255,48,48,.05); --lj-arow-edge: rgba(255,60,60,.12);
  --lj-gold: #e7c877; --lj-gold-hi: #f7e7b6; --lj-gold-lo: #a9812f; --lj-gold-deep: #8a6320;
}
.gv2-me-card.gv2-me-theme-win, .gv2-me-hero-card.gv2-me-theme-win {
  --lj-neon: #22e39d; --lj-neon-hi: #8ff3cf; --lj-neon-glow: rgba(34,227,157,.5);
  --lj-script: linear-gradient(180deg,#9ff5d6,#16c48c 60%,#0a7d5a);
  --lj-bg-mid: #063528; --lj-bg-1: #052018; --lj-bg-2: #02100c;
  --lj-panel: #052a20; --lj-panel-edge: #0c5a44;
  --lj-arow-bg: rgba(34,227,157,.05); --lj-arow-edge: rgba(34,227,157,.14);
}

.gv2-me-crown {
  display: flex; flex-direction: column; align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
/* ROUND n label (#mo .round) — gold mono, wide tracking. */
.gv2-me-crown-round {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px; letter-spacing: .42em;
  color: var(--lj-gold-hi);
  text-transform: uppercase;
}
/* The ornate gold LUCKY JACK wordmark from the comp. NOTE lucky-jack-logo.png is NOT this —
   it is the winking chip FACE, the same art as chip.png; using it here would put the same
   face twice on one modal (crown + medallion). New filename on purpose: /images is not
   asset-versioned, so replacing art in place is cache-served stale for a week. */
.gv2-me-crown-mark {
  height: 56px; width: auto; display: block;
  filter: drop-shadow(0 2px 10px rgba(0,0,0,.7));
}
/* Verdict word (#mo .defeat). Cormorant italic 700 in the outcome SCRIPT gradient, flanked
   by two neon diamonds (#mo .fk). The gradient + flanks are scoped to a theme class so the
   last-resort fallback card (js ~1820, NO theme class, inline color:#ece3cf) renders a plain
   neutral word — NOT defeat-red script on a neutral "Match Over". color:transparent (not
   -webkit-text-fill-color) so that inline color override still wins there. */
.gv2-me-crown-verdict {
  font-family: 'Cormorant Garamond', serif;
  font-size: 60px; line-height: .95;
  font-style: italic; font-weight: 700;
  letter-spacing: .02em;
  margin: 2px 0 0;
  color: var(--bone);
  display: inline-flex; align-items: center; gap: 20px;
}
.gv2-me-theme-win .gv2-me-crown-verdict,
.gv2-me-theme-loss .gv2-me-crown-verdict {
  background: var(--lj-script);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
  filter: drop-shadow(0 0 22px var(--lj-neon-glow));
}
.gv2-me-theme-win .gv2-me-crown-verdict::before,
.gv2-me-theme-win .gv2-me-crown-verdict::after,
.gv2-me-theme-loss .gv2-me-crown-verdict::before,
.gv2-me-theme-loss .gv2-me-crown-verdict::after {
  content: ''; width: 12px; height: 12px; flex: none;
  background: var(--lj-neon); transform: rotate(45deg);
  filter: drop-shadow(0 0 8px var(--lj-neon));
}

/* (ROUND TIMELINE removed — D5, Matt: out of the post-match modal, kept in the share card.) */

/* ===== SCORE ROW ===== */
/* #6 score+seal wrapper. display:contents → transparent on desktop (score-row +
   seal behave exactly as before: seal centered below the row). The mobile media
   block flips it to flex so the seal moves to the empty space on the right.
   NOTE: relies on display:contents — do NOT add a child (>) or adjacent (+)
   selector to the banner-content that targets .gv2-me-score-row / .gv2-me-seal,
   or this wrapper would break it. */
.gv2-me-score-seal { display: contents; }
.gv2-me-score-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 24px;
  padding: 10px 0 0;
}
.gv2-me-score-spacer {
  width: 180px; height: 12px; opacity: 0;
}
.gv2-me-player-panel {
  display: flex;
  align-items: center;
  gap: 18px;
}
.gv2-me-player-panel[data-align="right"] { flex-direction: row-reverse; }
.gv2-me-player-info {
  flex: 1; min-width: 0;
  text-align: left;
}
.gv2-me-player-panel[data-align="right"] .gv2-me-player-info { text-align: right; }
/* PLAYER / OPPONENT label (#mo .plabel) */
.gv2-me-player-role {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; letter-spacing: .24em;
  color: rgba(236,227,207,.62);
  text-transform: uppercase;
}
/* Name (#mo .pname) — Cormorant italic bone */
.gv2-me-player-name {
  font-family: 'Cormorant Garamond', serif;
  font-size: 34px; line-height: 1.05;
  font-style: italic; font-weight: 600;
  color: var(--bone);
  margin-top: 3px;
  word-break: break-word;
}
.gv2-me-player-score-row {
  margin-top: 8px;
  display: flex; align-items: baseline; gap: 10px;
}
.gv2-me-player-panel[data-align="right"] .gv2-me-player-score-row {
  flex-direction: row-reverse;
}
/* Score (#mo .score) — big upright Cormorant in the outcome neon-highlight. The mock paints
   BOTH sides neon-hi; the winner keeps a stronger glow + full opacity as an at-a-glance cue. */
.gv2-me-player-score-num {
  font-family: 'Cormorant Garamond', serif;
  font-size: 64px; line-height: .85;
  font-weight: 700;
  color: var(--lj-neon-hi);
  text-shadow: 0 0 16px var(--lj-neon-glow), 0 2px 0 rgba(0,0,0,.5);
  font-variant-numeric: tabular-nums;
  opacity: .72;
}
.gv2-me-player-score-num[data-won="1"] {
  opacity: 1;
  text-shadow: 0 0 28px var(--lj-neon-glow), 0 2px 0 rgba(0,0,0,.5);
}
/* WINS (#mo .score span) */
.gv2-me-player-score-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px; letter-spacing: .2em;
  color: rgba(236,227,207,.55);
  text-transform: uppercase;
}

/* Circular gold-ring avatar (#mo .hex) — gold ring + outcome-neon glow, CONSTANT for both
   players (winner gets a stronger glow + hairline). Was a heraldic hex shield. Base is now
   already a circle, so the iOS-portrait clip-path:none / hero circle overrides are redundant
   but harmless. */
.gv2-me-crest {
  position: relative;
  width: 92px; height: 92px;
  flex-shrink: 0;
}
.gv2-me-crest-rim {
  width: 92px; height: 92px;
  border-radius: 50%;
  background: linear-gradient(180deg, var(--lj-gold-hi), var(--lj-gold-lo));
  padding: 4px;
  box-shadow: 0 0 20px var(--lj-neon-glow);
}
.gv2-me-crest[data-won="1"] .gv2-me-crest-rim {
  box-shadow: 0 0 28px var(--lj-neon-glow), 0 0 0 1px var(--lj-neon);
}
.gv2-me-crest-inner {
  width: 100%; height: 100%;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 34%, #2a1013, #130406 72%);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  position: relative;
}
.gv2-me-crest-avatar {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.gv2-me-crest-initial {
  font-family: 'Cormorant Garamond', serif;
  font-size: 38px; font-style: italic; font-weight: 600;
  color: var(--lj-gold-hi);
  text-transform: uppercase;
  line-height: 1;
}

/* ===== CENTRE MASCOT MEDALLION (#mo .chipface) =====
   The Lucky Jack mascot (pose4-kick) bursts UP through a chip ring: dark disc + an outcome-
   neon glow ring + a white inset ring2, mascot overflowing the top. The ring colour is the
   variable (--lj-neon); the pose is CONSTANT. NOTE .gv2-me-score-seal is NOT here — it is a
   LAYOUT wrapper (display:contents -> display:flex at <=900px, flex:1;min-width:0 on the
   score row) and deleting it reintroduces a fixed 601-767px overflow bug. */
.gv2-me-seal {
  display: flex; align-items: center; justify-content: center;
}
.gv2-me-seal-disc {
  position: relative;
  width: 124px; height: 124px; border-radius: 50%;
  background: #160305;
  box-shadow:
    0 0 0 2px var(--lj-neon),
    0 0 34px 6px var(--lj-neon-glow),
    inset 0 0 18px rgba(0,0,0,.7);
  overflow: visible;
}
/* white inset hairline ring (#mo .ring2) */
.gv2-me-seal-ring {
  position: absolute; inset: 12%; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.2);
  pointer-events: none;
}
/* mascot bursting up through the ring (#mo .chipface img); overflow-visible disc lets it
   pop above the medallion. width is a % of the disc so it scales with the responsive sizes. */
.gv2-me-seal-mascot {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -54%);
  width: 138%; height: auto; object-fit: contain; display: block;
  filter: drop-shadow(0 8px 20px rgba(0,0,0,.55));
  pointer-events: none;
}

/* ===== REWARDS ROW (XP + trinkets pillar) ===== */
.gv2-me-rewards-section {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 28px;
  padding: 14px 4px 0;
  border-top: 1px solid var(--lj-panel-edge);
  margin-top: 12px;
}
.gv2-me-rewards-xp {
  min-width: 0;
}
.gv2-me-xp-row {
  display: flex; align-items: baseline;
  gap: 10px; margin-bottom: 8px;
  white-space: nowrap;
}
/* Level (#mo .xp .lv) — bone, title + delta in the outcome neon-highlight. */
.gv2-me-xp-level {
  font-family: 'Cormorant Garamond', serif;
  font-size: 18px; font-style: italic;
  color: var(--bone);
  font-weight: 500;
}
.gv2-me-xp-sep {
  color: rgba(236,227,207,.5);
}
.gv2-me-xp-title {
  font-family: 'Cormorant Garamond', serif;
  font-size: 14px; font-style: italic;
  color: var(--lj-neon-hi);
}
.gv2-me-xp-spacer { flex: 1; }
.gv2-me-xp-delta {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; letter-spacing: .12em;
  color: var(--lj-neon-hi);
  text-transform: uppercase;
}
/* XP bar (#mo .bar) — dark track, outcome-neon fill + glow, faint tick overlay.
   .gv2-me-xp-fill KEEPS position:absolute + width:0 + the width transition — animateXP
   drives fill.style.width and relies on all three. */
.gv2-me-xp-bar {
  position: relative; height: 12px; border-radius: 7px;
  background: #16060a;
  border: 1px solid var(--lj-panel-edge);
  overflow: hidden;
}
.gv2-me-xp-bar::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg, transparent 0 23px, rgba(0,0,0,.4) 23px 24px);
}
.gv2-me-xp-fill-prior {
  position: absolute; left: 0; top: 0; bottom: 0;
  background: linear-gradient(90deg, var(--lj-bg-2), var(--lj-bg-mid));
}
.gv2-me-xp-fill {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: 0;
  background: linear-gradient(90deg, var(--lj-bg-mid), var(--lj-neon));
  box-shadow: 0 0 14px var(--lj-neon-glow);
  transition: width 600ms cubic-bezier(.2,.8,.3,1);
}
.gv2-me-xp-numbers-row {
  display: flex; justify-content: space-between;
  margin-top: 6px;
}
.gv2-me-xp-progress {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px; letter-spacing: .1em;
  color: rgba(236,227,207,.52);
  font-variant-numeric: tabular-nums;
}
.gv2-me-xp-tonext {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px; letter-spacing: .12em;
  color: rgba(236,227,207,.52);
  text-transform: uppercase;
}
.gv2-me-xp-levelup {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px; letter-spacing: .2em;
  color: var(--lj-gold-hi);
  margin-left: 6px;
  text-transform: uppercase;
  font-weight: 600;
}

/* Skippable XP — preserves PR 10 cursor + hint */
.gv2-me-xp-section[data-xp-animating="1"] { cursor: pointer; }
@media (hover: hover) {
  .gv2-me-xp-section[data-xp-animating="1"]:hover .gv2-me-xp-delta::after {
    content: " · click to skip";
    color: rgba(231,200,119,.6);
    font-size: 9px; letter-spacing: .15em; margin-left: 4px;
  }
}

/* CHIPS pillar (#mo .chipsbox) — mono label + big Cormorant neon-hi number + chip glyph. */
.gv2-me-trinkets-pillar {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 0 18px;
  border-left: 1px solid var(--lj-panel-edge);
  min-width: 130px;
}
.gv2-me-trinkets-pillar[data-empty="1"] { opacity: .55; }
.gv2-me-trinkets-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; letter-spacing: .24em;
  color: rgba(236,227,207,.55);
  text-transform: uppercase;
}
.gv2-me-trinkets-amount {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px;
}
.gv2-me-trinkets-num {
  font-family: 'Cormorant Garamond', serif;
  font-size: 40px; line-height: 1;
  font-weight: 700;
  color: var(--lj-neon-hi);
  text-shadow: 0 0 14px var(--lj-neon-glow);
  font-variant-numeric: tabular-nums;
}
.gv2-me-trinkets-glyph {
  align-self: center;
  font-size: 20px; line-height: 1;
  color: rgba(236,227,207,.55);
}
/* #7: chip icon for trinkets, right of the number in the amount row. Declared
   here rather than reused from auth-ui.css because game-v2.html loads only
   game-v2.css — same reason this clone existed when it was CSS-drawn discs.
   `display` is explicit: the rule sets align-self/flex but no display, and an
   inline empty span carrying only a background-image paints nothing. */
.gv2-me-coin-stack {
  display: inline-block;
  position: relative;
  width: 30px; height: 30px;
  align-self: center;
  flex: none;
  background: url('/images/lucky-jack/chip.png') center / contain no-repeat;
}
/* Ticket sibling. px-based (not em) so it can't resolve against the wrong
   font-size. align-self is declared because these glyphs sit in three different
   parents: .gv2-me-trinkets-amount (align-items:baseline — where it is REQUIRED),
   .gv2-me-hero-pot-amount (align-items:center — redundant), and
   .gv2-me-pot-coin-amount (flex only under html.bja-ios — inert on web). The
   .gv2-me-coin-stack sibling carries the same declaration for the same reason. */
.gv2-me-ticket-glyph {
  display: inline-block;
  width: 24px; height: 12px;
  align-self: center;
  flex: none;
  background: url('/images/lucky-jack/ticket-transparent.png') center / contain no-repeat;
}

/* ===== ACHIEVEMENTS PANEL (#mo .ach) ===== */
.gv2-me-roll-of-honour {
  position: relative;
  margin: 22px auto 0;
  width: calc(100% - 88px);
  max-width: 880px;
  padding: 16px 22px 18px;
  background: var(--lj-panel);
  border: 1px solid var(--lj-panel-edge);
  border-radius: 14px;
  box-shadow: 0 12px 28px rgba(0,0,0,.5);
}
.gv2-me-roll-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 4px;
}
.gv2-me-roll-title {
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px; letter-spacing: .26em;
  color: var(--lj-gold-hi);
  font-weight: 600;
  text-transform: uppercase;
}
.gv2-me-roll-meta {
  display: flex; align-items: center; gap: 14px;
}
.gv2-me-roll-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px; letter-spacing: .14em;
  color: rgba(236,227,207,.55);
  text-transform: uppercase;
}
/* device-QA item 1: the "Keep climbing" count is now a <button> CTA to
   /achievements. Reset button chrome; scoped to button. so the remaining
   <span> count labels (loading/empty states) don't get a pointer cursor. */
button.gv2-me-roll-count {
  background: none; border: none; padding: 0; cursor: pointer;
}
.gv2-me-roll-viewall {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px; letter-spacing: .14em;
  color: var(--lj-gold-hi);
  background: transparent;
  border: none; padding: 0 0 1px;
  cursor: pointer;
  border-bottom: 1px solid var(--lj-gold-lo);
  text-transform: uppercase;
}
.gv2-me-roll-list {
  display: flex; flex-direction: column; gap: 10px;
  max-height: 240px; overflow-y: auto;
  margin-top: 10px;
  /* device-QA item 11: this nested scroller must not chain its overscroll to
     the (now-pinned) outer modal, or a drag here still rubber-bands the modal. */
  overscroll-behavior: contain;
}
/* Achievement row (#mo .arow) — outcome-neon-tinted card. */
.gv2-me-ach-row {
  display: grid;
  grid-template-columns: 46px 1fr auto;
  align-items: center; gap: 18px;
  padding: 12px 14px;
  border-radius: 10px;
  background: var(--lj-arow-bg);
  border: 1px solid var(--lj-arow-edge);
  animation: gv2-me-v5-ach-fade 280ms ease-out both;
  animation-delay: calc(var(--gv2-me-ach-i, 0) * 80ms);
}
@keyframes gv2-me-v5-ach-fade {
  from { opacity: 0; transform: translateX(-4px); }
  to   { opacity: 1; transform: translateX(0); }
}
/* T-tier badge (#mo .t1) — outcome-neon gradient square. The tier NUMBER stays; the colour
   is now the outcome neon (was a per-tier steel/gold ladder), matching the locked comp. */
.gv2-me-ach-tier-badge {
  width: 46px; height: 46px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px; letter-spacing: .04em; font-weight: 700;
  color: #fff;
  background: linear-gradient(180deg, var(--lj-neon), var(--lj-bg-mid));
  box-shadow: 0 0 14px var(--lj-neon-glow);
}
.gv2-me-ach-text { min-width: 0; }
.gv2-me-ach-name {
  font-family: 'Cormorant Garamond', serif;
  font-size: 20px; font-style: italic;
  color: var(--bone);
  line-height: 1.1; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gv2-me-ach-desc {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; letter-spacing: .1em;
  color: rgba(236,227,207,.5);
  margin-top: 4px;
  text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gv2-me-ach-progress {
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px; color: rgba(236,227,207,.6);
  font-variant-numeric: tabular-nums; letter-spacing: .06em;
  text-align: right; white-space: nowrap;
}

/* #8 never-empty Achievements fallback: neutral edge + tag + in-progress bar so
   "Your best" / "Next up" rows read as distinct from freshly-earned ones. */
.gv2-me-ach-row-fb { border-color: rgba(236,227,207,.14); background: rgba(236,227,207,.03); }
.gv2-me-ach-tag {
  display: inline-block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 8px; letter-spacing: .18em; text-transform: uppercase;
  padding: 2px 6px; margin-left: 8px; vertical-align: middle;
  border: 1px solid rgba(236,227,207,.2); color: rgba(236,227,207,.6);
}
.gv2-me-ach-tag-best { color: var(--lj-gold-hi); border-color: var(--lj-gold-lo); }
.gv2-me-ach-bar {
  height: 5px; margin-top: 6px; overflow: hidden; border-radius: 3px;
  background: rgba(255,255,255,.06);
  border: 1px solid var(--lj-panel-edge);
}
.gv2-me-ach-bar > span {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--lj-bg-mid), var(--lj-neon));
  box-shadow: 0 0 8px var(--lj-neon-glow);
}

/* Claim states. The JS emits these classes but no sheet styled them before this reskin, so
   the earned check, the claimable ember, and the fail flash were invisible. The locked comp
   shows the earned CHECK (#mo .chk); the claimable/failed cues keep the live claim feature
   discoverable. All in the outcome-neon palette. */
.gv2-me-ach-claimed-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--lj-neon); color: #fff;
  font-size: 15px; line-height: 1;
  box-shadow: 0 0 12px var(--lj-neon-glow);
}
.gv2-me-ach-row-claimable {
  cursor: pointer;
  border-color: var(--lj-neon);
  animation: gv2-me-ach-ember 2.2s ease-in-out infinite;
}
@media (hover: hover) {
  .gv2-me-ach-row-claimable:hover { filter: brightness(1.08); }
}
@keyframes gv2-me-ach-ember {
  0%, 100% { box-shadow: inset 0 0 0 1px var(--lj-arow-edge), 0 0 10px var(--lj-neon-glow); }
  50%      { box-shadow: inset 0 0 0 1px var(--lj-neon), 0 0 20px var(--lj-neon-glow); }
}
.gv2-me-ach-row-claimed { opacity: .82; }
.gv2-me-ach-row-claim-failed { animation: gv2-me-ach-fail 1.2s ease-out; }
@keyframes gv2-me-ach-fail {
  0%, 100% { border-color: var(--lj-arow-edge); background: var(--lj-arow-bg); }
  20%, 60% { border-color: #ff6b6b; background: rgba(255,60,60,.14); }
}

/* ===== CTA ROW ===== */
.gv2-me-cta {
  display: grid;
  gap: 12px;
  margin: 20px auto 0;
  width: calc(100% - 88px);
  max-width: 880px;
}
.gv2-me-cta[data-grid="1v1-human"]           { grid-template-columns: 1fr 1.1fr 1fr 1.4fr; }
.gv2-me-cta[data-grid="1v1-human-nosocial"]  { grid-template-columns: 1fr 1fr 1.4fr; }
.gv2-me-cta[data-grid="1v1-bot-with-rematch"]{ grid-template-columns: 1fr 1fr 1.4fr; } /* PR 2: bot 3-button (lobby+newgame+greyedRematch) */
.gv2-me-cta[data-grid="1v1-bot"]             { grid-template-columns: 1fr 1fr; }
.gv2-me-cta[data-grid="tour-human"]          { grid-template-columns: 1fr 1.1fr 1fr; }
.gv2-me-cta[data-grid="tour-bot"]            { grid-template-columns: 1fr 1fr; }
.gv2-me-cta[data-grid="fallback"]            { grid-template-columns: 1fr; }

/* ============================================================
   Post-Match CTA pulse + hover glow (2026-05-16)
   Shared affordance for active CTAs on /game and /campaign
   match-end overlays. Subtle box-shadow breathe + brighter
   shadow on hover. Suppressed for disabled / passive / state-
   managed buttons via combo selectors with specificity (0,2,1)
   that win regardless of source order. Pulse color + inset alpha
   exposed as CSS custom properties so the WIN-theme primary
   button (stronger static shadow today) preserves its profile.
   Plan: docs/plans/2026-05-16-post-match-cta-polish-plan.md
   ============================================================ */
/* Shared base — ALSO used outside the match-end overlay by the campaign "Fight Next Boss"
   button (.gv2-overlay-btn.gv2-cta-pulse, campaign/game-ui.js), so its default glow stays the
   original red. The match-end gold default is scoped to .gv2-me-cta-btn below. */
.gv2-cta-pulse {
  --gv2-cta-pulse-rgb: 160, 36, 45;
  --gv2-cta-pulse-inset-alpha: .175;
  animation: gv2-cta-pulse 2.5s ease-in-out infinite;
}
/* Match-end CTAs are gold-bordered → gold glow default (0,2,0, scoped so the shared base above
   is untouched for the campaign boss button). */
.gv2-me-cta-btn.gv2-cta-pulse {
  --gv2-cta-pulse-rgb: 231, 200, 119;   /* gold #e7c877 */
}
/* The neon-filled primary (New Game / Rematch) glows in the OUTCOME neon, not gold. Base =
   LOSS red neon; the theme-win rule below re-points it to jade. Both this and the gold rule
   are (0,2,0); this one is AFTER, so it wins for the primary. The (0,3,0) suppressions below
   still silence it on disabled/waiting/declined, and opp-voted's later (0,2,0) rule still wins
   the tie to show its border pulse. */
.gv2-me-cta-btn-primary.gv2-cta-pulse {
  --gv2-cta-pulse-rgb: 255, 47, 47;     /* loss = red neon #ff2f2f */
  --gv2-cta-pulse-inset-alpha: .25;
  animation: gv2-cta-pulse-win 2.5s ease-in-out infinite;
}
.gv2-me-theme-win .gv2-me-cta-btn-primary.gv2-cta-pulse {
  --gv2-cta-pulse-rgb: 34, 227, 157;    /* win = jade #22e39d */
}
/* WIN-theme + opp-voted special case: the WIN-theme rule above (0,3,0)
   would otherwise outrank the existing opp-voted border-color pulse rule
   below (0,2,0), suppressing the "impossible to miss" oxblood signal that
   tells the winner the opponent voted to rematch first. Restore it with a
   (0,4,0) combo selector so WIN-themed winners still see the border pulse.
   Caught in review pass 2. */
.gv2-me-theme-win .gv2-me-cta-btn-primary[data-rematch-state="opp-voted"].gv2-cta-pulse {
  animation: gv2-me-rematch-pulse 1.6s ease-in-out infinite;
}
@keyframes gv2-cta-pulse {
  0%, 100% {
    box-shadow:
      0 0 14px rgba(var(--gv2-cta-pulse-rgb), .30),
      inset 0 1px 0 rgba(255, 255, 255, var(--gv2-cta-pulse-inset-alpha));
  }
  50% {
    box-shadow:
      0 0 22px rgba(var(--gv2-cta-pulse-rgb), .60),
      inset 0 1px 0 rgba(255, 255, 255, var(--gv2-cta-pulse-inset-alpha));
  }
}
@keyframes gv2-cta-pulse-win {
  0%, 100% {
    box-shadow:
      0 0 18px rgba(var(--gv2-cta-pulse-rgb), .30),
      inset 0 1px 0 rgba(255, 255, 255, var(--gv2-cta-pulse-inset-alpha));
  }
  50% {
    box-shadow:
      0 0 28px rgba(var(--gv2-cta-pulse-rgb), .60),
      inset 0 1px 0 rgba(255, 255, 255, var(--gv2-cta-pulse-inset-alpha));
  }
}
@media (hover: hover) {
  .gv2-cta-pulse:not(:disabled):not([data-passive="1"]):hover {
    animation: none;
    box-shadow:
      0 0 26px rgba(var(--gv2-cta-pulse-rgb), .75),
      inset 0 1px 0 rgba(255, 255, 255, var(--gv2-cta-pulse-inset-alpha));
  }
  .gv2-me-theme-win .gv2-me-cta-btn-primary.gv2-cta-pulse:not(:disabled):not([data-passive="1"]):hover {
    box-shadow:
      0 0 30px rgba(var(--gv2-cta-pulse-rgb), .75),
      inset 0 1px 0 rgba(255, 255, 255, var(--gv2-cta-pulse-inset-alpha));
  }
}
/* Suppression — combo selectors (specificity 0,3,0: two classes
   + one attribute, all in spec column B) win over .gv2-cta-pulse
   (0,1,0). Attribute selectors are weight-class B (alongside
   classes/pseudo-classes), NOT column C. WIN-theme override above
   (0,3,0) ties on specificity with these suppressions. Source-order
   tie-break favors the LATER rule — so these suppressions WIN over
   the WIN-theme rule (they appear below it). Keep WIN-theme rules
   ABOVE this suppression block so the tie continues to resolve in
   the suppression's favor and pulse stays silenced on declined /
   waiting / disabled states regardless of theme.
   Opp-voted is INTENTIONALLY OMITTED — existing rule below
   (specificity 0,2,0) already beats .gv2-cta-pulse alone. But the
   WIN-theme rule (0,3,0) would outrank opp-voted's (0,2,0) and
   suppress the existing oxblood border pulse for winners — fixed
   above by a (0,4,0) WIN+opp-voted override that restores it.
   Bot-rematch is ALSO OMITTED — the class is never applied to
   that button (would clobber existing 2.8s grey-out via animation
   shorthand). See plan for cascade rationale. */
.gv2-me-cta-btn:disabled.gv2-cta-pulse,
.gv2-me-cta-btn[data-passive="1"].gv2-cta-pulse,
.gv2-me-cta-btn[data-rematch-state="waiting"].gv2-cta-pulse,
.gv2-me-cta-btn[data-rematch-state="declined"].gv2-cta-pulse {
  animation: none;
}
.gv2-overlay-btn:disabled.gv2-cta-pulse {
  animation: none;
}

/* Action button (#mo .btn) — gold-bordered, dark fill, Cinzel title, Cormorant sub. */
.gv2-me-cta-btn {
  position: relative;
  height: 60px;
  padding: 0 20px;
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(0,0,0,.42));
  border: 1.5px solid var(--lj-gold-lo);
  cursor: pointer;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--lj-gold-hi);
  overflow: hidden;
  transition: transform .15s ease, filter .15s ease, background .15s ease, border-color .15s ease;
  font-family: inherit;
}
/* four gold corner nubs (#mo .btn .cnr) — CSS-only via four small corner background squares,
   so no per-button decorative markup is needed. */
.gv2-me-cta-btn::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background-image:
    linear-gradient(var(--lj-gold-lo), var(--lj-gold-lo)),
    linear-gradient(var(--lj-gold-lo), var(--lj-gold-lo)),
    linear-gradient(var(--lj-gold-lo), var(--lj-gold-lo)),
    linear-gradient(var(--lj-gold-lo), var(--lj-gold-lo));
  background-size: 6px 6px;
  background-repeat: no-repeat;
  background-position: 8px 8px, calc(100% - 8px) 8px, 8px calc(100% - 8px), calc(100% - 8px) calc(100% - 8px);
}
@media (hover: hover) {
  .gv2-me-cta-btn:not(:disabled):not([data-passive="1"]):hover {
    transform: translateY(-1px);
    filter: brightness(1.08);
  }
}
.gv2-me-cta-btn[data-passive="1"] { cursor: default; }
.gv2-me-cta-btn-glyph {
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  color: var(--lj-gold);
  position: relative; z-index: 3;
}
.gv2-me-cta-btn-text {
  display: flex; flex-direction: column;
  align-items: center; gap: 2px;
  position: relative; z-index: 3;
}
.gv2-me-cta-btn-label {
  font-family: 'BJACardCinzel', 'Cinzel', serif;
  font-size: 14px; letter-spacing: .14em;
  font-weight: 700;
  white-space: nowrap;
  text-transform: uppercase;
  color: var(--lj-gold-hi);
}
.gv2-me-cta-btn-sub {
  font-family: 'Cormorant Garamond', serif;
  font-size: 13px; font-style: italic;
  color: rgba(236,227,207,.55);
  white-space: nowrap;
}

/* Variants */
.gv2-me-cta-btn-ghost { /* default gold-bordered */ }
.gv2-me-cta-btn-secondary { border-color: var(--lj-gold); }
/* Primary (#mo .btn.prime) — outcome-neon border + neon-tinted fill + neon glow, white title. */
.gv2-me-cta-btn-primary {
  border-color: var(--lj-neon);
  background: linear-gradient(180deg, color-mix(in srgb, var(--lj-neon) 42%, transparent), color-mix(in srgb, var(--lj-neon) 16%, #0a0203) 62%, color-mix(in srgb, var(--lj-neon) 24%, #0a0203));
  box-shadow: 0 0 22px var(--lj-neon-glow), inset 0 0 20px var(--lj-neon-glow);
}
.gv2-me-cta-btn-primary .gv2-me-cta-btn-label { color: #fff; text-shadow: 0 0 10px var(--lj-neon-glow); }
.gv2-me-cta-btn-primary .gv2-me-cta-btn-sub { color: rgba(255,255,255,.8); }
.gv2-me-cta-btn-friend-added {
  border-color: var(--lj-gold);
  background: linear-gradient(180deg, rgba(231,200,119,.28), rgba(138,99,32,.2) 60%, rgba(60,44,16,.28));
  cursor: default;
}
.gv2-me-cta-btn-friend-added .gv2-me-cta-btn-label { color: var(--lj-gold-hi); }
.gv2-me-cta-btn-friend-added .gv2-me-cta-btn-sub { color: rgba(231,200,119,.7); }
.gv2-me-cta-btn-friend-added .gv2-me-cta-btn-glyph { font-size: 18px; color: var(--lj-gold-hi); }
.gv2-me-cta-btn:disabled { opacity: .55; cursor: default; }
/* Rematch waiting/declined/failed badge slot */
.gv2-me-cta-btn[data-rematch-state="declined"],
.gv2-me-cta-btn[data-rematch-state="failed"] {
  border-color: var(--lj-neon);
  color: var(--lj-neon-hi);
  cursor: default;
}
.gv2-me-cta-btn[data-rematch-state="waiting"] {
  opacity: .8; cursor: default;
}
/* PR-8 (TODO #7): opponent has voted, this side hasn't yet. Outcome-neon border-color pulse
   + sustained color emphasis makes the prompt impossible to miss. Border-color pulse (not
   box-shadow) so the base primary-button shadow stays intact. */
.gv2-me-cta-btn[data-rematch-state="opp-voted"] {
  border-color: var(--lj-neon-hi);
  color: var(--lj-neon-hi);
  animation: gv2-me-rematch-pulse 1.6s ease-in-out infinite;
}
.gv2-me-cta-btn[data-rematch-state="opp-voted"] .gv2-me-cta-btn-sub {
  color: rgba(231,200,119,.7);
}
@keyframes gv2-me-rematch-pulse {
  0%, 100% { border-color: var(--lj-neon); }
  50%      { border-color: var(--lj-neon-hi); }
}

/* Async fallback (when summary doesn't arrive in 10s) */
.gv2-me-async-fallback {
  text-align: center;
  padding: 24px 0;
  margin: 22px auto 0;
  width: calc(100% - 88px);
  max-width: 880px;
  background: var(--lj-panel);
  border: 1px solid var(--lj-panel-edge);
  border-radius: 14px;
}
.gv2-me-async-fallback .gv2-me-section-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px; letter-spacing: .26em;
  color: var(--lj-gold-hi);
  margin-bottom: 8px; text-transform: uppercase;
}
.gv2-me-placeholder {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; letter-spacing: .15em;
  color: rgba(236,227,207,.5);
}

/* ===== Responsive ===== */
@media (max-width: 900px) {
  .gv2-me-card { padding: 56px 16px 24px; }
  .gv2-me-banner { padding: 24px 28px 28px; }
  .gv2-me-h2h-corner { width: 130px; }
  .gv2-me-crown-verdict { font-size: 44px; }
  .gv2-me-score-row {
    grid-template-columns: 1fr;
    gap: 14px;
  }
  .gv2-me-score-spacer { display: none; }
  /* #6: place the seal in the empty space to the RIGHT of the stacked player rows
     (was centered below). Wrapper flips from display:contents to flex here. */
  .gv2-me-score-seal { display: flex; align-items: center; gap: 12px; }
  .gv2-me-score-seal .gv2-me-score-row { flex: 1; min-width: 0; }
  .gv2-me-score-seal .gv2-me-seal { flex: 0 0 auto; margin: 0; }
  .gv2-me-score-seal .gv2-me-seal-disc { width: 84px; height: 84px; }
  .gv2-me-player-panel { justify-content: center; }
  .gv2-me-player-panel[data-align="right"] { flex-direction: row; }
  .gv2-me-player-panel[data-align="right"] .gv2-me-player-info { text-align: left; }
  .gv2-me-player-panel[data-align="right"] .gv2-me-player-score-row { flex-direction: row; }
  .gv2-me-rewards-section { grid-template-columns: 1fr; gap: 14px; }
  .gv2-me-trinkets-pillar { border-left: none; border-top: 1px solid var(--lj-panel-edge); padding-top: 12px; }
  .gv2-me-cta[data-grid="1v1-human"] { grid-template-columns: 1fr 1fr; }
  .gv2-me-cta[data-grid="tour-human"] { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 600px) {
  /* Condensed post-match (2026-06-11): the old 48px top was timeline breathing
     room — the timeline is dropped on mobile, so tighten to a small top + a
     safe-area bottom. The CTA grid is now owned by the per-data-grid rules in
     the condensed block below (the blunt `1fr !important` here was removed so
     those rules can win — see plan 2026-06-11-postmatch-condensed-mobile). */
  .gv2-me-card { padding: 16px 8px max(16px, env(safe-area-inset-bottom)); }
  .gv2-me-banner { padding: 20px 16px 20px; }
  .gv2-me-h2h-corner { position: static; width: 100%; align-items: center; margin-bottom: 8px; }
  .gv2-me-crown-verdict { font-size: 36px; }
  .gv2-me-player-name { font-size: 22px; }
  .gv2-me-player-score-num { font-size: 40px; }
  .gv2-me-seal-disc { width: 96px; height: 96px; }
}

/* Portrait mobile: the top-left Share pill FLOWS as its own row above the crown instead of
   floating in the corner — a ~150px labeled pill absolute at left:16 would clip the centered
   ROUND n / verdict on narrow phones. The H2H ribbon is display:none here (condensed-mobile
   block), so the pill has the top strip to itself. Portrait-only: landscape keeps the
   absolute/condensed treatment (and iOS re-show) untouched. */
@media (max-width: 767px) and (orientation: portrait) {
  /* align-self:flex-start keeps the pill compact + left-aligned: .gv2-me-banner is a flex
     column here (~line 3686), so a static button would otherwise stretch full-width. */
  .gv2-me-share-btn { position: static; align-self: flex-start; margin: 0 0 14px; }
}

/* ===== Post-Match V5 — Bundle D3 narrow-width fixes (2026-05-14) =====
   Targeted breakpoint tuning. The existing @≤900px stack-collapse
   and @≤600px scaling don't cover the 601-767px range where the
   verdict word (still 44px at 900px override) collides with the
   130px-wide absolute-positioned H2H corner ribbon. Extend the
   ≤600px static-stack behavior up to ≤767px-or-landscape via the
   Bundle B-D2 comma-OR gate so the ribbon stops overlapping the
   verdict in the 601-767px band. Also adds ≤400px reflows for the
   rewards row's pillar layout where the XP-row nowrap and
   trinkets pillar min-width force horizontal overflow. */

/* Issue 1: H2H corner collision in the 601-767px-or-landscape band. */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-me-h2h-corner {
    position: static;
    width: 100%;
    align-items: center;
    margin-bottom: 8px;
  }
}

/* Issue 2: rewards row pillar layout reflow at narrow widths.
   400px catches Pixel 5 (393px wide) and iPhone 12 mini (375px);
   ≤380px alone would miss Pixel 5. Horizontal card padding stays
   at 8px from the ≤600px block — don't shrink further to keep
   safe-area-inset breathing room on notched devices. */
@media (max-width: 400px) {
  .gv2-me-rewards-section { gap: 10px; padding: 12px 0 0; }
  .gv2-me-trinkets-pillar { min-width: 0; padding: 12px 0 0; }
  .gv2-me-trinkets-num { font-size: 26px; }
  /* XP row uses white-space: nowrap; allow wrap so label + delta
     can break onto a second line when the row exceeds container width. */
  .gv2-me-xp-row { flex-wrap: wrap; white-space: normal; }
}

/* ============================================================
   POST-MATCH — CONDENSED MOBILE REDESIGN (2026-06-11)
   Plan: docs/plans/2026-06-11-postmatch-condensed-mobile-plan.md
   Mockup: docs/plans/assets/postmatch-condensed/*.png|.html
   ------------------------------------------------------------
   CSS-led: the .gv2-me-* DOM is unchanged. These blocks OVERRIDE
   the prior mobile/landscape rules so the overlay reads as a
   condensed, no-scroll card. Placed AFTER the @900/@600/@400 +
   Bundle-D3 blocks so equal-specificity rules win by source order.
   Dropped on mobile: H2H ribbon, wax seal, round-timeline dots,
   achievements LIST (claiming routes to /achievements via View all).
   Three blocks: (A) shared COMPACT, (B) portrait fit, (C) landscape
   2-up grid. Companion JS change: single +N XP delta (deltaText).
   ============================================================ */

/* ---- (A) SHARED — both orientations (COMPACT gate) ---- */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  /* Drops. (h2h is also position:static'd at 3166/3145 — display:none wins
     regardless; placed here, after those blocks, for source-order clarity.) */
  /* The chip medallion (was the wax seal) is NOT hidden here: it is the comp's centrepiece.
     The old seal was dropped at this breakpoint for space; the medallion replaces it as the
     design's focal point and must render. */
  .gv2-me-h2h-corner,
  .gv2-me-roll-list { display: none; }

  /* Score wrapper: revert the 3128 display:flex now the seal is gone, so the
     score-row is the layout unit (and the real grid item under landscape). */
  .gv2-me-score-seal { display: contents; }

  /* Score line: crest · 5 — 3 · crest, names under the scores. Override the
     base 1fr-auto-1fr grid (2420) + the 900px 1fr collapse (3122). */
  .gv2-me-score-row {
    display: flex; flex-direction: row;
    align-items: center; justify-content: center;
    gap: 12px; padding-top: 4px;
    grid-template-columns: none;
  }
  .gv2-me-score-spacer {
    display: block;            /* revert the 3125 @900px display:none */
    width: auto; height: auto; opacity: 1;
    align-self: center;
  }
  .gv2-me-score-spacer::before {
    content: '\2014';
    font-family: 'Cormorant Garamond', serif;
    font-size: 28px; line-height: 1;
    color: rgba(236, 227, 207, .4);
  }
  .gv2-me-player-panel { gap: 8px; }
  /* keep crest on the OUTER edge of each panel (3133 flipped it back to row) */
  .gv2-me-player-panel[data-align="right"] { flex-direction: row-reverse; }
  /* info is a flex ITEM, not a container — must set display:flex before order
     reorders score above name (critique P1). */
  .gv2-me-player-info {
    flex: 0 1 auto;
    display: flex; flex-direction: column; align-items: center;
    text-align: center;
  }
  .gv2-me-player-panel[data-align="right"] .gv2-me-player-info { text-align: center; }
  .gv2-me-player-role,
  .gv2-me-player-score-label { display: none; }
  .gv2-me-player-score-row { order: -1; margin-top: 0; }
  .gv2-me-player-panel[data-align="right"] .gv2-me-player-score-row { flex-direction: row; }
  .gv2-me-player-score-num { font-size: 40px; }
  .gv2-me-player-name {
    font-size: 14px; margin-top: 2px;
    max-width: 38vw;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    word-break: normal;
  }
  /* crest: 76px heraldic shield → compact */
  .gv2-me-crest, .gv2-me-crest-rim { width: 48px; height: 48px; }
  .gv2-me-crest-initial { font-size: 20px; }
  .gv2-me-crown-verdict { font-size: 42px; }

  /* Reward card: bordered box (theme-aware --lj-panel-edge reads on win + loss). */
  .gv2-me-rewards-section {
    border: 1px solid var(--lj-panel-edge);
    border-radius: 8px;
    background: rgba(0, 0, 0, .28);
    padding: 12px 14px;
    margin-top: 14px;
  }

  /* Achievements: strip the card chrome → one centered line (count + View all).
     Keep .gv2-me-roll-meta + .gv2-me-roll-viewall visible + tab-reachable. */
  .gv2-me-roll-of-honour {
    width: 100%; max-width: none;
    margin: 10px auto 0; padding: 4px 0 0;
    background: none; border: none; box-shadow: none;
  }
  .gv2-me-roll-header {
    justify-content: center;
    border-bottom: none; margin-bottom: 0; padding-bottom: 0;
  }
  .gv2-me-roll-title { display: none; }
  /* center the transient "Loading achievements…" placeholder under the now-
     centered achievements line (scoped to roll — the rewards "Loading XP…"
     placeholder stays left-aligned with its XP block). */
  .gv2-me-roll-of-honour .gv2-me-placeholder { text-align: center; }

  /* CTA: full width on mobile (base reserves calc(100% - 88px)). */
  .gv2-me-cta { width: 100%; max-width: none; margin: 12px auto 0; }
  .gv2-me-cta-btn { height: 50px; }
  /* Primary (Rematch) leads on mobile for HUMAN matches (live rematch) — thumb
     reachability, both orientations. Bot rematch is dead ("Opponent left"), so
     it stays AFTER the live Lobby/New Game actions (no reorder for the bot
     variant). Base .gv2-me-cta is display:grid so `order` applies here. */
  .gv2-me-cta[data-grid="1v1-human"] .gv2-me-cta-btn-primary,
  .gv2-me-cta[data-grid="1v1-human-nosocial"] .gv2-me-cta-btn-primary { order: -2; }
  .gv2-me-cta[data-grid="1v1-human"] .gv2-me-cta-btn-secondary,
  .gv2-me-cta[data-grid="1v1-human-nosocial"] .gv2-me-cta-btn-secondary { order: -1; }
}

/* Post-match pot medallion + XP-share button: base display:none so desktop /
   landscape / mobile-web keep the 2-item rewards grid (xp + trinkets). The iOS
   portrait block below re-enables them. (Mirrors the .gv2-port-score pattern.) */
.gv2-me-pot-coin,
.gv2-me-xp-share-btn { display: none; }

/* ---- (B) PORTRAIT — vertical fit + CTA stack ---- */
@media (max-width: 767px) and (orientation: portrait) {
  /* Fill the viewport and CENTER the condensed stack (robust no-overlap; the
     prior margin-top:auto bottom-seat crammed the achievements line under the
     Rematch button when content ran tall). */
  .gv2-me-card { min-height: 100dvh; justify-content: center; }
  .gv2-me-banner { flex: 1; display: flex; flex-direction: column; justify-content: center; }
  .gv2-me-banner-content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 6px; }
  .gv2-me-cta { margin-top: 16px; }

  /* CTA grid — attribute selectors per data-grid (beat the 900px [data-grid]
     rules at 3139-3140, specificity 0,2,0; critique P2). Primary-first order
     for human variants is set in the shared block above. */
  .gv2-me-cta { display: grid; gap: 8px; }
  .gv2-me-cta[data-grid="1v1-human"] { grid-template-columns: 1fr 1fr; }
  .gv2-me-cta[data-grid="1v1-human"] .gv2-me-cta-btn-primary { grid-column: 1 / -1; }
  .gv2-me-cta[data-grid="1v1-human"] .gv2-me-cta-btn-secondary { grid-column: 1 / -1; }
  .gv2-me-cta[data-grid="1v1-human-nosocial"],
  .gv2-me-cta[data-grid="1v1-bot-with-rematch"],
  .gv2-me-cta[data-grid="1v1-bot"],
  .gv2-me-cta[data-grid="tour-human"],
  .gv2-me-cta[data-grid="tour-bot"],
  .gv2-me-cta[data-grid="fallback"] { grid-template-columns: 1fr; }

  /* ===== iOS portrait post-match polish (device QA 2026-06-25) — gated to the
     iOS shell (html.bja-ios) so mobile-web portrait + desktop + landscape are
     untouched. Items 4/5/6/7/8. ===== */

  /* (5/7) Clear the notch, and grow + DROP "Victory" so it floats centered
     between the top and the player scores (big top margin pushes it down from the
     eyebrow; bottom margin spaces it off the players). */
  html.bja-ios .gv2-me-card { padding-top: max(env(safe-area-inset-top), 16px); }
  html.bja-ios .gv2-me-crown-verdict { font-size: 66px; margin: 48px 0 30px; }

  /* (4) Crests → larger plain circles. Override the heraldic shield clip-path on
     both rim + inner; keep the won/loss rim gradient + the avatar clip (inner
     keeps overflow:hidden, so the object-fit:cover avatar is clipped to circle). */
  html.bja-ios .gv2-me-crest-rim,
  html.bja-ios .gv2-me-crest-inner { clip-path: none; border-radius: 50%; }
  html.bja-ios .gv2-me-crest,
  html.bja-ios .gv2-me-crest-rim { width: 58px; height: 58px; }
  html.bja-ios .gv2-me-crest-initial { font-size: 26px; }

  /* (device QA 2026-06-25) Lift the crest above the score line. The panel is
     `display:flex; align-items:center` (~2489); align-self:flex-start tops-aligns
     the 58px crest with the score (score-row is order:-1, on top), and the -8px
     nudge floats it clearly above. Starting value — device-tunable. */
  html.bja-ios .gv2-me-crest { align-self: flex-start; transform: translateY(-8px); }
  /* (device QA 2026-06-25) Tighten the row gap so the centered score row narrows
     and the outer avatars pull inward off the modal edges. */
  html.bja-ios .gv2-me-score-row { gap: 8px; }

  /* Player names larger, but width-capped so long names don't push the row past
     the viewport and clip the outer avatar (device QA 2026-06-25: 42vw → 32vw,
     19px → 17px; nowrap+ellipsis inherited from the compact block at ~3314). */
  html.bja-ios .gv2-me-player-name { font-size: 17px; max-width: 32vw; }

  /* (8) Share button OUTSIDE the XP box — its own right-aligned pill ABOVE the
     rewards card (renderFullCard emits it before renderRewardsRow). Banner one
     hidden. */
  html.bja-ios .gv2-me-share-btn { display: none; }
  html.bja-ios .gv2-me-xp-share-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    align-self: flex-end; margin: 0 2px 8px; padding: 0 14px;
    height: 34px; border-radius: 17px;
    border: 1px solid var(--lj-gold-lo);
    background: rgba(10, 4, 7, .6);
    color: var(--lj-gold-hi);
    font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
    cursor: pointer;
  }

  /* (6) "The Pot" coin medallion directly UNDER the players (renderFullCard's
     banner-content, between the score row and the rewards section) — like the
     share modal. Trinkets pillar hidden in portrait; data-empty (≤0 pot) hides
     the coin (a 0-pot/free match shows no coin — winnings are 0 anyway). */
  html.bja-ios .gv2-me-trinkets-pillar { display: none; }
  html.bja-ios .gv2-me-pot-coin {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
    width: 156px; height: 156px; border-radius: 50%; margin: 10px auto 8px;
    overflow: hidden; /* safety net — formatPotNum keeps the number inside the coin */
    background: radial-gradient(circle at 38% 28%, #241016 0%, #150710 68%, #0a0308 100%);
    box-shadow: inset 0 0 0 3px var(--lj-gold), inset 0 0 0 5px rgba(0, 0, 0, .55),
                inset 0 0 34px rgba(0, 0, 0, .6), 0 0 30px rgba(231, 200, 119, .3),
                0 16px 36px rgba(0, 0, 0, .55);
  }
  html.bja-ios .gv2-me-pot-coin[data-empty="1"] { display: none; }
  html.bja-ios .gv2-me-pot-coin-label {
    font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: .3em;
    text-transform: uppercase; color: var(--lj-gold-hi);
  }
  html.bja-ios .gv2-me-pot-coin-amount { display: flex; align-items: center; justify-content: center; gap: 7px; }
  html.bja-ios .gv2-me-pot-coin-num {
    font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 600;
    font-size: 38px; line-height: .9; color: var(--lj-gold-hi); white-space: nowrap;
  }
}

/* ---- (C) LANDSCAPE — 2-up grid, no scroll ≤500px tall ---- */
@media (orientation: landscape) and (max-height: 500px) {
  .gv2-me-card { padding-top: 12px; }
  /* Share button hidden in condensed landscape on WEB: the score column goes flush-right
     into the top-right corner the button occupies (verdict pins left, score pins right), so
     they'd overlap. On iOS the share sheet is the whole point, so re-show it (html.bja-ios
     hook, set by the injector) nudged into the free top strip above the centered content,
     top-LEFT clear of the right-pinned score. Exact placement is a device-QA item. */
  .gv2-me-share-btn { display: none; }
  /* iOS re-show: labeled pill nudged into the free top-left strip. Cramped 2-col landscape —
     exact placement is a device-QA item (revisit when iOS un-freezes). */
  html.bja-ios .gv2-me-share-btn { display: flex; top: 6px; left: 8px; right: auto; }
  /* banner-content → 2-col grid (overrides the portrait flex-column above) */
  .gv2-me-banner-content {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: 10px 22px; align-items: center;
  }
  /* crown left + score right share row 1; rewards/roll/cta span full width.
     Under display:contents the score-row (not the wrapper) is the grid item. */
  .gv2-me-crown { text-align: left; }
  .gv2-me-crown-round { text-align: left; }
  .gv2-me-crown-verdict { font-size: 40px; margin-top: 0; text-align: left; }
  .gv2-me-score-row { justify-content: flex-end; }
  .gv2-me-rewards-section,
  .gv2-me-async-fallback,
  .gv2-me-roll-of-honour,
  .gv2-me-cta { grid-column: 1 / -1; }
  /* reward card internal: XP | trinkets side by side; drop internal dividers */
  .gv2-me-rewards-section {
    grid-template-columns: 1.5fr 1fr;
    align-items: center; gap: 20px; margin-top: 0;
  }
  .gv2-me-trinkets-pillar { border-left: none; border-top: none; padding-top: 0; }
  .gv2-me-roll-of-honour { margin-top: 0; }
  /* CTA — one row; column count per data-grid variant */
  .gv2-me-cta { display: grid; gap: 10px; margin-top: 4px; }
  .gv2-me-cta-btn { height: 46px; }
  .gv2-me-cta[data-grid="1v1-human"] { grid-template-columns: repeat(4, 1fr); }
  .gv2-me-cta[data-grid="1v1-human-nosocial"],
  .gv2-me-cta[data-grid="1v1-bot-with-rematch"],
  .gv2-me-cta[data-grid="tour-human"] { grid-template-columns: repeat(3, 1fr); }
  .gv2-me-cta[data-grid="1v1-bot"],
  .gv2-me-cta[data-grid="tour-bot"] { grid-template-columns: repeat(2, 1fr); }
  .gv2-me-cta[data-grid="fallback"] { grid-template-columns: 1fr; }
}

/* (Tooltip CSS removed with the round-dot timeline — nothing builds .gv2-me-tooltip any more.) */

/* ============================================================
   PHASE D PR 12 — Mobile layout reflow
   Breakpoint: <= 767px (matches Phase C overlay rules at 1423 + 1846).
   Most overrides scoped under .gv2-page so /demo-v2 (which renders
   primitives bare, outside .gv2-page) is not affected.
   Plan: docs/plans/2026-04-30-gameboard-phase-d-mobile-plan.md
   ============================================================ */
/* ============================================================
   MOBILE — width-OR-phone-landscape (Bundle C, 2026-05-14)
   ------------------------------------------------------------
   Width gate handles portrait (≤767px wide). Landscape gate
   catches phones in landscape (typically 844-932px wide × ≤500px
   tall) that previously fell through the width gate and showed
   desktop layout with white side bars + horizontal overflow.
   Tablets in landscape are 1024+ wide AND 700+ tall — excluded
   by both gates so iPad keeps its desktop-grade layout.
   All existing mobile rules inside this block (player area
   ordering, BoardChrome collapse, CenterAxis horizontal strip,
   ScorePlaque shrink, BANK chip shrink, RoundsWonRail pip-only,
   safe-area insets on overlays) inherit landscape-phone coverage
   atomically via this single OR query — no rule duplication.
   ============================================================ */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  /* ---- Page shell: tighten padding, claw back ~50px horizontal ---- */
  .gv2-page { padding: 8px; }

  /* ---- Game grid: 3-col grid → flex column ----
     DOM order: P1 → axis → P2. CSS order makes visual stack
     P2 (opponent, top) → axis → P1 (self, bottom). Keyboard / SR
     order stays P1-first (correct semantic). */
  .gv2-page .gv2-game-grid {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  /* Default order (when myIdx not yet resolved): DOM order — P0 top, P1 bottom.
     Once myIdx is known, .gv2-page gets data-my-idx="0" or "1" via game-ui.js
     and the per-POV rules below put SELF on TOP + OPPONENT on BOTTOM
     (Bundle B, 2026-05-14). Thumb-reachability + consistent POV regardless
     of which slot the player occupies in the match.
     PORTRAIT NOTE (Phase 5, 2026-06-24): the iOS PortDuel board reverses this
     to OPPONENT on TOP + SELF on BOTTOM. That flip is a portrait-ONLY override
     placed in the `(max-width:767px) and (orientation:portrait)` block near the
     end of this file (search "PORTRAIT BOARD — POV self-on-bottom"); it ties the
     specificity of these per-idx rules and wins by later source order. This
     block's SELF-on-TOP order therefore still governs landscape phones (which
     re-grid via grid-area at the landscape board block regardless).
     Note: P1 mirror rules below (line ~3055) are identity-based, not
     position-based — they apply to slot 1's content regardless of whether
     P1 is currently rendered at top or bottom of the stack. The mirror
     reflects who the player IS (P0=purple, P1=orange + deal-anim slot
     math), not where their area sits visually. */
  .gv2-page .gv2-game-grid > .gv2-pa[data-gv2-player-area="0"] { order: 1; }
  .gv2-page .gv2-game-grid > .gv2-axis { order: 2; }
  .gv2-page .gv2-game-grid > .gv2-pa[data-gv2-player-area="1"] { order: 3; }
  /* Self = P0 (purple): self (P0) on top, opponent (P1) on bottom */
  .gv2-page[data-my-idx="0"] .gv2-game-grid > .gv2-pa[data-gv2-player-area="0"] { order: 1; }
  .gv2-page[data-my-idx="0"] .gv2-game-grid > .gv2-pa[data-gv2-player-area="1"] { order: 3; }
  /* Self = P1 (orange): self (P1) on top, opponent (P0) on bottom */
  .gv2-page[data-my-idx="1"] .gv2-game-grid > .gv2-pa[data-gv2-player-area="0"] { order: 3; }
  .gv2-page[data-my-idx="1"] .gv2-game-grid > .gv2-pa[data-gv2-player-area="1"] { order: 1; }

  /* ---- BoardChrome: 240/1fr/240 (520px min) → auto/1fr/auto ---- */
  .gv2-page .gv2-bc {
    grid-template-columns: auto 1fr auto;
    gap: 8px;
  }
  .gv2-page .gv2-bc-counter-num { font-size: 22px; }
  /* nowrap so the scoreline can't orphan "· MATCH POINT" onto a 2nd line in the
     center timeline (the only visible caption on mobile — top strip is hidden). */
  .gv2-page .gv2-bc-caption { font-size: 9px; letter-spacing: .25em; white-space: nowrap; }
  /* Forfeit: icon-only on mobile but keep 44×44 touch target.
     (Text node wrapped in .gv2-bc-forfeit-text in BoardChrome.js
     so CSS can hide it.) */
  .gv2-page .gv2-bc-forfeit, .gv2-page .gv2-bc-menu { min-height: 44px; min-width: 44px; padding: 8px 10px; }
  .gv2-page .gv2-bc-forfeit-text { display: none; }

  /* ---- CenterAxis: vertical column → horizontal strip ----
     Override padding-top:110px (used to align deck with desktop player
     areas). On mobile axis sits between P2 and P1 areas — no alignment
     needed. flex-wrap so children flow to 2nd row at narrow widths. */
  .gv2-page .gv2-ca {
    flex-direction: row;
    padding: 4px 0;
    gap: 12px;
    align-items: center;
    flex-wrap: wrap;
    justify-content: center;
  }
  /* Hide rules callout — quick-rules `?` button is always available */
  .gv2-page .gv2-ca-rules { display: none; }
  /* MP relocated out of .gv2-ca-rules (QA #5) → hide it on mobile. The single
     mobile Match Point now lives in the center round-timeline below. */
  .gv2-page .gv2-ca-mp { display: none; }
  /* Item 1 (mobile QA 2026-06-05): move the round timeline (dots + scoreline +
     Match Point) out of the top strip into the center, next to the deck. The
     [data-gv2-mobile-timeline] node is a .gv2-game-grid sibling placed right
     after .gv2-axis in the DOM; order:2 (tying with .gv2-axis, also order:2)
     makes it render in the strip immediately below the deck. */
  .gv2-page [data-gv2-mobile-timeline] {
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    order: 2; padding: 2px 0 4px;
  }
  /* Hide the top-strip timeline copy (scoreline + dots + its .gv2-bc-mp); the
     Round counter + Forfeit are siblings of .gv2-bc-center, so they survive. */
  .gv2-page .gv2-bc-center { display: none; }
  /* Mobile center axis = deck totem only. Toggles, count strip, and help
     link are relocated to .gv2-mobile-footer below the player areas. */
  .gv2-page .gv2-ca .gv2-dctl { display: none; }
  .gv2-page .gv2-ca .gv2-ccs { display: none; }
  .gv2-page .gv2-ca .gv2-ca-help { display: none; }
  /* Deck totem shrinks 130 → 96px tall */
  .gv2-page .gv2-dt { height: 96px; }
  /* DeckControls: 5 buttons in a row, 44×44 each (keep aspect-ratio
     auto so fixed min-height takes effect — desktop uses aspect-ratio: 1).
     Targets the footer copy on mobile (axis copy is hidden above). */
  .gv2-page .gv2-dctl {
    grid-template-columns: repeat(5, 44px);
  }
  .gv2-page .gv2-dctl-btn {
    aspect-ratio: auto;
    min-height: 44px;
    min-width: 44px;
    font-size: 14px;
  }
  /* Card count strip — slightly smaller font (targets footer copy on mobile). */
  .gv2-page .gv2-ccs-label { font-size: 7px; }
  .gv2-page .gv2-ccs-count { font-size: 14px; }
  .gv2-page .gv2-ccs-deck { font-size: 11px; }
  /* Help link styling — ensure 44×44 touch target on the footer copy. */
  .gv2-page .gv2-ca-help { margin-top: 0; }
  .gv2-page .gv2-ca-help-link {
    display: inline-block;
    min-height: 44px;
    padding: 12px 8px;
    line-height: 20px;
  }

  /* Mobile-footer container: 4th child of .gv2-game-grid. Holds the
     relocated toggles + count strip + how-to-play link as a strip at the
     bottom of the mobile board (order: 4 places it below opponent's area). */
  .gv2-page .gv2-mobile-footer {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    justify-content: center;
    padding: 8px 12px;
    background: rgba(0,0,0,.25);
    border: 1px solid var(--brass-lo);
    order: 4;
  }

  /* ---- Player area: tighten padding + claw back vertical space ----
     Bundle B (2026-05-14): padding 10/12 → 8/10, gap 10 → 8.
     Section labels (HAND / ACTIONS · N/M / SPENT TACTICS) shrunk from
     base 9px to 8px to reclaim ~12-16px vertical across the three
     labels stacked inside each player area. Letter-spacing 0.3em
     preserves the mono-uppercase rhythm at the smaller size. */
  .gv2-page .gv2-pa { padding: 8px 10px; gap: 8px; }
  .gv2-page .gv2-pa-row { flex-wrap: wrap; gap: 8px; }
  .gv2-page .gv2-pa-section-label { font-size: 8px; margin: 2px 0; letter-spacing: 0.3em; }

  /* ---- PlayerHeader: compact on mobile ---- */
  .gv2-page .gv2-ph { gap: 6px; }
  .gv2-page .gv2-ph-seal { width: 26px; height: 26px; border-width: 1px; }
  .gv2-page .gv2-ph-seal-initial { font-size: 13px; }
  .gv2-page .gv2-ph-name { font-size: 18px; }

  /* ---- ScorePlaque: 130 → 100 wide; numeral 56 → 44 ---- */
  .gv2-page .gv2-sp { width: 100px; padding: 8px 10px 10px; }
  .gv2-page .gv2-sp-numeral { font-size: 44px; }

  /* ---- Hand fan: clip overflow so high-card hands don't punch
     through .gv2-pa border or trigger horizontal scroll. shiftForCount
     unchanged (JS); cards 8+ visually clip without ellipsis (acceptable).
     PORTRAIT NOTE (Phase 5): the portrait-only block near the end of this file
     reverses this to `overflow-x:auto; overflow-y:hidden` so a tall hand scrolls
     horizontally instead of clipping (the absolute fan cards are NOT
     re-positioned — desktop-safe). Landscape phones keep this clip. */
  .gv2-page .gv2-pa-fan { overflow: hidden; }

  /* ---- BANK chip (TimeBankInline): KEEP visible; shrink only.
     Hiding it would silently regress time bank countdown UX in 1v1
     ranked matches. Target inner classes directly — .gv2-tb-label and
     .gv2-tb-time both have explicit font-sizes (7px / 12px) so the outer
     .gv2-pa-bank font-size won't cascade to them. */
  .gv2-page .gv2-pa-bank .gv2-tb { padding: 2px 5px; gap: 4px; }
  .gv2-page .gv2-pa-bank .gv2-tb-label { font-size: 6px; }
  .gv2-page .gv2-pa-bank .gv2-tb-time { font-size: 10px; }
  .gv2-page .gv2-pa-bank .gv2-tb-idle-dot { width: 3px; height: 3px; }

  /* ---- RoundsWonRail: collapse to pip-only horizontal rail ----
     Hide numeral / label / footer; only pips stay. Override the
     existing flex: 0 0 140px column so the rail doesn't reserve
     140px next to PlayerControls. */
  .gv2-page .gv2-rwr {
    flex: 0 0 auto;
    flex-direction: row;
    padding: 4px 8px;
    gap: 6px;
    align-items: center;
  }
  .gv2-page .gv2-rwr-label,
  .gv2-page .gv2-rwr-num,
  .gv2-page .gv2-rwr-footer { display: none; }
  .gv2-page .gv2-rwr-pips { flex-wrap: wrap; }
  .gv2-page .gv2-rwr-pip { width: 6px; height: 6px; }

  /* FootStrip is single-column (block) post-tracker-removal — no mobile override needed. */

  /* ---- CountdownOverlay (FH/FF): clamp width to viewport ----
     Keep top: 44% (measured: no collision with disconnect banner on
     iPhone SE — ~250px gap remains). Body grid stays auto/1fr/auto;
     glyph + dial shrink below 400px to make room for ribbon text. */
  .gv2-co {
    width: calc(100vw - 32px);
    max-width: 480px;
  }
  /* Below 400px viewport — extra-tight cells */
  @media (max-width: 399px) {
    .gv2-co-glyph { width: 40px; height: 40px; font-size: 22px; }
    .gv2-co-dial { width: 56px; height: 56px; }
  }

  /* ---- Disconnect modal: fit small / landscape-short screens ---- */
  .gv2-db-card {
    max-width: calc(100vw - 32px);
    padding: 22px 20px;
  }
  .gv2-db-title { font-size: 22px; }
  .gv2-db-time-visible { font-size: 34px; }

  /* ---- Toast: clear iOS notch ---- */
  .gv2-toast {
    top: max(24px, env(safe-area-inset-top) + 8px);
  }

  /* ---- Match-found overlay: symmetric safe-area padding so card
     stays clear of notch + home-indicator on landscape iPhone. ----
     Note: this rule is also inert on the homescreen (index.html does
     NOT load game-v2.css) — only takes effect when match-found fires
     from /game, which is rare. Documented as out-of-scope gap. */
  .gv2-mf-overlay {
    padding:
      env(safe-area-inset-top)
      env(safe-area-inset-right)
      env(safe-area-inset-bottom)
      env(safe-area-inset-left);
  }

  /* ---- Chooser overlays (SW/FC): 2-col → 1-col vertical stack ---- */
  .gv2-overlay-chooser-cols {
    grid-template-columns: 1fr;
  }

  /* SW swap modal — portrait (device QA 2026-06-25): the base SW grid
     `"yh oh" / "mid mid"` (~1228) keeps the two hands side by side; with a single
     1fr column track YOUR HAND wraps in the leftover width while OPPONENT'S sizes
     to content — an asymmetric stack. Restack the two HANDS vertically instead:
     Opponent's Hand row, Your Hand row, then mid (score preview + Swap button) —
     opponent on top to match the board (device-QA item 5, 2026-07-02).
     Each hand gets the full modal width via `.gv2-overlay-chooser-col-cards`
     (flex/wrap/center) so cards stay a centered row and only wrap symmetrically
     when a hand is genuinely too wide. The landscape block (~4100) re-sets
     `"yh mid oh"` after this, so short-landscape keeps its 3-column layout. */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-cols {
    /* device-QA (item 5): opponent's hand on TOP, your hand below, so the swap
       modal matches the board (opponent-top / player-bottom). The short-landscape
       block below re-sets "yh mid oh", so this only affects narrow portrait. */
    grid-template-areas: "oh" "yh" "mid";
    gap: 18px;
  }
  /* Collapse the empty boost-chip reserve in the vertical stack — renderBoostChips
     always emits the wrapper (min-height 44px at ~5599), so without this each hand
     carries its own 44px empty band, stacking ~88px of dead space above mid.
     Mirrors the landscape block's treatment (~4130). */
  .gv2-overlay-chooser-sw .gv2-chooser-boost-chip-row {
    min-height: 0; margin: 2px 0;
  }

  /* ---- PR 13 §4 — Chooser safe-area + viewport-clamping ----
     Mirror .gv2-mf-overlay pattern from PR 12 so chooser submit button
     stays clear of iOS notch / home-indicator on landscape iPhone.
     Paired max-height subtracts safe-area insets so card stays inside
     parent's content box (parent uses align-items: center). */
  .gv2-overlay-chooser {
    padding:
      env(safe-area-inset-top)
      env(safe-area-inset-right)
      env(safe-area-inset-bottom)
      env(safe-area-inset-left);
  }
  .gv2-overlay-chooser-card {
    max-height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
    max-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
    overflow-y: auto;
  }
  /* 2026-05-06 FC modal redesign — clamp the new wider modal on small screens
     without touching the bare .gv2-overlay-chooser-card rule above (which is
     intentionally shared with chooser-sw for the safe-area max-height). */
  .gv2-overlay-chooser-fc .gv2-overlay-chooser-card {
    max-width: min(1280px, 95vw);
    padding: 24px 32px;
  }
  .gv2-overlay-chooser-fc .gv2-overlay-chooser-title {
    font-size: 28px;
  }
  .gv2-overlay-chooser-fc-scores {
    grid-template-columns: 1fr 1fr; gap: 16px;
  }
  .gv2-overlay-chooser-fc-score-num {
    font-size: 26px;
  }

  /* ---- PR 13 §6 — P0/P1 mobile flex symmetry ----
     Desktop mirrors P1 (orange) with row-reverse + direction:rtl. On mobile
     vertical stack, mirror semantics are meaningless; both areas should read
     top-down with LTR flow. JS slot-math branch in render-deal-animation.js
     keeps deal animation aligned with the flipped layout. */
  .gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-bottom { flex-direction: row; }
  .gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-row { flex-direction: row; }
  .gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-hand { flex-direction: row; }
  .gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-section-label { flex-direction: row; }
  .gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-pa-spent { justify-content: flex-start; }
  .gv2-page .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable { direction: ltr; }
}

/* ============================================================
   LANDSCAPE PHONES — FC / SW chooser fit (mobile QA F1, 2026-06-11)

   On a short landscape viewport the centered chooser card overflowed: the
   "If kept / If discarded" outcome row and the Keep / Discard / Swap buttons
   fell below the fold. The card already gets max-height + overflow-y:auto
   (PR 13 §4 block above), but scroll alone left the actions out of view.

   This block (1) compacts the vertical rhythm, (2) shrinks the oversized FC
   card preview (120×165, bumped 1.25× for the desktop modal) via a child
   transform — scaling the child keeps card+pip+corners proportional, whereas
   a --pc-w/--pc-h var swap would desync the inline --pc-pip — and reclaims its
   layout height, and (3) pins the action bar to the bottom of the scrollable
   card so the primary control is ALWAYS reachable.

   Scope: the generic .gv2-overlay-chooser-card / -actions / -title rules also
   compact the OVERFLOW-alert modal (shares the chooser-family classes) — this
   is intentional: it has the same landscape-overflow risk and a single OK
   button that benefits from the sticky bar. The FC card-preview shrink does
   NOT touch it (it uses .gv2-overlay-chooser-overflow-card, not -fc-card).

   Landscape-only (orientation + max-height:500px, matching the existing
   breakpoint) — portrait and desktop keep their roomier layout untouched.
   ============================================================ */
@media (orientation: landscape) and (max-height: 500px) {
  .gv2-overlay-chooser-card { padding-top: 14px; padding-bottom: 0; }
  .gv2-overlay-chooser-fc .gv2-overlay-chooser-card { padding: 14px 32px 0; }
  .gv2-overlay-chooser-eyebrow { margin-bottom: 6px; }
  .gv2-overlay-chooser-title,
  .gv2-overlay-chooser-fc .gv2-overlay-chooser-title { font-size: 19px; }
  .gv2-overlay-chooser-sub { margin-top: 5px; }
  .gv2-overlay-chooser-cols { margin-top: 12px; }

  /* FC card preview: shrink ~29% and reclaim its layout height so the score
     row + actions move up into view. Exclude the Phase-2 outcome stamp — it's
     a position:absolute overlay that owns its own translate(-50%,-50%) rotate
     transform for centering, which scale() here would clobber (it sits in the
     SAME container, .gv2-overlay-chooser-fc-card-stamped). */
  .gv2-overlay-chooser-fc-card { margin: 10px 0 0; height: 118px; align-items: flex-start; }
  .gv2-overlay-chooser-fc-card > *:not(.gv2-overlay-chooser-stamp-kept):not(.gv2-overlay-chooser-stamp-rejected) {
    transform: scale(.71);
    transform-origin: top center;
  }

  .gv2-overlay-chooser-fc-scores { margin-top: 10px; padding: 10px 0; }
  .gv2-overlay-chooser-fc-score-num { font-size: 24px; }
  .gv2-overlay-chooser-sw-preview { margin-top: 10px; padding: 8px 0; }

  /* Pin the action bar to the bottom of the (scrollable) card so Keep /
     Discard / Swap / OK stay visible and tappable. The fade resolves to the
     card's bottom gradient colour (#0a0608) so scrolled content slips under. */
  .gv2-overlay-chooser-actions {
    position: sticky;
    bottom: 0;
    margin-top: 12px;
    padding: 10px 0 14px;
    background: linear-gradient(180deg, rgba(10, 6, 8, 0) 0%, #0a0608 40%);
  }

  /* ---- SW swap modal: true 3-column landscape layout (fits with NO scroll) ----
     Hands fill the width; the score preview + Swap button live in the middle
     column so nothing stacks below the hands. SW-scoped — FC keeps the sticky
     bottom bar above. Applies to 1v1 AND campaign SW (same modal root). */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-cols {
    grid-template-columns: 1fr auto 1fr;
    grid-template-areas: "yh mid oh";
    align-items: center;
    gap: 10px;
    margin-top: 8px;
  }
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-mid { gap: 8px; min-width: 88px; }
  /* Passive-mirror banner lives in the middle column now — compact + width-capped
     so the long "Waiting for opponent to swap…" wraps tidily instead of ballooning
     the auto middle column and squeezing the hands. */
  .gv2-overlay-chooser-sw .gv2-chooser-mirror-banner {
    font-size: 10px; line-height: 1.3; margin-top: 0; max-width: 116px;
  }
  /* score preview → vertical stack in the narrow middle column (2-col would overflow) */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-sw-preview {
    grid-template-columns: 1fr; gap: 2px; margin: 0; padding: 6px 0;
  }
  /* shrink the hand tiles so each hand stays ~one row across its full-width column.
     PlayingCard (--pc-*) AND DeckCard (--gv2-dc-*, campaign boss face-down) both set
     their size inline, so !important. Scoped to .gv2-overlay-chooser-col-cards so the
     Phase 2 reveal cards (.gv2-overlay-chooser-sw-side) keep their own size below. */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-col-cards .gv2-pc {
    --pc-w: 54px !important; --pc-h: 74px !important; --pc-pip: 32px !important;
  }
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-col-cards .gv2-dc {
    --gv2-dc-w: 54px !important; --gv2-dc-h: 74px !important;
  }
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-col-cards { min-height: 0; gap: 4px; }
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-col-label { margin-bottom: 4px; }
  .gv2-overlay-chooser-sw .gv2-chooser-boost-chip-row { min-height: 0; margin: 2px 0; }
  /* Swap button now lives in the middle column — undo the shared sticky bottom bar. */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-actions {
    position: static; margin-top: 0; padding: 0; background: none;
  }
  /* Phase 2 outcome reveal (80×110 cards) — shrink so it fits landscape too. */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-sw-side .gv2-pc {
    --pc-w: 54px !important; --pc-h: 75px !important; --pc-pip: 32px !important;
  }

  /* ---- "[opponent] is acting" box (landscape center column) — clamp compact ----
     The [data-gv2-ls-turn] FootStrip has no height cap; a long "X is acting…"
     balloons the c-turn grid row and shoves/overlaps the deck axis (How-to-Play
     link) + concede below it. Compact it + ellipsize the name so the row stays
     small. Un-scoped → covers 1v1 AND campaign; the desktop .gv2-board-bottom
     FootStrip is untouched (different node). Bug fix: acting-box overlap. */
  [data-gv2-ls-turn] .gv2-fs-msg { padding: 8px 14px; }
  [data-gv2-ls-turn] .gv2-fs-msg-title {
    font-size: 18px;
    max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  [data-gv2-ls-turn] .gv2-fs-msg-body {
    font-size: 11px; margin-top: 2px;
    max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
}

/* ============================================================
   PORTRAIT PHONES — deck totem: shrink + center (mobile QA 2026-06-09)
   Two fixes in one portrait-scoped block:
   (1) Shrink. The default 70px-wide cards / 96px totem read too big on a phone.
   (2) Center. The totem's .gv2-dc cards are position:absolute (out of flow), so in
       the portrait row-flex center strip (.gv2-ca flex-direction:row) the totem
       shrink-wraps to 0 width and the absolutely-positioned cards land off-screen
       left. An explicit width gives the cards a real containing block and a real box
       for justify-content:center to center.
   Portrait-scoped via (orientation:portrait) so the landscape layout — where .gv2-ca
   is a column with align-items:stretch (totem already full-width) and cards are 38px —
   is untouched. Not mode-scoped, so it covers both 1v1 and campaign portrait.
   ============================================================ */
@media (max-width: 767px) and (orientation: portrait) {
  /* F5 (mobile QA 2026-06-11): deck took too much space — shrink ~50% (area).
     Totem height = card-h(50) + max card offsetY(6) so the offset stack stays
     contained. Keep the explicit width:var(--gv2-dc-w) — it's load-bearing for
     centering the absolute .gv2-dc cards (see the block header above). */
  .gv2-page .gv2-dt {
    height: 56px;
    width: var(--gv2-dc-w, 38px);
    --gv2-dc-w: 38px;
    --gv2-dc-h: 50px;
  }
  /* Portrait deck-card size: DeckCard.js writes --gv2-dc-w/h INLINE on each .gv2-dc
     (70x94), shadowing the .gv2-dt value above — so the deck rendered at 70x94 and
     overflowed its 38x56 box. Override on the card with !important. KEEP the .gv2-dt
     --gv2-dc-w (38px): the box uses `width: var(--gv2-dc-w)` above, load-bearing for
     centering the absolute .gv2-dc cards. Un-mode-scoped (1v1 + campaign portrait),
     matching the .gv2-dt rule. See the landscape #6-redux fix + its solution doc. */
  .gv2-page .gv2-dt .gv2-dc {
    --gv2-dc-w: 38px !important; --gv2-dc-h: 50px !important;
  }
  .gv2-page .gv2-dc-glyph { font-size: 15px; }
  /* #4: put the round timeline to the RIGHT of the deck (was stacked below). The
     wrapper flips from display:contents to flex-row here; deck (.gv2-axis, order 0)
     sits left, timeline (order:2, its own rule) right. order:2 on the wrapper keeps
     it between the two player areas in the column. Portrait-scoped so landscape
     (where the wrapper stays display:contents for grid-area) is untouched. */
  .gv2-page .gv2-axis-mt {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 12px;
  }
  .gv2-page .gv2-game-grid > .gv2-axis-mt { order: 2; }
}

/* ============================================================
   LANDSCAPE BOARD — 1v1 (Option B), 2026-06-06
   Condensed 3-column layout for landscape phones. Top bar + bottom strip
   removed; round #, timeline, rule, deck, count, settings, turn message +
   concede fold into the center column to reclaim vertical space. Placed
   AFTER the 3361 mobile block so it wins the equal-specificity tie on
   landscape phones (3361 also matches them via its OR clause). Scoped
   :not([data-mode="campaign"]) — campaign keeps the vertical stack until PR 2.
   Design: mockups/landscape-board-mockups.html
   Plan: docs/plans/2026-06-06-landscape-board-1v1-plan.md
   ============================================================ */
@media (orientation: landscape) and (max-height: 500px) {
  /* Page: no scroll (also clips the shake transient); notch-safe sides. */
  .gv2-page:not([data-mode="campaign"]) {
    overflow: hidden;
    /* Hard-cap to the viewport so the grid's flex:1 distributes slack instead
       of the page growing to content (base only sets min-height:100dvh). */
    height: 100dvh; max-height: 100dvh; min-height: 0;
    padding: 4px max(6px, env(safe-area-inset-right)) 4px max(6px, env(safe-area-inset-left));
    gap: 0;
  }

  /* Top bar + bottom strip gone — content lives in the center column. */
  .gv2-page:not([data-mode="campaign"]) .gv2-board-top,
  .gv2-page:not([data-mode="campaign"]) .gv2-board-bottom { display: none; }
  /* Game log hidden too. It's an open ~218px panel that game-log.js appends as a
     flex sibling of the board grid; on the hard-capped landscape page (height:100dvh,
     overflow:hidden) it steals that height and crushes the grid, clipping HIT/STAND
     off the bottom of the player columns. Hiding it reclaims the space. Still visible
     in portrait (page scrolls) and in campaign landscape (page scrolls, log below). */
  .gv2-page:not([data-mode="campaign"]) .gv2-log { display: none; }

  /* 3-column grid; center column stacks the relocated chrome. */
  .gv2-page:not([data-mode="campaign"]) .gv2-game-grid {
    display: grid;
    grid-template-columns: 1fr minmax(150px, 178px) 1fr;
    grid-template-rows: auto auto 1fr auto auto;
    grid-template-areas:
      "you c-round opp"
      "you c-time  opp"
      "you c-axis  opp"
      "you c-turn  opp"
      "you c-cede  opp";
    gap: 3px 8px;
    flex: 1 1 auto;
    min-height: 0;
    align-items: stretch;
  }

  /* Player columns: default placement, POV swap once myIdx resolves. */
  .gv2-page:not([data-mode="campaign"]) [data-gv2-player-area="0"] { grid-area: you; }
  .gv2-page:not([data-mode="campaign"]) [data-gv2-player-area="1"] { grid-area: opp; }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) [data-gv2-player-area="0"] { grid-area: opp; }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) [data-gv2-player-area="1"] { grid-area: you; }

  /* Center-column children → areas. */
  .gv2-page:not([data-mode="campaign"]) [data-gv2-ls-round] { grid-area: c-round; }
  .gv2-page:not([data-mode="campaign"]) [data-gv2-mobile-timeline] { grid-area: c-time; display: flex; margin: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-axis { grid-area: c-axis; min-height: 0; }
  .gv2-page:not([data-mode="campaign"]) [data-gv2-ls-turn] { grid-area: c-turn; }
  .gv2-page:not([data-mode="campaign"]) [data-gv2-ls-concede] { grid-area: c-cede; }
  .gv2-page:not([data-mode="campaign"]) .gv2-mobile-footer { display: none; }

  /* Show + style the new center nodes. */
  .gv2-page:not([data-mode="campaign"]) [data-gv2-ls-round] {
    display: block; text-align: center;
    font-size: 9px; letter-spacing: .28em; color: var(--brass-hi);
  }
  .gv2-page:not([data-mode="campaign"]) [data-gv2-ls-turn] { display: block; text-align: center; }
  .gv2-page:not([data-mode="campaign"]) [data-gv2-ls-concede] {
    display: inline-flex; align-items: center; justify-content: center; gap: 5px;
    justify-self: center; padding: 5px 14px; min-height: 30px;
    background: transparent; border: 1px solid var(--brass-lo); color: var(--vellum-dim);
    font-size: 9px; letter-spacing: .18em;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ls-concede-icon { font-size: 11px; }

  /* Center axis: vertical column again (3361 made it a horizontal strip). */
  .gv2-page:not([data-mode="campaign"]) .gv2-ca {
    flex-direction: column; flex-wrap: nowrap;
    align-items: stretch; justify-content: flex-start;
    padding: 0; gap: 4px; min-height: 0;
  }
  /* Re-show axis pieces the 3361 block hid; keep timeline flex (placed above). */
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-ca-rules {
    display: block; padding: 4px 8px; align-self: stretch;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-title { font-size: 12px; line-height: 1.1; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-target { font-size: 12px; line-height: 1.1; margin-top: 1px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-sub { font-size: 7px; letter-spacing: .2em; margin-top: 2px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-dctl { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-dctl-btn {
    aspect-ratio: 1; min-height: 0; min-width: 0; font-size: 14px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-ccs { display: block; padding: 3px 8px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-ca-help { display: block; margin-top: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-help-link { min-height: 0; padding: 2px 0; line-height: 1.4; }
  /* #5 (device-QA): relocate Match Point from the inline timeline caption
     (.gv2-bc-mp, which crowds the narrow center caption) to BELOW the How-to-Play
     link. Reuse the existing CenterAxis .gv2-ca-mp (hidden on mobile by :3769) —
     un-hide it and flex-order it last so it sits under .gv2-ca-help; size it for
     the column. Hide the caption copy so only ONE Match Point shows (preserves
     the one-per-viewport invariant). Landscape + 1v1 only. */
  .gv2-page:not([data-mode="campaign"]) [data-gv2-mobile-timeline] .gv2-bc-mp { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-ca-mp {
    display: block; order: 1; margin-top: 1px;
    font-size: 13px; letter-spacing: .22em; text-align: center; line-height: 1;
  }
  /* Deck totem + cards: small. */
  .gv2-page:not([data-mode="campaign"]) .gv2-dt { height: 38px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-dc-glyph { font-size: 10px; }
  /* #6 redux (device-QA): the deck CARDS must be sized on .gv2-dc, NOT via
     .gv2-dt's --gv2-dc-w/h — DeckCard.js writes those custom props INLINE on each
     .gv2-dc (70x94 default, DeckTotem passes no w/h), which SHADOWS the inherited
     .gv2-dt value. The batch-1 attempt set them on .gv2-dt and was a silent no-op,
     leaving a 70x94 deck that overflowed (.gv2-dt is overflow:visible) onto the
     deck-controls row below — hiding the ♪/◉/◌ buttons. Override on the card with
     !important (beats the inline non-important value). Box height (38px above) =
     card-h(32) + max offsetY(6) so the stack stays contained. */
  .gv2-page:not([data-mode="campaign"]) .gv2-dt .gv2-dc {
    --gv2-dc-w: 26px !important; --gv2-dc-h: 32px !important;
  }

  /* Player area: fill column, distribute, never overflow. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa {
    min-height: 0; overflow: hidden;
    justify-content: space-between;
    gap: 2px; padding: 4px 7px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-section-label { margin: 1px 0; }
  /* Controls + header/hand/spent hold their size; the action grid is the
     shrink + scroll zone so accumulated action cards (no hard cap) can never
     push HIT/STAND off-screen mid-match (review pass 1 #1). When content fits
     (common case) space-between distributes normally — no scrollbar appears. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-hand,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-section-label,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bottom { flex-shrink: 0; }
  /* Score plaque smaller than the portrait 100px (SE columns ~227px wide). */
  .gv2-page:not([data-mode="campaign"]) .gv2-sp { width: 76px; padding: 4px 6px 6px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-numeral { font-size: 30px; }
  /* Drop the 122px hand-container floor to match the shrunk cards (66px) —
     reclaims ~56px of wasted column height (review pass 2 #1). Keep a small
     min-height so the empty pre-deal fan doesn't collapse and shift on deal. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-hand { min-height: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan { min-height: 66px; height: auto; }
  /* Playing cards: shrink the fixed 80x110 (inline custom props → !important). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pc {
    --pc-w: 50px !important; --pc-h: 66px !important; --pc-pip: 30px !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pc-corner { font-size: 12px; }
  /* Action cards: shrink the fixed 110px height + fluid width; reflow tight.
     overflow-y:auto + min-height:0 makes this the scroll zone when a player has
     accumulated many action cards on a short viewport (controls stay pinned). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-actions { min-height: 0; overflow-y: auto; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable {
    grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
    gap: 4px; min-height: 0;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ac { --gv2-ac-h: 50px !important; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ac-fluid { max-width: 52px; }
  /* Score-mod corner badge (+1/+2/-5) is redundant with the center glyph in the
     TRAY (cards aren't overlapped there) and collides with it at landscape size
     → reads as "double print". Hide it in the tray only; the fan keeps it (cards
     overlap there, so the corner is the only readable part). */
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-corner-tl { display: none; }
  /* Flight-face parity for the hide above: flights are body-mounted (outside
     .gv2-page) so the tray rule can't reach them — the corner flew visible and
     vanished at the landing paint. Unscoped on purpose: flights carry no mode
     marker and both 1v1 + campaign trays hide the corner under this query
     (campaign's hide lives in its own block, same media query). .gv2-fly =
     the FH slam (persistent class across its slam→slide swap). */
  .gv2-flight .gv2-ac-corner-tl,
  .gv2-fly .gv2-ac-corner-tl { display: none; }
  /* HIT/STAND scaled down. */
  .gv2-page:not([data-mode="campaign"]) .gv2-cb { min-height: 31px; padding: 4px 8px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-label { font-size: 17px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-sub { display: none; }
}

/* ============================================================
   LANDSCAPE PHONES — played-card fan polish (mobile QA F3/F4/F9/F10, 2026-06-11)

   The landscape board already shrinks playing cards to 50×66 (.gv2-pc var
   override) and hand action cards to ≤52×50 (.gv2-ac-fluid max-width). But
   PLAYED score-mods (+1/+2/−5) in the fan are rendered NON-fluid (render-fan.js
   passes w:80), so .gv2-ac-fluid{max-width:52px} misses them — only their
   height is squashed (--gv2-ac-h:50 !important), leaving an 80×50 squat chip
   beside the 50×66 playing cards (F3 "~2× width", F10 "oversized chip").

   Un-mode-scoped (applies to 1v1 AND campaign — render-fan is shared and .gv2-pc
   is 50×66 in both). Specificity is (0,4,0) — `.gv2-page .gv2-pa-fan
   .gv2-fan-card .gv2-ac` — so it wins over the generic (0,3,0)
   `.gv2-ac{--gv2-ac-h:50 !important}` rules regardless of source order; width
   uses !important to beat ActionCard's inline `width:80px`. Landscape-only →
   desktop/portrait untouched.
   ============================================================ */
@media (orientation: landscape) and (max-height: 500px) {
  /* F3 + F10 — match the played score-mod footprint to the fan's playing cards. */
  .gv2-page .gv2-pa-fan .gv2-fan-card .gv2-ac {
    width: 50px !important;
    --gv2-ac-h: 66px !important;
  }
  /* F9 — the centered +N is JS-sized for an 80px card (refW*0.56 ≈ 45px); shrink
     it to read like a playing-card value on the 50px card. Corner (shown only on
     overlapped cards, see F4) trimmed to sit in the ~24px visible strip. */
  .gv2-page .gv2-pa-fan .gv2-fan-card .gv2-ac-glyph { font-size: 28px !important; }
  .gv2-page .gv2-pa-fan .gv2-fan-card .gv2-ac-corner-tl { font-size: 15px; }

  /* F4 — the corner +N reads ONLY when the card is overlapped by a newer one.
     render-fan appends in order with rising z-index + left, so :last-child is the
     fully-visible top card → centered glyph only; every earlier card is overlapped
     → corner only (its center sits under the next card). */
  .gv2-page .gv2-pa-fan .gv2-fan-card:last-child .gv2-ac-corner-tl { display: none; }
  .gv2-page .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-glyph { display: none; }

  /* F9 + action-card scaling fix (max_action_cards=7): tie the tray card's label,
     glyph AND sigil to the card's ACTUAL width via container-query units, so a
     hand of 5–7 cards shrinks gracefully instead of scrunching fixed-px content
     into a too-narrow card. cqw needs a query container → container-type on the
     card. JS sizes glyph/sigil inline (refW=70), so !important is required to win
     (replaces the prior fixed-24px glyph rule). Applies to BOTH 1v1 and campaign
     trays (identical 50×≤52px cards in landscape, shared component); the fan is
     untouched. Bug fix: action-card scaling at high card counts. */
  .gv2-page .gv2-ar-playable .gv2-ac { container-type: inline-size; }
  .gv2-page .gv2-ar-playable .gv2-ac-sub {
    font-size: clamp(5px, 15cqw, 7.5px) !important;
    letter-spacing: clamp(0em, 1.2cqw, .2em) !important;
  }
  .gv2-page .gv2-ar-playable .gv2-ac-sub-fh {
    font-size: clamp(4.5px, 13cqw, 6.5px) !important;
  }
  .gv2-page .gv2-ar-playable .gv2-ac-glyph {
    font-size: clamp(14px, 46cqw, 22px) !important;
  }
  /* Sigil sized by font-size (text sigils ⧗⇄⚡❄) AND box width/height (the FC
     inline <svg> is width/height:100% of this div per :200, so it ignores
     font-size — resize the box to scale the gift icon). flex-center keeps the
     emoji/icon centered after the box is fixed-size. */
  .gv2-page .gv2-ar-playable .gv2-ac-sigil {
    font-size: clamp(13px, 42cqw, 20px) !important;
    width: clamp(13px, 42cqw, 20px) !important;
    height: clamp(13px, 42cqw, 20px) !important;
    display: flex; align-items: center; justify-content: center;
  }
}

/* ============================================================
   LANDSCAPE PHONES — center column & header chrome (mobile QA F2/F5/F7/F6-L1, 2026-06-11)

   F2 + F6-L1: the mobile round-timeline (caption "First to N · X–Y MATCH POINT"
   set white-space:nowrap, plus 14px round dots) overflows the ~178px landscape
   center column (13 dots = 278px) and clips under the side player panels. Shrink
   the dots and let the caption wrap so the whole timeline fits. Un-mode-scoped —
   the same .gv2-rd dots render in 1v1 AND campaign.

   F7: the BANK time-bank badge sat inline beside the player name and crowded it
   (the name ellipsis-truncated). Drop the bank onto its own line BELOW the name +
   "Your Turn" badge. The opponent header row is un-reversed on mobile (line ~3703
   flips it back to flex-direction:row), so both seats are row → the status sits at
   the row end and justify-content:flex-end seats the bank directly under it. 1v1
   only — campaign has no time bank (.gv2-pa-bank is empty/hidden there).

   F5: the deck landscape size is handled in the deck block above (mode-scoped at
   3903); here we only shrink the 1v1 landscape totem further to match the F5 ask.

   NOT addressed here (flagged — not reproducible from the code, need on-device
   repro): F12 (.gv2-ca-mp is display:none in landscape — the only Match Point is
   the .gv2-bc-mp inside the timeline caption, fixed by the F2/L1 wrap above);
   F6-L5 (no DOM overlap — center items are grid/flex-stacked; F5's freed space
   helps); F6-L6 (no round-transition overlay exists in the v2 board).
   ============================================================ */
@media (orientation: landscape) and (max-height: 500px) {
  /* F2 + F6-L1 — fit the timeline into the center column. */
  .gv2-page [data-gv2-mobile-timeline] .gv2-bc-dots {
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
  }
  .gv2-page [data-gv2-mobile-timeline] .gv2-rd { width: 9px; height: 9px; }
  .gv2-page [data-gv2-mobile-timeline] .gv2-bc-caption {
    white-space: normal;
    font-size: 8px;
    text-align: center;
  }

  /* F7 — bank drops to its own line under name + "Your Turn"; shrink the
     desktop-scale name so it's fully visible in the narrow column. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bank {
    order: 3;
    flex-basis: 100%;
    display: flex;
    justify-content: flex-end;
  }
  /* Bank is empty until the match clock renders (game-ui.js injects TimeBankInline
     into it); a flex-basis:100% empty box would wrap to a 0-height 2nd line and
     leave an 8px row-gap. Collapse it while empty so no dead gap shows pre-clock. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bank:empty { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-name { font-size: 16px; }

  /* F5 — 1v1 landscape deck totem box height = card-h(32) + max offsetY(6) so the
     offset stack stays contained. Card size itself is set on .gv2-dc (see #6 redux
     above — .gv2-dt's --gv2-dc-* are shadowed by DeckCard's inline values). */
  .gv2-page:not([data-mode="campaign"]) .gv2-dt {
    height: 38px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-dc-glyph { font-size: 10px; }
}

/* (Portrait redesign PR 2: the .gv2-port-score node was deleted from
   game-ui.js — its base hide went with it. The portrait header cluster
   `.gv2-hdr` [ornament + WinCounter] replaces it; base hide below.) */
.gv2-hdr { display: none; }
.gv2-hdr-center { display: contents; }
.gv2-hdr-side,
.gv2-hdr-sponsor { display: none; }
/* Desktop-only first-to-N line under the forged score bug. */
.gv2-hdr-target { display: none; }
/* HOW TO PLAY header link (desktop 1v1 redesign PR 1): base-hidden so the
   portrait board — which displays .gv2-hdr — stays pixel-identical; the
   desktop block at the end of this file un-hides it. */
.gv2-hdr-help { display: none; }
/* Table-edge console chips (desktop 1v1 redesign PR 3): base-hidden — the
   console child renders inside every PlayerControls (both seats, all
   breakpoints); only the desktop-1v1 block un-hides it. */
.gv2-pcl-console { display: none; }
/* (Portrait redesign PR 3: the .gv2-ca-turn rules-box turn line was deleted
   — injector removed from game-ui.js; prow captions + the DEALING… ornament
   carry turn/phase messaging at portrait.) */

/* ============================================================
   (Portrait redesign PR 3: the ENTIRE PortDuel block — Phase 5's
   html.bja-ios-prefixed portrait board — is DELETED. Every surface it
   styled is now owned by the unprefixed PORTRAIT 1v1 REDESIGN block
   below, serving mobile web AND iOS identically: page pinning, band
   order, open-felt center, panel sizing, palette swap, table-edge
   HIT/STAND, and the retired center-axis/footer/board-bottom chrome.)
   ============================================================ */

/* ============================================================
   PHASE E PR 14 — Campaign-mode rules (scoped under [data-mode="campaign"])
   Plan: docs/plans/2026-04-30-gameboard-phase-e-campaign-plan.md
   ============================================================ */

/* BANK chip hidden — campaign has no time bank.
   Specificity 0,3,0 wins over base .gv2-page .gv2-pa-bank (0,2,0). */
.gv2-page[data-mode="campaign"] .gv2-pa-bank { display: none; }

/* Boss rounds-won score box uses orange trim only (border, numeral, lit pips).
   The rest of the boss area keeps its per-boss thematic accent (StatusRibbon,
   backdrop, etc.). `!important` is required because RoundsWonRail.js sets
   --gv2-rwr-accent inline via style="" — inline non-important would otherwise
   win. Match-point (.gv2-rwr-mp footer-tint, PR 1 stripped the border/glow/pulse)
   and won-state (.gv2-rwr-won brass) still trump the baseline; expected. */
.gv2-page[data-mode="campaign"] [data-gv2-player-area="1"] .gv2-rwr {
  --gv2-rwr-accent: var(--orange) !important;
  --gv2-rwr-accent-hi: var(--orange-hi) !important;
}

/* ============================================================
   PORTRAIT 1v1 REDESIGN — PR 1: cards & fan (2026-07-01)
   ------------------------------------------------------------
   UNPREFIXED: serves mobile WEB and the iOS app identically (PortDuel's card
   sizing/band metrics were deleted above; its remaining chrome retires in
   redesign PRs 2-3). Scoped to portrait-AND-width so SE-class landscape can
   never match, and :not([data-mode="campaign"]) so campaign stays frozen.
   Design reference: the approved board mockup (docs/plans/
   2026-07-01-board-redesign-portrait-plan.md).

   CSS/JS CONTRACT: JS no longer mirrors these. render-geometry.js#readProbe
   READS the resolved sizes off the hidden .gv2-sizeprobe, so CSS is the single
   source of truth. Do NOT hand-mirror the clamps back into JS — CSS svh and
   window.innerHeight are different viewports (~60-100px apart on iOS Safari),
   so an arithmetic mirror drifts on exactly the devices a desktop browser
   cannot reproduce. shiftForCount's portrait tiers still read fanCardWidth().

   Seat parity (device-QA item 7): the un-keyed base now carries the FULL size
   (72×99), so BOTH seats render large; the 4-combination my-idx overrides
   below are now redundant duplicates of the base, kept as no-ops to keep the
   parity diff small. `!important` on .gv2-pc is required — PlayingCard writes
   --pc-* INLINE (the documented shadowing trap).
   ============================================================ */
@media (max-width: 767px) and (orientation: portrait) {
  /* ============================================================
     PORTRAIT CARD TOKENS — height-aware, so the board fits short viewports.

     WHY: the foe panel is the board's designated clip zone (flex-shrink:1 +
     overflow:hidden, ~:5290 below). The felt absorbs slack first; once its
     slack hits zero the foe panel is squeezed, and inside it the ACTION TRAY is
     the only row with no floor (prow is real text; .gv2-pa-fan declares a
     min-height; the tray's painted cards contribute ~0 min-content because
     .gv2-ac-fluid is min-width:0 and its aspect-ratio height comes from a
     PERCENTAGE padding strut, which contributes nothing intrinsic). So the tray
     is sacrificed 100% before anything else moves. Measured: the clip begins at
     ~713px of viewport height and reaches 104px at 320x568.

     Two other fixes were tried and killed by measurement — do not retry:
       - A min-height floor on the tray is a NO-OP. .gv2-pa is min-height:0, which
         defeats the flex automatic minimum, so raising a grid track's base size
         raises the OVERFLOW, not the container. Visible card pixels are
         byte-identical with and without it.
       - Lowering .gv2-pa-fan's min-height AMPUTATES every card. .gv2-fan-card is
         position:absolute, so the fan has zero in-flow content and its height IS
         its min-height, unconditionally — not "under compression".
     The only lever that shrinks the panel without amputating anything is the
     card size itself.

     UNIT: svh, never vh. This page is height:100dvh (~:5290) — the DYNAMIC
     viewport — while vh is the LARGE viewport. On a 844px device showing Safari
     chrome the effective height is ~672 but 100vh still reads 844, so a
     vh-keyed clamp sits at its max and shrinks NOTHING on the exact device this
     bug was reported from. svh (small viewport) sizes for the chrome-visible
     worst case and, unlike dvh, is constant per orientation, so cards never
     resize mid-match when the URL bar retracts.

     Break-even is 750: at >= 750svh every value is EXACTLY today's number, so
     phones with room to spare are untouched. Exact ratios (not decimal vh
     coefficients) keep break-even a point rather than a 0.04px spread.
     Declared on :root too, for symmetry with --gv2-deskcard-w. NOTE: unlike
     the desktop token, nothing outside .gv2-page consumes the portrait ones
     today — the :root copy is future-proofing for a body-mounted portrait
     chooser, not a live dependency. Do not cite it as one.
     JS reads these via the .gv2-sizeprobe, never by re-implementing them.
     ============================================================ */
  :root,
  .gv2-page:not([data-mode="campaign"]) {
    --gv2-portcard-w: clamp(48px, calc(100svh * 72 / 750), 72px);
    --gv2-portcard-h: clamp(66px, calc(100svh * 99 / 750), 99px);
    --gv2-porttray-w: clamp(38px, calc(100svh * 52 / 750), 52px);
    /* The fixed control bar, tokenised for the same reason the desktop one is
       (--gv2-pcl-h). Leaving it flat kept ~70px reserved on the shortest
       viewports where the card clamp had already bottomed out at its floor with
       nothing left to give. */
    --gv2-portpcl-h: clamp(52px, calc(100svh * 72 / 750), 72px);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sizeprobe {
    width: var(--gv2-portcard-w);
    height: var(--gv2-portcard-h);
    padding-left: var(--gv2-porttray-w);
  }

  /* ---- Playing cards: base = full size, both seats (device-QA item 7 parity) ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pc {
    --pc-w: var(--gv2-portcard-w) !important; --pc-h: var(--gv2-portcard-h) !important;
    --pc-pip: calc(var(--gv2-portcard-w) * 43 / 72) !important;
    background: linear-gradient(165deg, #fbfbfb 0%, #eeeeee 100%);
    border-color: #8a744a;
  }
  /* Derived, not literal: 16px was tuned for a constant 72px card. Portrait
     cards are no longer constant, so a literal drifts from 0.222 of the card at
     72px to 0.333 at the 48px floor — a 50% oversized rank glyph. Desktop has
     always derived this (calc(var(--gv2-deskcard-w) * .22464)). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pc-corner {
    font-size: calc(var(--gv2-portcard-w) * 16 / 72);
  }
  /* Flights are body-mounted (outside .gv2-page) so the page-scoped cream
     rule above can't reach them — the card would fly parchment and snap to
     cream at the landing paint. The engine tags portrait-1v1 flights with
     .gv2-flight-p1v1 (flyCard); campaign never gets the marker
     (isPortrait1v1 reads data-mode). Values mirror the .gv2-pc rule above. */
  .gv2-flight-p1v1 .gv2-pc {
    background: linear-gradient(165deg, #fbfbfb 0%, #eeeeee 100%);
    border-color: #8a744a;
  }
  /* Flight ActionCard faces — same body-mounted gap as the .gv2-pc rule
     above: the tray hides the score-mod corner badge at this breakpoint
     (both 1v1 AND campaign — see the .gv2-ar-playable corner rules), but no
     page-scoped rule reaches a flight, so +1/+2/-5 flew with a large
     duplicate corner that vanished at the landing paint (iOS device QA).
     Unscoped on purpose: flights carry no mode marker and both modes' trays
     hide the corner under this query. .gv2-fly covers the FH slam, which
     swaps gv2-fly-fh-slam → gv2-fly-fh-slide mid-animation — anchor on the
     persistent gv2-fly (sole remaining consumer), like the desktop d1v1
     rules do. */
  .gv2-flight .gv2-ac-corner-tl,
  .gv2-fly .gv2-ac-corner-tl { display: none; }
  /* Glyph parity with the landed 52×64 tray tile (mirror of the desktop
     d1v1 flight rules): ActionCard writes the fluid glyph/sigil size INLINE
     (~39px) — without !important these are silent no-ops. FH-slam size is
     untouched (deliberately full-size at every non-desktop breakpoint). */
  /* Landed/flight parity for the rank corner, mirroring the desktop pair
     (.gv2-flight-d1v1 .gv2-pc-corner). Without it a flight kept the 20px base
     while the landed card derives from the token — a 20 -> 12.1px snap at the
     handoff on short phones. --gv2-portcard-w is on :root inside this query,
     so a body-mounted flight can read it. */
  .gv2-flight-p1v1 .gv2-pc-corner {
    font-size: calc(var(--gv2-portcard-w) * 16 / 72);
  }
  .gv2-flight-p1v1 .gv2-ac-glyph,
  .gv2-flight-p1v1 .gv2-ac-sigil { font-size: calc(var(--gv2-porttray-w) * 22 / 52) !important; }
  .gv2-flight-p1v1 .gv2-ac-sub { font-size: 6px !important; }
  /* ---- Own-seat overrides: redundant duplicates of the base (item 7 parity
         made both seats full size); kept as no-ops to minimize diff.
         They are only no-ops while they MATCH the base — these are (0,6,0)
         against the base's (0,3,0), both !important, so they win. Leaving the
         literals here while the base moved to tokens pinned your own cards at
         72x99 inside a fan band that had shrunk to var(--gv2-portcard-h), and
         .gv2-pa-fan is overflow-y:hidden, so it clipped 16px off your OWN hand
         at 375x629 — a worse bug than the one being fixed, on the most-watched
         surface on the board. Keep every value here identical to the base. ---- */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pc,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pc {
    --pc-w: var(--gv2-portcard-w) !important; --pc-h: var(--gv2-portcard-h) !important;
    --pc-pip: calc(var(--gv2-portcard-w) * 43 / 72) !important;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pc-corner,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pc-corner {
    /* Out-specifies the base — keep IDENTICAL to it. See the .gv2-pc note above. */
    font-size: calc(var(--gv2-portcard-w) * 16 / 72);
  }
  /* ---- Card bands sized for the new cards (replaces PortDuel's 64px cap) ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-hand { min-height: 0; align-items: center; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan {
    min-height: var(--gv2-portcard-h); height: auto;
    overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pa-fan,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-fan {
    min-height: var(--gv2-portcard-h);
  }

  /* ---- Played score-mods in the fan: playing-card footprint, both seats ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac {
    width: var(--gv2-portcard-w) !important;
    --gv2-ac-h: var(--gv2-portcard-h) !important;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pa-fan .gv2-fan-card .gv2-ac,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-fan .gv2-fan-card .gv2-ac {
    width: var(--gv2-portcard-w) !important;
    --gv2-ac-h: var(--gv2-portcard-h) !important;
  }
  /* Buried mods show ONLY the TL corner glyph (the visible strip); the
     :last-child fully-visible card shows the full face (glyph + sub) and no
     corner — the mockup-approved rule, same mechanism as the landscape board
     (game-v2.css landscape block). renderScoreMod emits label:true at
     portrait-1v1 so the sub node exists. */
  /* Derived from the card, like .gv2-pc-corner above. These are a LIVE path,
     not a fallback: render-fan passes face:'css' for score mods, so a real
     glyph node renders and the !important also beats ActionCard's inline
     refW * 0.56. Left literal they drift 0.417 -> 0.550 of the card. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-glyph {
    font-size: calc(var(--gv2-portcard-w) * 30 / 72) !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-corner-tl {
    font-size: calc(var(--gv2-portcard-w) * 14 / 72);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-glyph,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-sub { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:last-child .gv2-ac-corner-tl { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-sub {
    font-size: calc(var(--gv2-portcard-w) * 7 / 72);
  }

  /* ---- Action tray tiles: identical design AND size both seats (device-QA
     item 7 parity — was thumb-on-yours / mini-on-theirs). The grid TRACK is
     inline-written to 52px on portrait via render-geometry actionTrackMax
     (device-QA item 4) so track == tile == tight, left-packed cards. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-actions { min-height: 0; overflow: visible; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable { min-height: 0; gap: 6px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac {
    /* Dead on PAINTED cards (.gv2-ac-art sets height:auto + aspect-ratio, which
       overrides .gv2-ac's height:var(--gv2-ac-h)). LIVE on the img-load-failure
       CSS-face fallback, so it must track the tile or a failed card renders too
       tall in a shrunk tray. 704/512 is the painted art's aspect ratio. */
    --gv2-ac-h: calc(var(--gv2-porttray-w) * 704 / 512) !important;
  }
  /* 52px matches the inline grid track (actionTrackMax) so cards fill their track
     tight. The !important is now belt-and-braces: ActionCard USED to write
     max-width:80px inline on every fluid card, which made caps like this one a
     silent no-op (tiles rendered 80px wide at ≤4 tray cards) — Batch B removed that
     inline, since .gv2-ac-fluid already declares the identical default. Harmless to
     keep, and it still documents the trap. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-fluid { max-width: var(--gv2-porttray-w) !important; }
  /* Fallback-face only (painted tiles render an <img>, no glyph node) — but the
     same reasoning as --gv2-ac-h above: it must track the tile or a card whose
     art fails renders a 22px glyph in a 28.8px tile. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-glyph,
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-sigil {
    font-size: calc(var(--gv2-porttray-w) * 22 / 52) !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-sub { font-size: 6px; }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ar-playable .gv2-ac,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable .gv2-ac {
    --gv2-ac-h: calc(var(--gv2-porttray-w) * 704 / 512) !important;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ar-playable .gv2-ac-fluid,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable .gv2-ac-fluid {
    max-width: var(--gv2-porttray-w) !important;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ar-playable .gv2-ac-glyph,
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ar-playable .gv2-ac-sigil,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable .gv2-ac-glyph,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable .gv2-ac-sigil {
    font-size: 22px !important;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ar-playable .gv2-ac-sub,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable .gv2-ac-sub {
    font-size: 6px;
  }
  /* Tray corner glyph duplicates the center glyph (cards not overlapped in
     the tray) — hidden, matching the landscape + PortDuel convention. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-corner-tl { display: none; }

  /* (Portrait redesign PR 3: PR 1's interim bja-ios panel-height override
     is DELETED with PortDuel's flex — the banded layout below owns band
     sizing: panels content-driven, the open-felt center absorbs slack,
     HIT/STAND is a fixed table-edge bar that can never be clipped.) */

  /* ============================================================
     PR 2: information chrome — header band, prow, USED rail, ⋯ menu
     (mockup: public/_redesign/board.html; unprefixed = web + iOS)
     ============================================================ */

  /* ---- Header band: ✦ ROUND N ✦ ornament + forged WinCounter.
     .gv2-hdr is display:none at base; populated by renderFromState. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr {
    display: flex; flex-direction: column; align-items: center; gap: 9px;
    padding: 2px 0 16px; position: relative; z-index: 6;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn {
    display: flex; align-items: center; gap: 10px; width: 100%; padding: 0 26px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-line {
    flex: 1; height: 1px;
    background: linear-gradient(90deg, transparent, #8a6a2f);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-line-rt {
    background: linear-gradient(270deg, transparent, #8a6a2f);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-plaque {
    display: flex; align-items: center; gap: 8px;
    font-weight: 700; font-size: 12px; letter-spacing: .25em;
    color: var(--brass-hi); text-shadow: 0 2px 8px rgba(0,0,0,.8);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-plaque i {
    font-style: normal; font-size: 7px; color: #8a6a2f; transform: translateY(-1px);
  }
  /* Forged bar: iron/brass, inset gem sockets (gold=you, ruby=foe). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc {
    position: relative; display: inline-flex; align-items: center; gap: 6px;
    padding: 10px 16px; border-radius: 15px;
    background:
      linear-gradient(180deg, rgba(51,39,22,.2) 0%, transparent 40%),
      linear-gradient(180deg, #241a0e, #170f06 55%, #0e0903);
    box-shadow:
      0 0 0 1px #060301,
      0 0 0 2.5px rgba(216,169,78,.7),
      0 0 0 4px #060301,
      inset 0 1px 0 rgba(245,217,138,.28),
      inset 0 -6px 12px rgba(0,0,0,.6),
      0 12px 30px rgba(0,0,0,.65);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-side { display: flex; flex-direction: column; gap: 5px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-row { display: flex; gap: 6px; }
  /* device-QA item 2: the FOE side (right of the center divider) fills
     right→left TOWARD center — mirroring the "you" side (left of the divider),
     which already fills left→right toward center. socketRows lights
     leftmost-first in DOM order; row-reverse flips the foe fill to
     rightmost-first so both sides converge on the divider. */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-side-foe .gv2-wc-row { flex-direction: row-reverse; }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-sk {
    width: 15px; height: 15px; border-radius: 50%; flex: none;
    background: radial-gradient(circle at 50% 42%, #080401, #1a1108);
    box-shadow: inset 0 2px 4px rgba(0,0,0,.95), inset 0 -1px 0 rgba(245,217,138,.14);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-sk-you.gv2-wc-sk-on {
    background: radial-gradient(circle at 35% 28%, #d9fbe8, #2fae6d 52%, #10532f 95%);
    box-shadow: 0 0 8px 1px rgba(80,230,150,.5), inset 0 -2px 3px rgba(8,50,28,.55);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-sk-foe.gv2-wc-sk-on {
    background: radial-gradient(circle at 35% 28%, #ffc4b5, #c22b2b 52%, #5e0f0f 95%);
    box-shadow: 0 0 8px 1px rgba(255,107,84,.45), inset 0 -2px 3px rgba(60,8,4,.55);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-div {
    width: 10px; height: 10px; transform: rotate(45deg); flex: none; margin: 0 3px;
    background: linear-gradient(135deg, #f5d98a 0%, #d8a94e 45%, #6e4e18 100%);
    box-shadow: 0 0 0 1.5px #060301, inset 0 1px 0 rgba(255,255,255,.4);
  }
  /* ≥11 target: compact numeral pair (sockets won't fit). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-numeral {
    font-size: 17px; font-weight: 700; line-height: 1; min-width: 18px;
    text-align: center; color: #f3e9c9; font-variant-numeric: tabular-nums;
  }
  /* MATCH POINT: frame smolders + forged banner. Banner hangs beneath the
     single-row bar; on the 2-row stack it moves ON TOP (approved first-to-10
     take — beneath would collide with the foe prow). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-mp { animation: gv2WcBurn 1.6s ease-in-out infinite; }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-flag {
    position: absolute; left: 50%; bottom: -15px; transform: translateX(-50%);
    white-space: nowrap; font-weight: 700; font-size: 8px; letter-spacing: .3em;
    color: #ffcbb8; padding: 4px 15px 5px;
    clip-path: polygon(7% 0, 93% 0, 100% 50%, 93% 100%, 7% 100%, 0 50%);
    background: linear-gradient(180deg, #6e1c12, #400d07);
    box-shadow: inset 0 0 0 1px rgba(255,120,90,.55);
    text-shadow: 0 0 10px rgba(255,120,90,.8);
    animation: gv2WcFlag 1.6s ease-in-out infinite;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-2row .gv2-wc-flag { bottom: auto; top: -15px; }
  /* Headroom for the on-top flag: without it the ~17px banner overlaps the
     ✦ ROUND N ✦ plaque above (the hdr column gap is only 9px). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-2row { margin-top: 10px; }

  /* ---- Top strip at portrait: BoardChrome's counter + timeline retire; the
     ⋯ menu survives as a borderless dim glyph floating top-right over the
     header band (existing wired control — markup untouched). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-board-top { position: relative; padding: 0; min-height: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-bc { display: block; padding: 0; border: none; background: none; box-shadow: none; min-height: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-counter,
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-center { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-right { position: absolute; right: 8px; top: 2px; z-index: 7; }
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-menu {
    border: none; background: transparent; box-shadow: none;
    padding: 6px 10px; min-height: 0; min-width: 0; color: #9a8f72;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-menu-icon { font-size: 16px; }

  /* ---- One MATCH POINT per viewport: the forged counter owns it; the
     mobile timeline + RoundsWonRail rails + CenterAxis MP tag retire. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-mt { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-rail { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-mp { display: none; }

  /* ---- Typographic prow: CSS REFLOW of the existing nodes. The panel goes
     grid; .gv2-pa-hand dissolves (display:contents) so the ScorePlaque and
     the fan become grid items — plaque joins the header row as the leading
     score numeral (mockup: `21 DOCTORNOTA · YOUR TURN`), fan gets its own
     band, the USED rail rides the right edge of the fan+actions rows.
     .gv2-pa-row itself stays a flex line (name · status · countdown).
     NOTE for PR 3: the banded layout replaces this grid wholesale. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "sc   prow  prow"
      "fan  fan   used"
      "act  act   used"
      "bottom bottom bottom";
    column-gap: 12px;
    /* Height reclaim on short viewports, paired with the card tokens above.
       LONGHANDS on purpose: the inherited rule (~:4030) writes `padding` and
       `gap` SHORTHANDS, and that rule is NOT :not([data-mode="campaign"])-scoped
       — re-declaring the shorthands here would either clobber the 10px inline
       padding / 12px column-gap or leak into campaign. Break-even 750svh, so
       roomy phones keep today's 8px exactly. */
    padding-block: clamp(2px, calc(100svh * 8 / 750), 8px);
    row-gap: clamp(4px, calc(100svh * 8 / 750), 8px);
  }
  /* Same reclaim on the fan's own margin. The base rule (~:1780) is TOP-LEVEL
     with no media query and no campaign scope, so it must be overridden here,
     scoped, with the longhand — not edited in place. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan {
    margin-block: clamp(3px, calc(100svh * 8 / 750), 8px);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-hand { display: contents; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa .gv2-sp { grid-area: sc; align-self: baseline; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row { grid-area: prow; align-self: baseline; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa [data-gv2-fan] { grid-area: fan; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-actions { grid-area: act; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent { grid-area: used; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bottom { grid-area: bottom; }
  /* Section labels retire (mockup is label-free; USED gets its own tag). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-section-label { display: none; }
  /* Fading hairline under the full prow row (numeral + name + status):
     a grid-placed pseudo overlaying row 1, bottom-aligned. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa::after {
    content: ""; grid-row: 1; grid-column: 1 / -1; align-self: end;
    height: 1px; margin-top: 6px; pointer-events: none;
    background: linear-gradient(90deg, rgba(190,80,60,.5), transparent 82%);
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"]::after,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"]::after {
    background: linear-gradient(90deg, rgba(138,106,47,.6), transparent 82%);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"]::after {
    height: 1.5px;
    background: linear-gradient(90deg, #f5d98a, transparent 82%);
  }

  /* Prow score numeral: strip the plaque chrome, keep the numeral + the
     BUST/★21★ subrow (state logic + score-sync gate carry over untouched). */
  .gv2-page:not([data-mode="campaign"]) .gv2-sp {
    width: auto; padding: 0; border: none; background: none; box-shadow: none;
    overflow: visible; display: block;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-band,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-tick,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-halo,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-caption,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-status { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-numeral-wrap { margin: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-numeral {
    font-size: 28px; font-style: normal; line-height: 1; letter-spacing: 0;
    color: #e8b3a6; text-shadow: 0 3px 10px rgba(0,0,0,.9);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-subrow {
    height: auto; margin: 0; font-size: 6px; letter-spacing: .2em;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-subrow:empty,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-blackjack .gv2-sp-subrow { display: none; }
  /* Own seat: bigger, parchment-white numeral. */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-sp-numeral,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-sp-numeral {
    font-size: 34px; color: #f3e9c9;
  }
  /* Turn-lit numeral glows white; off-turn (waiting) dims. Bust keeps the
     base red struck numeral (higher-specificity state rules untouched). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"] .gv2-sp:not(.gv2-sp-bust) .gv2-sp-numeral {
    color: #fff; text-shadow: 0 3px 10px rgba(0,0,0,.9), 0 0 26px rgba(245,217,138,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="waiting"] .gv2-sp:not(.gv2-sp-bust) .gv2-sp-numeral {
    color: #8d8874;
  }
  /* Item 10 (device-QA): a frozen player's score washed out against the bright
     freeze cracks (.gv2-id-frost-surface / .gv2-id-root, z-index:50) — the prow
     numeral is z-index:51 (above them) but chrome-stripped, so a thin numeral
     lost contrast. Icy-white fill + dark stroke + dark shadow lifts it clearly
     ON TOP of the cracks. Specificity (0,7,0) beats the own-seat numeral rule
     above by specificity, so it wins for either seat frozen; :not(.gv2-sp-bust)
     mirrors the turn/waiting rules so a busted plaque keeps its red struck numeral. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="frozen"] .gv2-sp:not(.gv2-sp-bust) .gv2-sp-numeral {
    color: #eaf6ff;
    -webkit-text-stroke: 0.75px #06131e;
    text-shadow: 0 0 4px #06131e, 0 1px 2px #06131e, 0 2px 8px rgba(0,0,0,.95);
  }

  /* Prow line: name · status · (ember countdown), baseline; ✉ absolute at
     the right edge (position context for it + the hairline). The area-1
     selector matches the base row-reverse rule's (0,3,0) specificity —
     without it the foe prow packs right; base justify-content:center is
     also overridden (mockup packs the line left, tight gaps). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-row {
    display: flex; flex-direction: row; align-items: baseline; gap: 13px;
    justify-content: flex-start;
    /* nowrap kills the mobile-base flex-wrap — a max-length name + caption +
       countdown would otherwise wrap to a second line under the hairline;
       the name ellipsizes instead (rule below). padding-right reserves the
       absolute ✉ glyph's zone so line content can't slide under it. */
    flex-wrap: nowrap;
    min-height: 0; padding-bottom: 7px; padding-right: 44px; position: relative;
  }
  /* flex:0 kills the base flex:1 (it shoved the status caption to the far
     edge; mockup keeps score·name·status a tight 13px-gapped line). The ✉
     quick-chat lives INSIDE PlayerHeader markup — absolute at the prow's
     right edge (its decision row) so it can't wrap the line. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ph {
    order: 1; display: flex; align-items: baseline; flex: 0 1 auto; min-width: 0;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-seal { display: none; } /* no avatars (flagged decision) */
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-name {
    font-size: 14px; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
    text-shadow: 0 2px 6px rgba(0,0,0,.8);
    /* long names ellipsize on the nowrap prow line instead of pushing the
       status caption off the row */
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sr { order: 2; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bank { display: contents; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-qc {
    position: absolute; right: 0; top: 0;
    min-height: 0; min-width: 0; padding: 4px 9px;
    font-size: 16px; line-height: 1;
    color: var(--brass-hi);
    background: rgba(40,30,14,.92);
    border: 1px solid var(--brass);
    border-radius: 7px;
    box-shadow: 0 1px 5px rgba(0,0,0,.5);
    opacity: 1;
  }
  /* Foe name slightly smaller (mockup 13px vs 14px). */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ph-name,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ph-name {
    font-size: 13px;
  }
  /* Status caption: strip the ribbon pill → bare letterspaced caption. The
     state color rules below override the pill palette; visibility rules
     (idle hidden pre-state) keep working on the same node. */
  .gv2-page:not([data-mode="campaign"]) .gv2-sr {
    padding: 0; border: none; background: none !important; box-shadow: none;
    font-size: 8px; letter-spacing: .31em; color: #9a8f72; opacity: 1;
    min-height: 0; line-height: 1.4;
    /* the caption never shrinks or wraps — the NAME is the designed
       give-element (it ellipsizes); without this, multi-word captions
       ("THEIR TURN") wrapped a word onto the hairline at long names */
    white-space: nowrap; flex: 0 0 auto;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"] .gv2-sr {
    color: var(--brass-hi); text-shadow: 0 0 8px rgba(245,217,138,.6);
    animation: gv2ProwPulse 1.6s ease-in-out infinite;
  }
  /* device-QA item 8: a stood player must read clearly. (1) Dim the CARDS,
     not the whole area — whole-area opacity (base .gv2-pa[data-state="stood"]
     opacity:.78, line ~927) is GROUP opacity and would dim the badge too, so
     override it to 1 and dim .gv2-pa-fan instead (score + STOOD stay bright).
     (2) Strong STOOD badge: bone-bordered dark chip, bold bright text (vs the
     muted 8px caption). background needs !important — the portrait .gv2-sr
     sets background:none !important. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="stood"] { opacity: 1; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="stood"] .gv2-pa-fan { opacity: .55; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="stood"] .gv2-sr {
    font-size: 10.5px; font-weight: 700; letter-spacing: .22em;
    color: #f3e9c9; opacity: 1;
    background: rgba(20,16,10,.9) !important;
    border: 1px solid var(--bone); padding: 2px 9px;
    text-shadow: 0 1px 3px rgba(0,0,0,.9);
  }
  /* Ember countdown (bank chip re-anchored): visible whenever the bank is
     ACTIVE (Matt 2026-07-02 — desktop-redesign decision applied here too;
     was active+low). gv2-tb-low (<60s, TimeBankInline/bankTick own it) is
     styling-only now: it gates the pulse below, not visibility. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb.gv2-tb-active {
    display: inline-flex; align-items: baseline; order: 3;
    background: none; border: none; box-shadow: none; padding: 0; min-height: 0;
    /* the base ≤30s emberpulse animates box-shadow back onto the stripped
       chip — the time text carries its own pulse here instead */
    animation: none;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-label,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-idle-dot { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-time {
    font-size: 10px; letter-spacing: .12em; color: #ff8d6e;
    text-shadow: 0 0 8px rgba(255,120,90,.6);
  }
  /* Urgency escalation: the pulse fires only under 60s (gv2-tb-low). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb.gv2-tb-low .gv2-tb-time {
    animation: gv2ProwPulse 1.6s ease-in-out infinite;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-time::before {
    content: "· "; color: #9a8f72; text-shadow: none;
  }

  /* ---- USED rail: recessed chips on the right edge; foe grows DOWN toward
     the table middle, yours grows UP (mockup .used columns). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent {
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    justify-content: flex-start; min-height: 0; min-width: 30px; padding-top: 2px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent::before {
    content: "USED"; font-size: 6.5px; letter-spacing: .28em; color: #9a8f72;
  }
  /* Own seat: grow up — column-reverse points the main axis UP, so default
     packing (flex-start) anchors the label at the BOTTOM edge with chips
     stacking upward above it (mockup .meused). */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pa-spent,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-spent {
    flex-direction: column-reverse;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent .gv2-uc {
    width: 26px !important; height: 26px !important; border-radius: 50%;
    border: none;
    background: radial-gradient(circle at 50% 40%, #0c0703, #1c130a);
    box-shadow: inset 0 2px 4px rgba(0,0,0,.9), 0 0 0 1px #3a2a18;
    filter: saturate(.75); opacity: 1;
    color: var(--gv2-uc-accent, var(--brass-hi));
  }

  /* ============================================================
     PR 3: layout & controls — banded page, open felt, table-edge
     HIT/STAND, log pill, totem anchor (mockup bands; unprefixed =
     web + iOS; replaces the deleted PortDuel block wholesale)
     ============================================================ */

  /* ---- Page: pin to the viewport (no scroll). Bottom padding clears the
     fixed table-edge controls; top clears notch/status chrome. position:
     relative anchors the absolute deck-totem anchor below. GREEN FELT
     backdrop (purple 2026-07-16, reverted to green QA item 3
     2026-07-17 — keep in sync with body.gv2-1v1-bg-active) — the doubled .gv2-page
     class (0,3,0) beats body.gv2-1v1-bg-active's LATER transparent-page
     rule (0,2,0). NOTE: this rule PAINTS OVER the body felt, so portrait
     is styled HERE — a body-only edit is invisible at this breakpoint.
     Keep the 3 felt layers below in sync with body.gv2-1v1-bg-active
     (~:5630). CAREFUL when syncing: the lists are NOT 1:1. The body
     carries the vignette as its FIRST layer; here the vignette lives on
     ::before instead (below), so the body has 4 layers to this rule's 3.
     Sync the felt + glow stops, and keep the ::before vignette's stop
     equal to the body's first layer. ---- */
  .gv2-page.gv2-page:not([data-mode="campaign"]) {
    position: relative;
    height: 100dvh; max-height: 100dvh; overflow: hidden;
    padding: calc(env(safe-area-inset-top, 0px) + 10px) 0
             calc(env(safe-area-inset-bottom, 0px) + var(--gv2-portpcl-h) + 6px);
    gap: 0;
    background:
      radial-gradient(70% 34% at 50% 20%, rgba(194,43,43,.08), transparent 70%),
      radial-gradient(80% 40% at 50% 76%, rgba(245,217,138,.09), transparent 70%),
      radial-gradient(130% 100% at 50% 46%, #1a3a25 0%, #122b1b 42%, #0a1a10 72%, #060d08 100%);
  }
  /* Vignette. Stop MUST match body.gv2-1v1-bg-active's first layer (~:5632)
     — the two paint the same surface at different breakpoints, so a drift
     here shows up as portrait and desktop grading differently. */
  .gv2-page:not([data-mode="campaign"])::before {
    content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
    background: radial-gradient(92% 70% at 50% 46%, transparent 44%, rgba(0,0,0,.6) 100%);
  }
  /* Open felt = no boxed panels: strip the panel chrome (the prow hairline
     + lit typography carry seat identity and turn state now). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa {
    background: none; border: none; box-shadow: none;
    position: relative; z-index: 2;
  }

  /* ---- Bands: foe block / open felt / own block (POV order — foe on top,
     self at thumb reach; out-specifies the mobile self-on-top order rules
     and lands later in source). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-game-grid {
    display: flex; flex-direction: column;
    flex: 1 1 auto; min-height: 0; height: auto;
    gap: clamp(3px, calc(100svh * 6 / 750), 6px); padding: 0 10px;
    align-items: stretch;
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-game-grid > .gv2-pa[data-gv2-player-area="0"] { order: 3; }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-game-grid > .gv2-pa[data-gv2-player-area="1"] { order: 1; }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-game-grid > .gv2-pa[data-gv2-player-area="0"] { order: 1; }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-game-grid > .gv2-pa[data-gv2-player-area="1"] { order: 3; }
  /* Panels: content height. Squeeze model: the felt's GROW slack evaporates
     first (its flex-basis is ~0 — all CenterAxis children hidden/fixed, so
     the 999 shrink weight is basis-scaled to nothing); true overflow then
     clips the FOE panel (shrink 1 + overflow hidden), the OWN panel never
     (shrink 0) — controls are fixed and can never clip regardless. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa { flex: 0 0 auto; min-height: 0; }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"],
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] {
    flex-shrink: 1; overflow: hidden;
  }

  /* ---- Open felt: the center strip is the empty band (deal/splash flight
     space). CenterAxis chrome retires; the deck-totem ANCHOR survives (see
     below). Mobile timeline/port-score died in PR 2. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-axis-mt {
    order: 2; display: flex; flex-direction: column; align-items: stretch;
    flex: 1 999 auto; min-height: 0;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-axis { flex: 1 1 auto; width: 100%; min-height: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca { padding: 0; height: 100%; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules,
  .gv2-page:not([data-mode="campaign"]) .gv2-ccs,
  .gv2-page:not([data-mode="campaign"]) .gv2-dctl,
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-help { display: none; }

  /* ---- Hidden deck-totem anchor (plan decision 1). ENGINE CONTRACT:
     deal/hit/FH flights originate from [data-gv2-deck-totem]'s live rect
     (getDeckTotemRect, render-deal-animation.js) and INSTANT-PLACE if the
     element is missing. Constraints (hard):
       * NEVER display:none — that yields a truthy 0×0 rect at the screen
         corner and every card flies from the top-left (opacity:0 keeps a
         laid-out box);
       * exactly ONE element carries the attribute (CenterAxis's .gv2-dt —
         adding a second carrier would win/lose querySelector by document
         order per breakpoint);
       * its ancestor chain (.gv2-axis/.gv2-axis-mt/.gv2-game-grid) must
         never be display-hidden at portrait.
     Anchored behind the header counter's center so flights originate from
     behind the forged bar. position:fixed (not absolute) — the .gv2-ca
     ancestor is positioned and would re-anchor an absolute box to the felt
     band's middle. (Transient: an ancestor transform during the 320ms
     screen shake re-scopes fixed positioning — deals don't start mid-shake.) ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-dt {
    position: fixed;
    top: calc(env(safe-area-inset-top, 0px) + 58px);
    left: 50%; margin-left: -22px;
    width: 44px; height: 60px;
    opacity: 0; pointer-events: none;
  }

  /* ---- Table-edge HIT/STAND: fixed split bar at the bottom edge (mockup
     .ctlB) — no button chrome, rising light, dimmed + desaturated off-turn.
     Foe's whole bottom row retires. z-index 900: above the board, below
     flights (9400) and overlays. ---- */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-bottom,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pa-bottom { display: none; }
  /* Pre-resolution (no data-my-idx yet): seat 1's bottom row hides so two
     fixed bars never overlap during the skeleton paint (seat 0 = own by
     the pre-resolution convention). */
  .gv2-page:not([data-my-idx]):not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-bottom { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bottom { min-height: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pcl {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 900;
    display: flex;
    /* gap:0 kills the base .gv2-pcl 10px gap — the mockup bar is a
       CONTIGUOUS split (halves separated only by the divider shadow);
       the gap read as a see-through slot down the middle */
    gap: 0;
    height: calc(var(--gv2-portpcl-h) + env(safe-area-inset-bottom, 0px));
    /* #6: the safe-area padding is moved OFF the bar and ONTO the button content
       (.gv2-cb-hit / .gv2-cb-stand below) so the colored gradients fill to the physical
       bottom edge instead of stopping above the home-indicator inset. Height still includes
       the inset; the buttons are border-box (.gv2-cb below), so no double-count. */
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb {
    flex: 1; min-height: 0; padding: 0; border: none; border-radius: 0;
    box-shadow: none; display: flex; align-items: center; justify-content: center;
  }
  /* Divider on the STAND edge (was `.gv2-cb + .gv2-cb` — the desktop PR 3
     console child between the buttons breaks `+` adjacency even while
     display:none; HIT→STAND source order is fixed, so targeting the stand
     button directly is a visual no-op incl. the disabled state). */
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand { box-shadow: -1px 0 0 rgba(0,0,0,.6); }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-hit {
    background: linear-gradient(180deg, rgba(120,26,16,0), rgba(120,26,16,.55) 40%, #4a0e08);
    border-top: 1px solid rgba(255,154,128,.5);
    /* #6: reserve the home-indicator inset INSIDE the button (border-box) so the gradient
       fills to the physical edge while the label stays above the inset. Later source + equal
       specificity than .gv2-cb{padding:0}, so this longhand wins. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand {
    background: linear-gradient(180deg, rgba(122,90,34,0), rgba(122,90,34,.5) 40%, #3a2a10);
    border-top: 1px solid rgba(245,217,138,.55);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-label {
    font-size: 24px; font-style: normal; letter-spacing: .19em;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-hit .gv2-cb-label {
    color: #ffd9cf; text-shadow: 0 0 18px rgba(255,107,84,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand .gv2-cb-label {
    color: #ffedbe; text-shadow: 0 0 18px rgba(245,217,138,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-sub,
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-glow { display: none; }
  /* Off-turn dim: keep the edge gradients (the base disabled rule swaps in
     a dark slab with !important — override per variant), fade the bar. */
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-hit.gv2-cb-disabled {
    background: linear-gradient(180deg, rgba(120,26,16,0), rgba(120,26,16,.55) 40%, #4a0e08) !important;
    border-top: 1px solid rgba(255,154,128,.5) !important;
    box-shadow: none !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand.gv2-cb-disabled {
    background: linear-gradient(180deg, rgba(122,90,34,0), rgba(122,90,34,.5) 40%, #3a2a10) !important;
    border-top: 1px solid rgba(245,217,138,.55) !important;
    box-shadow: none !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-disabled {
    opacity: .32; filter: saturate(.4);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-disabled .gv2-cb-label { text-shadow: none; }
  /* Off-turn tactic-row dim — OWN tray only (foe's is informational). */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"]:not([data-state="turn"]) .gv2-ar-playable,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"]:not([data-state="turn"]) .gv2-ar-playable {
    opacity: .4; filter: saturate(.45);
  }

  /* ---- Palette: YOU brass / FOE oxblood at portrait (was PortDuel's;
     board accents are --purple/--orange refs so this recolors ribbons,
     hairlines, accents, and the deal glow in one place). ---- */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) {
    --purple: var(--brass);       --purple-hi: var(--brass-hi);
    --orange: var(--oxblood-hi);  --orange-hi: #d44552;
  }
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) {
    --purple: var(--oxblood-hi);  --purple-hi: #d44552;
    --orange: var(--brass);      --orange-hi: var(--brass-hi);
  }

  /* ---- GAME LOG pill (mockup .gl-pill): borderless text pill fixed above
     STAND's end of the control bar; the entry panel opens UPWARD.
     game-log.js collapses it by default at portrait. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-log {
    position: fixed; right: 12px;
    bottom: calc(env(safe-area-inset-bottom, 0px) + var(--gv2-portpcl-h) + 6px);
    z-index: 901; width: auto; max-width: none;
    margin: 0; padding: 0; border: none; background: none; box-shadow: none;
    /* the base .gv2-log overflow:hidden would clip the upward-opening body
       (absolute at bottom:32px, entirely outside this shrink-to-fit pill) */
    overflow: visible;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-head {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 7px 6px; cursor: pointer;
    border: none; background: none;
    justify-content: flex-end; width: auto; float: right;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-title {
    font-size: 8.5px; letter-spacing: .29em; text-transform: uppercase;
    color: #9a8f72; font-style: normal;
    text-shadow: 0 2px 6px rgba(0,0,0,.8);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-chevron { font-size: 8px; color: #8a6a2f; }
  /* device-QA item 9: scroll/max-height moved to #gv2-log-entries below; NO
     `display` here (it would outrank the collapsed-hide — see base rule note).
     The body inherits `display:flex; flex-direction:column` from the base. */
  .gv2-page:not([data-mode="campaign"]) .gv2-log-body {
    position: absolute; bottom: 32px; right: 0;
    width: min(300px, calc(100vw - 24px));
    background: linear-gradient(180deg, rgba(26,18,8,.95), rgba(14,9,3,.95));
    border: 1px solid #3a2a18; border-radius: 12px;
    box-shadow: 0 14px 34px rgba(0,0,0,.65);
    padding: 10px 14px 12px;
  }
  /* the id + .gv2-page prefix outranks the base .gv2-log-entries class cap
     (168px), so the portrait popover scrolls at 300px. */
  .gv2-page:not([data-mode="campaign"]) #gv2-log-entries { max-height: 300px; overflow-y: auto; }
  /* show the X close button on the portrait pill; align-self:flex-end (from the
     base rule) puts it at the popover's top-right. */
  .gv2-page:not([data-mode="campaign"]) .gv2-log-close { display: block; }

  /* ---- Retired at portrait: the mobile footer (deck/count strip — the
     header tap-peek replaces it) and the FootStrip band (messages folded
     into the prow captions + the DEALING… ornament). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-mobile-footer { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-board-bottom { display: none; }

}

/* Header-chrome keyframes (referenced by the portrait block AND the desktop
   1v1 redesign block at the end of this file). */
@keyframes gv2WcBurn {
  0%, 100% {
    box-shadow:
      0 0 0 1px #060301, 0 0 0 2.5px rgba(216,169,78,.7), 0 0 0 4px #060301,
      inset 0 1px 0 rgba(245,217,138,.28), inset 0 -6px 12px rgba(0,0,0,.6),
      0 12px 30px rgba(0,0,0,.65);
  }
  50% {
    box-shadow:
      0 0 0 1px #060301, 0 0 0 2.5px rgba(255,120,90,.95), 0 0 0 4px #060301,
      0 0 22px 4px rgba(220,60,40,.45), inset 0 1px 0 rgba(255,180,150,.35),
      inset 0 -6px 12px rgba(40,4,2,.7), 0 12px 30px rgba(0,0,0,.65);
  }
}
@keyframes gv2WcFlag {
  50% { color: #fff; text-shadow: 0 0 16px rgba(255,140,100,1); }
}
@keyframes gv2ProwPulse {
  50% { opacity: .55; }
}
/* Portrait-scoped so the reduce override can't touch desktop/landscape
   ribbon or bank-chip animations (frozen surfaces). */
@media (max-width: 767px) and (orientation: portrait) and (prefers-reduced-motion: reduce) {
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-mp,
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-flag,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"] .gv2-sr,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb.gv2-tb-low .gv2-tb-time { animation: none; }
}

/* ─── Boss-tier backdrops (2026-05-07, updated 2026-05-12) ─────────────
   Body backdrop swap — campaign uses per-boss-tier image instead of any
   generic emblem. Same 35% ink overlay opacity as 1v1 body.
   `campaign/game-ui.js` sets --gv2-c-boss-bg via setProperty() on every
   campaignState arrival; CSS does not change per-boss. Var() default is
   `none` (no image) so the `background-color: var(--ink)` line shows
   through during the loading window — the prior `/images/background.jpg`
   fallback exposed the legacy crowned-emblem artwork before first state
   arrived (2026-05-12 fix per docs/plans/2026-05-12-campaign-bugs-batch-plan.md).
   IMPORTANT: keep `background-color: var(--ink)` line below — it's the
   final-fallback behind the gradient overlay when --gv2-c-boss-bg is
   unset. Removing it is a regression.
   Plan: docs/plans/2026-05-07-campaign-boss-backdrops-plan.md */
body.gv2-campaign-bg-active {
  background:
    linear-gradient(rgba(11,11,13,0.35), rgba(11,11,13,0.35)),
    var(--gv2-c-boss-bg, none) center/cover no-repeat fixed;
  background-color: var(--ink);
}

/* Lucky Jack (2026-07-16): 1v1 / arena / tournament boards (all routed
   through /game) get FLAT FELT at every viewport size — no per-match
   image, no medieval boss art, NO IMAGE AT ALL.
   QA item 3 (2026-07-17): the felt is GREEN again. It shipped purple on
   2026-07-16 sampled from the Board.png reference art (#911FAB center ->
   #3B0642 corners); Matt reverted it a day later. The revert restored the
   WHOLE pre-purple recipe, not just the four hexes — purple had also
   retuned the felt geometry (130% 100% -> 120% 95%), the stops
   (0/42/72/100 -> 0/28/62/100), the glow alphas and the vignette stop
   (44% -> 40%), so a hex-only revert would have produced a green graded
   differently from the original. Board.png is NO LONGER the reference for
   these colors — do not re-sample it.
   That art also carried an ornate gold frame, which was DROPPED (Matt,
   2026-07-16: "purple felt on its own is fine"): the art is 2.36:1 while
   the board is ~1.6:1, so an undistorted, uncropped frame could not
   enclose the board's content at any desktop size — it would need to be
   ~2128px wide to fit, i.e. wider than the screen. Felt-only sidesteps
   that entirely and costs nothing: a gradient scales to any viewport.
   game-ui.js#applyBoardFelt adds the class (guarded off campaign + trial).
   Supersedes PR-20's per-match boss-background hashing.
   Plan: docs/plans/2026-07-16-casino-backdrop-board-plan.md */
body.gv2-1v1-bg-active {
  background:
    radial-gradient(92% 70% at 50% 46%, transparent 44%, rgba(0,0,0,.6) 100%),
    radial-gradient(70% 34% at 50% 20%, rgba(194,43,43,.08), transparent 70%),
    radial-gradient(80% 40% at 50% 76%, rgba(245,217,138,.09), transparent 70%),
    radial-gradient(130% 100% at 50% 46%, #1a3a25 0%, #122b1b 42%, #0a1a10 72%, #060d08 100%);
  background-attachment: fixed;
  background-color: var(--ink);
}
/* Make .gv2-page transparent on /game just like /campaign so the body
   backdrop shows through the page container. */
body.gv2-1v1-bg-active .gv2-page { background: transparent; }

/* Campaign page becomes transparent so body backdrop bleeds through. 1v1
   keeps the solid var(--ink) background from the base .gv2-page rule.
   Specificity 0,2,0 wins over base .gv2-page (0,1,0). */
.gv2-page[data-mode="campaign"] {
  background: transparent;
}

/* ─── Arena backdrop (2026-05-13, updated 2026-05-15) ──────────────────
   Arena-launched matches set body class via socket-boot.js when the
   sessionStorage stash has source='arena' (written by arena/bootstrap.js).
   .gv2-page becomes transparent so a per-arena body backdrop (added by
   the follow-up backdrop work) can bleed through. Until that lands, body
   is solid ink (base rule) and arena renders identically to 1v1 — the
   transparent .gv2-page is the wiring waiting for backdrop assets.
   Specificity 0,2,0 ties .gv2-page[data-mode="campaign"] above — order
   matters only if both classes co-occur (they don't: arena flow only
   lands on /game, never /campaign). */
body.gv2-source-arena .gv2-page {
  background: transparent;
}

/* Run state strip wrap — anchor parent for the boss-name tooltip surface
   (sibling of the strip). position: relative so the absolutely-positioned
   tooltip resolves top:100% against the wrap (which sizes to the strip's
   height). Inline `position: relative` lives on the mountShell template too
   but the rule here is the durable surface. */
.gv2-c-runstrip-wrap {
  position: relative;
}

/* Boss-name trigger — visual hint that it's interactive.
   Scoped under .gv2-page[data-mode="campaign"] for symmetry with the rest
   of the campaign CSS section; the trigger only exists inside the campaign
   runstrip so the practical leak risk is zero either way. */
.gv2-page[data-mode="campaign"] .gv2-c-runstrip-boss[data-gv2-boss-tooltip] {
  cursor: help;
  border-bottom: 1px dotted var(--brass-lo);
  outline: none;
  /* Long-press handler uses preventDefault after 400ms; touch-action: none
     prevents native double-tap-zoom on the boss name only (not the parent strip). */
  touch-action: none;
}
.gv2-page[data-mode="campaign"] .gv2-c-runstrip-boss[data-gv2-boss-tooltip]:focus-visible {
  border-bottom-color: var(--brass-hi);
  box-shadow: 0 1px 0 0 var(--brass-hi);
}

/* Boss-powers tooltip surface — vellum card listing tier powers.
   Anchored below the run-state strip (top: 100% against .gv2-c-runstrip-wrap).
   z-index 9050 → above grid content (no explicit z-index in normal flow),
   below chooser ladder (9150). Campaign matchEnd surface is z-index 9000;
   tooltip would render above it briefly if visible, but in practice
   matchEnd phase suppresses hover targets so collision is theoretical. */
.gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 8px;
  z-index: 9050;
  max-width: 320px;
  min-width: 220px;
  padding: 10px 14px;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 70%, #050306 100%);
  border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass-hi);
  box-shadow: 0 8px 22px rgba(0,0,0,.75);
  color: var(--vellum);
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  animation: gv2-c-boss-tooltip-in 140ms ease-out;
  pointer-events: none;
}
.gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip[hidden] {
  display: none;
}

/* Tooltip rows — reuse the .campaign-power-tag markup shared with v1. */
.gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip .campaign-power-tag {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip .power-short {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 600;
  color: var(--brass-hi);
  font-size: 13px;
}
.gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip .power-full {
  color: var(--vellum-dim);
  font-size: 11px;
  line-height: 1.4;
}
/* Match rule (not a power) — "First to N Wins" header line, separated from the
   power rows by a brass divider so it reads as a rule, not an ability. */
.gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip .gv2-c-tooltip-rule {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 700;
  font-size: 13px;
  color: var(--brass-hi);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--brass-lo);
}

@keyframes gv2-c-boss-tooltip-in {
  from { opacity: 0; transform: translate(-50%, -4px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .gv2-page[data-mode="campaign"] .gv2-c-boss-tooltip { animation: none; }
}

/* Run state strip — TIER · BOSS · BOONS thin bar above the game grid */
.gv2-c-runstrip {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 8px 16px;
  background: rgba(14,14,18,.75);
  border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass-hi);
  margin: 0 auto;
  max-width: 720px;
  font-size: 11px;
}
.gv2-c-runstrip-loading {
  color: var(--vellum-dim);
  font-size: 9px;
  letter-spacing: .35em;
}
.gv2-c-runstrip-tier {
  color: var(--brass-hi);
  font-size: 10px;
  letter-spacing: .35em;
}
.gv2-c-runstrip-boss {
  color: var(--vellum);
  font-size: 16px;
  font-style: italic;
  letter-spacing: .02em;
}
.gv2-c-runstrip-boons {
  color: var(--vellum-dim);
  font-size: 10px;
  letter-spacing: .3em;
}
.gv2-c-runstrip-sep {
  color: var(--brass-lo);
  font-size: 12px;
}

/* Minimal matchEnd surface — "Return to Crusade" button overlay */
/* ============================================================
   V1 (2026-05-28) — Campaign loading overlay.
   Masks the bare-wireframe load with the /auth "felt" look, reproduced + scoped
   here (auth-felt.css is NOT linked on the board — its html/body reset would
   restyle it). Felt tokens defined locally so they don't leak; --brass/--brass-hi
   come from :root. Dismissed by campaign/game-ui.js on the first campaign state.
   z-index 9000: above all board content, below the error toast (10000). ============ */
.gv2-c-loading {
  --ld-night: #050508;
  --ld-ob-bg1: #4a0d14; --ld-ob-bg2: #181221; --ld-ob-bg3: #150307;
  --ld-ob1: #5a1118; --ld-ob2: #3a070d; --ld-ob3: #220308;
  position: fixed; inset: 0; z-index: 9000; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(ellipse 80% 60% at 50% 30%, var(--ld-ob-bg1) 0%, var(--ld-ob-bg2) 60%, var(--ld-ob-bg3) 100%),
    var(--ld-night);
  opacity: 1;
  transition: opacity .4s ease;
}
.gv2-c-loading-out { opacity: 0; pointer-events: none; }
/* Bottom-anchored brass disc (matches /auth .bja-felt-disc). */
.gv2-c-loading-disc {
  position: absolute; left: 50%; bottom: -480px; transform: translateX(-50%);
  width: 1720px; height: 1720px; border-radius: 50%;
  background:
    radial-gradient(ellipse 100% 60% at 50% 0%, rgba(224,196,137,0.10), transparent 50%),
    linear-gradient(180deg, var(--ld-ob1) 0%, var(--ld-ob2) 60%, var(--ld-ob3) 100%);
  box-shadow: 0 -40px 80px rgba(0,0,0,0.6), inset 0 0 120px rgba(0,0,0,0.7), inset 0 6px 0 rgba(224,196,137,0.18);
}
.gv2-c-loading-disc::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  border: 2px solid var(--brass);
  box-shadow: 0 0 0 2px rgba(0,0,0,0.6), 0 0 0 4px rgba(224,196,137,0.25);
}
.gv2-c-loading-ring {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  border-radius: 50%; pointer-events: none;
}
.gv2-c-loading-ring-mid   { width: 1340px; height: 1340px; border: 1px solid rgba(183,162,122,0.28); }
.gv2-c-loading-ring-inner { width: 1140px; height: 1140px; border: 1px dashed rgba(183,162,122,0.16); }
.gv2-c-loading-body { position: relative; z-index: 1; text-align: center; padding: 0 24px; }
.gv2-c-loading-motto {
  font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 600;
  font-size: 34px; color: var(--brass-hi); letter-spacing: .12em;
  text-shadow: 0 2px 8px rgba(0,0,0,.7);
  animation: gv2-c-loading-pulse 1.8s ease-in-out infinite;
}
@keyframes gv2-c-loading-pulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .gv2-c-loading-motto { animation: none; opacity: 1; }
}
@media (max-width: 480px) { .gv2-c-loading-motto { font-size: 26px; } }

.gv2-c-matchend {
  position: fixed; inset: 0;
  z-index: 9000;
  display: flex; align-items: center; justify-content: center;
  background: rgba(11,11,13,.92);
}
.gv2-c-matchend[hidden] { display: none; }
.gv2-c-matchend-inner {
  position: relative;  /* PR 16: anchor for absolutely-positioned sparkles */
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  padding: 40px 48px;
  background: linear-gradient(180deg, #1a140e 0%, #0a0608 100%);
  border: 1px solid var(--brass-lo);
  border-top: 3px solid var(--brass-hi);
  box-shadow: 0 12px 40px rgba(0,0,0,.7);
  max-width: calc(100vw - 32px);
  min-width: 320px;
}
.gv2-c-matchend-eyebrow {
  color: var(--brass-hi);
  font-size: 10px;
  letter-spacing: .4em;
}

/* Toast for campaignError */
.gv2-c-toast {
  position: fixed;
  top: max(24px, env(safe-area-inset-top) + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(-20px);
  padding: 10px 16px;
  background: var(--oxblood);
  border: 1px solid var(--oxblood-hi);
  color: #fff;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: .15em;
  z-index: 10000;
  opacity: 0;
  transition: opacity .2s, transform .2s;
  pointer-events: none;
}
.gv2-c-toast-active {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Mobile reflow — run state strip becomes more compact */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-page[data-mode="campaign"] .gv2-c-runstrip {
    padding: 6px 8px;
    gap: 6px;
    max-width: 100%;
  }
  .gv2-page[data-mode="campaign"] .gv2-c-runstrip-boss { font-size: 13px; }
  .gv2-page[data-mode="campaign"] .gv2-c-runstrip-tier,
  .gv2-page[data-mode="campaign"] .gv2-c-runstrip-boons { font-size: 8px; letter-spacing: .25em; }
}

/* ============================================================
   PHASE E PR 15 — Boons row + boon trigger overlay
   Plan: docs/plans/2026-04-30-gameboard-phase-e-campaign-plan.md (PR 15 §)
   ============================================================ */

/* Boons row — vellum chips between run-state-strip and game-grid.
   Mirrors .gv2-game-grid's track layout (1fr 240px 1fr, gap 28px)
   so the chips land in column 1 — directly above player area 0 (self, left
   column). It's a DIRECT SIBLING of .gv2-game-grid under .gv2-page, so the
   shared content box + identical template keep the columns aligned at any
   width. NOTE: the `240px` axis width is duplicated from .gv2-game-grid
   (css ~782) — if that grid template changes, update this in lockstep. */
.gv2-page[data-mode="campaign"] .gv2-c-boons-row {
  display: grid;
  grid-template-columns: 1fr 240px 1fr;
  gap: 28px;
  align-items: center;
  padding: 6px 0;
  margin: 0;
  min-height: 32px;
}
.gv2-page[data-mode="campaign"] .gv2-c-boons-inner {
  grid-column: 1;            /* over player area 0 (self) */
  min-width: 0;              /* keep the 1fr track stable — a wide chip can't
                                blow column 1 out of sync with .gv2-game-grid */
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: flex-start;
  align-items: center;
  padding-left: 21px;        /* match .gv2-pa content inset (20px pad + 1px border) */
}
.gv2-page[data-mode="campaign"] .gv2-c-boons-row[hidden] { display: none; }
.gv2-c-boon-chip {
  display: inline-flex;
  align-items: center;
  padding: 5px 12px;
  background: rgba(20,16,10,.55);
  border: 1px solid var(--brass-lo);
  color: var(--brass-hi);
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-size: 16px;
  font-weight: 500;
  cursor: help;
  transition: background .12s, border-color .12s;
}
@media (hover: hover) {
  .gv2-c-boon-chip:hover {
    background: rgba(20,16,10,.85);
    border-color: var(--brass-hi);
  }
}
/* Mobile: grid collapses to a stacked column, so revert the boons-row to the
   flex horizontal-scroll fallback (6+ chips overflow). display:contents on the
   inner dissolves its box so chips flow as direct flex children of the row. */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-page[data-mode="campaign"] .gv2-c-boons-row {
    display: flex;
    grid-template-columns: none;
    flex-wrap: nowrap;
    overflow-x: auto;
    justify-content: flex-start;
    padding: 6px 8px;
  }
  .gv2-page[data-mode="campaign"] .gv2-c-boons-inner { display: contents; }
  .gv2-c-boon-chip {
    flex-shrink: 0;
    font-size: 13px;
    padding: 2px 8px;
  }
}

/* ============================================================
   v2 consumable chip row (PR 2 of 4)
   ============================================================
   Renders active-category consumables as clickable chips below
   the boons-row. Disarm + Neutralize wired; Sacrifice + STF
   appear as dimmed "coming soon" chips until PR 3.
   Mirrors boons-row layout pattern. */
.gv2-page[data-mode="campaign"] .gv2-c-consumables-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
  align-items: center;
  padding: 4px 12px 6px;
  margin: 0 auto;
  max-width: 1100px;
  min-height: 32px;
}
.gv2-page[data-mode="campaign"] .gv2-c-consumables-row[hidden] { display: none; }

.gv2-c-consumable-chip {
  /* Button reset baseline */
  background: rgba(60, 40, 20, 0.18);
  border: 1px solid var(--brass);
  padding: 4px 12px;
  font: inherit;
  color: var(--oxblood-hi, #c93945);
  cursor: pointer;
  /* Chip styling */
  display: inline-flex;
  align-items: center;
  border-radius: 0;
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  transition: background 120ms ease, border-color 120ms ease, transform 80ms ease, opacity 120ms ease;
  user-select: none;
}
@media (hover: hover) {
  .gv2-c-consumable-chip:hover:not([data-disabled]):not([disabled]) {
    background: rgba(201, 57, 69, 0.18);
    border-color: var(--brass-hi);
  }
}
.gv2-c-consumable-chip:active:not([data-disabled]):not([disabled]) {
  transform: translateY(1px);
}
.gv2-c-consumable-chip:focus-visible {
  outline: 2px solid var(--brass-hi);
  outline-offset: 2px;
}

/* Disabled variants — phase/charges/unwired all use the same dim look;
   submitting adds pointer-events: none to belt-and-suspenders the click. */
.gv2-c-consumable-chip[data-disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  color: var(--brass-lo);
  border-color: var(--brass-lo);
}
.gv2-c-consumable-chip[data-disabled="submitting"] {
  pointer-events: none;
  opacity: 0.35;
}

/* PR 3 Bug E: per-fight Suppress preaction toggle chip. Visually
   distinct from consumable chips — uses iron-edge accent instead of
   brass/oxblood so it reads as a UI toggle, not a consumable action.
   Filled state when ON (suppress active for this match). */
.gv2-c-preaction-suppress {
  color: var(--vellum-dim);
  border-color: var(--iron-edge);
  background: transparent;
  margin-left: auto; /* push to right end of the row */
}
@media (hover: hover) {
  .gv2-c-preaction-suppress:hover:not([data-disabled]):not([disabled]) {
    background: rgba(255, 255, 255, 0.04);
    border-color: var(--bone);
  }
}
.gv2-c-preaction-suppress-on {
  color: var(--bone);
  border-color: var(--brass-hi);
  background: rgba(201, 159, 91, 0.18);
}
@media (hover: hover) {
  .gv2-c-preaction-suppress-on:hover:not([data-disabled]):not([disabled]) {
    background: rgba(201, 159, 91, 0.28);
  }
}

/* Mobile: horizontal scroll fallback if chips overflow.
   Matches boons-row's combined query for portrait phones + landscape short viewports. */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-page[data-mode="campaign"] .gv2-c-consumables-row {
    flex-wrap: nowrap;
    overflow-x: auto;
    justify-content: flex-start;
    padding: 4px 8px 6px;
  }
  .gv2-c-consumable-chip {
    flex-shrink: 0;
    font-size: 12px;
    padding: 3px 10px;
  }
}

/* Boon trigger overlay — vellum on brass, scale-in/hold/fade.
   z-index 9500: above matchEnd (9000) + overlays (9100-9200), below toast (10000). */
.gv2-c-boon-overlay {
  z-index: 9500;
  padding: 22px 44px;
  background: linear-gradient(180deg, var(--vellum) 0%, var(--bone) 100%);
  border: 1px solid var(--brass-hi);
  border-top: 2px solid var(--brass);
  color: var(--ink);
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-size: 34px;
  font-weight: 600;
  letter-spacing: .02em;
  text-shadow: 0 1px 0 rgba(255,255,255,.4);
  box-shadow: 0 8px 32px rgba(0,0,0,.6), 0 0 24px rgba(224,196,137,.4);
  pointer-events: none;
  /* B3 (2026-05-28): bigger overlay. wrap within the viewport instead of nowrap
     so a long coalesced message can't overflow at the larger font. */
  max-width: min(90vw, 600px);
  white-space: normal;
  text-align: center;
  animation: gv2-pr15-boon-overlay 2.5s ease-out forwards;
}
.gv2-c-boon-overlay-compressed {
  /* Compressed timing when queue ≥ 3: 0.2s in + 0.6s hold + 0.3s fade = 1.1s total */
  animation: gv2-pr15-boon-overlay-compressed 1.1s ease-out forwards;
}
@keyframes gv2-pr15-boon-overlay {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
  8%   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  88%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
}
@keyframes gv2-pr15-boon-overlay-compressed {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3); }
  18%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  73%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
}

/* Mobile: smaller overlay font */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-c-boon-overlay { font-size: 24px; padding: 14px 26px; }
}

/* Reduced motion: keep minimal fade so animationend still fires (queue
   pump depends on it). 0.5s total — no scale, just opacity. */
@media (prefers-reduced-motion: reduce) {
  .gv2-c-boon-overlay,
  .gv2-c-boon-overlay-compressed {
    animation: gv2-pr15-boon-overlay-reduced 2.5s linear forwards;
    transform: translate(-50%, -50%);
  }
  .gv2-c-boon-overlay-compressed {
    animation: gv2-pr15-boon-overlay-reduced 1.1s linear forwards;
  }
}
@keyframes gv2-pr15-boon-overlay-reduced {
  0%, 100% { opacity: 0; }
  10%, 90% { opacity: 1; }
}

/* Boon GRANT reveal modal (2026-05-28) — centered modal (FC/SW-chooser family)
   shown when a boon grants the player an action card: power name · description ·
   the card(s) gained. Non-interactive; auto-dismisses (3s) or click/tap/Esc.
   z-index 9200: above the overlay band (9100-9200) so the reward reads clearly,
   below the boon text-overlay (9500) + toast (10000). */
.gv2-c-boon-grant-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(8,6,4,.55);
  cursor: pointer;
  animation: gv2-bg-fade-in .18s ease-out;
}
.gv2-c-boon-grant-modal {
  max-width: min(88vw, 420px);
  padding: 20px 26px 16px;
  background: linear-gradient(180deg, var(--vellum) 0%, var(--bone) 100%);
  border: 1px solid var(--brass-hi);
  border-top: 3px solid var(--brass);
  border-radius: 4px;
  color: var(--ink);
  text-align: center;
  box-shadow: 0 10px 36px rgba(0,0,0,.6), 0 0 28px rgba(224,196,137,.35);
  animation: gv2-bg-pop-in .22s cubic-bezier(.2,.8,.3,1.2);
}
.gv2-c-bg-header {
  font-family: 'Cinzel', serif;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--brass-lo);
  margin-bottom: 10px;
}
.gv2-c-bg-reveal { padding: 8px 0; }
.gv2-c-bg-reveal + .gv2-c-bg-reveal { border-top: 1px solid rgba(46,36,16,.18); }
.gv2-c-bg-name {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-size: 26px;
  font-weight: 600;
  line-height: 1.1;
}
.gv2-c-bg-desc {
  font-size: 13px;
  color: rgba(46,36,16,.78);
  margin: 4px auto 12px;
  max-width: 320px;
  line-height: 1.35;
}
.gv2-c-bg-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  align-items: center;
}
.gv2-c-bg-card { width: 72px; }
.gv2-c-bg-card-fallback {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 48px; padding: 8px 12px;
  border: 1px solid var(--brass-lo); border-radius: 4px;
  font-family: 'Cormorant Garamond', serif; font-weight: 600; font-size: 20px;
  color: var(--ink);
}
.gv2-c-bg-hint {
  margin-top: 14px;
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(46,36,16,.5);
}
@keyframes gv2-bg-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes gv2-bg-pop-in {
  from { opacity: 0; transform: scale(.85); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .gv2-c-boon-grant-backdrop, .gv2-c-boon-grant-modal { animation: gv2-bg-fade-in .2s linear; }
}
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-c-boon-grant-modal { max-width: 92vw; padding: 16px 18px 12px; }
  .gv2-c-bg-name { font-size: 22px; }
  .gv2-c-bg-card { width: 60px; }
}

/* ============================================================
   PHASE E PR 16 — Enhanced matchEnd surface (win/loss variants)
   + boss-area slam
   ============================================================ */

/* Headline + sub copy */
.gv2-c-matchend-headline {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-size: 56px;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
  margin: 0;
}
.gv2-c-matchend[data-variant="win"] .gv2-c-matchend-headline {
  color: var(--brass-hi);
  text-shadow: 0 2px 12px rgba(224, 196, 137, 0.5), 0 0 24px rgba(224, 196, 137, 0.3);
}
.gv2-c-matchend-sub {
  color: var(--vellum-dim);
  font-size: 11px;
  letter-spacing: 0.4em;
  margin-top: 4px;
}

/* CSS-only sparkles for win path */
.gv2-c-matchend-sparkles {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.gv2-c-spark {
  position: absolute;
  width: 6px;
  height: 6px;
  background: var(--brass-hi);
  border-radius: 50%;
  box-shadow: 0 0 8px var(--brass-hi);
  opacity: 0;
  animation: gv2-pr16-spark 1.6s ease-out forwards;
}
.gv2-c-spark-1 { top: 12%; left: 15%; animation-delay: 0.0s; }
.gv2-c-spark-2 { top: 28%; right: 20%; animation-delay: 0.2s; }
.gv2-c-spark-3 { bottom: 35%; left: 25%; animation-delay: 0.4s; }
.gv2-c-spark-4 { bottom: 18%; right: 18%; animation-delay: 0.6s; }
.gv2-c-spark-5 { top: 50%; left: 50%; animation-delay: 0.8s; }
@keyframes gv2-pr16-spark {
  0%   { opacity: 0; transform: scale(0); }
  20%  { opacity: 1; transform: scale(1.4); }
  60%  { opacity: 0.8; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.5) translateY(-20px); }
}

/* Boss area slam — large text positioned over boss area */
.gv2-c-boss-slam {
  position: fixed;
  z-index: 9300;
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-weight: 700;
  font-size: 48px;
  letter-spacing: 0.06em;
  pointer-events: none;
  white-space: nowrap;
  text-shadow: 0 4px 16px rgba(0, 0, 0, 0.8);
  animation: gv2-pr16-boss-slam 1.5s ease-out forwards;
}
.gv2-c-boss-slam-win {
  color: var(--brass-hi);
  text-shadow: 0 4px 16px rgba(0, 0, 0, 0.8), 0 0 32px rgba(224, 196, 137, 0.6);
}
.gv2-c-boss-slam-loss {
  color: var(--oxblood-hi);
  text-shadow: 0 4px 16px rgba(0, 0, 0, 0.8), 0 0 32px rgba(143, 116, 208, 0.6);
}
@keyframes gv2-pr16-boss-slam {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.3) rotate(-3deg); }
  15%  { opacity: 1; transform: translate(-50%, -50%) scale(1.1) rotate(2deg); }
  25%  { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(0); }
  75%  { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(0); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.95) rotate(0); }
}

/* Mobile: smaller headline + slam */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-c-matchend-headline { font-size: 38px; }
  .gv2-c-matchend-sub { font-size: 9px; letter-spacing: 0.3em; }
  .gv2-c-boss-slam { font-size: 32px; }
}

/* Reduced motion: skip animations, just hold visible */
@media (prefers-reduced-motion: reduce) {
  .gv2-c-spark {
    animation: gv2-pr16-spark-reduced 1.6s linear forwards;
  }
  .gv2-c-boss-slam {
    animation: gv2-pr16-boss-slam-reduced 1.5s linear forwards;
    transform: translate(-50%, -50%);
  }
}
@keyframes gv2-pr16-spark-reduced {
  0%, 100% { opacity: 0; }
  20%, 80% { opacity: 0.8; }
}
@keyframes gv2-pr16-boss-slam-reduced {
  0%, 100% { opacity: 0; }
  10%, 90% { opacity: 1; }
}

/* ============================================================
   C1 PR 2 — SW chooser mirror + boost chips
   ============================================================
   - .gv2-chooser-boost-chip-row: flex row above each hand column showing
     the player's currently-applied +1/+2/-5 score boosts as small chips.
   - .gv2-chooser-boost-chip: non-interactive — no hover lift, no cursor
     pointer, no selection ring. ~40% size of full ActionCard.
   - .gv2-overlay-chooser-card[data-pov="passive"]: passive mirror modal.
     Accent color set inline via --gv2-sr-accent (the actor's color, which
     is the opponent's color from the waiting viewer's POV).
   - .gv2-chooser-mirror-banner: status copy on waiting player's view
     (replaces the submit button). */

.gv2-chooser-boost-chip-row {
  display: flex;
  flex-wrap: wrap;
  /* QA #7 (2026-05-31): center the applied-boost chip(s) under the column label
     instead of left-floating, which read as a stray detached card above the hands. */
  justify-content: center;
  align-items: center;
  gap: 4px;
  margin: 4px 0 6px 0;
  min-height: 44px;
}

.gv2-chooser-boost-chip {
  display: inline-block;
  pointer-events: none;
  cursor: default;
  user-select: none;
  /* Inner ActionCard is 32x44 — wrapper just provides isolation from any
     ancestor hover/click affordances. No transform, no shadow lift. */
}

/* Bug fix (2026-06-18): the inner ActionCard is 32x44 — too small for the
   score-mod corner badge (+1/+2/-5) to clear the center glyph, so it reads as
   "double print" in the SW chooser. Hide the corner; the center glyph alone is
   the chip. (The tray hides this only at landscape size; the chip is small at
   every viewport, so this rule is un-gated.) */
.gv2-chooser-boost-chip .gv2-ac-corner-tl { display: none; }

.gv2-chooser-boost-chip:hover {
  /* Explicit override: defeat any inherited :hover lift from ancestor
     selectors. Non-interactive means non-interactive. */
  transform: none;
  filter: none;
  box-shadow: none;
}

.gv2-overlay-chooser-card[data-pov="passive"] {
  /* Use the inline --gv2-sr-accent (set per-render from actor's slot color)
     to tint the eyebrow + title strip. Falls back to brass if unset. */
  border-color: var(--gv2-sr-accent, var(--brass));
}

.gv2-overlay-chooser-card[data-pov="passive"] .gv2-overlay-chooser-eyebrow {
  color: var(--gv2-sr-accent, var(--brass));
}

/* Passive mirror tiles are non-interactive — CSS-layer defense in addition to
   the wasActor early-return in handleClick. Also kills the cursor: pointer
   affordance inherited from the active-view tile styling. */
.gv2-overlay-chooser-card[data-pov="passive"] .gv2-overlay-chooser-tile {
  pointer-events: none;
  cursor: default;
}

.gv2-chooser-mirror-banner {
  font-size: 13px;
  color: var(--vellum-dim);
  margin-top: 20px;
  font-style: italic;
  letter-spacing: 0.05em;
  text-align: center;
}

/* ============================================================
   C1 PR 4 item 11 — Action-card overflow alert modal
   ============================================================
   Inherits the .gv2-overlay-chooser styles; only adds positioning
   for the single overflow card preview between title and OK button. */
.gv2-overlay-chooser-overflow-card {
  display: flex;
  justify-content: center;
  margin: 16px 0 8px 0;
}

/* ============================================================
   v2 consumable prompts (PR 1 of 4)
   ============================================================
   Server-triggered modal for Second Chance (bust) and Insurance
   (round-loss). Bare-class selectors because the modal is
   document.body-appended (matches boon-trigger-overlay + chooser-fc
   convention). Containment to /campaign comes from the script tag
   being loaded only by campaign-v2.html; the .gv2-overlay-prompt-
   consumable class is unique to this module. Medieval vellum + brass
   + oxblood vocabulary, sharp corners, Cormorant italic.
   z-index 9550 (above boon-trigger 9500). */
.gv2-overlay-prompt-consumable {
  z-index: 9550;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-card {
  max-width: 560px;
  padding: 32px 36px;
  background: linear-gradient(180deg, #f4ead2 0%, #e7d8b0 100%);
  border: 2px solid var(--brass);
  border-radius: 0;
  color: var(--ink);
  box-shadow:
    0 0 0 1px rgba(60, 40, 20, 0.4) inset,
    0 18px 48px rgba(0, 0, 0, 0.6),
    0 0 40px rgba(201, 57, 69, 0.15);
  text-align: center;
  position: relative;
  animation: gv2-prompt-mount 240ms ease-out;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-title {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 32px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--oxblood, #c93945);
  margin: 0 0 12px;
  line-height: 1.1;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-body {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 17px;
  line-height: 1.45;
  color: #2b1d10;
  margin: 0 0 22px;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-charges {
  display: block;
  margin-top: 6px;
  font-size: 14px;
  color: #6a4a2a;
  font-style: italic;
  letter-spacing: 0.03em;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  align-items: stretch;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn {
  flex: 1 1 auto;
  min-width: 0;
  padding: 12px 20px;
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 0;
  cursor: pointer;
  transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn[disabled] {
  cursor: wait;
  opacity: 0.55;
  filter: saturate(0.7);
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-primary {
  background: linear-gradient(180deg, #c93945 0%, #8e2530 100%);
  color: #faf8f3;
  border: 1px solid var(--brass);
  box-shadow: 0 4px 12px rgba(201, 57, 69, 0.3);
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-primary:hover:not([disabled]) {
  filter: brightness(1.08);
  transform: translateY(-1px);
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-primary:active:not([disabled]) {
  transform: translateY(1px);
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-secondary {
  background: transparent;
  color: #2b1d10;
  border: 1px solid #6a4a2a;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-secondary:hover:not([disabled]) {
  background: rgba(60, 40, 20, 0.08);
  transform: translateY(-1px);
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-secondary:active:not([disabled]) {
  transform: translateY(1px);
}

@keyframes gv2-prompt-mount {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Per-prompt "Auto-decline this fight" toggle (2026-05-25) — extends the
   DoN/Mulligan preaction suppress pattern to Second Chance + Insurance.
   Lives in the modal footer below the action buttons. Dark-on-vellum
   palette matches the modal's parchment card body (prompt-btn-secondary
   uses #2b1d10 — same idiom). */
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-suppress-row {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid rgba(60, 40, 20, 0.18);
  display: flex;
  justify-content: center;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-suppress-btn {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: transparent;
  border: 1px solid rgba(60, 40, 20, 0.3);
  color: #5a432a;
  padding: 6px 14px;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-suppress-btn:hover {
  border-color: var(--brass-lo);
  color: #2b1d10;
}
.gv2-overlay-prompt-consumable .gv2-overlay-prompt-suppress-btn[aria-pressed="true"] {
  border-color: var(--brass);
  color: #2b1d10;
  background: rgba(183, 162, 122, 0.15);
}

@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-card {
    max-width: 90vw;
    padding: 24px 22px;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-title {
    font-size: 26px;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-body {
    font-size: 15px;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-actions {
    flex-direction: column;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn {
    width: 100%;
  }
}

@media (max-width: 399px) {
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-card {
    padding: 20px 16px;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-title {
    font-size: 22px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-card {
    animation: none;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn {
    transition: none;
  }
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-primary:hover:not([disabled]),
  .gv2-overlay-prompt-consumable .gv2-overlay-prompt-btn-secondary:hover:not([disabled]) {
    transform: none;
  }
}

/* ============================================================
   v2 See the Future peek modal (PR 3 of 4)
   ============================================================
   Inherits .gv2-overlay-chooser (z-index 9150, fixed inset 0).
   Renders up-to-3 cards face-up with labels. Empty-deck case
   shows a "deck is empty" headline. */
.gv2-overlay-chooser-peek .gv2-c-peek-card-frame {
  max-width: 580px;
  padding: 28px 32px;
  background: linear-gradient(180deg, #f4ead2 0%, #e7d8b0 100%);
  border: 2px solid var(--brass);
  color: var(--ink);
  border-radius: 0;
  text-align: center;
  box-shadow:
    0 0 0 1px rgba(60, 40, 20, 0.4) inset,
    0 18px 48px rgba(0, 0, 0, 0.6);
}
.gv2-overlay-chooser-peek .gv2-c-peek-title {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 28px;
  font-weight: 600;
  color: var(--oxblood-hi, #c93945);
  margin: 0 0 18px;
  line-height: 1.1;
}
.gv2-overlay-chooser-peek .gv2-c-peek-row {
  display: flex;
  gap: 18px;
  justify-content: center;
  align-items: flex-start;
  margin: 8px 0 22px;
}
.gv2-overlay-chooser-peek .gv2-c-peek-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.gv2-overlay-chooser-peek .gv2-c-peek-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #6a4a2a;
}
.gv2-overlay-chooser-peek .gv2-c-peek-card { display: inline-block; }
.gv2-overlay-chooser-peek .gv2-c-peek-empty-headline {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 18px;
  color: #2b1d10;
  margin: 24px 0 22px;
}
.gv2-overlay-chooser-peek .gv2-c-peek-actions {
  display: flex;
  justify-content: center;
}

/* Mobile clamp + reduced-motion */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-overlay-chooser-peek .gv2-c-peek-card-frame {
    max-width: 92vw;
    padding: 22px 18px;
  }
  .gv2-overlay-chooser-peek .gv2-c-peek-row {
    gap: 10px;
  }
  .gv2-overlay-chooser-peek .gv2-c-peek-title { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  .gv2-overlay-chooser-peek .gv2-c-peek-card-frame { animation: none; }
}

/* ============================================================
   v2 Sacrifice hand-card chooser (PR 3 of 4)
   ============================================================
   Inherits .gv2-overlay-chooser. Renders player's playing
   cards as tiles; user selects one, Confirm emits with
   the original hand index. */
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-card {
  max-width: 720px;
  padding: 28px 32px;
  background: linear-gradient(180deg, #f4ead2 0%, #e7d8b0 100%);
  border: 2px solid var(--brass);
  color: var(--ink);
  border-radius: 0;
  text-align: center;
  box-shadow:
    0 0 0 1px rgba(60, 40, 20, 0.4) inset,
    0 18px 48px rgba(0, 0, 0, 0.6);
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-title {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 26px;
  font-weight: 600;
  color: var(--oxblood-hi, #c93945);
  margin: 0 0 6px;
  line-height: 1.1;
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-sub {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 15px;
  color: #2b1d10;
  margin: 0 0 18px;
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  margin: 8px 0 22px;
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tile {
  background: transparent;
  border: 2px solid transparent;
  padding: 4px;
  border-radius: 0;
  cursor: pointer;
  transition: border-color 120ms ease, transform 80ms ease, box-shadow 120ms ease;
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tile:hover:not([disabled]) {
  border-color: rgba(183, 162, 122, 0.55);
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tile-selected {
  border-color: var(--brass-hi);
  box-shadow: 0 0 18px rgba(183, 162, 122, 0.55);
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tile:focus-visible {
  outline: 2px solid var(--brass-hi);
  outline-offset: 2px;
}
.gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
}

/* Mobile + reduced-motion */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-card {
    max-width: 92vw;
    padding: 22px 18px;
  }
  .gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tiles { gap: 8px; }
  .gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-title { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  .gv2-overlay-chooser-sacrifice .gv2-c-sacrifice-tile { transition: none; }
}

/* ============================================================
   v2 preAction surface (PR 4 of 4)
   ============================================================
   Banner shown during phase === 'preAction' with Mulligan + DoN +
   Continue buttons. Ancestor-scoped under .gv2-page[data-mode="campaign"]
   — surface is rendered INSIDE #root via game-ui.js innerHTML template,
   not document.body-appended, so ancestor selectors match. */
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface {
  margin: 0 auto;
  max-width: 720px;
  padding: 4px 12px 8px;
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface[hidden] { display: none; }

.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-card {
  padding: 14px 22px;
  background: linear-gradient(180deg, #f4ead2 0%, #e7d8b0 100%);
  border: 2px solid var(--brass);
  color: var(--ink);
  border-radius: 0;
  text-align: center;
  box-shadow:
    0 0 0 1px rgba(60, 40, 20, 0.4) inset,
    0 12px 32px rgba(0, 0, 0, 0.4);
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-title {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 22px;
  font-weight: 600;
  color: var(--oxblood-hi, #c93945);
  margin: 0 0 4px;
  line-height: 1.1;
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-sub {
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 14px;
  color: #2b1d10;
  margin: 0 0 12px;
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}

.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn {
  background: transparent;
  border: 1px solid var(--brass);
  padding: 8px 18px;
  font: inherit;
  font-family: 'Cormorant Garamond', 'Cormorant', serif;
  font-style: italic;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
  cursor: pointer;
  border-radius: 0;
  transition: background 120ms ease, transform 80ms ease, opacity 120ms ease;
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn-primary {
  background: linear-gradient(180deg, #c93945 0%, #8e2530 100%);
  color: #faf8f3;
  border-color: var(--brass);
  box-shadow: 0 4px 12px rgba(201, 57, 69, 0.3);
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn:hover:not([disabled]) {
  background: rgba(60, 40, 20, 0.08);
  transform: translateY(-1px);
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn-primary:hover:not([disabled]) {
  filter: brightness(1.08);
  background: linear-gradient(180deg, #c93945 0%, #8e2530 100%);
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn:active:not([disabled]) {
  transform: translateY(1px);
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn:focus-visible {
  outline: 2px solid var(--brass-hi);
  outline-offset: 2px;
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn[disabled],
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn[data-disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}
.gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn[data-disabled="submitting"] {
  pointer-events: none;
  opacity: 0.35;
}

/* Mobile clamp */
@media (max-width: 767px), (orientation: landscape) and (max-height: 500px) {
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface {
    padding: 4px 8px 6px;
  }
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-card {
    padding: 12px 14px;
  }
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-title { font-size: 19px; }
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn {
    font-size: 14px;
    padding: 8px 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn {
    transition: none;
  }
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn:hover:not([disabled]),
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface .gv2-c-preaction-btn:active:not([disabled]) {
    transform: none;
  }
}

/* ─────────────────────────────────────────────────────────────────
   PR 2 — Instant-card defender drama (charging phase)
   See: public/js/game-v2/instant-drama.js
   Mounts inside defender's [data-gv2-player-area] for 3s during
   FH/FF charging. PR 3 adds impact + aftermath layers.
   ───────────────────────────────────────────────────────────────── */

/* Root + containment */
.gv2-id-root {
  position: absolute;
  inset: 0;
  isolation: isolate;
  z-index: 50;
  pointer-events: none;
  contain: layout;
  overflow: visible; /* particles bleed past pa edges */
}

/* Locked-target suppression — applied when FF cast on already-frozen/stood
   defender (their .gv2-fo is already showing). Keep cool-wash + nucleations only.
   Part A (2026-06-14): the dendrites surface is now a persistent `.gv2-pa` child
   (sibling to `.gv2-id-root`, not a descendant), so the old descendant selector
   no longer reaches it — locked suppression is JS-driven instead (mountCharging
   simply doesn't show/reveal the surface on a locked cast). Only the frost-edge
   clause remains here (it's still inside `.gv2-id-root`). */
.gv2-id-locked-target .gv2-id-frost-edge {
  display: none;
}

/* ─── Keyframes ──────────────────────────────────────────────── */

@keyframes gv2-id-sky-darken {
  0% { opacity: 0; }
  60% { opacity: .9; }
  100% { opacity: .85; }
}
@keyframes gv2-id-sky-charge-red {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes gv2-id-vignette-build {
  0% { box-shadow: inset 0 0 0 0 rgba(0,0,0,0), inset 0 0 0 0 rgba(143,116,208,0); }
  100% { box-shadow: inset 0 -80px 120px -20px rgba(20,4,2,.85), inset 0 0 120px 10px rgba(143,116,208,.45); }
}
@keyframes gv2-id-umbra-grow-outer {
  0% { transform: translate(-50%,-50%) scale(.12); opacity: 0; filter: blur(8px); }
  20% { opacity: .55; }
  100% { transform: translate(-50%,-50%) scale(1); opacity: 1; filter: blur(0); }
}
@keyframes gv2-id-umbra-grow-inner {
  0% { transform: translate(-50%,-50%) scale(.18); opacity: 0; }
  100% { transform: translate(-50%,-50%) scale(1); opacity: 1; }
}
@keyframes gv2-id-umbra-breathe {
  0%,100% { filter: blur(0) brightness(1); }
  50% { filter: blur(0) brightness(1.15); }
}
@keyframes gv2-id-ground-ripple {
  0% { transform: translate(-50%,-50%) scale(.18); opacity: 0; border-width: 1.5px; }
  8% { opacity: .9; }
  100% { transform: translate(-50%,-50%) scale(2.3); opacity: 0; border-width: 0; }
}
@keyframes gv2-id-heat-shimmer {
  0%,100% { transform: translateX(0) skewX(0deg); }
  25% { transform: translateX(-2px) skewX(.6deg); }
  50% { transform: translateX(1.5px) skewX(-.4deg); }
  75% { transform: translateX(-1px) skewX(.3deg); }
}
@keyframes gv2-id-ember-fall {
  0% { transform: translateY(-100%) translateX(var(--gv2-id-drift, 0px)); opacity: 0; }
  20% { opacity: 1; }
  100% { transform: translateY(110%) translateX(calc(var(--gv2-id-drift, 0px) + 8px)); opacity: 0; }
}
@keyframes gv2-id-converge-dust {
  0% { transform: translate(-50%,-50%) translate(var(--gv2-id-cx, 0px), var(--gv2-id-cy, 0px)) scale(.4); opacity: 0; }
  15% { opacity: .9; }
  100% { transform: translate(-50%,-50%) translate(0,0) scale(.1); opacity: 0; }
}
@keyframes gv2-id-card-tremor-heavy {
  0%,100% { transform: translate(0,0) rotate(0); }
  12% { transform: translate(-2.2px, 1.1px) rotate(-.35deg); }
  28% { transform: translate(2.2px, -1.2px) rotate(.35deg); }
  44% { transform: translate(-1.6px, 1.4px); }
  60% { transform: translate(2.1px, .6px) rotate(-.25deg); }
  78% { transform: translate(-1.4px, -1px); }
}
@keyframes gv2-id-frost-wash-in {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes gv2-id-nucleate {
  0% { opacity: 0; transform: scale(0); }
  55% { opacity: 1; transform: scale(1.5); }
  100% { opacity: .85; transform: scale(1); }
}
@keyframes gv2-id-nucleate-twinkle {
  0%,100% { opacity: .55; }
  50% { opacity: 1; }
}
/* gv2-id-frost-draw / -creep / -halo-bloom / -stipple keyframes removed 2026-06-08:
   the FF surface now renders static and its entrance is the gv2-id-frost-reveal mask
   sweep, so the old per-element draw-in / creep / bloom keyframes are unreferenced. */

/* ─── FH styles ──────────────────────────────────────────────── */

.gv2-id-fh .gv2-id-sky {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 130% 90% at 50% -15%, rgba(30,6,3,.98), rgba(18,4,2,.78) 45%, transparent 78%);
  animation: gv2-id-sky-darken 2.8s ease-out forwards;
}
.gv2-id-fh .gv2-id-sky-red {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 100% 65% at 50% -10%, rgba(255,80,30,.5), transparent 65%);
  animation: gv2-id-sky-charge-red 3s ease-in forwards;
  mix-blend-mode: screen;
  opacity: 0;
}
.gv2-id-fh .gv2-id-vignette {
  position: absolute; inset: 0;
  animation: gv2-id-vignette-build 3s ease-in forwards;
  pointer-events: none;
}
.gv2-id-fh .gv2-id-umbra {
  position: absolute; left: 50%; top: 60%;
  width: 460px; height: 300px;
  pointer-events: none;
}
.gv2-id-umbra-outer {
  position: absolute; left: 50%; top: 50%;
  width: 100%; height: 100%;
  transform: translate(-50%,-50%) scale(.12);
  background: radial-gradient(ellipse 50% 50% at 50% 50%, rgba(2,1,1,.92) 0%, rgba(10,4,2,.7) 30%, rgba(20,6,3,.42) 55%, rgba(30,10,6,.18) 75%, transparent 90%);
  animation: gv2-id-umbra-grow-outer 2.9s cubic-bezier(.42,.0,.42,1) forwards;
}
.gv2-id-umbra-inner {
  position: absolute; left: 50%; top: 50%;
  width: 55%; height: 55%;
  transform: translate(-50%,-50%) scale(.18);
  background: radial-gradient(ellipse, rgba(0,0,0,.95) 0%, rgba(20,4,2,.7) 45%, transparent 78%);
  animation: gv2-id-umbra-grow-inner 2.9s cubic-bezier(.42,.0,.42,1) .35s forwards;
  filter: blur(2px);
}
.gv2-id-umbra-glow {
  position: absolute; left: 50%; top: 50%;
  width: 30%; height: 30%;
  transform: translate(-50%,-50%);
  background: radial-gradient(circle, rgba(255,80,30,.5), transparent 70%);
  mix-blend-mode: screen;
  animation: gv2-id-umbra-breathe 1.6s ease-in-out infinite;
}
.gv2-id-ground-ripple {
  position: absolute; left: 50%; top: 50%;
  width: 120%; height: 60%;
  border-radius: 50%;
  border: 1.5px solid rgba(140,82,42,.55);
  box-shadow: 0 0 14px rgba(143,116,208,.35), inset 0 0 14px rgba(255,120,40,.18);
  transform: translate(-50%,-50%) scale(.18);
  opacity: 0;
  animation: gv2-id-ground-ripple 2.4s cubic-bezier(.2,.7,.4,1) infinite;
}
.gv2-id-shimmer {
  position: absolute; left: 50%; bottom: 55%;
  width: 140px; height: 220px;
  transform: translateX(-50%);
  overflow: hidden;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: .85;
}
.gv2-id-shimmer > div {
  position: absolute; left: 0; right: 0;
  height: 30%;
  background: linear-gradient(180deg, transparent, rgba(255,170,90,.16) 50%, transparent);
  filter: blur(2.2px);
}
.gv2-id-ember {
  position: absolute;
  border-radius: 50%;
  will-change: transform, opacity;
}
.gv2-id-dust {
  position: absolute;
  background: linear-gradient(90deg, rgba(255,140,60,0), rgba(255,180,90,.85) 70%, rgba(255,220,150,1));
  box-shadow: 0 0 6px rgba(255,140,60,.6);
  opacity: 0;
  mix-blend-mode: screen;
  will-change: transform, opacity;
}

/* FH card tremor — uses :has() to scope from the pa (drama root is a CHILD
   of pa, not an ancestor of the fan). PR 3 review caught this — the original
   `.gv2-id-tremoring [data-gv2-fan] .gv2-fan-card` descendant selector was
   structurally wrong (root and fan are siblings inside the pa).
   Survives renderFan re-render because `.gv2-id-tremoring` lives on the
   drama root inside pa, and `:has()` re-evaluates on DOM changes.
   Per-card stagger reads --gv2-id-card-i (set inline in render-fan.js + render-actions-row.js). */
.gv2-pa:has(.gv2-id-tremoring) [data-gv2-fan] .gv2-fan-card {
  animation: gv2-id-card-tremor-heavy 0.4s ease-in-out infinite;
  animation-delay: calc(var(--gv2-id-card-i, 0) * 40ms);
}
.gv2-pa:has(.gv2-id-tremoring) [data-gv2-actions-row] .gv2-ar-card {
  animation: gv2-id-card-tremor-heavy 0.42s ease-in-out infinite;
  animation-delay: calc(var(--gv2-id-card-i, 0) * 50ms);
}

/* ─── FF styles ──────────────────────────────────────────────── */

.gv2-id-ff .gv2-id-frost-wash {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 110% 90% at 50% 50%, rgba(232,242,250,.07), rgba(200,222,238,.05) 60%, rgba(170,196,218,.04) 100%);
  mix-blend-mode: screen;
  opacity: 0;
  animation: gv2-id-frost-wash-in 2.8s ease-in forwards;
}
.gv2-id-ff .gv2-id-frost-edge {
  position: absolute; inset: 0;
  box-shadow: inset 0 0 50px rgba(220,232,245,.18), inset 0 0 110px rgba(190,212,232,.10);
  opacity: 0;
  animation: gv2-id-frost-wash-in 3s ease-in forwards;
}
.gv2-id-frost-dendrites {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
/* Part A (2026-06-14): the frost surface is now a PERSISTENT direct `.gv2-pa` child
   (built once per side, shown/hidden per cast) instead of living inside `.gv2-id-root`
   (z-index:50). Restore that exact layer here so the frost stays OVER the cards and
   `.gv2-fo` (z:1) and UNDER the score plaque `.gv2-sp` (z:51) — see the stack note at
   the `.gv2-sp` rule. Without this the surface would stack at auto (≈0) and render
   behind the cards. The class sits on the `<svg class="gv2-id-frost-dendrites">` itself
   (so `.gv2-id-frost-dendrites.gv2-id-frost-revealing` still matches). */
.gv2-id-frost-surface {
  z-index: 50;
}
/* QA 2026-06-08 FF jank fix (Mechanism Fix 1): live-entrance reveal sweep.
   The frost SVG now renders STATIC (filters rasterize once); the entrance is this
   radial mask growing from the surface center outward — a COMPOSITOR-only animation
   over the cached filter output, so the per-frame feDisplacementMap/rime re-raster
   that caused the "frost spread" stutter is gone. At the keyframe end the mask grows
   far past the surface bounds (every corner inside the solid core) and `forwards`
   holds it there, so the settled surface is fully shown = pixel-identical to the old
   design. Non-live (baseline / reduced-motion) omits this class → instant static. */
.gv2-id-frost-dendrites.gv2-id-frost-revealing {
  -webkit-mask-image: radial-gradient(circle at 50% 46%, #000 72%, rgba(0,0,0,0) 92%);
          mask-image: radial-gradient(circle at 50% 46%, #000 72%, rgba(0,0,0,0) 92%);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-size: 8% 8%;       mask-size: 8% 8%;
  animation: gv2-id-frost-reveal 2.6s cubic-bezier(.33, 0, .4, 1) forwards;
}
@keyframes gv2-id-frost-reveal {
  from { -webkit-mask-size: 8% 8%;     mask-size: 8% 8%; }
  to   { -webkit-mask-size: 280% 280%; mask-size: 280% 280%; }
}
@media (prefers-reduced-motion: reduce) {
  /* No sweep under reduced motion — the static surface shows at once. */
  .gv2-id-frost-dendrites.gv2-id-frost-revealing {
    -webkit-mask-image: none; mask-image: none;
    animation: none;
  }
}
.gv2-id-nucleation {
  position: absolute;
  border-radius: 50%;
  background: rgba(248,252,255,.95);
  opacity: 0;
  will-change: transform, opacity;
}

/* ─── Suppress entry on baseline events (matchStart, gameRejoin, etc) ─── */
.gv2-id-no-entry .gv2-id-sky,
.gv2-id-no-entry .gv2-id-sky-red,
.gv2-id-no-entry .gv2-id-vignette,
.gv2-id-no-entry .gv2-id-frost-wash,
.gv2-id-no-entry .gv2-id-frost-edge {
  animation-duration: 0s; /* snap to final state, skip the build-in */
}

/* ─── Reduced-motion fallback ──────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  /* No particles. NOTE: .gv2-id-frost-dendrites is intentionally NOT hidden —
     it now carries the entire FF frost surface (rime + ferns + halos + stipple),
     which instant-drama.js renders STATIC (no animation) under reduced motion via
     its isLive=false branch. Hiding it would leave reduced-motion users with no
     frost. */
  .gv2-id-ember,
  .gv2-id-dust,
  .gv2-id-nucleation,
  .gv2-id-ground-ripple,
  .gv2-id-shimmer {
    display: none;
    will-change: auto;
  }
  /* No tremor */
  .gv2-pa:has(.gv2-id-tremoring) [data-gv2-fan] .gv2-fan-card,
  .gv2-pa:has(.gv2-id-tremoring) [data-gv2-actions-row] .gv2-ar-card {
    animation: none;
  }
  /* No umbra grow */
  .gv2-id-umbra-outer, .gv2-id-umbra-inner {
    animation: none;
    transform: translate(-50%,-50%) scale(1);
  }
  .gv2-id-umbra-glow { animation: none; }
  /* Instant tint, no keyframe build */
  .gv2-id-sky,
  .gv2-id-sky-red,
  .gv2-id-vignette,
  .gv2-id-frost-wash,
  .gv2-id-frost-edge {
    animation: none;
    opacity: 1;
  }
}

/* ─────────────────────────────────────────────────────────────────
   PR 3 — Instant-card defender drama (impact + aftermath phases)
   See: public/js/game-v2/instant-drama.js
   Impact (~950ms FH / ~350ms FF) and aftermath (~4800ms FH / persistent FF)
   ───────────────────────────────────────────────────────────────── */

/* ─── Shared layer positioning ─────────────────────────────── */
.gv2-id-impact-layer,
.gv2-id-aftermath-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* ─── Charred / frozen filter on defender cards ────────────── */
/* Uses :has() — drama root is a child of pa, not an ancestor of the fan. */
.gv2-pa:has(.gv2-id-charred-target) [data-gv2-fan] .gv2-fan-card,
.gv2-pa:has(.gv2-id-charred-target) [data-gv2-actions-row] .gv2-ar-card {
  filter: brightness(.55) saturate(.65) sepia(.5) hue-rotate(-15deg) contrast(1.05);
  transition: filter .8s ease-out;
}
.gv2-pa:has(.gv2-id-frozen-target) [data-gv2-fan] .gv2-fan-card,
.gv2-pa:has(.gv2-id-frozen-target) [data-gv2-actions-row] .gv2-ar-card {
  filter: brightness(.94) saturate(.55) contrast(.96);
  transition: filter .8s ease-out;
}

/* ─── Shake on defender pa during FH impact ────────────────── */
@keyframes gv2-id-shake-heavy {
  0%,100% { transform: translate(0,0); }
  4% { transform: translate(-10px, 6px) rotate(-.3deg); }
  9% { transform: translate(8px, -7px) rotate(.3deg); }
  14% { transform: translate(-7px, 6px); }
  20% { transform: translate(6px, 6px) rotate(-.2deg); }
  27% { transform: translate(-5px, -4px); }
  34% { transform: translate(5px, 4px); }
  42% { transform: translate(-4px, 4px) rotate(.15deg); }
  50% { transform: translate(3px, -3px); }
  58% { transform: translate(-3px, 2px); }
  66% { transform: translate(2px, 2px); }
  74% { transform: translate(-2px, -1px); }
  82% { transform: translate(1px, 1px); }
  90% { transform: translate(-1px, 1px); }
}
.gv2-pa.gv2-id-shaking {
  animation: gv2-id-shake-heavy .9s cubic-bezier(.22,.61,.36,1);
}
/* Portrait redesign PR 3: at portrait-1v1 the OWN panel contains the fixed
   HIT/STAND bar — the 950ms panel transform would re-scope it (~78px
   teleport when FH lands on you). Re-target the impact shake to the fan
   (CSS-only; instant-drama.js untouched): panel animation off, same
   keyframes on the fan child. */
@media (max-width: 767px) and (orientation: portrait) {
  .gv2-page:not([data-mode="campaign"]) .gv2-pa.gv2-id-shaking { animation: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa.gv2-id-shaking [data-gv2-fan] {
    animation: gv2-id-shake-heavy .9s cubic-bezier(.22,.61,.36,1);
  }
}

/* ─── FH impact ────────────────────────────────────────────── */
@keyframes gv2-id-impact-flash {
  0% { opacity: 0; }
  8% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes gv2-id-shockwave {
  0% { transform: translate(-50%,-50%) scale(.12); opacity: 1; border-width: 6px; filter: blur(0); }
  60% { opacity: .55; }
  100% { transform: translate(-50%,-50%) scale(3.6); opacity: 0; border-width: 0; filter: blur(2px); }
}
@keyframes gv2-id-burst-ring {
  0% { transform: translate(-50%,-50%) scale(.2); opacity: 1; }
  100% { transform: translate(-50%,-50%) scale(3); opacity: 0; }
}
@keyframes gv2-id-debris-fly {
  0% { transform: translate(-50%,-50%) translate(0,0) rotate(var(--gv2-id-rot, 0deg)) scale(.4); opacity: 0; }
  10% { opacity: 1; }
  100% { transform: translate(-50%,-50%) translate(var(--gv2-id-tx, 0), var(--gv2-id-ty, 0)) rotate(calc(var(--gv2-id-rot, 0deg) + 540deg)) scale(1); opacity: 0; }
}
@keyframes gv2-id-spall-pop {
  0% { transform: translate(-50%,-50%) translate(0,0) scale(.2) rotate(0); opacity: 0; }
  15% { opacity: 1; }
  100% { transform: translate(-50%,-50%) translate(var(--gv2-id-sx, 0), var(--gv2-id-sy, 0)) scale(1) rotate(var(--gv2-id-sr, 180deg)); opacity: .8; }
}

.gv2-id-flash {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 80% 60% at 50% 58%, rgba(255,250,220,1), rgba(255,160,70,.85) 25%, rgba(220,60,40,.55) 55%, transparent 85%);
  animation: gv2-id-impact-flash 900ms ease-out forwards;
  mix-blend-mode: screen;
  z-index: 4;
}
.gv2-id-shockwave {
  position: absolute; left: 50%; top: 60%;
  width: 100px; height: 100px;
  border-radius: 50%;
  border: 5px solid rgba(255,200,120,.95);
  box-shadow: 0 0 50px rgba(255,140,60,.9), inset 0 0 30px rgba(255,140,60,.7);
  transform: translate(-50%,-50%) scale(.12);
  animation: gv2-id-shockwave 950ms cubic-bezier(.18,.74,.34,1) forwards;
  z-index: 5;
}
.gv2-id-shockwave-secondary {
  border: 3px solid rgba(255,160,80,.85);
  box-shadow: 0 0 30px rgba(255,140,60,.6);
  animation: gv2-id-shockwave 950ms cubic-bezier(.18,.74,.34,1) 200ms forwards;
}
.gv2-id-burst-ring {
  position: absolute; left: 50%; top: 58%;
  width: 70px; height: 70px;
  transform: translate(-50%,-50%) scale(.2);
  border-radius: 50%;
  border-style: solid;
  border-color: rgba(255,180,80,.85);
  box-shadow: 0 0 36px rgba(255,140,60,.8);
  opacity: 0;
  animation: gv2-id-burst-ring 800ms ease-out forwards;
}
.gv2-id-debris {
  position: absolute; left: 50%; top: 58%;
  opacity: 0;
  will-change: transform, opacity;
  box-shadow: 0 0 6px rgba(255,100,40,.7);
  z-index: 5;
}
.gv2-id-spall {
  position: absolute; left: 50%; top: 60%;
  opacity: 0;
  box-shadow: 0 0 6px rgba(255,140,60,.7);
  will-change: transform, opacity;
  z-index: 5;
}

/* ─── FF impact: shimmer sweep + bloom ─────────────────────── */
@keyframes gv2-id-shimmer-sweep {
  0% { transform: translateX(-60%) rotate(-22deg); opacity: 0; }
  18% { opacity: 1; }
  82% { opacity: 1; }
  100% { transform: translateX(360%) rotate(-22deg); opacity: 0; }
}
@keyframes gv2-id-frost-bloom {
  0% { filter: brightness(1); opacity: 0; }
  35% { filter: brightness(1.45); opacity: 1; }
  100% { filter: brightness(1); opacity: 0; }
}
.gv2-id-shimmer-sweep {
  position: absolute; top: 0; bottom: 0;
  left: 0; width: 22%;
  background: linear-gradient(90deg, transparent 0%, rgba(220,238,252,.10) 25%, rgba(250,253,255,.55) 50%, rgba(220,238,252,.10) 75%, transparent 100%);
  mix-blend-mode: screen;
  filter: blur(2.5px);
  opacity: 0;
  animation: gv2-id-shimmer-sweep 350ms cubic-bezier(.4,.0,.6,1) forwards;
}
.gv2-id-frost-bloom {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 90% 70% at 50% 50%, rgba(232,244,254,.28), rgba(200,220,240,.10) 55%, transparent 80%);
  mix-blend-mode: screen;
  opacity: 0;
  animation: gv2-id-frost-bloom 350ms ease-out forwards;
}

/* ─── FH aftermath: crater + fissures + rising embers + ash ─ */
@keyframes gv2-id-crater-reveal {
  0% { opacity: 0; transform: translate(-50%,-50%) scale(.5); }
  20% { opacity: 1; transform: translate(-50%,-50%) scale(1.1); }
  100% { opacity: .9; transform: translate(-50%,-50%) scale(1); }
}
@keyframes gv2-id-fissure-grow {
  0% { stroke-dashoffset: 600; opacity: 0; }
  10% { opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 1; }
}
@keyframes gv2-id-ring-fracture {
  0% { stroke-dashoffset: 800; opacity: 0; }
  20% { opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 1; }
}
@keyframes gv2-id-ember-float {
  0% { transform: translateY(0) translateX(var(--gv2-id-drift, 0px)); opacity: 0; }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% { transform: translateY(-160%) translateX(calc(var(--gv2-id-drift, 0px) + 6px)); opacity: 0; }
}
@keyframes gv2-id-ash-drift {
  0% { transform: translateY(-15%) translateX(var(--gv2-id-ax, 0px)) rotate(0); opacity: 0; }
  10% { opacity: 1; }
  100% { transform: translateY(120%) translateX(calc(var(--gv2-id-ax, 0px) + 26px)) rotate(220deg); opacity: 0; }
}
@keyframes gv2-id-ember-tint-fade {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

.gv2-id-ember-tint {
  position: absolute; inset: 0;
  background: radial-gradient(ellipse 90% 70% at 50% 58%, rgba(217,136,79,.32), rgba(143,116,208,.16) 50%, transparent 80%);
  opacity: 0;
  animation: gv2-id-ember-tint-fade 1.4s ease-out 300ms forwards;
  mix-blend-mode: screen;
}
.gv2-id-crater {
  position: absolute; left: 50%; top: 60%;
  width: 540px; height: 340px;
  transform: translate(-50%,-50%);
  pointer-events: none;
  animation: gv2-id-crater-reveal 900ms cubic-bezier(.22,.7,.34,1) 240ms both;
}
.gv2-id-fissures {
  position: absolute; left: 50%; top: 60%;
  width: 540px; height: 340px;
  transform: translate(-50%,-50%);
  pointer-events: none;
}
.gv2-id-rising-embers {
  position: absolute; inset: 0;
  opacity: 0;
  animation: gv2-id-ember-tint-fade 1.6s ease-out 700ms forwards;
}
.gv2-id-rising-ember {
  position: absolute; bottom: 8%;
  border-radius: 50%;
  will-change: transform, opacity;
}
.gv2-id-ash-drift {
  position: absolute; inset: 0;
  opacity: 0;
  animation: gv2-id-ember-tint-fade 2s ease-out 1100ms forwards;
}
.gv2-id-ash-flake {
  position: absolute; top: 0;
  will-change: transform, opacity;
}

/* QA #10 (2026-05-31): fade the FH impact aftermath out shortly after it forms /
   the card migrates up to the hand, gone within ~1.5s — instead of holding at full
   visibility until the 4800ms tearDown (the "crater lingers too long" bug). The fade
   is on the FH-only marker class (.gv2-id-aftermath-fh), so FF's frost aftermath —
   which shares .gv2-id-aftermath-layer and must persist until next round — is left
   alone. Fading the parent's opacity cross-fades crater + ember-tint + fissures +
   embers together (no "half-finished" look) while preserving each child's own
   reveal + the crater's per-instance transform. Delay (1400ms) sits just past the
   crater-reveal completion (~1140ms) so the impact is fully formed first. The 4800ms
   tearDown still removes the now-invisible DOM. Shared 1v1 + campaign. Not gated by
   reduced-motion: an opacity fade-out (not large motion) also fixes the lingering
   for those users, and composes with the reduced-motion static crater above. */
.gv2-id-aftermath-fh {
  animation: gv2-id-aftermath-fade 1.5s ease-out 1400ms forwards;
}
@keyframes gv2-id-aftermath-fade {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ─── FF aftermath: persistent frost edge + nucleation twinkle ─ */
.gv2-id-frost-edge.gv2-id-persistent {
  position: absolute; inset: 0;
  box-shadow: inset 0 0 55px rgba(195,218,236,.22), inset 0 0 14px rgba(225,237,247,.30);
  opacity: 1;
  pointer-events: none;
}

/* ─── Reduced-motion fallback (PR 3 additions) ─────────────── */
@media (prefers-reduced-motion: reduce) {
  .gv2-id-flash,
  .gv2-id-shockwave,
  .gv2-id-burst-ring,
  .gv2-id-debris,
  .gv2-id-spall,
  .gv2-id-shimmer-sweep,
  .gv2-id-frost-bloom,
  .gv2-id-rising-embers,
  .gv2-id-ash-drift {
    display: none;
    will-change: auto;
  }
  .gv2-pa.gv2-id-shaking { animation: none; }
  /* portrait redesign PR 3 fan re-target of the impact shake — higher
     specificity than the panel rule above, needs its own reduce kill */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa.gv2-id-shaking [data-gv2-fan] { animation: none; }
  .gv2-id-crater { animation: none; opacity: .9; transform: translate(-50%,-50%); }
  .gv2-id-fissures path { animation: none; opacity: 1; stroke-dashoffset: 0; }
  .gv2-id-ember-tint { animation: none; opacity: 1; }
}

/* ─────────────────────────────────────────────────────────────────
   PR 4 — Forced Hit card slam-hold-slide
   See: public/js/game-v2/render-deal-animation.js#flyForcedHitCard
   Slam 500ms → hold 900ms → slide 700ms (~2.1s total)
   ───────────────────────────────────────────────────────────────── */

/* Slam phase: deck → impact. Uses a dramatic cubic-bezier curve.
   NOTE on transition vs animation precedence: both target transform. CSS
   animations win over transitions on the same property for the animation's
   duration, so the keyframe drives scale/rotate during slam. `forwards` fill
   holds the end state (scale 1, rotate 0) until the slam class is removed at
   t=1400ms — at which point transform reverts cleanly to identity (matches
   keyframe end state, no visual jump). Fragile to keyframe edits: if the
   keyframe end-state ever differs from identity, the slide-phase visual
   baseline shifts. */
.gv2-fly.gv2-fly-fh-slam {
  transition: left 500ms cubic-bezier(.32,.05,.2,1),
              top 500ms cubic-bezier(.32,.05,.2,1),
              transform 500ms cubic-bezier(.32,.05,.2,1);
  animation: gv2-fly-fh-slam-feel 500ms cubic-bezier(.32,.05,.2,1) forwards;
  z-index: 9500; /* above fly default (.gv2-fly is body-mounted); ensures slam visible above PR 3 crater */
}
@keyframes gv2-fly-fh-slam-feel {
  0% { transform: scale(1.4) rotate(-8deg); filter: drop-shadow(0 0 16px rgba(255,140,60,.6)); }
  60% { transform: scale(1.1) rotate(2deg); filter: drop-shadow(0 0 12px rgba(255,180,80,.55)); }
  100% { transform: scale(1) rotate(0); filter: drop-shadow(0 0 6px rgba(255,140,60,.35)); }
}

/* Slide phase: impact → hand slot. Smoother curve, no drop-shadow. */
.gv2-fly.gv2-fly-fh-slide {
  transition: left 700ms cubic-bezier(.22,.7,.34,1),
              top 700ms cubic-bezier(.22,.7,.34,1);
  z-index: 9500;
}

/* Reduced-motion: slam classes still match but disable the keyframe.
   shouldSlamForcedHit() ALSO gates on reduced-motion (returning false →
   straight flyOneHitCard path), so this CSS is defense-in-depth. */
@media (prefers-reduced-motion: reduce) {
  .gv2-fly.gv2-fly-fh-slam { animation: none; transition: left 250ms linear, top 250ms linear; }
  .gv2-fly.gv2-fly-fh-slide { transition: left 250ms linear, top 250ms linear; }
}

/* ─── PR B (Bug 5): FH card flips face-down → face-up during the approach ───
   First 3D-transform card in v2. Strict 3-node contract:
     container (.gv2-fly-fh-slam/-slide, made transparent) →
     .gv2-fly-fh-card3d (perspective + rotateY, preserve-3d) →
     two absolute backface-hidden faces.
   The perspective lives in card3d's OWN transform (not as a property on the
   outer element, which runs the scale/rotate slam-feel keyframe and would
   flatten its children). The FRONT-showing state is card3d's BASE transform
   (rotateY 180) — NOT animation-fill-mode — because the flip keyframe is gated
   by the .gv2-fly-fh-slam class, which is removed at the t=1400 slide handoff;
   once removed, fill-mode stops applying and the inner reverts to its base.
   Base = rotateY(180) = front, identical to the keyframe end, so the slide
   shows the front with no snap-to-back. */
.gv2-fly.gv2-fly-fh-slam,
.gv2-fly.gv2-fly-fh-slide {
  background: none;
  border: none;
  box-shadow: none;
}
.gv2-fly-fh-card3d {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transform: perspective(600px) rotateY(180deg); /* base: front showing */
}
.gv2-fly.gv2-fly-fh-slam .gv2-fly-fh-card3d {
  animation: gv2-fly-fh-flip 500ms cubic-bezier(.32,.05,.2,1) forwards;
}
@keyframes gv2-fly-fh-flip {
  0%   { transform: perspective(600px) rotateY(0deg); }   /* back showing */
  100% { transform: perspective(600px) rotateY(180deg); } /* front showing */
}
.gv2-fly-fh-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  overflow: hidden;
}
/* Back face: the dark card-back look migrated from base .gv2-fly (which is now
   transparent for the FH variant). Kept byte-identical to the original. */
.gv2-fly-fh-face-back {
  border: 1px solid var(--brass-lo);
  border-top: 2px solid var(--brass);
  background:
    repeating-linear-gradient(45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    repeating-linear-gradient(-45deg, rgba(183,162,122,.08) 0 4px, transparent 4px 8px),
    linear-gradient(180deg, #181221, #0c0a11);
  box-shadow: 0 6px 14px rgba(0,0,0,.6), inset 0 0 14px rgba(0,0,0,.5);
}
.gv2-fly-fh-face-front {
  transform: rotateY(180deg);
}
@media (prefers-reduced-motion: reduce) {
  /* shouldSlamForcedHit already routes reduced-motion FH through plain
     flyOneHitCard (no flip element built). Defense-in-depth: no flip here. */
  .gv2-fly.gv2-fly-fh-slam .gv2-fly-fh-card3d { animation: none; }
}

/* ──────────────────────────────────────────────────────────────────────
   Options-bar (DeckControls) wiring — deck-controls.js (2026-05-31)
   Mute SFX / Mute Opp toggle states, quick-chat popover + bubble, report
   modal. z-index band 8000–8800: above the board, below the 8900+ blocking
   stack (disconnect banner 8900, match-end 9100, choosers 9150, countdown
   9300, boon overlays 9500, error toast 10000).
   ────────────────────────────────────────────────────────────────────── */
.gv2-dctl-btn.is-active {
  color: var(--ink);
  background: var(--brass-hi);
  border-color: var(--brass-hi);
  box-shadow: 0 0 8px 0 rgba(224,196,137,.45);
}
/* QA #5 (2026-05-31): the master SFX-mute button (♪) shows MUTED as a red slash through
   the note instead of the ambiguous gold highlight. Scoped to [data-act="mutesfx"] ONLY,
   so the separate mute-opponent button keeps the shared gold .is-active highlight above.
   Must revert color too: the shared rule sets color:var(--ink) (near-black), which would
   render the ♪ invisible on the reverted dark button. */
.gv2-dctl-btn[data-act="mutesfx"].is-active {
  position: relative;
  color: var(--brass-hi);
  background: rgba(20,16,10,.7);
  border-color: #2a2520;
  box-shadow: none;
}
.gv2-dctl-btn[data-act="mutesfx"].is-active::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 80%; height: 2px;
  background: #e05454;
  border-radius: 1px;
  transform: translate(-50%, -50%) rotate(-45deg);
  pointer-events: none;
}
.gv2-dctl-btn.is-cooling { opacity: .4; cursor: default; }
/* Voice (P5) button states: waiting for the peer (pulse), self-muted (oxblood). */
.gv2-dctl-btn.is-voice-waiting { color: var(--brass-hi); animation: emberpulse 1.4s infinite; }
.gv2-dctl-btn.is-voice-muted { color: #fff; background: var(--oxblood); border-color: var(--oxblood-hi); }
@media (prefers-reduced-motion: reduce) { .gv2-dctl-btn.is-voice-waiting { animation: none; } }

/* Quick-chat popover (body-mounted, positioned via JS rect) */
.gv2-qc-pop {
  position: fixed; z-index: 8600; max-width: 280px;
  padding: 10px; border: 1px solid var(--brass-lo); border-radius: 8px;
  background: var(--ink-2); box-shadow: 0 8px 26px rgba(0,0,0,.55);
}
.gv2-qc-phrases { display: flex; flex-wrap: wrap; gap: 6px; }
.gv2-qc-emotes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
  padding-top: 8px; border-top: 1px solid rgba(138,109,44,.4); }
.gv2-qc-item {
  cursor: pointer; font-size: 13px; padding: 6px 10px; border-radius: 6px;
  color: var(--vellum); background: var(--ink-3); border: 1px solid var(--brass-lo);
}
.gv2-qc-item:hover { background: var(--oxblood); border-color: var(--oxblood-hi); color: #fff; }
.gv2-qc-emote {
  cursor: pointer; font-size: 20px; line-height: 1; padding: 4px 6px; border-radius: 6px;
  background: var(--ink-3); border: 1px solid var(--brass-lo);
}
.gv2-qc-emote:hover { background: var(--oxblood); border-color: var(--oxblood-hi); }

/* ---- Board overflow menu (⋯) — BoardChrome trigger + body-mounted popover (Batch B) ---- */
.gv2-bc-menu {
  padding: 8px 12px; background: transparent;
  border: 1px solid var(--brass-lo); color: var(--vellum-dim);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: transform .12s ease;
}
.gv2-bc-menu:hover { color: var(--brass-hi); }
.gv2-bc-menu:active { transform: scale(.95); }
.gv2-bc-menu-icon { font-size: 18px; line-height: 1; letter-spacing: .12em; }

.gv2-board-menu-pop {
  position: fixed; z-index: 8600; min-width: 184px; max-width: 240px;
  padding: 6px; border: 1px solid var(--brass-lo); border-radius: 8px;
  background: var(--ink-2); box-shadow: 0 8px 26px rgba(0,0,0,.55);
  display: flex; flex-direction: column; gap: 2px;
}
/* Menu rows. mute/voice/report rows ALSO carry .gv2-dctl-btn (for the shared handler +
   is-active state) — override its 1:1 icon-button shape into a full-width row here. The
   pop is body-mounted (outside .gv2-page), so only the base .gv2-dctl-btn matches it;
   the iOS voice-hide (html.bja-ios .gv2-dctl-btn[data-act="voice"]) still applies. */
.gv2-board-menu-pop .gv2-bm-item {
  width: 100%; aspect-ratio: auto; box-sizing: border-box;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; border-radius: 6px; cursor: pointer; text-align: left;
  background: var(--ink-3); border: 1px solid transparent; color: var(--vellum);
  font-size: 13px;
}
.gv2-board-menu-pop .gv2-bm-item:hover { background: var(--oxblood); border-color: var(--oxblood-hi); color: #fff; }
.gv2-board-menu-pop .gv2-bm-glyph { flex: 0 0 18px; width: 18px; text-align: center; font-size: 15px; line-height: 1; }
.gv2-board-menu-pop .gv2-bm-label { flex: 1 1 auto; }
.gv2-board-menu-pop .gv2-bm-item.is-active { border-color: var(--brass-lo); color: var(--brass-hi); }
/* The mute-SFX active slash (::after) was sized for the old 44px square icon — on a
   full-width menu row it draws a long diagonal across the whole row. Suppress it in the
   menu; the .is-active brass border/color above already signals the muted state. */
.gv2-board-menu-pop .gv2-dctl-btn[data-act="mutesfx"].is-active::after { display: none; }
.gv2-bm-divider { height: 1px; margin: 4px 2px; background: rgba(138,109,44,.4); }
.gv2-board-menu-pop .gv2-bm-danger { color: var(--oxblood-hi); }
.gv2-board-menu-pop .gv2-bm-danger .gv2-bm-glyph { color: var(--oxblood-hi); }
.gv2-board-menu-pop .gv2-bm-danger:hover { background: var(--oxblood); color: #fff; }
.gv2-board-menu-pop .gv2-bm-danger:hover .gv2-bm-glyph { color: #fff; }

/* ✉ quick-chat button beside the local player's name (Batch B; moved out of the retired
   DeckControls cluster). Carries .gv2-dctl-btn for the shared open + cooldown handlers;
   .gv2-page scoping out-specifies the base .gv2-dctl-btn aspect-ratio:1. */
.gv2-page .gv2-ph-qc {
  flex: 0 0 auto; aspect-ratio: auto;
  /* The ✉ carries .gv2-dctl-btn, so the (now-orphaned, cluster-was-removed) mobile
     rule `.gv2-page .gv2-dctl-btn{min-*:44px}` would force it to a 44×44 square and
     inflate the header row. Neutralize the min-* here (this rule is later in source,
     same specificity, so it wins on mobile too). */
  min-height: 0; min-width: 0;
  padding: 3px 7px; font-size: 13px; line-height: 1; border-radius: 6px;
  background: rgba(20,16,10,.7); border: 1px solid #2a2520; color: var(--brass-hi);
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
}
.gv2-page .gv2-ph-qc:hover { border-color: var(--brass-lo); }
.gv2-page .gv2-ph-qc.is-cooling { opacity: .5; }

/* Incoming quick-chat bubble (over the sender's player area) */
.gv2-qc-bubble {
  position: fixed; z-index: 8400; max-width: 240px;
  padding: 7px 12px; border-radius: 12px;
  font-family: 'EB Garamond', Georgia, serif; font-size: 15px; font-style: italic;
  color: var(--ink); background: var(--vellum); border: 1px solid var(--brass-lo);
  box-shadow: 0 4px 14px rgba(0,0,0,.45);
  opacity: 0; transform: translateY(4px); transition: opacity .18s ease, transform .18s ease;
  animation: gv2-qc-bubble-in .18s ease forwards; pointer-events: none;
}
.gv2-qc-bubble-emote { font-size: 26px; padding: 4px 10px; }
.gv2-qc-bubble-out { opacity: 0 !important; transform: translateY(-4px); }
@keyframes gv2-qc-bubble-in { to { opacity: 1; transform: translateY(0); } }

/* Report modal (no-op submit; backend filed as follow-up) */
.gv2-report-backdrop {
  position: fixed; inset: 0; z-index: 8700;
  background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center;
}
.gv2-report-card {
  position: relative; width: min(92vw, 380px); padding: 22px 20px 18px;
  background: var(--ink-2); border: 1px solid var(--brass-lo); border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0,0,0,.6); text-align: center;
}
.gv2-report-x {
  position: absolute; top: 8px; right: 10px; width: 32px; height: 32px;
  background: none; border: none; color: var(--vellum-dim); font-size: 22px; cursor: pointer;
}
.gv2-report-x:hover { color: var(--vellum); }
.gv2-report-title { font-size: 22px; color: var(--brass-hi); margin-bottom: 4px; }
.gv2-report-sub { font-size: 13px; color: var(--vellum-dim); margin-bottom: 16px; }
.gv2-report-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-bottom: 18px; }
.gv2-report-chip {
  cursor: pointer; font-size: 13px; padding: 7px 12px; border-radius: 18px;
  color: var(--vellum); background: var(--ink-3); border: 1px solid var(--brass-lo);
}
.gv2-report-chip.is-selected { background: var(--oxblood); border-color: var(--oxblood-hi); color: #fff; }
.gv2-report-submit {
  cursor: pointer; font-size: 14px; padding: 9px 18px; border-radius: 6px;
  color: var(--ink); background: var(--brass-hi); border: 1px solid var(--brass-hi);
}
.gv2-report-submit:disabled { opacity: .45; cursor: default; }
@media (prefers-reduced-motion: reduce) {
  .gv2-qc-bubble { animation: none; opacity: 1; transform: none; transition: opacity .18s ease; }
}

/* ──────────────────────────────────────────────────────────────────────
   Game log — game-log.js (2026-05-31). Collapsible panel appended under the
   board (.gv2-page last child), open by default, whole-match scroll + round
   dividers. Medieval vellum/brass.
   ────────────────────────────────────────────────────────────────────── */
.gv2-log {
  margin: 10px auto 0; width: 100%;
  /* QA #2 (2026-05-31): widen the log to span the full board width — it's the last
     child of the padded .gv2-page, so width:100% aligns its edges with the FootStrip
     (.gv2-board-bottom) bar directly above it, instead of the old narrow 760px cap. */
  border: 1px solid var(--brass-lo); border-radius: 8px;
  background: rgba(11,11,13,.72); overflow: hidden;
}
.gv2-log-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 14px; cursor: pointer; user-select: none;
  background: rgba(111,82,190,.28); border-bottom: 1px solid var(--brass-lo);
}
.gv2-log-title { font-size: 15px; color: var(--brass-hi); letter-spacing: .04em; }
.gv2-log-chevron { color: var(--vellum-dim); font-size: 13px; transition: transform .15s ease; }
.gv2-log-collapsed .gv2-log-chevron { transform: rotate(-90deg); }
.gv2-log-collapsed .gv2-log-body { display: none; }
/* device-QA item 9: body is a flex-column [X (sibling), #gv2-log-entries];
   the scroll moved to #gv2-log-entries so the X stays fixed top-right. display
   MUST stay on THIS base rule only — a display on the portrait .gv2-log-body
   (0,3,0) would outrank .gv2-log-collapsed .gv2-log-body{display:none} (0,2,0)
   and the default-collapsed portrait pill would render expanded. */
.gv2-log-body {
  display: flex; flex-direction: column; padding: 8px 14px;
  font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 12px; line-height: 1.5;
  color: var(--vellum-dim);
}
.gv2-log-entries { max-height: 168px; overflow-y: auto; }
/* X close: hidden by default; shown only on the portrait pill (portrait block). */
.gv2-log-close {
  display: none; align-self: flex-end;
  background: none; border: none; cursor: pointer;
  color: var(--vellum-dim); font-size: 15px; line-height: 1;
  /* generous hit area for touch (item 9 = easy close); negative margin keeps
     the glyph tight in the popover corner. */
  padding: 7px 9px; margin: -5px -6px 1px 0;
}
.gv2-log-close:hover { color: var(--brass-hi); }
.gv2-log-empty { opacity: .6; font-style: italic; }
.gv2-log-divider {
  margin: 8px 0 4px; padding-top: 6px; border-top: 1px dashed rgba(138,109,44,.4);
  color: var(--brass); font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
}
.gv2-log-divider:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.gv2-log-entry { padding: 1px 0; }
.gv2-log-deal { color: var(--vellum); }
.gv2-log-stand { color: var(--brass); }
.gv2-log-bust { color: var(--oxblood-hi); }
.gv2-log-ff { color: #6fb6d6; }
.gv2-log-result { color: var(--brass-hi); font-weight: 600; }
.gv2-log-matchend { color: var(--brass-hi); font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .gv2-log-chevron { transition: none; }
}

/* ============================================================
   LANDSCAPE BOARD — CAMPAIGN (PR 2 of 2), 2026-06-06
   Same condensed 3-column landscape layout as the 1v1 block above, scoped
   .gv2-page[data-mode="campaign"]. Placed at end-of-file so it wins source-order
   ties with the earlier campaign Phase-E media blocks. Campaign deltas vs 1v1:
   boss name/tier folds into a center row; the standalone runstrip is hidden;
   boons + consumables drop BELOW the board (vertical scroll is acceptable on
   campaign); preAction prompt pins to the top. The grid is capped to one
   viewport (board fills the screen, strips scroll into view below it).
   NOTE: sizing rules are intentionally duplicated from the 1v1 block to leave
   that merged+verified block untouched (future DRY refactor: shared scope).
   Plan: docs/plans/2026-06-06-landscape-board-campaign-plan.md
   ============================================================ */
@media (orientation: landscape) and (max-height: 500px) {
  /* Page: NOT capped — scroll allowed so boons/consumables reach below the board. */
  .gv2-page[data-mode="campaign"] {
    min-height: 100dvh;
    padding: 4px max(6px, env(safe-area-inset-right)) 4px max(6px, env(safe-area-inset-left));
    gap: 0;
  }

  /* Top bar, bottom strip, and standalone runstrip gone — folded into center. */
  .gv2-page[data-mode="campaign"] .gv2-board-top,
  .gv2-page[data-mode="campaign"] .gv2-board-bottom,
  .gv2-page[data-mode="campaign"] .gv2-c-runstrip-wrap { display: none; }

  /* preAction prompt (in-flow) pins to the very top so it's reachable. */
  .gv2-page[data-mode="campaign"] .gv2-c-preaction-surface { order: -1; }
  /* Boons + consumables drop below the board (scroll to reach). */
  .gv2-page[data-mode="campaign"] .gv2-c-boons-row { order: 2; }
  .gv2-page[data-mode="campaign"] .gv2-c-consumables-row { order: 3; }

  /* Grid capped to one viewport; board fills the screen, strips scroll below. */
  .gv2-page[data-mode="campaign"] .gv2-game-grid {
    display: grid;
    grid-template-columns: 1fr minmax(150px, 178px) 1fr;
    grid-template-rows: auto auto auto 1fr auto auto;
    grid-template-areas:
      "you c-boss  opp"
      "you c-round opp"
      "you c-time  opp"
      "you c-axis  opp"
      "you c-turn  opp"
      "you c-cede  opp";
    gap: 3px 8px;
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
    align-items: stretch;
    order: 0;
  }

  /* Player columns (campaign is always data-my-idx="0": you left, boss right). */
  .gv2-page[data-mode="campaign"] [data-gv2-player-area="0"] { grid-area: you; }
  .gv2-page[data-mode="campaign"] [data-gv2-player-area="1"] { grid-area: opp; }

  /* Center-column children → areas. */
  .gv2-page[data-mode="campaign"] [data-gv2-ls-boss] { grid-area: c-boss; }
  .gv2-page[data-mode="campaign"] [data-gv2-ls-round] { grid-area: c-round; }
  .gv2-page[data-mode="campaign"] [data-gv2-mobile-timeline] { grid-area: c-time; display: flex; margin: 0; }
  .gv2-page[data-mode="campaign"] .gv2-axis { grid-area: c-axis; min-height: 0; }
  .gv2-page[data-mode="campaign"] [data-gv2-ls-turn] { grid-area: c-turn; }
  .gv2-page[data-mode="campaign"] [data-gv2-ls-concede] { grid-area: c-cede; }
  .gv2-page[data-mode="campaign"] .gv2-mobile-footer { display: none; }

  /* Show + style the new center nodes. */
  .gv2-page[data-mode="campaign"] [data-gv2-ls-boss] {
    display: block; text-align: center; line-height: 1.1;
    font-style: italic; color: var(--brass-hi); font-size: 14px;
  }
  .gv2-page[data-mode="campaign"] [data-gv2-ls-round] {
    display: block; text-align: center;
    font-size: 9px; letter-spacing: .28em; color: var(--brass-hi);
  }
  .gv2-page[data-mode="campaign"] [data-gv2-ls-turn] { display: block; text-align: center; }
  .gv2-page[data-mode="campaign"] [data-gv2-ls-concede] {
    display: inline-flex; align-items: center; justify-content: center; gap: 5px;
    justify-self: center; padding: 5px 14px; min-height: 30px;
    background: transparent; border: 1px solid var(--brass-lo); color: var(--vellum-dim);
    font-size: 9px; letter-spacing: .18em;
  }
  .gv2-page[data-mode="campaign"] .gv2-ls-concede-icon { font-size: 11px; }

  /* Center axis: vertical column again; re-show the pieces the 3368 block hid
     (need 0,4,0 to beat the 3368 `.gv2-ca X` 0,3,0 hides). */
  .gv2-page[data-mode="campaign"] .gv2-ca {
    flex-direction: column; flex-wrap: nowrap;
    align-items: stretch; justify-content: flex-start;
    padding: 0; gap: 4px; min-height: 0;
  }
  .gv2-page[data-mode="campaign"] .gv2-ca .gv2-ca-rules {
    display: block; padding: 4px 8px; align-self: stretch;
  }
  .gv2-page[data-mode="campaign"] .gv2-ca-rules-title { font-size: 12px; line-height: 1.1; }
  .gv2-page[data-mode="campaign"] .gv2-ca-rules-target { font-size: 12px; line-height: 1.1; margin-top: 1px; }
  .gv2-page[data-mode="campaign"] .gv2-ca-rules-sub { font-size: 7px; letter-spacing: .2em; margin-top: 2px; }
  .gv2-page[data-mode="campaign"] .gv2-ca .gv2-dctl { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; }
  .gv2-page[data-mode="campaign"] .gv2-ca .gv2-dctl-btn { aspect-ratio: 1; min-height: 0; min-width: 0; font-size: 14px; }
  .gv2-page[data-mode="campaign"] .gv2-ca .gv2-ccs { display: block; padding: 3px 8px; }
  .gv2-page[data-mode="campaign"] .gv2-ca .gv2-ca-help { display: block; margin-top: 0; }
  .gv2-page[data-mode="campaign"] .gv2-ca-help-link { min-height: 0; padding: 2px 0; line-height: 1.4; }
  .gv2-page[data-mode="campaign"] .gv2-dt { height: 38px; }
  .gv2-page[data-mode="campaign"] .gv2-dc-glyph { font-size: 10px; }
  /* Campaign mirror of the 1v1 #6-redux deck fix: DeckCard.js writes --gv2-dc-w/h
     INLINE on each .gv2-dc (70x94 default), shadowing the .gv2-dt value — so sizing
     on .gv2-dt was a no-op and the oversized deck overflowed (.gv2-dt overflow:visible)
     onto the deck-controls row, hiding ♪/◉/◌. Size the card itself with !important.
     Box height (38px above) = card-h(32) + max offsetY(6). See:
     docs/solutions/ui/device-qa-batch3-deck-shrink-1v1-font-20260623.md */
  .gv2-page[data-mode="campaign"] .gv2-dt .gv2-dc {
    --gv2-dc-w: 26px !important; --gv2-dc-h: 32px !important;
  }

  /* Player area: fill column, distribute, never clip controls (reuse 1v1). */
  .gv2-page[data-mode="campaign"] .gv2-pa {
    min-height: 0; overflow: hidden;
    justify-content: space-between;
    gap: 2px; padding: 4px 7px;
  }
  .gv2-page[data-mode="campaign"] .gv2-pa-section-label { margin: 1px 0; }
  .gv2-page[data-mode="campaign"] .gv2-pa-row,
  .gv2-page[data-mode="campaign"] .gv2-pa-hand,
  .gv2-page[data-mode="campaign"] .gv2-pa-section-label,
  .gv2-page[data-mode="campaign"] .gv2-pa-spent,
  .gv2-page[data-mode="campaign"] .gv2-pa-bottom { flex-shrink: 0; }
  .gv2-page[data-mode="campaign"] .gv2-sp { width: 76px; padding: 4px 6px 6px; }
  .gv2-page[data-mode="campaign"] .gv2-sp-numeral { font-size: 30px; }
  .gv2-page[data-mode="campaign"] .gv2-pa-hand { min-height: 0; }
  .gv2-page[data-mode="campaign"] .gv2-pa-fan { min-height: 66px; height: auto; }
  .gv2-page[data-mode="campaign"] .gv2-pc {
    --pc-w: 50px !important; --pc-h: 66px !important; --pc-pip: 30px !important;
  }
  .gv2-page[data-mode="campaign"] .gv2-pc-corner { font-size: 12px; }
  .gv2-page[data-mode="campaign"] .gv2-pa-actions { min-height: 0; overflow-y: auto; }
  .gv2-page[data-mode="campaign"] .gv2-ar-playable {
    grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); gap: 4px; min-height: 0;
  }
  .gv2-page[data-mode="campaign"] .gv2-ac { --gv2-ac-h: 50px !important; }
  .gv2-page[data-mode="campaign"] .gv2-ac-fluid { max-width: 52px; }
  /* Hide redundant score-mod corner badge in the tray (see 1v1 block). */
  .gv2-page[data-mode="campaign"] .gv2-ar-playable .gv2-ac-corner-tl { display: none; }
  .gv2-page[data-mode="campaign"] .gv2-cb { min-height: 31px; padding: 4px 8px; }
  .gv2-page[data-mode="campaign"] .gv2-cb-label { font-size: 17px; }
  .gv2-page[data-mode="campaign"] .gv2-cb-sub { display: none; }
}

/* ===== Post-match SHARE sheet (share-panel.js) ===== */
.gv2-share-overlay {
  position: fixed; inset: 0; z-index: 9600;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6,3,6,.80);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  padding: 20px;
  animation: gv2-share-fade .18s ease;
}
@keyframes gv2-share-fade { from { opacity: 0; } to { opacity: 1; } }
.gv2-share-panel {
  position: relative;
  /* 100% = the overlay's padded content box (viewport − its 20px sides), so the panel
     never exceeds the viewport on narrow phones; capped at 420px on larger screens. */
  width: min(100%, 520px);
  max-height: 92vh; overflow-y: auto;
  background: linear-gradient(180deg, #1a0f14, #120a0e);
  border: 1px solid var(--brass-lo, #5a4418);
  box-shadow: 0 20px 60px rgba(0,0,0,.7);
  border-radius: 10px;
  padding: 20px 18px 16px;
  display: flex; flex-direction: column; gap: 14px;
}
.gv2-share-close {
  position: absolute; top: 6px; right: 10px;
  width: 36px; height: 36px;
  background: transparent; border: none;
  color: var(--vellum-dim, #cfc7b8); font-size: 26px; line-height: 1;
  cursor: pointer;
}
.gv2-share-close:hover { color: var(--vellum, #f4f0e8); }
.gv2-share-preview {
  display: flex; align-items: center; justify-content: center;
  min-height: 120px; aspect-ratio: 1 / 1;
  /* Cap by viewport height so the square never pushes the action buttons below the
     fold in short/landscape viewports; the canvas (square) then scales to this height. */
  max-height: min(520px, 62vh);
  border: 1px solid rgba(183,162,122,.18);
  background: rgba(0,0,0,.35);
}
.gv2-share-canvas { width: auto; height: 100%; max-width: 100%; display: block; object-fit: contain; }
.gv2-share-spinner {
  font-family: 'JetBrains Mono', monospace; font-size: 12px; letter-spacing: .3em;
  color: var(--vellum-dim, #cfc7b8); text-transform: uppercase;
}
.gv2-share-targets { display: flex; gap: 10px; }
.gv2-share-target {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 13px 12px;
  background: rgba(183,162,122,.10);
  border: 1px solid rgba(183,162,122,.40);
  color: var(--vellum, #f4f0e8);
  font-family: 'Crimson Text', serif; font-size: 15px; font-weight: 600;
  cursor: pointer; border-radius: 8px;
  transition: background .12s ease, border-color .12s ease, opacity .12s ease, transform .1s ease;
}
.gv2-share-target:hover:not(:disabled) { background: rgba(224,196,137,.18); border-color: var(--brass-hi, #e0c489); transform: translateY(-1px); }
.gv2-share-target:active:not(:disabled) { transform: translateY(0); }
.gv2-share-target:disabled { opacity: .45; cursor: default; }
.gv2-share-target-icon { display: inline-flex; }
.gv2-share-target-icon svg { width: 19px; height: 19px; display: block; }
.gv2-share-referral-cta {
  width: 100%;
  padding: 10px 12px;
  background: rgba(34, 227, 157, .08);
  border: 1px solid rgba(34, 227, 157, .34);
  border-radius: 8px;
  color: var(--vellum, #f4f0e8);
  font-family: 'Crimson Text', serif;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.gv2-share-referral-cta:hover { border-color: rgba(34, 227, 157, .62); background: rgba(34, 227, 157, .13); }
@media (prefers-reduced-motion: reduce) {
  .gv2-share-overlay { animation: none; }
  .gv2-share-target { transition: none; }
  .gv2-share-target:hover:not(:disabled),
  .gv2-share-target:active:not(:disabled) { transform: none; }
}

/* Desktop board referral offer. The header mount is fixed beneath the Game Log
   at the desktop breakpoint below. */
.gv2-ref-widget {
  display: none;
  box-sizing: border-box;
  width: 300px;
  height: 108px;
  max-width: 300px;
  max-height: 108px;
  overflow: hidden;
  color: var(--vellum);
  text-align: right;
  pointer-events: none;
}
.gv2-ref-widget-body {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  width: 100%;
  height: 100%;
  padding-top: 2px;
}
.gv2-ref-widget-line {
  display: block;
  width: 100%;
  font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
  font-size: 29px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0;
  text-align: right;
  text-transform: uppercase;
  white-space: nowrap;
}
.gv2-ref-widget-bonus,
.gv2-ref-widget-code-line {
  color: #f5d98a;
  text-shadow: 0 2px 0 rgba(0,0,0,.5), 0 0 12px rgba(245,217,138,.2);
}
.gv2-ref-widget-match {
  margin-top: 6px;
  color: var(--vellum, #f4f0e8);
  font-size: 26px;
  text-shadow: 0 2px 0 rgba(0,0,0,.45);
}
.gv2-ref-widget-code-line {
  margin-top: 6px;
}
@media (min-width: 1440px) and (min-height: 501px) and (hover: hover) {
  .gv2-ref-widget.is-ready { display: block; }
}
/* Landscape (matches the hero modal breakpoint): the share card is a square that's
   height-bound to min(520px,62vh), so a full-width panel left ~120px dead space each
   side. Hug the card — fit-content panel + card-width preview; the Copy/Share targets
   (flex:1 1 0) then size to the card width. Assumes the targets row's max-content stays
   ≤ the card width (true for the 2 short-labeled targets); a long label or a 3rd target
   could push the panel wider. */
@media (orientation: landscape) and (max-height: 500px) {
  .gv2-share-panel { width: fit-content; max-width: 100%; }
  .gv2-share-preview { width: min(520px, 62vh); }
}

/* ============================================================================
   POST-MATCH — HERO LAYOUT (landscape mobile, 1v1/custom/arena)
   "The Standard" recast: LEFT = composed share card (timeline + verdict +
   players/score + THE POT) + Share Result; RIGHT = ledger (XP + Roll of Honour
   + New Game/Rematch). Rendered by renderHeroCard() only when shouldUseHero()
   (landscape ≤500px tall) is true, so these classes never exist on desktop.
   ============================================================================ */
/* Lucky Jack casino-noir. Block F is a symmetric two-sided --hero-* token API consumed
   with NO fallback, so both sides need complete values — there is no "inversion" to perform
   here, there are 12 values to supply. Loss's --hero-verdict was var(--oxblood-hi) = #8f74d0,
   i.e. VIOLET: a theme-win-only grep would have shipped a violet Defeat card in landscape
   mobile while every other surface rendered neon red. */
.gv2-me-hero-card.gv2-me-theme-win {
  --hero-verdict: #22e39d; --hero-rule: rgba(34,227,157,.40); --hero-dim: rgba(236,227,207,.62);
  --hero-page: radial-gradient(ellipse 80% 70% at 50% 28%, #063528 0%, #02100c 60%, #000804 100%);
  --hero-cloth: linear-gradient(180deg, #052a20 0%, #02140d 58%, #000804 100%);
  --hero-vshadow: 0 0 30px rgba(34,227,157,.5), 0 2px 0 rgba(0,0,0,.5);
}
.gv2-me-hero-card.gv2-me-theme-loss {
  --hero-verdict: #ff2f2f; --hero-rule: rgba(255,47,47,.50); --hero-dim: rgba(236,227,207,.5);
  --hero-page: radial-gradient(ellipse 80% 70% at 50% 28%, #3a0810 0%, #1c0407 60%, #0a0203 100%);
  --hero-cloth: linear-gradient(180deg, #280610 0%, #1c0407 58%, #0a0203 100%);
  --hero-vshadow: 0 0 22px rgba(255,47,47,.5), 0 2px 0 rgba(0,0,0,.5);
}
/* Fill the landscape frame — reset the desktop card chrome (max-width/padding/border). */
.gv2-me-hero-card {
  position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; min-height: 0;
  margin: 0; padding: 0; border: none; border-radius: 0; box-shadow: none;
  background: var(--hero-page); color: var(--parchment); overflow: hidden;
  font-family: 'Inter', system-ui, sans-serif;
}
.gv2-me-hero-card::before { content: ''; position: absolute; inset: 0; background: var(--hero-cloth); z-index: 0; }
.gv2-me-hero-card::after { content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(ellipse 110% 80% at 50% 48%, transparent 52%, rgba(0,0,0,.42) 100%); }
.gv2-me-hero {
  position: relative; z-index: 2; height: 100%; display: grid; grid-template-columns: 0.92fr 1fr; gap: 18px;
  padding: 20px max(22px, env(safe-area-inset-right)) 12px max(36px, env(safe-area-inset-left));
}
.gv2-me-hero-home {
  position: absolute;
  top: max(12px, env(safe-area-inset-top) + 8px);
  left: max(12px, env(safe-area-inset-left) + 8px);
  z-index: 6; width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid rgba(236,227,207,.26); color: var(--parchment);
  cursor: pointer; transition: border-color .15s, background .15s;
}
.gv2-me-hero-home:hover { border-color: var(--lj-gold-hi); background: rgba(231,200,119,.08); }

/* ---- LEFT: share card ---- */
.gv2-me-hero-share {
  display: flex; flex-direction: column; min-height: 0; gap: 12px;
  padding-right: 18px; border-right: 1px solid var(--hero-rule);
}
.gv2-me-hero-sharecard {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: space-between; text-align: center; gap: 6px;
}
.gv2-me-hero-verdict {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: clamp(34px, 7vh, 52px); line-height: 1;
  letter-spacing: .05em; color: var(--hero-verdict); text-shadow: var(--hero-vshadow);
}
.gv2-me-hero-players { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; }
.gv2-me-hero-player { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; min-width: 0; max-width: 112px; }
.gv2-me-hero-player .gv2-me-crest { width: 44px; height: 44px; flex-shrink: 0; }
.gv2-me-hero-pname-row { display: flex; align-items: center; gap: 5px; max-width: 100%; }
.gv2-me-hero-pname { font-family: 'JetBrains Mono', monospace; font-size: 8.5px; color: var(--vellum);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gv2-me-hero-ptag { font-family: 'JetBrains Mono', monospace; font-size: 6.5px; letter-spacing: .3em;
  text-transform: uppercase; color: rgba(236,227,207,.4); }
.gv2-me-hero-ptag-you { color: var(--hero-verdict); }
.gv2-me-hero-friend-slot { display: inline-flex; align-items: center; justify-content: center; }
.gv2-me-hero-friend { width: 18px; height: 18px; border-radius: 50%; background: transparent;
  border: 1px solid rgba(236,227,207,.4); color: var(--hero-dim); font-size: 12px; line-height: 1; cursor: pointer; }
.gv2-me-hero-friend:hover { border-color: var(--lj-gold-hi); color: var(--lj-gold-hi); }
.gv2-me-hero-friend-on { color: var(--lj-gold-hi); font-size: 11px; }
.gv2-me-hero-scores { display: flex; align-items: baseline; justify-content: center; gap: 8px; flex-shrink: 0; }
.gv2-me-hero-score { font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 600;
  font-size: 34px; line-height: .85; color: rgba(236,227,207,.6); }
.gv2-me-hero-score-big { font-size: 46px; color: var(--hero-verdict); text-shadow: var(--hero-vshadow); }
.gv2-me-hero-score-dash { font-family: 'Cormorant Garamond', serif; font-size: 30px; color: var(--hero-dim); line-height: 1; }
.gv2-me-hero-pot { display: flex; flex-direction: column; align-items: center; gap: 4px; margin-bottom: 10px; }
.gv2-me-hero-pot-label { display: flex; align-items: center; gap: 10px; color: var(--vellum);
  font-family: 'JetBrains Mono', monospace; font-size: 13px; letter-spacing: .18em; text-transform: uppercase; white-space: nowrap; }
.gv2-me-hero-rule { width: 26px; height: 1px; background: var(--hero-rule); }
.gv2-me-hero-pot-amount { display: flex; align-items: center; justify-content: center; gap: 6px; }
.gv2-me-hero-pot-num { font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 600;
  font-size: 30px; line-height: .9; color: var(--parchment); text-shadow: var(--hero-vshadow); white-space: nowrap; }
.gv2-me-hero-share-btn { width: 100%; height: 42px; flex-shrink: 0; }

/* ---- RIGHT: ledger ---- */
.gv2-me-hero-ledger { display: flex; flex-direction: column; min-height: 0; justify-content: space-between; gap: 10px; padding-top: 12px; }
.gv2-me-hero-xp { padding-bottom: 10px; border-bottom: 1px solid var(--hero-rule); }
.gv2-me-hero-xp-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; white-space: nowrap; }
.gv2-me-hero-xp-level { font-family: 'Cormorant Garamond', serif; font-style: italic; font-weight: 500; font-size: 15px; color: var(--parchment); }
.gv2-me-hero-xp-title { font-family: 'JetBrains Mono', monospace; font-size: 8px; letter-spacing: .2em; text-transform: uppercase; color: var(--hero-dim); }
.gv2-me-hero-xp-spacer { flex: 1; }
.gv2-me-hero-xp-delta { font-family: 'JetBrains Mono', monospace; font-size: 8px; letter-spacing: .3em; text-transform: uppercase; color: var(--hero-verdict); }
.gv2-me-hero-xp-levelup { color: var(--lj-gold-hi); }
.gv2-me-hero-xp-bar { position: relative; height: 7px; background: rgba(0,0,0,.45); border: 1px solid var(--hero-rule); overflow: hidden; }
.gv2-me-hero-xp-fill-prior { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--lj-bg-2), var(--lj-bg-mid)); }
.gv2-me-hero-xp-fill { position: absolute; left: 0; top: 0; bottom: 0;
  background: linear-gradient(90deg, var(--lj-bg-mid), var(--lj-neon)); box-shadow: 0 0 10px var(--lj-neon-glow); }
.gv2-me-hero-xp-num { display: flex; justify-content: space-between; margin-top: 4px;
  font-family: 'JetBrains Mono', monospace; font-size: 8px; letter-spacing: .12em; color: var(--hero-dim); }
.gv2-me-hero-xp-tonext { color: var(--hero-dim); }
/* Achievements: reuse renderRollOfHonour DOM; just let it flex inside the ledger.
   The landscape media block (:3300) hides .gv2-me-roll-list — un-hide it in the
   hero (the right ledger has room for it). */
.gv2-me-hero-ledger .gv2-me-roll-of-honour { flex: 1; min-height: 0; background: none; border: none; padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.gv2-me-hero-ledger .gv2-me-roll-header { margin-bottom: 6px; }
.gv2-me-hero-ledger .gv2-me-roll-list { display: flex; flex-direction: column; gap: 7px; overflow: hidden; min-height: 0; }
/* Circular crest (the share-card look), overriding the desktop hex. Renders the
   avatar/initial directly (no -rim/-inner shield scaffolding — renderCrest 'circle'
   variant). flex-center keeps the no-avatar initial centered now that the centering
   -inner wrapper is gone. */
.gv2-me-hero-player .gv2-me-crest {
  clip-path: none; border-radius: 50%; border: 2px solid var(--hero-verdict);
  background: radial-gradient(circle at 38% 30%, #1f1416 0%, #140b0d 70%, #0b0708 100%);
  box-shadow: inset 0 0 0 1px #09090b, 0 0 0 3px var(--lj-neon-glow), 0 0 14px var(--lj-neon-glow);
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.gv2-me-hero-player .gv2-me-crest img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.gv2-me-hero-player .gv2-me-crest-initial { font-size: 18px; }
.gv2-me-hero-cta { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin: 0; }
.gv2-me-hero-cta .gv2-me-cta-btn { height: 44px; }
.gv2-me-hero .gv2-me-placeholder { color: var(--hero-dim); font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: .2em; padding: 6px 0; }

/* ---- HERO — Phase 2: entrance choreography + motion polish ----
   Default (non-.gv2-me-hero-lit / reduced-motion) state IS the final visible
   state — every animated element is visible/in-place by default; the keyframes
   only run under .gv2-me-hero-lit + (prefers-reduced-motion: no-preference). The
   win flash defaults to opacity:0 so reduced-motion never leaves it stuck white. */
.gv2-me-hero-flash { position: absolute; inset: 0; z-index: 20; background: #fff; opacity: 0; pointer-events: none; }
/* Timeline dots → diamonds (hero-scoped; desktop dots stay circular). Rotation is
   about the center, so each dot's center — the tooltip anchor — is preserved. */
@media (prefers-reduced-motion: no-preference) {
  /* XP fill transition lives here (gated) so reduced-motion gets the instant final width. */
  .gv2-me-hero-xp-fill { transition: width 900ms cubic-bezier(.2, .8, .3, 1); }
  .gv2-me-hero-lit .gv2-me-hero-verdict { animation: gv2HeroStamp 520ms cubic-bezier(.2, .8, .3, 1.4) 120ms both; }
  .gv2-me-hero-lit .gv2-me-hero-share { animation: gv2HeroInL 520ms ease 60ms both; }
  .gv2-me-hero-lit .gv2-me-hero-flash { animation: gv2HeroFlash 440ms ease-out both; }
}
@keyframes gv2HeroStamp { 0% { opacity: 0; transform: scale(1.45); } 60% { opacity: 1; transform: scale(.96); } 100% { opacity: 1; transform: scale(1); } }
@keyframes gv2HeroInL { from { opacity: 0; transform: translateX(-22px); } to { opacity: 1; transform: translateX(0); } }
@keyframes gv2HeroFlash { 0% { opacity: 0; } 12% { opacity: .8; } 100% { opacity: 0; } }

/* ============================================================
   PORTRAIT BOARD — Campaign (the Crusade) reflow (Phase 6, 2026-06-24)
   ------------------------------------------------------------
   DESKTOP-SAFETY (identical contract to the Phase 5 1v1 portrait block):
   EVERY rule below is inside `@media (max-width:767px) and (orientation:portrait)`
   ONLY, AND scoped `.gv2-page[data-mode="campaign"]`.
   - Portrait and landscape are mutually exclusive media queries. The campaign
     LANDSCAPE board block (`@media (orientation:landscape) and (max-height:500px)`,
     css ~6669) re-grids the board and shrinks its cards; it can NEVER match here.
   - Desktop (≥768px, any landscape) fails both the width AND the orientation gate.
   - The 1v1 board (`:not([data-mode="campaign"])`) can never match these
     `[data-mode="campaign"]` rules — the Phase 5 portrait block (css ~4458) is a
     fully independent sibling; no selector overlaps.

   WHY this block is needed: the base mobile block (css ~3687, OR query) sets the
   campaign grid to flex-column + base player-area sizing, but it does NOT shrink
   the campaign playing cards (.gv2-pc), action cards (.gv2-ac) or score plaque
   (.gv2-sp) — those shrinks live ONLY in the two LANDSCAPE blocks (~3993 1v1,
   ~6669 campaign) and the Phase 5 1v1 PORTRAIT block (~4458, `:not(campaign)`).
   Campaign had NO portrait block at all, so its cards rendered at desktop 80×110
   in portrait. This block adds the campaign portrait card sizing + the POV flip.

   DEPENDENCY (load-bearing): the `order` flip below works ONLY because the base
   mobile rule at css ~3695 sets `.gv2-page .gv2-game-grid { display:flex;
   flex-direction:column }` UNSCOPED (covers campaign). If that rule is ever
   scoped `:not(campaign)`, the campaign grid stays `display:grid` and `order` is
   ignored — the flip would silently die. Keep ~3695 unscoped.

   Target (PortCrusade): BOSS area on TOP, center axis (deck/round/turn) middle,
   SELF area on BOTTOM with HIT/STAND + face-up playable cards (thumb reach).
   CSS-only reflow of the EXISTING campaign board DOM — no JS change. Campaign is
   ALWAYS data-my-idx="0" (player=area 0, boss=area 1); only that POV exists.
   ============================================================ */
@media (max-width: 767px) and (orientation: portrait) {
  /* ---- POV: boss-on-top / self-on-bottom ----
     The base mobile per-idx rules (css ~3721) order self (area 0) order:1 (TOP)
     and boss (area 1) order:3 (BOTTOM) — selector (0,5,0). These overrides add
     `[data-mode="campaign"]` → (0,6,0) AND land later in source order, so the flip
     can never silently no-op. Axis stays order:2 (middle) from ~3718 — unchanged.
     Mirror of the Phase 5 1v1 flip (~4466) but campaign-scoped. */
  .gv2-page[data-my-idx="0"][data-mode="campaign"] .gv2-game-grid > .gv2-pa[data-gv2-player-area="0"] { order: 3; }
  .gv2-page[data-my-idx="0"][data-mode="campaign"] .gv2-game-grid > .gv2-pa[data-gv2-player-area="1"] { order: 1; }

  /* ---- Player area: fill column, distribute, never clip controls. Mirrors the
     campaign landscape block (~6772) but a touch more vertical air for the taller
     portrait viewport so boss-top / self-bottom read as two distinct panels. ---- */
  .gv2-page[data-mode="campaign"] .gv2-pa {
    min-height: 0; overflow: hidden;
    justify-content: space-between;
    gap: 8px; padding: 10px 12px;
  }
  .gv2-page[data-mode="campaign"] .gv2-pa-row,
  .gv2-page[data-mode="campaign"] .gv2-pa-hand,
  .gv2-page[data-mode="campaign"] .gv2-pa-section-label,
  .gv2-page[data-mode="campaign"] .gv2-pa-spent,
  .gv2-page[data-mode="campaign"] .gv2-pa-bottom { flex-shrink: 0; }

  /* ---- Score plaque: 88px (matches the Phase 5 1v1 portrait plaque ~4476 — the
     wider portrait viewport affords more than the campaign-landscape 76px; campaign
     hides the BANK chip so the row has room). ---- */
  .gv2-page[data-mode="campaign"] .gv2-sp { width: 88px; padding: 8px 9px 9px; }
  .gv2-page[data-mode="campaign"] .gv2-sp-numeral { font-size: 40px; }

  /* ---- Hand fan: drop the desktop 122px floor + let a tall hand scroll instead
     of clipping. BOTH overflow axes are set explicitly — a lone `overflow-x:auto`
     computes the other axis to `auto` (per spec) and surfaces a stray vertical bar
     (the Phase 5 1v1 fix ~4486 hit this; replicate both lines). Fan cards stay
     absolute-positioned by render-fan.js — not re-positioned here. ---- */
  .gv2-page[data-mode="campaign"] .gv2-pa-hand { min-height: 0; }
  .gv2-page[data-mode="campaign"] .gv2-pa-fan {
    min-height: 66px; height: auto;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
  }

  /* ---- Playing cards: shrink the fixed 80×110 → 50×66 (inline custom props on
     .gv2-pc → !important, same pattern as the campaign landscape block ~6785). ---- */
  .gv2-page[data-mode="campaign"] .gv2-pc {
    --pc-w: 50px !important; --pc-h: 66px !important; --pc-pip: 30px !important;
  }
  .gv2-page[data-mode="campaign"] .gv2-pc-corner { font-size: 12px; }

  /* ---- Action cards: the actions tray is the shrink + scroll zone so accumulated
     action cards (no hard cap) can never push HIT/STAND off-screen mid-match. ---- */
  .gv2-page[data-mode="campaign"] .gv2-pa-actions { min-height: 0; overflow-y: auto; }
  .gv2-page[data-mode="campaign"] .gv2-ar-playable {
    grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); gap: 5px; min-height: 0;
  }
  .gv2-page[data-mode="campaign"] .gv2-ac { --gv2-ac-h: 56px !important; }
  .gv2-page[data-mode="campaign"] .gv2-ac-fluid { max-width: 56px; }
  /* Hide the redundant score-mod corner badge in the tray (cards aren't overlapped
     there → it double-prints with the center glyph). Fan keeps it (cards overlap).
     Same treatment as the campaign landscape block ~6796 + the 1v1 portrait path. */
  .gv2-page[data-mode="campaign"] .gv2-ar-playable .gv2-ac-corner-tl { display: none; }
}

/* ============================================================
   DESKTOP 1v1 REDESIGN — PR 1: felt + header band (2026-07-02)
   ------------------------------------------------------------
   Port of the approved portrait design language to the desktop 1v1
   board. Plan: docs/plans/2026-07-02-desktop-board-redesign-plan.md;
   design authority: public/_redesign/desktop.html?opt=a (local mockup —
   this block carries the spec so the mockup never needs committing).
   Scope wall: every rule lives inside `(min-width:768px) and
   (min-height:501px)` and is scoped `.gv2-page:not([data-mode=
   "campaign"])`. NOTE (2026-07-16): an earlier version of this comment
   claimed "the body felt rule keys on .gv2-1v1-bg-active, which campaign
   never sets". That was FALSE — campaign-v2.html loads this same
   game-ui.js, and applyMatchBackdrop added the class to EVERY board,
   campaign included. Campaign safety now comes from the inline `data-mode`
   guard inside game-ui.js#applyBoardFelt, not from this comment's
   assumption. (Note it is an inline check, NOT the similarly-named
   isCampaignMode() helper further down that file.)
   PR 1 surface: forged header band (✦ ROUND N ✦ ornament + WinCounter +
   MATCH POINT flag + HOW TO PLAY), retire the BoardChrome
   counter/timeline + CenterAxis MP tag + seat wins rails. Seats/fan/
   tactics (PR 2) and table-edge HIT/STAND + console (PR 3) follow.
   Typography: BJACardCinzel — the uniquely-named local Cinzel faces
   declared at the top of this file (700 + 900); fallback Cormorant
   Garamond is already loaded on /game so a swap-flash lands on a serif.
   ============================================================ */
@media (min-width: 768px) and (min-height: 501px) {
  /* ---- Felt: REMOVED 2026-07-16 (Lucky Jack). This block used to
     re-declare body.gv2-1v1-bg-active with an opaque GREEN felt whose
     `background` shorthand dropped var(--gv2-bg) entirely. Because it
     was gated on (min-height:501px), shrinking a window below 501px tall
     switched it off and exposed the medieval boss art underneath — the
     "shrink vertically → reverts to the old UI" bug. The base rule
     (~:5676) paints the felt UNGATED at EVERY size, so no desktop
     override is needed and the breakpoint cannot expose anything.
     NOTE the felt is GREEN again as of QA item 3 (2026-07-17) — the
     colour reverted, this tombstone did NOT. The trap is about the
     size GATE plus the `background` SHORTHAND, not the hue: green is
     exactly what was gated the first time. Do NOT reintroduce a
     size-gated body background here. ---- */

  /* ---- Header band: un-hide .gv2-hdr (base display:none; markup mounted
     at all widths, populated by renderFromState). Mockup .header values;
     capped at the board canvas width. z-index:6 so the MATCH POINT flag
     hanging below the bar can't be painted over by the seat panels. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    padding: 20px 0 16px; position: relative; z-index: 6;
    width: 100%; max-width: 1520px; margin: 0 auto;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn {
    display: flex; align-items: center; gap: 12px; width: 100%; padding: 0 40px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-line {
    flex: 1; height: 1px;
    background: linear-gradient(90deg, transparent, #7a5a22);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-line-rt {
    background: linear-gradient(270deg, transparent, #7a5a22);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-plaque {
    display: flex; align-items: center; gap: 9px;
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 900; font-size: 14px; letter-spacing: 3.5px;
    color: #f5d98a; text-shadow: 0 2px 8px rgba(0,0,0,.8);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn-plaque i {
    font-style: normal; font-size: 8px; color: #7a5a22; transform: translateY(-1px);
  }

  /* ---- Forged WinCounter: portrait ruleset (~:4816) at mockup .sb sizes
     (19px sockets, 13/22 padding, r17, 11px diamond). Covers every class
     WinCounter.js emits: side/row (foe row-reverse fill toward center),
     lit sockets, divider, 2-row (target 6–10), numeral (≥11), MP burn +
     flag. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc {
    position: relative; display: inline-flex; align-items: center; gap: 9px;
    padding: 13px 22px; border-radius: 17px;
    background:
      linear-gradient(180deg, rgba(51,39,22,.2) 0%, transparent 40%),
      linear-gradient(180deg, #241a0e, #170f06 55%, #0e0903);
    box-shadow:
      0 0 0 1px #060301,
      0 0 0 2.5px rgba(216,169,78,.7),
      0 0 0 4px #060301,
      inset 0 1px 0 rgba(245,217,138,.28),
      inset 0 -6px 12px rgba(0,0,0,.6),
      0 12px 30px rgba(0,0,0,.65);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-side { display: flex; flex-direction: column; gap: 6px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-row { display: flex; gap: 9px; }
  /* Foe side fills toward the center divider (portrait device-QA item 2). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-side-foe .gv2-wc-row { flex-direction: row-reverse; }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-sk {
    width: 19px; height: 19px; border-radius: 50%; flex: none;
    background: radial-gradient(circle at 50% 42%, #080401, #1a1108);
    box-shadow: inset 0 2px 4px rgba(0,0,0,.95), inset 0 -1px 0 rgba(245,217,138,.14);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-sk-you.gv2-wc-sk-on {
    background: radial-gradient(circle at 35% 28%, #d9fbe8, #2fae6d 52%, #10532f 95%);
    box-shadow: 0 0 8px 1px rgba(80,230,150,.5), inset 0 -2px 3px rgba(8,50,28,.55);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-sk-foe.gv2-wc-sk-on {
    background: radial-gradient(circle at 35% 28%, #ffc4b5, #c22b2b 52%, #5e0f0f 95%);
    box-shadow: 0 0 8px 1px rgba(255,107,84,.45), inset 0 -2px 3px rgba(60,8,4,.55);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-div {
    width: 11px; height: 11px; transform: rotate(45deg); flex: none; margin: 0 4px;
    background: linear-gradient(135deg, #f5d98a 0%, #d8a94e 45%, #6e4e18 100%);
    box-shadow: 0 0 0 1.5px #060301, inset 0 1px 0 rgba(255,255,255,.4);
  }
  /* ≥11 target: compact numeral pair (sockets won't fit). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-numeral {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-size: 21px; font-weight: 700; line-height: 1; min-width: 22px;
    text-align: center; color: #f3e9c9; font-variant-numeric: tabular-nums;
  }
  /* MATCH POINT: frame smolders + forged banner (single MP surface at
     desktop — .gv2-ca-mp retires below; .gv2-bc-mp already desktop-hidden). */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-mp { animation: gv2WcBurn 1.6s ease-in-out infinite; }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-flag {
    position: absolute; left: 50%; bottom: -17px; transform: translateX(-50%);
    white-space: nowrap;
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 900; font-size: 10px; letter-spacing: 3.2px;
    color: #ffcbb8; padding: 4px 16px 5px;
    clip-path: polygon(7% 0, 93% 0, 100% 50%, 93% 100%, 7% 100%, 0 50%);
    background: linear-gradient(180deg, #6e1c12, #400d07);
    box-shadow: inset 0 0 0 1px rgba(255,120,90,.55);
    text-shadow: 0 0 10px rgba(255,120,90,.8);
    animation: gv2WcFlag 1.6s ease-in-out infinite;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-2row .gv2-wc-flag { bottom: auto; top: -17px; }
  /* Headroom for the on-top flag (2-row stack) — clears the ornament above. */
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-2row { margin-top: 12px; }

  /* ---- HOW TO PLAY: header copy un-hidden (base .gv2-hdr-help hide keeps
     portrait clean); reuses the delegated .gv2-ca-help-link handler
     (ui:toggleQuickRules). The center-axis copy retires below so
     quick-rules-panel's offsetParent scan finds exactly one candidate. ---- */
  /* margin-top 16px (mockup .howto) clears the MATCH POINT flag hanging
     17px below the bar (the hdr column gap alone is only 10px). */
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-target {
    display: block; margin-top: 8px;
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 900; font-size: 13px; letter-spacing: 2.8px;
    text-transform: uppercase; color: #f5d98a;
    text-shadow: 0 2px 8px rgba(0,0,0,.8), 0 0 12px rgba(216,169,78,.25);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-help { display: block; margin-top: 2px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr-help .gv2-ca-help-link {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 10px; letter-spacing: 2.8px;
    text-transform: uppercase; color: #d8a94e; text-decoration: none;
    border-bottom: 1px dotted rgba(216,169,78,.5); padding-bottom: 3px;
  }

  /* Wide desktop: keep the score mathematically centered while mounting the
     compact sponsor lockup directly beneath the fixed GAME LOG control. */
  @media (min-width: 1440px) and (hover: hover) {
    .gv2-page:not([data-mode="campaign"]) .gv2-hdr {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
      grid-template-rows: auto auto;
      column-gap: 0;
      row-gap: 10px;
      align-items: center;
    }
    .gv2-page:not([data-mode="campaign"]) .gv2-hdr-orn {
      grid-column: 1 / -1;
      grid-row: 1;
    }
    .gv2-page:not([data-mode="campaign"]) .gv2-hdr-side-left {
      display: block;
      grid-column: 1;
      grid-row: 2;
      min-width: 0;
    }
    .gv2-page:not([data-mode="campaign"]) .gv2-hdr-center {
      display: flex;
      grid-column: 2;
      grid-row: 2;
      flex-direction: column;
      align-items: center;
      gap: 10px;
    }
    .gv2-page:not([data-mode="campaign"]) .gv2-hdr-sponsor {
      display: block;
      position: fixed;
      top: 94px;
      right: 18px;
      z-index: 899;
      width: 300px;
      min-width: 0;
      max-width: 300px;
      height: 108px;
      max-height: 108px;
      overflow: hidden;
    }
  }

  /* ---- Retired at desktop 1v1: BoardChrome round counter + timeline (the
     ornament + WinCounter own round/wins), CenterAxis MP tag (the flag owns
     it), center-axis help copy (header owns it), seat wins-rail wrapper
     (WinCounter owns wins; hiding the wrapper also kills its flex gap).
     The ⋯ menu (.gv2-bc-right) survives until PR 3 — pin it to grid col 3
     or it auto-places into column 1 when its siblings hide. Deck totem,
     count/deck readout (.gv2-ccs), rules text, FootStrip: PR 2/3. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-counter,
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-center { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-right { grid-column: 3; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-mp { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-target { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca .gv2-ca-help { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-rail { display: none; }
}
/* Desktop companion to the portrait reduced-motion override: the ported
   WinCounter burn/flag animations respect the user setting here too. */
@media (min-width: 768px) and (min-height: 501px) and (prefers-reduced-motion: reduce) {
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-mp,
  .gv2-page:not([data-mode="campaign"]) .gv2-wc-flag { animation: none; }
}

/* ============================================================
   DESKTOP 1v1 REDESIGN — PR 2: seats + center wind-down (2026-07-02)
   ------------------------------------------------------------
   Same scope wall as PR 1 above. This PR: canvas widened to the 1520px
   board with a 52px brass-filament center column; seat panels stripped
   to open felt with a typographic prow (numeral · name · status · ember);
   fluid fan cards (--gv2-deskcard-w clamp; JS READS the resolved value via
   .gv2-sizeprobe in render-geometry.js#readProbe — never re-derive it in JS,
   or flights pop at the landing handoff); fluid tactic tiles + USED chip row; CenterAxis
   wind-down (.gv2-ccs hidden, deck totem = invisible fixed anchor,
   rules text re-homed bottom-center); FootStrip retired (prow captions
   + the DEALING… ornament carry messaging — portrait precedent).
   ============================================================ */
@media (min-width: 768px) and (min-height: 501px) {
  /* ---- Fluid card scale: min engages at exactly 768px (0.125*768=96),
     max at 1104px. Height/pip/corner derive from the SAME var, and JS reads the
     resolved value off .gv2-sizeprobe rather than re-deriving it, so the two
     agree at every width AND on iOS, where svh != innerHeight.
     Declared on :root TOO: flight cards are body-mounted (outside
     .gv2-page) and their cream-face rules below need the var. ---- */
  /* Height-aware as well as width-aware. The board's content was a CONSTANT
     841px tall at every viewport, so anything under ~841 scrolled and the fixed
     HIT/STAND bar painted over the tagline. Adding a height term to the same
     token fixes both without a media band (a band is unmirrorable — mq.js's
     DESKTOP has no upper height bound — and over-corrects across its range).
     svh, not vh: the pinned page is 100dvh, and iPad Safari's chrome makes the
     two differ. Exact ratio, not a decimal coefficient, so break-even is a
     point: at 900svh this is EXACTLY 138 and nothing on a large monitor moves.
     JS reads the resolved value off .gv2-sizeprobe — never re-implement here. */
  :root {
    --gv2-deskcard-w: clamp(84px, min(12.5vw, calc(100svh * 138 / 900)), 138px);
    --gv2-desktray-w: clamp(58px, calc(100svh * 92 / 900), 92px);
    --gv2-pcl-h: clamp(64px, calc(100svh * 84 / 900), 84px);
  }
  .gv2-page:not([data-mode="campaign"]) {
    --gv2-deskcard-w: clamp(84px, min(12.5vw, calc(100svh * 138 / 900)), 138px);
    --gv2-desktray-w: clamp(58px, calc(100svh * 92 / 900), 92px);
    --gv2-pcl-h: clamp(64px, calc(100svh * 84 / 900), 84px);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sizeprobe {
    width: var(--gv2-deskcard-w);
    height: calc(var(--gv2-deskcard-w) * 1.44928);
    padding-left: var(--gv2-desktray-w);
  }

  /* ---- Canvas: kill the page cap (base max-width 1440 + 40px side pads
     would silently cap the 1520 board); the grid owns the width now.
     position:relative (NO z-index — a stacking context here would cap
     in-page chrome under body-mounted overlays). ---- */
  .gv2-page:not([data-mode="campaign"]) {
    max-width: none; padding-left: 0; padding-right: 0;
    position: relative;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-game-grid {
    grid-template-columns: minmax(0, 1fr) 52px minmax(0, 1fr);
    gap: 0; padding: 0 52px;
    width: 100%; max-width: 1520px; margin: 0 auto;
    align-items: stretch;
  }
  /* Center gutter: 1px brass filament fading both ends (mockup .gutter).
     .gv2-axis is the grid item (its .gv2-axis-mt wrapper is
     display:contents) and stretches to the seats band. */
  .gv2-page:not([data-mode="campaign"]) .gv2-axis { position: relative; }
  .gv2-page:not([data-mode="campaign"]) .gv2-axis::before {
    content: ""; position: absolute; left: 50%; top: 8%; bottom: 6%;
    width: 1px;
    background: linear-gradient(180deg, transparent, rgba(122,90,34,.5) 22%, rgba(122,90,34,.5) 78%, transparent);
  }

  /* ---- Seat panels: open felt — strip the boxed chrome. position is NOT
     touched (base .gv2-pa position:relative anchors FrostOverlay + the
     instant-drama roots). The turn glow retires with the box (prow lit
     typography + hairline carry turn state now). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa {
    background: none; border: none; box-shadow: none; padding: 0;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"]:not(.fx-bust-flash) {
    border-color: transparent; box-shadow: none;
  }

  /* ---- Prow reflow (portrait pattern): the panel goes grid, .gv2-pa-hand
     dissolves so ScorePlaque + fan become grid items — the plaque joins
     the prow row as the leading 72px numeral. The kept ACTIONS · N/7
     label gets its own row (portrait hides it; desktop keeps it per the
     mockup). minmax(0,1fr) lets the nowrap name ellipsize. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "sc    prow"
      "fan   fan"
      "albl  albl"
      "act   act"
      "used  used"
      "bottom bottom";
    column-gap: 15px;
    align-content: center;
    row-gap: clamp(4px, calc(100svh * 10 / 900), 10px);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-hand { display: contents; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa .gv2-sp { grid-area: sc; align-self: baseline; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row { grid-area: prow; align-self: baseline; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa .gv2-pa-fan { grid-area: fan; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-label-actions { grid-area: albl; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-actions { grid-area: act; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent { grid-area: used; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bottom { grid-area: bottom; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-label-hand { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-label-spent { display: none; }
  /* Fading hairline under the full prow row; own seat gold, foe oxblood,
     turn brightens (mockup .prow::after right:18%). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa::after {
    content: ""; grid-row: 1; grid-column: 1 / -1; align-self: end;
    height: 1px; margin-right: 18%; pointer-events: none;
    background: linear-gradient(90deg, rgba(190,80,60,.5), transparent);
  }
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"]::after,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"]::after {
    background: linear-gradient(90deg, rgba(122,90,34,.6), transparent);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"]::after {
    height: 1.5px;
    background: linear-gradient(90deg, #f5d98a, transparent);
  }

  /* ---- Prow score numeral: strip the plaque chrome, keep the numeral +
     the BUST/★21★ subrow (state logic + score-sync gate carry over). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-sp {
    width: auto; padding: 0; border: none; background: none; box-shadow: none;
    overflow: visible; display: block;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-band,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-tick,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-halo,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-caption,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-status { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-numeral-wrap { margin: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-numeral {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 900; font-size: clamp(44px, calc(100svh * 72 / 900), 72px); font-style: normal;
    line-height: 1; letter-spacing: 0;
    color: #e8b3a6; text-shadow: 0 3px 10px rgba(0,0,0,.9);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-subrow {
    height: auto; margin: 2px 0 0; font-size: 9px; letter-spacing: .2em;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-subrow:empty,
  .gv2-page:not([data-mode="campaign"]) .gv2-sp-blackjack .gv2-sp-subrow { display: none; }
  /* Own seat: parchment numeral (mockup --parch; both seats 72px). */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-sp-numeral,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-sp-numeral {
    color: #efe3c4;
  }
  /* Turn-lit numeral glows white; off-turn dims. Bust keeps the base red
     struck numeral (state rules untouched). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"] .gv2-sp:not(.gv2-sp-bust) .gv2-sp-numeral {
    color: #fff; text-shadow: 0 3px 10px rgba(0,0,0,.9), 0 0 26px rgba(245,217,138,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="waiting"] .gv2-sp:not(.gv2-sp-bust) .gv2-sp-numeral {
    color: #8d8874;
  }
  /* Frozen: icy-white fill + dark stroke lifts the chrome-less numeral
     above the freeze cracks (portrait device-QA item 10 port). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="frozen"] .gv2-sp:not(.gv2-sp-bust) .gv2-sp-numeral {
    color: #eaf6ff;
    -webkit-text-stroke: 0.75px #06131e;
    text-shadow: 0 0 4px #06131e, 0 1px 2px #06131e, 0 2px 8px rgba(0,0,0,.95);
  }

  /* ---- Prow line: name · status · ember, baseline, tight 15px gaps; ✉
     absolute at the right edge. Doubled area-1 selector beats the base
     row-reverse (0,4,0); nowrap + name-ellipsize keeps one line. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-row {
    display: flex; flex-direction: row; align-items: baseline; gap: 15px;
    justify-content: flex-start; flex-wrap: nowrap;
    min-height: 0; padding-bottom: 10px; padding-right: 52px; position: relative;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph {
    order: 1; display: flex; align-items: baseline; flex: 0 1 auto; min-width: 0;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-seal { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-name {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 21px; letter-spacing: 2.5px;
    text-transform: uppercase;
    /* !important: PlayerHeader writes the per-seat accent color INLINE
       (style="color: accentHi") — the mockup retires seat accents; both
       names are gold. */
    color: #f5d98a !important;
    text-shadow: 0 2px 6px rgba(0,0,0,.8);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-sr { order: 2; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bank { display: contents; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-qc {
    position: absolute; right: 0; top: 0;
    min-height: 0; min-width: 0; padding: 5px 11px;
    font-size: 18px; line-height: 1;
    color: var(--brass-hi);
    background: rgba(40,30,14,.92);
    border: 1px solid var(--brass);
    border-radius: 8px;
    box-shadow: 0 1px 5px rgba(0,0,0,.5);
    opacity: 1;
  }
  /* Cooldown dim: the scoped opacity:1 above (0,3,0) would otherwise mask the base
     .is-cooling rule on the wide-board (equal specificity, later in source). This
     (0,4,0) restores the 5s cooldown dim. Portrait works via source order already. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ph-qc.is-cooling { opacity: .5; }
  /* Status caption: strip the ribbon pill → bare letterspaced caption. */
  .gv2-page:not([data-mode="campaign"]) .gv2-sr {
    padding: 0; border: none; background: none !important; box-shadow: none;
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 11.5px; letter-spacing: 3.5px;
    text-transform: uppercase;
    color: #a99878; opacity: 1;
    min-height: 0; line-height: 1.4;
    white-space: nowrap; flex: 0 0 auto;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"] .gv2-sr {
    color: var(--brass-hi); text-shadow: 0 0 8px rgba(245,217,138,.6);
    animation: gv2ProwPulse 1.6s ease-in-out infinite;
  }
  /* Stood: dim the CARDS, not the area (group opacity would dim the badge);
     strong bone-bordered STOOD chip (portrait device-QA item 8 port). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="stood"] { opacity: 1; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="stood"] .gv2-pa-fan { opacity: .55; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="stood"] .gv2-sr {
    font-size: 12px; font-weight: 700; letter-spacing: .22em;
    color: #f3e9c9; opacity: 1;
    background: rgba(20,16,10,.9) !important;
    border: 1px solid var(--bone); padding: 2px 10px;
    text-shadow: 0 1px 3px rgba(0,0,0,.9);
  }
  /* Time-bank ember: visible whenever ACTIVE (Matt 2026-07-02), stripped to
     the bare countdown; pulse only under 60s (gv2-tb-low). The idle boxed
     BANK chip retires at desktop-1v1 (mockup shows no idle bank). */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb.gv2-tb-active {
    display: inline-flex; align-items: baseline; order: 3;
    background: none; border: none; box-shadow: none; padding: 0; min-height: 0;
    /* the base ≤30s emberpulse animates box-shadow back onto the stripped
       chip — the time text carries the pulse instead */
    animation: none;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-label,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-idle-dot { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-time {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 900; font-size: 15px; letter-spacing: .06em; color: #ff8d6e;
    text-shadow: 0 0 10px rgba(255,141,110,.7);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb.gv2-tb-low .gv2-tb-time {
    animation: gv2ProwPulse 1.1s ease-in-out infinite;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb .gv2-tb-time::before {
    content: "· "; color: #9a8f72; text-shadow: none;
  }

  /* ---- Fan: fluid cream cards (JS reads the resolved size via .gv2-sizeprobe). PlayingCard
     writes --pc-* INLINE — the !important-on-element override is required
     (documented shadowing trap). Base .gv2-pa-fan is height:122px fixed —
     height:auto or 200px cards overflow the grid row. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pc {
    --pc-w: var(--gv2-deskcard-w) !important;
    --pc-h: calc(var(--gv2-deskcard-w) * 1.44928) !important;
    --pc-pip: calc(var(--gv2-deskcard-w) * .53623) !important;
    background: linear-gradient(165deg, #fbfbfb 0%, #eeeeee 100%);
    border-color: #8a744a; border-radius: 12px;
    box-shadow: 0 2px 0 #8a744a, 0 10px 24px rgba(0,0,0,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pc-corner {
    font-size: calc(var(--gv2-deskcard-w) * .22464);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan {
    height: auto;
    min-height: calc(var(--gv2-deskcard-w) * 1.44928 + 6px);
  }
  /* Fan score-mods (ActionCard in-fan): match the playing-card footprint
     (non-fluid ActionCard writes width inline → !important). Buried cards
     show the TL corner only; the LAST card shows a full face (glyph +
     SCORE sub) — render-fan.js emits label subs at desktop-1v1 now. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac {
    width: var(--gv2-deskcard-w) !important;
    --gv2-ac-h: calc(var(--gv2-deskcard-w) * 1.44928) !important;
    height: calc(var(--gv2-deskcard-w) * 1.44928) !important;
    border-radius: 12px;
    box-shadow: 0 2px 0 rgba(0,0,0,.5), 0 10px 24px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.18);
  }
  /* Buried mods: TL corner glyph only; the LAST fan card shows the full
     face (glyph + SCORE sub) — the portrait PR 1 toggling at desktop sizes. */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-glyph,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-sigil {
    font-size: calc(var(--gv2-deskcard-w) * 50 / 138) !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-corner-tl {
    font-size: calc(var(--gv2-deskcard-w) * 31 / 138);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-glyph,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-sigil,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:not(:last-child) .gv2-ac-sub { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card:last-child .gv2-ac-corner-tl { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-fan .gv2-fan-card .gv2-ac-sub {
    font-size: calc(var(--gv2-deskcard-w) * 11 / 138) !important; letter-spacing: 2.5px;
  }

  /* ---- Tactics row: ACTIONS · N/7 label + 92×126 tiles (track width comes
     from the JS actionTrackMax — the grid template is written inline).
     ActionCard writes width/font-size inline → !important caps. Foe tray
     flips LTR (atomic with useLtrForTray in render-geometry.js). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-label-actions,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-label-actions {
    flex-direction: row; justify-content: flex-start;
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 9px; letter-spacing: 2.2px;
    text-transform: uppercase; color: #a99878;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable {
    direction: ltr;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable { column-gap: 13px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-card { max-width: var(--gv2-desktray-w); }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-fluid { max-width: var(--gv2-desktray-w) !important; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac {
    /* Dead on painted cards (.gv2-ac-art wins with height:auto + aspect-ratio);
       LIVE on the img-load-failure CSS face, so it must track the tile. */
    --gv2-ac-h: calc(var(--gv2-desktray-w) * 704 / 512) !important; border-radius: 10px;
  }
  /* Fallback-face only (painted tiles emit an <img>) — but tracked for the same
     reason as the portrait twin and --gv2-ac-h: a card whose art fails must not
     render a full-size glyph in a shrunk tile. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-glyph,
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-sigil {
    font-size: calc(var(--gv2-desktray-w) * 30 / 92) !important;
  }
  /* FC's gift icon is an SVG that fills the sigil BOX (inlined 35px),
     ignoring font-size — size the box so it matches the 30px siblings
     (same trap + fix as the landscape-short block: resize + flex-center). */
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-sigil {
    width: 30px !important; height: 30px !important;
    display: flex; align-items: center; justify-content: center;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ar-playable .gv2-ac-sub {
    font-size: 9px !important; letter-spacing: 1.6px;
  }
  /* Score-mod corner badge (+1/+2/-5) is redundant with the center glyph in the
     TRAY (cards aren't overlapped there) and reads as "double print". Landscape-short
     (:4641/:7953) and portrait (:4964/:8283) already hide it; desktop was the only
     viewport that never got the fix. Both modes — campaign inherits by decision
     (docs/plans/2026-07-12-web-app-polish-batch-plan.md). The FAN keeps its corner:
     cards overlap there, so it is the only readable part (see :8716/:8720, which
     sizes it and hides it only on :last-child — the top card, which isn't covered). */
  .gv2-page .gv2-ar-playable .gv2-ac-corner-tl { display: none; }
  /* Flight-face parity for the hide above — MANDATORY, not optional. Flights are
     body-mounted (outside .gv2-page) so the tray rule cannot reach them; without
     this the corner flies visible and vanishes at the landing paint. Unscoped on
     purpose: flights carry no mode marker, and both trays hide the corner under
     this query. Same rule the other two viewports carry (:4648, :4877).
     .gv2-fly = the FH slam (persistent class across its slam→slide swap). */
  .gv2-flight .gv2-ac-corner-tl,
  .gv2-fly .gv2-ac-corner-tl { display: none; }
  /* Foe tiles: present but visibly not yours. */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-ar-playable .gv2-ac,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-ar-playable .gv2-ac {
    filter: saturate(.7) brightness(.82); cursor: default;
  }

  /* ---- USED row: label + 40px recessed chips (UsedChip writes size
     inline → !important; portrait precedent). Foe's base justify-flex-end
     killed — both seats pack left, label first. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-spent {
    display: flex; flex-direction: row; align-items: center; gap: 10px;
    justify-content: flex-start; min-height: 0;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent::before {
    content: "USED";
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 9px; letter-spacing: 2.2px; color: #a99878;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-spent .gv2-uc {
    width: clamp(26px, calc(100svh * 40 / 900), 40px) !important;
    height: clamp(26px, calc(100svh * 40 / 900), 40px) !important; border-radius: 50%;
    border: none;
    background: radial-gradient(circle at 50% 40%, #0c0703, #1c130a);
    box-shadow: inset 0 2px 4px rgba(0,0,0,.9), 0 0 0 1px #3a2a18;
    filter: saturate(.75); opacity: 1;
    font-size: 15px;
    color: var(--gv2-uc-accent, var(--brass-hi));
  }

  /* ---- Center wind-down: count/deck readout retires (the header tap-peek
     replaces it); the deck totem becomes an invisible FIXED anchor behind
     the header bar. ENGINE CONTRACT (portrait precedent): NEVER
     display:none — flights originate from this rect; fixed (not absolute)
     because the positioned .gv2-axis/.gv2-ca ancestors would re-anchor an
     absolute box into the seats band. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-ccs { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca { padding: 0; height: 100%; }
  .gv2-page:not([data-mode="campaign"]) .gv2-dt {
    position: fixed;
    top: calc(env(safe-area-inset-top, 0px) + 84px);
    left: 50%; margin-left: -22px;
    width: 44px; height: 60px;
    opacity: 0; pointer-events: none;
  }
  /* Rules text re-homed bottom-center (mockup .rules): anchored to .gv2-ca,
     which fills the stretched axis column (page-centered, so left:50% is
     exact). PR 2 INTERMEDIATE anchor: just BELOW the seats band (top:100%)
     — the band is content-height until PR 3 pins the page, so the mockup's
     bottom:106px would overlap the seats' USED/controls rows at every
     viewport. PR 3 (pinned page, table-edge controls) re-anchors it to
     bottom:106px over the open felt, directly above the GAME LOG pill.
     z-index 5 floats it; pointer-events:none keeps clicks working. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules {
    /* HIDDEN by default on desktop; the >=700 pin block below turns it back on
       with a fixed anchor above the control bar. This absolute anchor
       (top:100%, i.e. hanging below the seats band) is the PR-2 intermediate
       one, and the fixed HIT/STAND bar is z-index 900 against this rule's 5, so
       wherever it is VISIBLE and the page is not pinned, the bar paints straight
       over it. Under 700px tall the page must keep scrolling (a pinned page that
       does not fit clips seat content top and bottom), so there is nowhere safe
       to put it and hiding is the honest answer — same precedent the old
       900-1049 band used. The geometry below is kept so the pinned block only
       has to override position/anchor, not restyle the text. */
    display: none;
    position: absolute; left: 50%; top: 100%; bottom: auto;
    transform: translateX(-50%); margin-top: 18px;
    z-index: 5; white-space: nowrap; pointer-events: none;
    background: none; border: none; box-shadow: none; padding: 0;
    text-align: center;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-title {
    font-family: 'BJAFellItalic', 'Cormorant Garamond', serif;
    font-style: italic; font-weight: 400; font-size: 30px; line-height: 1.15;
    color: #efe3c4; text-shadow: 0 2px 8px rgba(0,0,0,.8);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-target {
    font-family: 'BJAFellItalic', 'Cormorant Garamond', serif;
    font-style: italic; font-weight: 400; font-size: 24px; line-height: 1.15;
    color: #efe3c4; text-shadow: 0 2px 8px rgba(0,0,0,.8); margin-top: 2px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules-sub {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 13px; letter-spacing: 3.2px;
    text-transform: uppercase; color: #a99878; margin-top: 9px;
  }

  /* ---- FootStrip retires (pulled forward from PR 3 — the rules text
     would collide with it; prow captions + the DEALING… ornament carry
     phase messaging, portrait precedent). ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-board-bottom { display: none; }

  /* ---- Flight cosmetics (body-mounted, outside .gv2-page — the page-scoped
     card rules can't reach them; engine tags gv2-flight-d1v1 at
     desktop-1v1). Mirrors the FULL landed paint so nothing snaps at the
     landing handoff: size vars, pip ratio, corner, radius, cream, lip. ---- */
  .gv2-flight-d1v1 .gv2-pc {
    --pc-w: var(--gv2-deskcard-w) !important;
    --pc-h: calc(var(--gv2-deskcard-w) * 1.44928) !important;
    --pc-pip: calc(var(--gv2-deskcard-w) * .53623) !important;
    background: linear-gradient(165deg, #fbfbfb 0%, #eeeeee 100%);
    border-color: #8a744a; border-radius: 12px;
    box-shadow: 0 2px 0 #8a744a, 0 10px 24px rgba(0,0,0,.6);
  }
  .gv2-flight-d1v1 .gv2-pc-corner {
    font-size: calc(var(--gv2-deskcard-w) * .22464);
  }
  /* ActionCard flight faces (tray-bound deal/hit + FH-slam): a fluid card carries
     a max-width:80px cap, so without this the face capped at 80 inside a wider fly
     box (render-confirmed). max-width:none lets the child fill size it; the FH-slam
     face has no child-fill rule at all, so size it explicitly. Glyph/sub match the
     landed tray caps so the type doesn't snap either.
     The cap used to be written INLINE by ActionCard, which is why this needs
     !important; Batch B removed the inline, but .gv2-ac-fluid still declares the
     same 80px default, so the override is still load-bearing — only its source
     moved. */
  .gv2-flight-d1v1 .gv2-ac-fluid { max-width: none !important; }
  .gv2-fly.gv2-flight-d1v1 .gv2-fly-fh-face .gv2-ac {
    width: 100% !important; max-width: none !important; height: 100% !important;
  }
  .gv2-flight-d1v1 .gv2-ac-glyph,
  .gv2-flight-d1v1 .gv2-ac-sigil { font-size: 30px !important; }
  .gv2-flight-d1v1 .gv2-ac-sigil {
    width: 30px !important; height: 30px !important;
    display: flex; align-items: center; justify-content: center;
  }
  .gv2-flight-d1v1 .gv2-ac-sub { font-size: 9px !important; letter-spacing: 1.6px; }
}
/* Desktop PR 2 reduced-motion companions (PR 1 block covers wc-mp/flag). */
@media (min-width: 768px) and (min-height: 501px) and (prefers-reduced-motion: reduce) {
  .gv2-page:not([data-mode="campaign"]) .gv2-pa[data-state="turn"] .gv2-sr,
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-row .gv2-tb.gv2-tb-low .gv2-tb-time { animation: none; }
}

/* ============================================================
   DESKTOP 1v1 REDESIGN — PR 3: table edge + console + log pill
   (2026-07-02)
   ------------------------------------------------------------
   Final PR of the desktop port (plan: docs/plans/
   2026-07-02-desktop-board-redesign-plan.md; PR 1 felt+header, PR 2
   seats+geometry). Same scope wall. This PR: fixed split table-edge
   HIT/STAND (mockup opt-a .ctlB values) with the center console
   (♪/◉/!/⋯ chips — deck-controls delegation wires them for free), the
   GAME LOG pill re-home (panel opens upward), top-right ⋯ retirement,
   and the page pin ladder (separate min-height blocks below — file
   convention: no nested @media).
   NO palette swap here: portrait's --purple/--orange remap is a no-op
   at desktop (every accent consumer is hidden, !important-overridden,
   or body-mounted where a page-scoped swap can't reach).
   ============================================================ */
@media (min-width: 768px) and (min-height: 501px) {
  /* ---- Fixed-bar clearance at ALL desktop heights (scroll mode too):
     in-flow content must be able to scroll clear of the 84px bar. ---- */
  .gv2-page:not([data-mode="campaign"]) {
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(76px, calc(100svh * 96 / 900), 96px));
  }

  /* ---- Table edge: fixed split bar (mockup opt-a). The own seat's
     .gv2-pcl leaves the flow; the foe's whole bottom row retires. The
     0-height in-grid .gv2-pa-bottom keeps the base 8px margin-top out
     (grid tracks count margins — it would asymmetrize the two seats'
     centered content by 4px). ---- */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-bottom,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"] .gv2-pa-bottom { display: none; }
  /* Pre-resolution (no data-my-idx yet): seat 1's bottom row hides so two
     fixed bars never overlap during the skeleton paint. */
  .gv2-page:not([data-my-idx]):not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"] .gv2-pa-bottom { display: none; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pa-bottom { min-height: 0; margin-top: 0; }
  .gv2-page:not([data-mode="campaign"]) .gv2-pcl {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 900;
    display: flex; gap: 0;
    height: calc(var(--gv2-pcl-h) + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  /* Split edges: 50/50 halves that meet flush at center. The console chips
     relocated OUT of the bar to the fixed top-right cluster, so there is no
     middle child to yield to — the halves fill the full bar width. */
  .gv2-page:not([data-mode="campaign"]) .gv2-cb {
    flex: 1 1 50%; min-height: 0; padding: 0; border: none;
    border-radius: 0; box-shadow: none;
    display: flex; align-items: center; justify-content: center;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-hit {
    /* outer (screen-edge, top-left) corner rounds now the halves meet flush at
       center — the old inner-corner radius framed the removed center console. */
    border-radius: 14px 0 0 0;
    background: linear-gradient(180deg, rgba(120,26,16,0), rgba(120,26,16,.42) 55%, #380a05);
    border-top: 1px solid rgba(255,154,128,.5);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand {
    border-radius: 0 14px 0 0;
    background: linear-gradient(180deg, rgba(122,90,34,0), rgba(122,90,34,.38) 55%, #2e2109);
    border-top: 1px solid rgba(245,217,138,.55);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-label {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 900; font-style: normal;
    font-size: 23px; letter-spacing: 8px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-hit .gv2-cb-label {
    color: #ffd9cf; text-shadow: 0 0 18px rgba(255,107,84,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand .gv2-cb-label {
    color: #ffedbe; text-shadow: 0 0 18px rgba(245,217,138,.6);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-sub,
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-glow { display: none; }
  /* Off-turn dim: re-assert the OPT-A edge gradients (the base disabled
     rule swaps in a dark slab with !important), fade + desaturate. */
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-hit.gv2-cb-disabled {
    background: linear-gradient(180deg, rgba(120,26,16,0), rgba(120,26,16,.42) 55%, #380a05) !important;
    border-top: 1px solid rgba(255,154,128,.5) !important;
    box-shadow: none !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-stand.gv2-cb-disabled {
    background: linear-gradient(180deg, rgba(122,90,34,0), rgba(122,90,34,.38) 55%, #2e2109) !important;
    border-top: 1px solid rgba(245,217,138,.55) !important;
    box-shadow: none !important;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-disabled {
    opacity: .32; filter: saturate(.4);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-cb-disabled .gv2-cb-label { text-shadow: none; }
  /* Off-turn tactic-row dim — OWN tray only (foe's is informational). */
  .gv2-page[data-my-idx="0"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="0"]:not([data-state="turn"]) .gv2-ar-playable,
  .gv2-page[data-my-idx="1"]:not([data-mode="campaign"]) .gv2-pa[data-gv2-player-area="1"]:not([data-state="turn"]) .gv2-ar-playable {
    opacity: .4; filter: saturate(.45);
  }

  /* ---- Option toggles: the 4 console chips (♪ mute · ◉ voice · ! report ·
     ⋯ menu) pulled OUT of the HIT/STAND bar into a fixed cluster in the
     board's top-right corner. position:fixed lifts the LOCAL seat's console
     out of the bar's flex flow (the foe's copy stays hidden via its
     .gv2-pa-bottom{display:none}, so exactly one cluster lands here). The
     GAME LOG pill sits directly below it. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-pcl-console {
    position: fixed; top: 16px; right: 18px; z-index: 902;
    display: flex; flex: 0 0 auto; min-width: 0;
    flex-direction: row; align-items: center; justify-content: flex-end;
    gap: 12px; padding: 0;
  }
  /* Chip base — EXACTLY three class-levels: html.bja-ios
     .gv2-dctl-btn[data-act="voice"] (0,3,1) must keep beating it so the
     voice chip hides on iOS. */
  .gv2-page:not([data-mode="campaign"]) .gv2-console-chip {
    width: 36px; height: 36px; border-radius: 50%; border: none;
    display: flex; align-items: center; justify-content: center;
    font-size: 14px; color: #b9a678; cursor: pointer;
    background: radial-gradient(circle at 50% 40%, #0c0703, #1c130a);
    box-shadow: inset 0 2px 4px rgba(0,0,0,.9), 0 0 0 1px #3a2a18;
    min-height: 0; min-width: 0; padding: 0;
  }
  /* Chip states (page-scoped so they beat the base chip rule AND the
     global .gv2-dctl-btn state rules). The mutesfx red-slash ::after and
     the voice-waiting emberpulse (reduced-motion covered globally) are
     inherited deliberately. */
  .gv2-page:not([data-mode="campaign"]) .gv2-console-chip.is-active {
    color: #f5d98a;
    box-shadow: inset 0 2px 4px rgba(0,0,0,.9), 0 0 0 1px var(--brass-lo),
                0 0 8px rgba(224,196,137,.35);
  }
  /* QA #5 parity (2026-05-31): muted-SFX reads as the red slash ONLY —
     revert the gold active treatment on the ♪ chip (the generic is-active
     rule above would out-specify the global mutesfx revert at :7426). */
  .gv2-page:not([data-mode="campaign"]) .gv2-console-chip[data-act="mutesfx"].is-active {
    color: #b9a678;
    box-shadow: inset 0 2px 4px rgba(0,0,0,.9), 0 0 0 1px #3a2a18;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-console-chip.is-voice-muted { color: #d44552; }
  .gv2-page:not([data-mode="campaign"]) .gv2-console-chip.is-voice-waiting { color: #f5d98a; }

  /* ---- Top-right ⋯ retires (the console ⋯ chip replaces it). Hide ONLY
     .gv2-bc-right: .gv2-board-top/.gv2-bc must NEVER be display:none at
     desktop — state-driver's screen-shake retarget needs .gv2-bc rendered
     at 0 height (offsetParent non-null) to pick .gv2-page over the grid,
     whose transform would teleport this fixed bar. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-bc-right { display: none; }

  /* ---- GAME LOG pill: fixed in the board's top-right corner, directly under
     the option-toggle chip cluster; the entry panel opens DOWNWARD. ---- */
  .gv2-page:not([data-mode="campaign"]) .gv2-log {
    position: fixed; top: 58px; right: 18px;
    z-index: 901; width: auto; max-width: none;
    margin: 0; padding: 0; border: none; background: none; box-shadow: none;
    overflow: visible;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-head {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 6px 8px; cursor: pointer;
    border: none; background: none; width: auto;
    justify-content: center;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-title {
    font-family: 'BJACardCinzel', 'Cormorant Garamond', serif;
    font-weight: 700; font-size: 9.5px; letter-spacing: 2.5px;
    text-transform: uppercase; color: #a99878; font-style: normal;
    text-shadow: 0 2px 6px rgba(0,0,0,.8);
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-chevron { font-size: 8px; color: #7a5a22; }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-body {
    position: absolute; top: 100%; right: 0; margin-top: 6px;
    width: min(340px, calc(100vw - 24px));
    background: linear-gradient(180deg, rgba(26,18,8,.95), rgba(14,9,3,.95));
    border: 1px solid #3a2a18; border-radius: 12px;
    box-shadow: 0 14px 34px rgba(0,0,0,.65);
    padding: 10px 14px 12px;
  }
  .gv2-page:not([data-mode="campaign"]) #gv2-log-entries { max-height: 320px; overflow-y: auto; }
  .gv2-page:not([data-mode="campaign"]) .gv2-log-close { display: block; }
}

/* ============================================================
   DESKTOP 1v1 REDESIGN — page pin (>=700px tall): viewport-height
   board, no scroll.

   Was >=900. The board's content was a CONSTANT 841px tall at every
   viewport (nothing was height-responsive), so 501-899 scrolled AND the
   fixed HIT/STAND bar (z-index 900) painted straight over the tagline
   (z-index 5) — measured overlap at 1280x800, 1440x760 and 1366x720.
   The svh-aware card/tray/numeral/chip/gap tokens above now shrink the
   content with the viewport, so the pin can come down to 700.

   NOT lower than 700: a pinned page that does not fit clips seat content,
   and the desktop .gv2-pa is align-content:center, so it would clip TOP
   AND BOTTOM — losing the score numeral and player name with no scrollbar
   to escape. Below 700 the page keeps scrolling, which is survivable.
   ============================================================ */
@media (min-width: 768px) and (min-height: 700px) {
  .gv2-page:not([data-mode="campaign"]) {
    height: 100dvh; max-height: 100dvh; overflow: hidden;
    padding-top: 12px; gap: 10px;
  }
  .gv2-page:not([data-mode="campaign"]) .gv2-hdr { padding: 12px 0 10px; }
  .gv2-page:not([data-mode="campaign"]) .gv2-game-grid { flex: 1 1 auto; min-height: 0; }
  /* Tagline: ONE anchor for the whole pinned range. It used to be
     display:none from 900-1049 and fixed at a hardcoded 106px only from
     1050, which left 501-899 on the PR-2 absolute anchor (top:100%) that
     the fixed bar painted over. Derived from --gv2-pcl-h so it tracks the
     now-variable bar height instead of drifting 40px->22px across the
     range, and so a future bar edit cannot silently re-create the overlap
     this rule exists to fix. */
  .gv2-page:not([data-mode="campaign"]) .gv2-ca-rules {
    display: block;
    position: fixed; left: 50%; top: auto; margin: 0;
    bottom: calc(env(safe-area-inset-bottom, 0px) + var(--gv2-pcl-h) + 22px);
    z-index: 5;
  }
}

/* ============================================================
   PR 13 — optimistic input (GS1) + press feedback (SS6) + turn pulse (GS4)
   ============================================================ */
/* SS6: press-feedback flash on board tappables (WKWebView :active is unreliable).
   Two-class selectors outrank the base .gv2-cb / .gv2-ar-card. */
.gv2-cb.gv2-cb-pressed { transform: translateY(1px) scale(.98); filter: brightness(1.08); }
.gv2-ar-card.gv2-ar-card-pressed { transform: scale(.95); filter: brightness(1.1); }
/* M3: the optimistic lock dims + blocks the LOCAL action row during the pre-echo window
   (defense-in-depth with the JS click guard). */
.gv2-ar-optimistic-lock .gv2-ar-card { opacity: .55; pointer-events: none; }
/* GS4: one-shot turn-change pulse on the local HIT/STAND bar (brass ring, matches the
   turn-glow palette). JS also skips adding the class under reduced-motion. */
@keyframes gv2-controls-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(224, 196, 137, .55); }
  100% { box-shadow: 0 0 0 16px rgba(224, 196, 137, 0); }
}
.gv2-controls-pulse { animation: gv2-controls-pulse .6s ease-out 1; }
@media (prefers-reduced-motion: reduce) { .gv2-controls-pulse { animation: none; } }

/* ===========================================================================
   PR 5a — Desktop chooser sizing (Swap / Free Card)
   ===========================================================================
   The Swap and Free Card choosers mount on document.body (chooser-sw.js /
   chooser-fc.js), i.e. OUTSIDE .gv2-page. The desktop card token
   --gv2-deskcard-w IS reachable — it is declared on :root precisely so
   body-mounted flight cards can see it. What CANNOT reach them is the CONSUMER
   rule, which is scoped `.gv2-page:not([data-mode="campaign"]) .gv2-pc`. So the
   swap tiles sat pinned at their hardcoded 64px while the board behind them
   rendered cards up to 138px.

   Three things this has to respect:

   1. SIZES ARE WRITTEN INLINE. PlayingCard.js writes --pc-w/--pc-h/--pc-pip as
      inline custom properties ON THE ELEMENT, and ActionCard.js writes `width`
      as a literal inline property while deriving its glyph size from refW in JS.
      Setting a var on the container does nothing. The override has to land on
      the card element itself with !important — or, for anything whose glyph is
      JS-derived, use transform: scale() (the only approach that keeps an
      ActionCard's proportions).

   2. CAMPAIGN MUST BE EXCLUDED, and the modal carries no mode of its own — hence
      the data-mode stamp in ensureModal(). Campaign boards deliberately do NOT
      get 138px cards, so upsizing their chooser would produce the very mismatch
      this fix removes, inverted.

   3. The bare .gv2-overlay-chooser-card rule is SHARED with overflow-alert.js.
      Do not touch it. Everything here is scoped to -sw / -fc.
   =========================================================================== */
@media (min-width: 768px) and (min-height: 501px) {
  /* The hand tiles: PlayingCard writes --pc-w/--pc-h/--pc-pip inline, so !important on the
     element is the only thing that wins.
     
     RATIOS MUST MATCH THE BOARD'S OWN (:8687-8696) EXACTLY. These cards sit in a modal directly
     in front of the board, at the same width, so any divergence reads as a bug. An earlier version
     of this rule used a 0.17 pip ratio — nothing else in the file is near that (PlayingCard.js
     writes --pc-pip: w * 0.6 inline; the board uses .53623; the fixed overrides are ~0.59). The
     card grew 64px -> 138px while its suit glyph SHRANK 38px -> 23px, ending 3x smaller than the
     identically-sized cards visible behind the modal: the exact mismatch this rule exists to
     remove, inverted. */
  .gv2-overlay-chooser-sw:not([data-mode="campaign"]) .gv2-pc {
    --pc-w: var(--gv2-deskcard-w) !important;
    --pc-h: calc(var(--gv2-deskcard-w) * 1.44928) !important;
    --pc-pip: calc(var(--gv2-deskcard-w) * .53623) !important;
  }
  /* The rank corner. The board scales this too (:8694), but that rule is scoped to .gv2-page and
     the modal is body-mounted, so it cannot reach here — the corner would stay at its 20px base
     on a 138px card while the board behind shows 31px. */
  .gv2-overlay-chooser-sw:not([data-mode="campaign"]) .gv2-pc-corner {
    font-size: calc(var(--gv2-deskcard-w) * .22464);
  }

  /* The FC preview. Both card types are constructed at 120x165 (chooser-fc.js), which is
     already close to the desktop board's --gv2-deskcard-w ceiling (clamp(96px, 12.5vw, 138px)) —
     unlike the SW tiles above, which were stranded at 64px. So this is a MODEST, MATCHED bump,
     not the aggressive one the SW row needs.

     Two things it must get right:
       - Scale BOTH types. The FC chooser reveals a PLAYING card in the common case and an
         ActionCard otherwise. Scaling only one made the same modal show 192px action previews
         next to 120px playing-card previews — and 192px is LARGER THAN ANY CARD ON THE BOARD.
       - Use scale(), not a width override. ActionCard derives its glyph size in JS from the
         width it was constructed with, so re-declaring width would leave the glyph behind at its
         original size. scale() keeps the face proportional. */
  .gv2-overlay-chooser-fc:not([data-mode="campaign"]) .gv2-ac,
  .gv2-overlay-chooser-fc:not([data-mode="campaign"]) .gv2-pc {
    /* Tracks the board's live card size instead of the old fixed 138 ceiling —
       the ceiling is now viewport-height-aware, so a hardcoded 1.15 would render
       the chooser LARGER than any card on the board at short heights.
       The `120px` divisor is load-bearing: <length> / <number> yields a LENGTH,
       and scale() requires a <number>, so `calc(var(--x) / 120)` computes to
       `transform: none` and silently drops the scale entirely. */
    transform: scale(calc(var(--gv2-deskcard-w) / 120px));
    transform-origin: center center;
    /* Anti-clip gutter: half the scaled overflow per axis. Note `/ 120px`
       AGAIN — <length>/<number> yields a LENGTH, so subtracting the unitless 1
       is a type error, the declaration is invalid at computed-value time, and
       the margin silently falls back to 0 — which is exactly the clipping this
       gutter exists to prevent. The transform above hit the same trap. */
    margin: max(0px, calc((var(--gv2-deskcard-w) / 120px - 1) * 165px / 2))
            max(0px, calc((var(--gv2-deskcard-w) / 120px - 1) * 120px / 2));
  }

  /* Latent bug, fixed here while the file is open: max-height + overflow-y exist
     ONLY inside the mobile block. On desktop the chooser could grow past the
     viewport and put KEEP / DISCARD / SWAP out of reach on a short laptop. */
  .gv2-overlay-chooser-sw .gv2-overlay-chooser-card,
  .gv2-overlay-chooser-fc .gv2-overlay-chooser-card {
    max-height: 88vh;
    overflow-y: auto;
  }
}
