/* @file star_realms/styling/styles-modal-grid.css
 * @project KAINYNE Website
 * @description STAR REALMS page styles — modal / drag / grid /
 *   responsive / animation tail extracted from `styles.css` to keep
 *   each file under the 90 KB cap (REQ-340 / Rule 9). Loaded AFTER
 *   `styles.css` from `star_realms/index.html` so the cascade order
 *   matches the original single-file layout byte-for-byte. Pure
 *   reorganization: zero rule changes, zero behavioural change.
 *   Cache-bust: bump `?v=N` in index.html when changing this file.
 * @module UI/StarRealms/Styles
 * @requirements REQ-460, REQ-343, REQ-349, REQ-350, REQ-376, REQ-387, REQ-388, REQ-389, REQ-393, REQ-394, REQ-416, REQ-417, REQ-418, REQ-447, REQ-452, REQ-455, REQ-457, REQ-458
 * @hazards Cascade order is load-bearing — this file MUST be loaded
 *   AFTER `styles.css` in `index.html`. Reordering or hoisting will
 *   silently regress later-in-cascade overrides.
 * @notes Originally lines 1788–3073 of `star_realms/styling/styles.css`.
 *   Extracted under REQ-460 as a pure reorganization.
 */

  /* [REQ-387] [REQ-393] Centered Win modal — replaces the in-place
     stage-button "P1/P2 wins" label with a real modal that dims the
     rest of the page and surfaces a clear New Game / Back to Lobby
     choice. REQ-393 made the centering EXPLICIT (position: fixed +
     translate(-50%, -50%)) instead of relying on the UA <dialog>
     default `margin: auto; inset: 0`, which drifted off-center on
     small mobile viewports + interacted poorly with REQ-350's
     body:has(#sr-game-area:not([hidden])) no-scroll lock. Translate-
     centered is robust across browsers and ignores any containing-
     block weirdness from the page grid. */
  .sr-win-modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    background: #0a0716;
    color: #f0f0f0;
    border: 2px solid #e8f020;
    border-radius: 6px;
    padding: 1.5rem 2rem;
    max-width: 420px;
    width: 90%;
    text-align: center;
    font-family: monospace;
    box-shadow: 0 0 32px rgba(232, 240, 32, 0.3);
  }
  .sr-win-modal::backdrop {
    background: rgba(0, 0, 0, 0.6);
  }
  .sr-win-modal-title {
    font-size: 1.5rem;
    letter-spacing: 0.08em;
    margin: 0 0 1.25rem 0;
    color: #e8f020;
  }
  .sr-win-modal-actions {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }
  .sr-win-modal-actions > button {
    background: transparent;
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 2px;
    padding: 0.6rem 1rem;
    cursor: pointer;
    font-family: monospace;
    font-size: 0.95rem;
    letter-spacing: 0.04em;
  }
  .sr-win-modal-actions > button:hover {
    background: #e8f020;
    color: #0f0f0f;
  }
  /* [REQ-457] [PTR-107] Win modal visibility is gated PURELY on the
     `data-sr-stage` body attribute — NO `<dialog>.showModal()`.
     `showModal()` was the root of PTR-102 / PTR-103 / PTR-104:
     each prior PR patched one of its failure paths (nested
     showModal, modal-stack ordering, renderBoard-throw races) and
     the API found another, leaving the user on a blank page after
     surrender. A matched CSS rule is deterministic — no JS throw,
     modal-stack quirk, InvalidStateError, or render-order race can
     prevent it from applying. `#sr-win-modal` is a `<dialog>`, so
     the UA stylesheet hides it (`dialog:not([open]) { display:
     none }`, specificity 0,0,1,1) by default; this rule
     (`body[...] #sr-win-modal`, specificity 0,1,1,1) wins the
     cascade and shows it on game-over. The `.sr-win-modal` rule
     above keeps the centring + palette. */
  body[data-sr-stage="game-over"] #sr-win-modal {
    display: flex;
    flex-direction: column;
  }
  /* [REQ-455] [PTR-105] Runtime diagnostic overlay (?srdebug=1).
     Pinned to top-right with high z-index so it floats above
     everything. CRITICAL: pointer-events: none ensures the overlay
     cannot ever itself become the source of an input-blocking
     freeze — three previous PRs shipped speculative fixes for the
     surrender bug; this overlay exists to BREAK the speculation
     cycle, not to add another failure point. Default-hidden via
     `display: none`; the overlay module flips to `display: block`
     only when it detects `?srdebug=1` in the URL. */
  #sr-debug-readout {
    display: none;
    position: fixed;
    top: 0;
    right: 0;
    z-index: 99999;
    pointer-events: none;
    background: rgba(0, 0, 0, 0.85);
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 0 0 0 4px;
    padding: 0.4rem 0.6rem;
    font-family: monospace;
    font-size: 0.7rem;
    line-height: 1.2;
    white-space: pre;
    max-width: 60vw;
    max-height: 80vh;
    overflow: hidden;
    margin: 0;
  }
  /* [REQ-389] Destroy-confirm dialog — opens on double-tap of an opp
     base. Same centered <dialog> shape as the win modal but uses the
     project's red AUTHORITY/COMBAT palette so the destroy decision
     reads as a danger action. The Yes button is filled red (commits
     the spend); the No button is the default transparent variant. */
  .sr-destroy-confirm {
    background: #0a0716;
    color: #f0f0f0;
    border: 2px solid #e84040;
    border-radius: 6px;
    padding: 1.5rem 2rem;
    max-width: 420px;
    width: 90%;
    text-align: center;
    font-family: monospace;
    box-shadow: 0 0 32px rgba(232, 64, 64, 0.3);
  }
  .sr-destroy-confirm::backdrop {
    background: rgba(0, 0, 0, 0.6);
  }
  .sr-destroy-confirm-title {
    font-size: 1.1rem;
    letter-spacing: 0.04em;
    margin: 0 0 1.25rem 0;
    color: #f0f0f0;
  }
  .sr-destroy-confirm-actions {
    display: flex;
    flex-direction: row;
    justify-content: center;
    gap: 0.6rem;
  }
  .sr-destroy-confirm-actions > button {
    background: transparent;
    color: #e84040;
    border: 1px solid #e84040;
    border-radius: 2px;
    padding: 0.6rem 1rem;
    cursor: pointer;
    font-family: monospace;
    font-size: 0.95rem;
    letter-spacing: 0.04em;
    min-width: 110px;
  }
  .sr-destroy-confirm-actions > button:hover {
    background: #e84040;
    color: #0f0f0f;
  }
  /* [REQ-417] End-turn confirm dialog — opens when the active human
     clicks the staged action button while still holding `combat > 0`.
     Same centered <dialog> shape as the destroy-confirm but uses the
     project's #e8f020 yellow palette so it reads as a proceed/abort
     affordance (matches the staged action button class) rather than a
     danger action. Both buttons use the transparent variant; the
     hover-fills mirror the win-modal buttons. */
  .sr-end-turn-confirm {
    background: #0a0716;
    color: #f0f0f0;
    border: 2px solid #e8f020;
    border-radius: 6px;
    padding: 1.5rem 2rem;
    max-width: 460px;
    width: 90%;
    text-align: center;
    font-family: monospace;
    box-shadow: 0 0 32px rgba(232, 240, 32, 0.3);
    /* [REQ-417] [PTR-076] Center the dialog vertically + horizontally
       in the viewport. UA default positions <dialog> at the top of the
       viewport; on tall mobile screens the prompt appeared glued to
       the status bar instead of in the optical centre where players
       are looking. inset:0 + margin:auto centres in both axes without
       a transform (no half-pixel blur, no width-collapse on long
       titles), and height:fit-content keeps the dialog wrapping its
       intrinsic content height instead of stretching to the viewport. */
    position: fixed;
    inset: 0;
    margin: auto;
    height: fit-content;
  }
  .sr-end-turn-confirm::backdrop {
    background: rgba(0, 0, 0, 0.6);
  }
  .sr-end-turn-confirm-title {
    font-size: 1.1rem;
    letter-spacing: 0.04em;
    margin: 0 0 1.25rem 0;
    color: #f0f0f0;
  }
  .sr-end-turn-confirm-actions {
    display: flex;
    flex-direction: row;
    justify-content: center;
    gap: 0.6rem;
  }
  .sr-end-turn-confirm-actions > button {
    background: transparent;
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 2px;
    padding: 0.6rem 1rem;
    cursor: pointer;
    font-family: monospace;
    font-size: 0.95rem;
    letter-spacing: 0.04em;
    min-width: 110px;
  }
  .sr-end-turn-confirm-actions > button:hover {
    background: #e8f020;
    color: #0f0f0f;
  }
  /* [REQ-389] Action button row inside the card-detail modal so the
     inspect surface is also the action surface (Play / Buy / Activate
     / Scrap / Destroy depending on where the displayed card lives).
     Boot.js stitches the action list per openModal closure; the
     buttons reuse the project's #e8f020 yellow palette so they read
     as the same affordance class as the staged action button. */
  .sr-card-detail-actions {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 0.85rem;
  }
  .sr-card-detail-actions:empty {
    display: none;
  }
  .sr-card-detail-action {
    background: transparent;
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 2px;
    padding: 0.5rem 0.85rem;
    cursor: pointer;
    font-family: monospace;
    font-size: 0.9rem;
    text-align: left;
  }
  .sr-card-detail-action:hover {
    background: #e8f020;
    color: #0f0f0f;
  }
  /* [REQ-387] Multi-ability picker dialog — double-tap on a played
     card with more than one manual ability (Blob Wheel: activated_*
     + scrap_for_*) opens this menu. Re-uses the choice-prompt cancel
     button styling (REQ-386) for the bail-out option. */
  .sr-ability-picker {
    background: #0a0716;
    color: #f0f0f0;
    border: 2px solid #2a2d38;
    border-radius: 6px;
    padding: 1.25rem 1.5rem;
    max-width: 360px;
    width: 90%;
    font-family: monospace;
  }
  .sr-ability-picker::backdrop {
    background: rgba(0, 0, 0, 0.55);
  }
  .sr-ability-picker-title {
    font-size: 1.05rem;
    letter-spacing: 0.04em;
    margin: 0 0 0.85rem 0;
  }
  .sr-ability-picker-list {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
  }
  .sr-ability-pick-option {
    background: transparent;
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 2px;
    padding: 0.5rem 0.85rem;
    cursor: pointer;
    font-family: monospace;
    font-size: 0.9rem;
    text-align: left;
  }
  .sr-ability-pick-option:hover {
    background: #e8f020;
    color: #0f0f0f;
  }
  /* [REQ-388] Long-press drag layer — visual lift on the source tile,
     dashed-yellow highlight on every valid drop zone, white outline +
     faint tint on the zone the pointer is currently over, plus the
     `.sr-drag-ghost` div that follows the pointer (mounted on
     document.body by installCardDragController). Reuses the project's
     #e8f020 yellow accent so the affordance reads as part of the
     existing palette. */
  /* [REQ-416] [PTR-071] Suppress the iOS text-selection callout
     ("Copy / Share / Select all / Web search") + Android context menu
     that fired during the REQ-388 long-press on every card class the
     drag controller touches. Without this the OS gesture would hijack
     the pointer sequence after ~250 ms, fire pointercancel on the page,
     and tear the drag down before the user could release on a drop
     zone. -webkit-touch-callout: none disables the iOS callout, the
     two user-select declarations stop the long-press from selecting
     the card label text, and touch-action: manipulation kills the
     300 ms double-tap-to-zoom delay on the drop-target classes
     (the source classes get the stricter touch-action: none below). */
  .sr-hand-card,
  .sr-trade-slot,
  .sr-inplay-card,
  .sr-base {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    touch-action: manipulation;
  }
  /* [REQ-416] [PTR-071] Drag-SOURCE classes need touch-action: none —
     touch-action: manipulation lets Safari consume single-finger pan
     as scroll mid-drag, which freezes the ghost as soon as the user
     starts moving. The two drop-target classes above keep manipulation
     because they're not drag sources; only the hand + trade-row tiles
     receive long-press → drag handling. setPointerCapture in
     enterDragMode (play-ui.js:933-935) takes over once the 250 ms
     hold completes; touch-action: none ensures the captured events
     reach the controller's pointermove listener untouched. */
  .sr-hand-card,
  .sr-trade-slot {
    touch-action: none;
  }
  /* [REQ-388] Drop-zone containers — without touch-action: none Chrome
     desktop and Chrome Android consume vertical pointer travel over
     these regions as a page-scroll mid-drag, retracting the address
     bar and reflowing the viewport so the dashed target slides up
     under the user's finger before they can release on it. The
     drag-controller's source classes already carry touch-action: none,
     but the gesture here is what the pointer crosses, not where it
     started; locking the drop zones too keeps the page steady for the
     whole drag. */
  [data-sr-drop-zone] {
    touch-action: none;
  }
  /* [PTR-092] :not(body) scopes opacity/transform/pointer-events to the
     source tile only — enterDragMode also adds .sr-dragging to document.body
     as a CSS hook for future body.sr-dragging zone-tinting rules, and without
     this guard body inherits scale(0.92)+opacity:0.4, dimming and shrinking
     the entire page the moment drag starts. */
  .sr-dragging:not(body) {
    opacity: 0.4;
    transform: scale(0.92);
    pointer-events: none;
    transition: opacity 0.1s, transform 0.1s;
  }
  .sr-drop-target-active {
    outline: 2px dashed #e8f020;
    outline-offset: -4px;
  }
  .sr-drop-target-hot {
    outline-color: #ffffff;
    background-color: rgba(232, 240, 32, 0.08);
  }
  .sr-drag-ghost {
    position: fixed;
    pointer-events: none;
    z-index: 9999;
    opacity: 0.85;
    transform: translate(-50%, -50%) scale(0.9);
    transition: none;
  }
  /* [REQ-386] Cancel button on activated_choice prompts — same
     button shape as .sr-choice-option but red-bordered so the
     bail-out reads visually distinct from the green/yellow option
     buttons. Tapping dispatches `cancelChoice` which clears the
     pendingChoice without flipping entry.activatedFired, so the
     player can re-activate the base later in the turn. */
  .sr-choice-cancel {
    background: transparent;
    color: #e84040;
    border: 1px solid #e84040;
    border-radius: 2px;
    /* [REQ-396] tighter padding + margin-top so Cancel doesn't push
       the picker past 32vh max-height on small viewports. */
    padding: 0.3rem 0.6rem;
    margin-top: 0.25rem;
    cursor: pointer;
    font-family: monospace;
    font-size: 0.78rem;
  }
  .sr-choice-cancel:hover {
    background: #e84040;
    color: #0f0f0f;
  }
  /* [REQ-385] [REQ-394] [REQ-412] Opp hand cards RIGHT-justify to
     huddle next to oppdeck (REQ-411 moved oppdeck to the RIGHT col).
     Mirror of player hand pinned next to youdeck on the RIGHT.
     [REQ-418] [PTR-074] Reserve one card-back's worth of vertical
     space on the row container so the `auto`-sized opp-hand grid
     row (`grid-template-rows`, REQ-385) does not collapse to label
     height when `renderOppHand` early-returns with `hand.length===0`
     — without this floor the four 1fr rows below absorb the freed
     pixels and the REQ-412 `border-bottom` dashed separator on the
     section drifts upward "towards the opponent". The player's own
     hand row sits in a `minmax(0, 1fr)` track (REQ-343 / REQ-385)
     and is already anchored — this rule restores symmetry. */
  #sr-opp-hand {
    justify-content: flex-start;
    min-height: var(--sr-card-h);
  }
  .sr-row-scroller[data-sr-row="opphand"] {
    justify-content: flex-start;
  }
  /* [REQ-402] oppdiscard tile bottom-pins to row 4 by making the
     ZONE itself a flex column with justify-content: flex-end. The
     mount is the only flex item; in a flex-column container the
     main axis is vertical, so flex-end pushes the mount to the
     bottom of the row. NO <section> involved — that was the trap.
     SIX attempts at this:
       REQ-394 set `align-self: end` on `.sr-pile-tile-mount` —
         no-op: parent zone was plain block, align-self requires a
         flex/grid parent.
       REQ-395 didn't touch alignment (picker overlay change).
       REQ-397 fixed oppplay's bottom-pin correctly (oppplay HAS a
         <section>), but skipped oppdiscard.
       REQ-399 added `.sr-zone[oppdiscard] > section.sr-section
         { ... }` — silent no-op: oppdiscard has no <section>
         child, only a bare `.sr-pile-tile-mount`.
       REQ-400 extended the oppplay/youplay flex chain to
         oppdiscard. Made the zone display:flex (good) but the
         second half (`flex: 1 1 auto` on > section.sr-section)
         targeted the same non-existent <section>. The mount, with
         fixed card-height in a flex-row zone, stayed at the TOP
         (default `align-items: stretch` is a no-op for items with
         definite cross-size). PR #550's bounding-rect Playwright
         spec failed on REQ-400's commit (5eccb24) — empirical
         confirmation.
       REQ-402 (this): zone is flex-column with
         justify-content: flex-end. One rule, one element, real
         effect. The negative invariant in script-scope.test.js
         guards against any future contributor re-adding a
         `> section.sr-section` rule under oppdiscard.
     - oppdeck: top-pin so the deck tile sits at the top of row 3
       (farthest from trade — mirror of youdeck bottom-pinned at
       bottom of row 7, also farthest from trade per REQ-393). The
       block-flow default top-alignment matches the desired
       position so the (also-no-op) `align-self: start` rule stays
       for documentation symmetry.
     - oppbases zone wrapper: align-self: end anchors the column to
       the BOTTOM of its 2-row span so REQ-386's #sr-their-bases
       inner `justify-content: flex-end` resolves to the trade-row
       edge instead of the top of the spanned cell. */
  .sr-zone[data-sr-area="oppdiscard"] {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }
  /* [REQ-412] oppdeck moved to RIGHT col; align-self: start top-pins
     the deck inside row 3 (mirror of youdeck bottom-pinned in row 7). */
  .sr-zone[data-sr-area="oppdeck"] .sr-pile-tile-mount {
    align-self: start;
  }
  /* Mirror of the youdeck zone — make the oppdeck zone a flex column
     and pin min-height: card-h so the deck cell can't collapse below
     one card's worth of vertical space. Without this floor the opp
     deck tile rendered shorter than the opp hand cards in the same
     row (the hand cards' section wrapper added padding+border that
     made the hand cell taller than the bare deck mount). */
  .sr-zone[data-sr-area="oppdeck"] {
    display: flex;
    flex-direction: column;
    align-items: end;
    justify-content: flex-start;
    min-height: var(--sr-card-h);
  }
  /* Mirror of `.sr-zone[data-sr-area="opphand"] section.sr-section`
     for the opp hand row — zero the section's vertical padding so
     the section is exactly card-h tall (matches the oppdeck cell
     next to it). border-bottom dashed separator stays for visual
     parity with the opp-content sections. */
  .sr-zone[data-sr-area="opphand"] section.sr-section {
    padding: 0;
    gap: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
  }
  /* Belt-and-suspenders: the opp hand zone reserves card-h vertical
     space so its baseline matches the oppdeck zone next to it, even
     if the grid track sizing oddly tries to shrink the cell. */
  .sr-zone[data-sr-area="opphand"] {
    min-height: var(--sr-card-h);
  }
  .sr-zone[data-sr-area="oppbases"] {
    align-self: end;
  }
  /* Pull the discard zones flush against their respective deck zones
     in the adjacent grid row. The discard zone naturally floats at the
     TOP of its row cell (block default), but each row is `minmax(0,
     1fr)` so the cell can be taller than the card-h tile — leaving
     empty space between the discard tile and the deck tile in the next
     row. align-self snaps the zone to the row edge that faces its deck:
       • youdiscard (row 7) → BOTTOM-pin so it hugs youdeck (row 8 below)
       • oppdiscard (row 3) → TOP-pin so it hugs oppdeck (row 2 above)
     With the zone box now collapsed to card-h and parked at the right
     edge of the row, the discard ↔ deck gap reduces to a single
     section-gap regardless of how tall the row stretches. */
  .sr-zone[data-sr-area="youdiscard"] {
    align-self: end;
    /* Eat the grid gap between row 7 (youdiscard) and row 8 (hand)
       so the bottom-pinned discard tile's bottom edge meets the hand
       row's top edge with no visible separator. Mirror of the
       HUD/trade gap-eating pattern at lines ~1296–1301. The negative
       margin lives on the zone (grid item) — applying it on the
       inner mount would be clipped by .sr-zone[data-sr-area] { overflow:
       hidden }. */
    margin-bottom: calc(-1 * var(--sr-section-gap));
  }
  .sr-zone[data-sr-area="oppdiscard"] {
    align-self: start;
    /* Same gap-collapse, opposite direction: pulls the top of the
       top-pinned oppdiscard up into the row 2/3 grid gap so it meets
       the opp-hand row's bottom edge — strict mirror parity with the
       player side. */
    margin-top: calc(-1 * var(--sr-section-gap));
  }
  .sr-opp-hand-card {
    width: var(--sr-card-w);
    height: var(--sr-card-h);
    background: rgba(0, 0, 0, 0.45);
    border: 1px dashed #2a2d38;
    border-radius: 2px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #555;
    font-family: monospace;
    font-size: clamp(0.6rem, 1.4vw, 0.85rem);
    letter-spacing: 0.1em;
  }
  .sr-trade-slot-empty {
    border: 1px dashed #2a2d38;
    background: rgba(255,255,255,0.02);
    color: #555;
    font-style: italic;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* [REQ-349] Card-sized pile tile — trade deck, scrap, every
     player's deck + discard share these dimensions so the whole
     board reads as a single grid of equally-sized cards. Sizes track
     `--sr-card-w/h` so pile tiles shrink with trade-row cards on
     mobile and grow on desktop; clamp() on the inner type sizes
     keeps the count digit legible at thumbnail size. */
  .sr-pile-row-stacks {
    display: flex; flex-wrap: wrap; gap: 0.5rem;
    align-items: flex-start; margin: 0.25rem 0;
  }
  .sr-pile-row-stacks .sr-pile-label,
  .sr-pile-row-stacks .sr-pile-chip { align-self: center; }
  /* [REQ-375] anchor REQ-365 overlay + REQ-362 corners. */
  .sr-pile-tile {
    position: relative;
    width: var(--sr-card-w); height: var(--sr-card-h);
    box-sizing: border-box;
    border: 1px dashed #2a2d38; border-radius: 2px;
    padding: clamp(2px, 0.6vw, 8px); background: rgba(0,0,0,0.25);
    cursor: pointer; display: flex; flex-direction: column;
    align-items: center; justify-content: center; text-align: center;
    font-family: monospace; color: #b0b0b0;
    transition: background 0.12s, box-shadow 0.12s;
  }
  .sr-pile-tile:hover, .sr-pile-tile:focus {
    background: rgba(255,255,255,0.06);
    box-shadow: 0 0 12px currentColor;
    outline: 1px dotted currentColor; outline-offset: 1px;
  }
  /* [REQ-365] Phase 10 redesign — face-up pile tiles render the top
     discard / scrapped card on the tile face. The overlay is a thin
     translucent banner across the top edge that carries the pile
     label + count, so the click affordance reads even with the card
     art behind it. The face-up tile drops the centred flex layout
     because `renderCardTile` fills the tile directly.
     [PTR-097] padding-top clears the overlay so card-detail text
     appears below the "DISCARD" banner, not behind it. */
  .sr-pile-tile-faceup {
    display: block;
    padding: 0;
    text-align: left;
    border-style: solid;
  }
  /* [PTR-097] `.sr-art-panel` renders as a thin 18–46 px strip in
     hand / trade-row cards. Inside a face-up pile tile the overlay
     sits at the top (z-index: 2) and covers the strip, leaving the
     rest of the tile showing only the dark base background. Fix:
     stretch the art panel to fill the entire tile so the faction
     gradient covers the full card face. `.sr-pile-tile` already
     carries `position: relative`, so `inset: 0` on an absolute
     child fills border-to-border without any extra wrapper. */
  .sr-pile-tile-faceup .sr-art-panel {
    position: absolute;
    inset: 0;
    height: auto;
    margin: 0;
    border-radius: 2px;
    border-bottom: none;
    z-index: 0;
  }
  /* [PTR-098] Extend the PTR-097 full-face art panel to hand / trade-row
     / in-play / base card tiles. The thin 18–46 px `.sr-art-panel` strip
     is promoted to a full-face backdrop so faction nebula gradient covers
     the whole card tile — matching the face-up discard tile fix.
     The containers already carry `position: relative; overflow: hidden`
     so `inset: 0` fills border-to-border cleanly. The text elements
     (.sr-card-faction, .sr-card-cost, .sr-card-name, .sr-card-type) are
     all `position: absolute` siblings added after the art panel in the
     DOM, so they naturally paint above z-index: 0 in source order. */
  .sr-hand-card .sr-art-panel,
  .sr-trade-slot .sr-art-panel,
  .sr-inplay-card .sr-art-panel,
  .sr-base .sr-art-panel {
    position: absolute;
    inset: 0;
    height: auto;
    margin: 0;
    border-radius: 2px;
    border-bottom: none;
    z-index: 0;
  }
  /* [REQ-365] Card-corner stamps on the face-up pile tile stay
     absolutely positioned (the global `.sr-card-faction` / `.sr-card-cost`
     / `.sr-card-name` / `.sr-card-type` rules above pin each to its
     own corner). This rule lifts them above the full-bleed art panel
     (`z-index: 0`) so the faction nebula paints behind the text.
     `.sr-card-cost` is included so the cost stamp top-right shows
     up on the discard pile face the same as on hand / trade cards. */
  .sr-pile-tile-faceup .sr-card-faction,
  .sr-pile-tile-faceup .sr-card-cost,
  .sr-pile-tile-faceup .sr-card-name,
  .sr-pile-tile-faceup .sr-card-type {
    z-index: 1;
  }
  .sr-pile-tile-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.15rem 0.4rem;
    background: rgba(0, 0, 0, 0.65);
    font-size: 0.55rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #e8e9ef;
    pointer-events: none;
    border-radius: 2px 2px 0 0;
    z-index: 2;
  }
  .sr-pile-tile-overlay-count {
    font-size: 0.7rem;
  }
  .sr-pile-tile-head {
    font-size: clamp(0.5rem, 1.4vw, 0.75rem);
    letter-spacing: 0.12em;
    color: #888; margin-bottom: clamp(2px, 1vw, 8px);
  }
  .sr-pile-tile-count {
    font-size: clamp(1rem, 6vw, 2.6rem); font-weight: bold;
    color: currentColor; line-height: 1;
  }
  /* [REQ-376] The "N cards" hint duplicates the count digit shown
     directly above it (DECK / 5 / "5 cards") and crowds the tile.
     Hide the line; the DOM emit in `_buildPileTile` stays so callers
     that read .textContent for tests still find the hint string. */
  .sr-pile-tile-hint {
    display: none;
    font-size: clamp(0.5rem, 1.2vw, 0.7rem);
    margin-top: clamp(2px, 1vw, 8px);
    color: #888; letter-spacing: 0.04em;
  }
  /* [REQ-377] Count digit hidden by default on every pile-tile face
     (deck, discard, scrap, trade-deck). The count is reachable only
     by clicking the tile — the existing pile-viewer modal lists every
     card so the count is implicit. Same "keep the DOM emit, hide via
     CSS" pattern as `.sr-pile-tile-hint` above and the REQ-371 /
     REQ-374 hides; tests that read `.textContent` still find the
     emitted digit. */
  .sr-pile-tile-count,
  .sr-pile-tile-overlay-count {
    display: none;
  }
  /* [REQ-377] Pile-tile zone mount points reserve full card-tile
     footprint so the inner `_buildPileTile` markup paints at the
     same size as every other card on the surface. */
  .sr-pile-tile-mount {
    width: var(--sr-card-w);
    height: var(--sr-card-h);
  }
  /* [REQ-419] [PTR-075] Pile-tile borders inherit the default
     `1px dashed #2a2d38` (gray) from `.sr-pile-tile`. The blue
     dashed border on both discard piles and the orange dashed
     border on the scrap pile were reported as visual noise — they
     duplicated the colour signal already carried by the per-tile
     pip text. Keeping `color:` only paints the pip in the accent
     shade while the border falls back to gray. The superseded
     REQ-404 opp-discard mirror is folded in: both player + opp
     discards share a single colour declaration and inherit the
     same gray border together, so a future palette tweak can't
     drift the two apart. */
  .sr-scrap-pile-tile { color: #ff8040; }
  .sr-discard-pile-tile-you { color: #40b4e8; }
  .sr-discard-pile-tile-opp { color: #40b4e8; }
  /* [REQ-350] In-play row + pile-tile column wrapper. Each player's
     in-play row mounts a 2-tile DECK + DISCARD column beside the
     row so deck/discard sit on the player's edge of the table. No
     overflow-x — cards fit by token math. The pile-tiles-col stacks
     deck+discard vertically (saves horizontal width); cards inside
     the .sr-row sibling scale via the responsive token so the row
     fits whatever width remains. */
  .sr-play-row {
    display: flex;
    gap: var(--sr-card-gap);
    align-items: flex-start;
    min-width: 0;
    min-height: 0;
  }
  .sr-play-row > .sr-row {
    flex: 1 1 auto;
    min-width: 0;
  }
  /* [REQ-377] The legacy `.sr-pile-tiles-col` half-height pile-tile
     column is retired — each pile lives in its own grid zone at full
     `--sr-card-w` × `--sr-card-h`. The half-height override (REQ-350)
     was the direct cause of the user-reported `DISCARD` overlay
     truncation. Selector kept defined as a back-compat shim with no
     sizing override; `renderPileTilesRow` (the back-compat function)
     can still mount into a column container if any caller still uses
     that pattern. */
  .sr-pile-tiles-col {
    display: flex;
    flex-direction: column;
    gap: var(--sr-card-gap);
    flex-shrink: 0;
  }
  /* [REQ-350] Bases section sits on the SAME visual row as the
     in-play ships (matching the commercial Star Realms client's
     table layout). The grid cell that owns the oppplay / youplay
     zone uses flex-wrap: nowrap so both <section> children sit
     side-by-side, and section labels collapse into a single line on
     mobile viewports where vertical space is tightest. */
  /* [REQ-402] REQ-400 mistakenly added `oppdiscard` to this
     selector list assuming it had a <section> child like oppplay /
     youplay. It doesn't (just a bare .sr-pile-tile-mount), so the
     `flex: 1 1 auto` on `> section.sr-section` matched nothing and
     the fix was a silent no-op. oppdiscard's actual bottom-pin
     lives at line 1758 above (zone-as-flex-column). */
  .sr-zone[data-sr-area="oppplay"],
  .sr-zone[data-sr-area="youplay"] {
    display: flex;
    flex-direction: row;
    gap: var(--sr-card-gap);
    align-items: stretch;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
  }
  /* [REQ-447] [PTR-099] The section MUST fill the zone's full grid-cell
     width so the play zone is always bounded by the trade-row line and
     the discard-pile column, not by the cards currently in it.
     `flex: 1 1 auto` (flex-grow: 1) achieves this. A previous
     `:last-child` override set `flex: 0 1 auto` here, which made sense
     when the zone had two sections (ships + legacy deck/discard column).
     REQ-377 retired that column, leaving exactly one section per zone;
     the override always fired and suppressed flex-grow on the only
     section. Removing it lets `flex: 1 1 auto` apply uncontested. */
  .sr-zone[data-sr-area="oppplay"] > section.sr-section,
  .sr-zone[data-sr-area="youplay"] > section.sr-section {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  /* Opp in-play cards TOP-pin in row 3 — they sit in-line with the
     oppdiscard tile (also top-pinned via align-self: start) and with
     the opp-hand row directly above (row 2). User wanted the play
     zone to move TOWARD the hand it belongs to, not away from it;
     for opp that's "up" toward the opp hand row above. The 1fr grid
     track stays large so the gap between this play row and the trade
     row below absorbs the freed vertical space (instead of pulling
     the opp-hand row down). */
  .sr-zone[data-sr-area="oppplay"] > section.sr-section {
    /* [REQ-716] Centre the in-play row in its card-h+6px track so the
       full-size card clears the overflow:hidden top/bottom edges (~3px
       each) instead of sitting flush and getting its border/label
       shaved. */
    justify-content: center;
    align-items: flex-start;
  }
  /* Player in-play cards BOTTOM-pin in row 7 — mirror of opp. They
     sit in-line with youdiscard (also bottom-pinned via align-self:
     end) and with the hand row directly below (row 8). User wanted
     the play zone to move DOWN toward the hand, not the hand to
     move up. The 1fr grid track stays large so the gap between this
     play row and the you-HUD above absorbs the freed vertical space
     (instead of pulling the hand row up). */
  .sr-zone[data-sr-area="youplay"] > section.sr-section {
    /* [REQ-716] Centre (was flex-end) so the full-size card clears the
       overflow:hidden bottom edge with ~3px of room instead of sitting
       flush and getting its bottom border/label shaved. */
    justify-content: center;
    align-items: flex-end;
  }
  /* [REQ-713] The opp/your in-play grid tracks are pinned to exactly
     one card tall (var(--sr-card-h)) so each in-play row sits in-line
     with its discard tile. The section that wraps the card row,
     however, also stacked an <h2> label + var(--sr-section-py) padding
     + a 1px dashed separator ABOVE the row — overhead that the bare
     discard tile in the same grid row does not carry. Inside a
     card-h-tall zone with overflow:hidden that overhead pushed the
     card down and clipped its BOTTOM edge (most visible on viewports
     wide enough to still show the h2, which mobile already hides at
     ~480px). Zero the section's padding + border and drop the label
     for these two zones (there is no vertical room for a stacked label
     in a single-card row, and the zone's position between the hand and
     trade row already identifies it) so the card fills the full
     card-h and is no longer cut off. Mirrors the hand zone's
     padding-strip fix (styles.css ~1847). */
  .sr-zone[data-sr-area="oppplay"] section.sr-section,
  .sr-zone[data-sr-area="youplay"] section.sr-section {
    padding: 0;
    border: 0;
  }
  .sr-zone[data-sr-area="oppplay"] section.sr-section h2,
  .sr-zone[data-sr-area="youplay"] section.sr-section h2 {
    display: none;
  }
  /* [REQ-712] Newly-played cards land to the LEFT of the card just
     played, on each owner's side from THEIR point of view:
     - opp (top): newest card sits to the OPPONENT's left = the human
       viewer's RIGHT. DOM order is oldest-first; flex-start + normal
       row direction lays oldest at the left edge (next to the opp
       discard in the LEFT column) and the newest at the right, so the
       most-recent opp card hugs the viewer's right. (Unchanged.)
     - you (bottom): newest card sits to the human's LEFT. DOM order is
       oldest-first; row-reverse + flex-start pins the OLDEST card at
       the right edge (next to the you-discard in the RIGHT column) and
       grows each newer card leftward into the play zone — so the card
       you just played appears to the left of the previous one, away
       from the deck/discard column rather than on top of it. */
  #sr-their-in-play {
    justify-content: flex-start;
  }
  #sr-your-in-play {
    flex-direction: row-reverse;
    justify-content: flex-start;
  }
  /* Explicit dashed separator at the TOP of the player content sections
     (mirror of the OPP border-bottom at styles.css:284-289 which paints
     a dashed line at the BOTTOM of opp content sections). The default
     `section.sr-section { border-top: 1px dashed #2a2d38; }` should
     already apply, but pin it explicitly so it can't be lost to cascade
     surprises. */
  .sr-zone[data-sr-area="yourbases"] section.sr-section,
  .sr-zone[data-sr-area="youplay"] section.sr-section,
  .sr-zone[data-sr-area="hand"] section.sr-section {
    border-top: 1px dashed #2a2d38;
  }
  /* [Card layout] Collapsible log panels via <details>/<summary>. */
  .sr-log-details > summary.sr-log-summary {
    list-style: none; cursor: pointer; user-select: none;
    font-size: 0.7rem; letter-spacing: 0.18em; color: #888;
    padding: 0.2rem 0; text-transform: uppercase;
  }
  .sr-log-details > summary.sr-log-summary::-webkit-details-marker { display: none; }
  .sr-log-details > summary.sr-log-summary::before {
    content: '▶'; display: inline-block;
    margin-right: 0.45rem; font-size: 0.65rem;
  }
  .sr-log-details[open] > summary.sr-log-summary::before { content: '▼'; }
  .sr-log-details > .sr-log-panel { margin-top: 0.5rem; }

  /* [REQ-343] Single-screen responsive fit — CSS Grid game area.
     Eight named grid areas consolidate the 14 underlying sections
     so the four reference viewports (375x812, 812x375, 800x1200,
     1440x900) fit within 100dvh with no scroll. Each section keeps
     its own DOM id (every getElementById('sr-…') target is preserved)
     and is wrapped by an `.sr-zone[data-sr-area="…"]` div that owns
     the grid-area assignment. Tap-to-read on individual cards is
     handled by the existing REQ-327 detail modal; collapsible logs
     remain owned by the existing `<details>/<summary>` pattern from
     the prior STAR REALMS log refactor. */
  /* [PTR-048] The `[hidden]` HTML attribute relies on the user-agent
     stylesheet's `[hidden] { display: none }` rule (specificity
     0,0,0,1). The ID + display:grid rule below has specificity
     0,1,0,0 and would otherwise win, so the lobby boots with the
     game area visible. Re-assert the hidden override with matching
     ID specificity — same pattern as `.sr-lobby-section[hidden]`
     and `.sr-choice-prompt[hidden]` above. */
  #sr-game-area[hidden] { display: none; }
  /* [REQ-452] [REQ-458] Surface mutual-exclusion via a single
     source-of-truth attribute on <body>. The renderer writes
     `data-sr-surface="game"` whenever a game is ON SCREEN — that is
     `state.status === 'in_progress'` OR a terminal `'p1_won'` /
     `'p2_won'` game-over, so the board stays painted behind the
     CSS-gated win modal (REQ-458 / PTR-108: a `"lobby"` surface on
     game-over hid the whole `#sr-game-area` and left the modal
     floating over a blank starfield). `"lobby"` is reached only via
     the explicit teardown resets in `boot.js` (`onWinLobby` /
     `performLeaveGameTeardown`). These two scoped attribute rules
     collapse the in-game-vs-lobby invariant to ONE attribute that
     both CSS and tests can probe. The existing per-element
     `[hidden]` writes above stay as defence-in-depth so a transient
     renderer skip can't paint both surfaces. Closes PTR-102 —
     surrender-confirm dismissal no longer surfaces the lobby behind
     the in-game board because the `[hidden]` flag flip that
     triggered the layout shift is now subordinate to the
     attribute-gated rule. */
  body[data-sr-surface="game"] .sr-lobby-section { display: none; }
  body[data-sr-surface="lobby"] #sr-game-area { display: none; }
  /* [REQ-350] Strict no-scroll grid — height locked to 100dvh, the 4
     card-rows (oppplay / trade / youplay / hand) split the remaining
     height via fr-units after chrome rows (top, opp-stats,
     you-stats, actions) claim what they need. minmax(0, 1fr) with a
     0 floor lets card-rows compress when chrome rows expand, never
     overflowing. */
  #sr-game-area {
    display: grid;
    gap: var(--sr-section-gap);
    height: 100dvh;
    width: 100%;
    /* [REQ-384] Three-column grid — LEFT column carries opp piles (top
       half) + player bases column (bottom half); CENTRE column carries
       the play surfaces; RIGHT column carries opp bases column (top
       half) + player piles (bottom half). [REQ-385] An opphand row is
       inserted between opp authority and opp in-play so each side
       visibly shows how many cards the opponent is holding (mirrors
       the player hand row at the bottom). */
    grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
    grid-template-rows:
      auto                /* row 1: top */
      auto                /* row 2: opp hand row (card backs, max 5) */
      calc(var(--sr-card-h) + 6px) /* row 3: opp in-play — card-h plus 6px so the full-size card centres with clearance and its top edge isn't clipped (REQ-716). */
      minmax(0, 1fr)      /* row 4: oppfiller — new 1fr filler that absorbs the vertical space the old oppplay 1fr used to claim, leaving the opp in-play row a single card tall above it. */
      auto                /* row 5: opp authority HUD */
      minmax(0, 1fr)      /* row 6: trade row */
      auto                /* row 7: you authority HUD */
      minmax(0, 1fr)      /* row 8: youfiller — symmetric to oppfiller; absorbs the freed 1fr space above the shrunk youplay row so the in-play cards sit just above the hand. */
      calc(var(--sr-card-h) + 6px) /* row 9: your in-play — card-h plus 6px so the full-size card centres with clearance and its bottom edge isn't clipped (REQ-716). */
      auto                /* row 10: your hand — content-sized. */
      auto;               /* row 11: action button row */
    /* `oppbases` extends across the new oppfiller row (rows 2–4) and
       `yourbases` extends across the new youfiller row (rows 8–10)
       so the bases column still has room for the full 3-base stack
       even though the in-play rows are now just card-h. */
    grid-template-areas:
      "top         top         top"
      "oppdeck     opphand     oppbases"
      "oppdiscard  oppplay     oppbases"
      "oppfiller   oppfiller   oppbases"
      "opp         opp         opp"
      "trade       trade       trade"
      "you         you         you"
      "yourbases   youfiller   youfiller"
      "yourbases   youplay     youdiscard"
      "yourbases   hand        youdeck"
      "stagebtn    stagebtn    stagebtn";
    overflow: hidden;
  }
  .sr-zone[data-sr-area]              { min-width: 0; min-height: 0; overflow: hidden; }
  .sr-zone[data-sr-area="top"]        { grid-area: top; }
  .sr-zone[data-sr-area="opp"]        { grid-area: opp; }
  .sr-zone[data-sr-area="oppbases"]   { grid-area: oppbases; }
  .sr-zone[data-sr-area="oppplay"]    { grid-area: oppplay; }
  .sr-zone[data-sr-area="trade"]      { grid-area: trade; }
  .sr-zone[data-sr-area="yourbases"]  { grid-area: yourbases; }
  .sr-zone[data-sr-area="youplay"]    { grid-area: youplay; }
  .sr-zone[data-sr-area="hand"]       { grid-area: hand; }
  .sr-zone[data-sr-area="opphand"]    { grid-area: opphand; }
  .sr-zone[data-sr-area="you"]        { grid-area: you; }
  .sr-zone[data-sr-area="stagebtn"]   { grid-area: stagebtn; }
  /* [REQ-377] Pile-tile zones — each holds a single
     `.sr-pile-tile-mount` div sized to one full card tile. */
  .sr-zone[data-sr-area="oppdeck"]    { grid-area: oppdeck; }
  .sr-zone[data-sr-area="oppdiscard"] { grid-area: oppdiscard; }
  .sr-zone[data-sr-area="youdeck"]    { grid-area: youdeck; }
  .sr-zone[data-sr-area="youdiscard"] { grid-area: youdiscard; }
  /* Filler zones — empty placeholders for the 1fr tracks freed when
     oppplay / youplay shrank to card-h. No styling: the page
     background shows through and the rows just reserve vertical
     space. Future content (info panel, ads, debug overlays) can drop
     into these zones without further grid changes. */
  .sr-zone[data-sr-area="oppfiller"]  { grid-area: oppfiller; }
  .sr-zone[data-sr-area="youfiller"]  { grid-area: youfiller; }
  /* [REQ-380] In-play rows scroll horizontally past 5 cards. The 5
     visible spots come from --sr-card-w + gap; extras are reachable by
     swiping the row.
     [REQ-389] flex-shrink: 0 on the cards forces the 6th card to push
     the row into horizontal scroll instead of compressing the existing
     5 — without this lock the cards just shrink and the user sees no
     scroll affordance. The shared sizing rule above sets width via
     --sr-card-w; this override pins the size against flex compression. */
  #sr-your-in-play,
  #sr-their-in-play {
    overflow-x: auto;
    overflow-y: hidden;
    /* Explicit touch-action so a horizontal swipe on the in-play row
       is captured by this scroller instead of bubbling up to the page
       scroller (which may pick it up as a swipe-back gesture on iOS).
       `pan-x` allows horizontal pan only — the row never needs to
       scroll vertically, and locking out vertical pan also makes the
       gesture recognizer commit to horizontal sooner. */
    touch-action: pan-x;
    -webkit-overflow-scrolling: touch;
    /* [REQ-389] The in-play row is a `.sr-row` flex container sitting
       inside a section that has `display: flex; flex-direction: column;
       overflow: hidden`. Without an explicit width the row sizes to its
       content and never overflows the section, so `overflow-x: auto`
       can't trigger — the 6th+ cards just clip behind the section's
       hidden boundary. `width: 100%` + `min-width: 0` + `flex: 1 1 auto`
       force the row to fill the section column, making the children
       overflow the row's box and giving swipe-scroll something to grab.
       `max-width` then caps the visible width at exactly 5 cards (plus
       4 inter-card gaps), so the 6th+ in-play card overflows
       horizontally and only becomes reachable via the swipe-scroll
       affordance. Paired with the `margin-inline: 0` override above
       on `.sr-inplay-card` so the cap math is exact. */
    width: 100%;
    min-width: 0;
    flex: 1 1 auto;
    max-width: calc(5 * var(--sr-card-w) + 4 * var(--sr-card-gap));
  }
  #sr-your-in-play .sr-inplay-card,
  #sr-their-in-play .sr-inplay-card {
    flex-shrink: 0;
  }
  /* [REQ-380] YOUR · AUTHORITY zone: AUTHORITY / COMBAT / TRADE chips
     on the inner side (next to the trade row), player name on the
     outer side. Both sit in a single centered flex row so the chip
     group + name read as one centered HUD unit inside the gold
     frame. The staged action button moved out to the youdeck zone
     below the deck tile. */
  .sr-zone[data-sr-area="you"] section.sr-you-section {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: flex-start;
    gap: 0.75rem;
  }
  /* Hide h2 in gold box — chip labels already identify each chip
     (TRADE / COMBAT / AUTHORITY), so the section heading is redundant
     and was pushing the chip row off the box's vertical centre. */
  .sr-zone[data-sr-area="you"] section.sr-you-section > h2 {
    display: none;
  }
  .sr-zone[data-sr-area="you"] section.sr-you-section .sr-authority-line {
    display: none;
  }
  /* Promote the three chips inside #sr-resource-chips to direct flex
     children of the gold HUD section so AUTHORITY can be repositioned
     independently of TRADE/COMBAT. With `display: contents` the
     container disappears from layout but the chips inherit the
     section's flex context (align-items: center, etc.). */
  .sr-zone[data-sr-area="you"] #sr-resource-chips {
    display: contents;
  }
  /* AUTHORITY chip overrides — wider than the default chip (so the
     "AUTHORITY" label has room to render) but the SAME height as
     TRADE/COMBAT chips (no fixed height; auto-sizes to padding +
     font like the other chips). Green border + green numbers;
     per-seat background (P1 red / P2 blue) is set below via
     data-seat. order: 99 pins this chip at the END of the flex
     flow. */
  .sr-zone[data-sr-area="you"] section.sr-you-section .sr-chip-health {
    width: clamp(5rem, calc(var(--sr-card-w) * 1.8), 7rem);
    border-color: #40e840;
    color: #40e840;
    box-shadow: 0 0 6px rgba(64, 232, 64, 0.35);
    order: 99;
  }
  .sr-zone[data-sr-area="you"] section.sr-you-section .sr-chip-health .sr-chip-label,
  .sr-zone[data-sr-area="you"] section.sr-you-section .sr-chip-health .sr-chip-value {
    color: #40e840;
  }
  /* Background by player number (data-seat is stamped by
     renderResourceChips at render time). */
  .sr-zone[data-sr-area="you"] section.sr-you-section .sr-chip-health[data-seat="0"] {
    background: rgba(232, 64, 64, 0.55);
  }
  .sr-zone[data-sr-area="you"] section.sr-you-section .sr-chip-health[data-seat="1"] {
    background: rgba(64, 180, 232, 0.55);
  }
  /* Player name sits BETWEEN the centered avatar and the AUTHORITY
     chip — no auto-margins needed now that the avatar's auto-margins
     absorb the flex slack and pin everything after the avatar to
     the right side. order: 51 places the name immediately before
     AUTHORITY (which has order: 99). */
  .sr-zone[data-sr-area="you"] #sr-your-hud-name {
    margin: 0;
    align-self: center;
    order: 51;
  }
  /* Avatar zone — centered placeholder for a future profile image.
     margin: 0 auto on left+right absorbs all flex slack equally on
     both sides, parking the avatar at the visual centre of the
     gold HUD. Items with smaller order numbers (TRADE/COMBAT)
     pack against the LEFT edge; items with larger order (NAME,
     AUTHORITY) pack against the RIGHT edge. */
  .sr-zone[data-sr-area="you"] #sr-your-avatar {
    margin: 0 auto;
    order: 50;
  }
  /* Avatar / PFP placeholder zone inside each gold HUD. Sized to
     fit between the chip rows; circular with a dashed grey border
     so it reads as a "drop your face here" affordance until a real
     image is wired up. Per-side positioning (margin: 0 auto +
     order) lives in the .sr-zone[data-sr-area="you"|"opp"] blocks
     above so the placement matches the rest of the chip row. */
  .sr-hud-avatar {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.06);
    border: 1px dashed #888;
    flex-shrink: 0;
    box-sizing: border-box;
  }
  /* Player + opp name labels rendered inside the gold-framed HUDs.
     Gold-tinted to match the frame, uppercase + letter-spacing for
     "nice format", clipped to one line so a long lobby name can't
     blow up the HUD height. Hidden via `[hidden]` when state.playerNames
     is empty so the gold frame collapses cleanly to the chip row. */
  .sr-hud-name {
    color: #e8d040;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-size: var(--sr-h2-size);
    max-width: 7rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Player deck cell — bottom-pin the deck tile so it sits at the
     bottom of row 8, vertically inline with the bottom-pinned hand
     cards in the adjacent cell. The action button has been moved out
     to its own grid row (stagebtn area in row 9) below row 8, so this
     zone is now single-child and aligns purely via flex layout.
     `min-height` belt-and-suspenders so the zone reserves card-h
     even if the grid-track sizing oddly tries to shrink the cell. */
  .sr-zone[data-sr-area="youdeck"] {
    display: flex;
    flex-direction: column;
    align-items: end;
    justify-content: flex-end;
    min-height: var(--sr-card-h);
  }
  /* Same belt-and-suspenders for the hand zone — independent of the
     row track size, the zone reserves card-h vertical space so the
     hand cards inside have room to render and the cell baseline
     matches the youdeck cell next to it. */
  .sr-zone[data-sr-area="hand"] {
    min-height: var(--sr-card-h);
  }
  /* Action button row — sits directly under row 8 (the hand row),
     right-aligned so the button stays in the same column lane as the
     deck above it. align-self: end on the zone keeps the button
     hugging its row's bottom edge with no extra slack. */
  .sr-zone[data-sr-area="stagebtn"] {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    padding-block: var(--sr-section-gap) 0;
    /* Triple the zone height so the action button has obvious
       vertical real estate (was ~28px → now ~90px). align-items:
       center above keeps the button vertically centered in the
       expanded zone. */
    min-height: 90px;
  }
  /* The right grid column auto-sizes to its widest child. The default
     #sr-stage-btn (padding 0.55rem 1.4rem + 1rem font) is ~3× the
     card-w token, which pushed the right column wide enough that the
     hand row (in the centre column) lost ~80px of space to the deck
     tile — the hand and deck were no longer adjacent and the 5-card
     cap effectively capped at 4. Constrain the button to card-w
     wide so the right column stays card-w (= deck tile width) and
     the hand sits flush against the deck. Font + padding shrink to
     keep "END TURN" / "PLAY ALL" legible at the narrower width. */
  .sr-zone[data-sr-area="stagebtn"] #sr-stage-btn {
    /* Now that the stagebtn row spans all three grid columns, the
       button can be wider than the right column without growing it
       — column widths are determined by rows 1-8, the full-width
       row 9 doesn't participate in column auto-sizing. Bump width
       to 2.4 × card-w and keep the font-size scaled with card-w so
       the widest label ("PLAY ALL" / "END TURN", 8 bold uppercase
       monospace chars ≈ 0.65em each) fits with ~1 card-w of slack
       inside the button.
       Vertical padding + font cap reduced so the stagebtn row stays
       short enough to fit under 100dvh on phones. The previous
       0.75rem padding + 1.1rem font cap pushed the row to ~47px,
       which the chrome-h reserve (160px) wasn't accounting for —
       the eight other auto/fr rows + the inflated card-w then
       pushed this row past the viewport bottom and the button got
       clipped. */
    width: calc(var(--sr-card-w) * 2.4);
    /* Double the button's intrinsic height (was ~24px content + 2×0.3rem
       padding ≈ 28px → now ~56px) so it sits as a comfortably-sized
       tap target inside the tripled zone. The zone's align-items:
       center handles vertical centering of this fixed-height button. */
    height: 56px;
    padding: 0 0.4rem;
    font-size: clamp(0.65rem, calc(var(--sr-card-w) * 0.22), 0.95rem);
    letter-spacing: 0;
    line-height: 1.1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Opp HUD mirrors the player HUD: chips on the inner side (next to
     the trade row, towards the player), AI name on the outer side.
     `justify-content: center` keeps the [name + chips] group
     visually centered in the gold frame; the chip container itself
     uses `row-reverse` so the chip order reads AUTHORITY | COMBAT |
     TRADE left-to-right (mirror of player's TRADE | COMBAT |
     AUTHORITY). */
  .sr-zone[data-sr-area="opp"] section.sr-section {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: flex-start;
    gap: 0.75rem;
  }
  /* Hide h2 in gold box — chip labels already identify each chip
     (TRADE / COMBAT / AUTHORITY), so the section heading is redundant
     and was pushing the chip row off the box's vertical centre. */
  .sr-zone[data-sr-area="opp"] section.sr-section > h2 {
    display: none;
  }
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-authority-line {
    display: none;
  }
  /* Flatten opp chips into the gold section's flex (mirror of the
     YOU pattern) so AUTHORITY can be repositioned independently and
     get the same green-box treatment. */
  .sr-zone[data-sr-area="opp"] #sr-opp-resource-chips {
    display: contents;
  }
  /* AUTHORITY chip on the opp HUD — mirror of the player's. The
     opponent's discard pile (oppdiscard) is in the LEFT column on
     mobile portrait, so the opp AUTHORITY box pins to the LEFT
     edge of the gold HUD (order: -1) instead of the right. Same
     green styling and per-seat background.
     TRADE / COMBAT get pushed to the RIGHT edge — margin-left: auto
     on the FIRST of them (TRADE, order: 1) absorbs all slack between
     the AUTHORITY+NAME group on the left and the chip pair on the
     right. */
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-health {
    width: clamp(5rem, calc(var(--sr-card-w) * 1.8), 7rem);
    border-color: #40e840;
    color: #40e840;
    box-shadow: 0 0 6px rgba(64, 232, 64, 0.35);
    order: -1;
  }
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-health .sr-chip-label,
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-health .sr-chip-value {
    color: #40e840;
  }
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-health[data-seat="0"] {
    background: rgba(232, 64, 64, 0.55);
  }
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-health[data-seat="1"] {
    background: rgba(64, 180, 232, 0.55);
  }
  /* Opp name sits between AUTHORITY (order: -1, left edge) and the
     centered avatar (order: 1). order: 0 keeps source-order semantics
     for siblings of equal order. */
  .sr-zone[data-sr-area="opp"] #sr-opp-hud-name {
    margin: 0;
    align-self: center;
    order: 0;
  }
  /* Avatar — centered placeholder. Order between NAME (0) and the
     TRADE/COMBAT pair (2). margin: 0 auto absorbs all slack on both
     sides, pinning AUTHORITY+NAME to the LEFT edge and TRADE+COMBAT
     to the RIGHT edge of the gold HUD. */
  .sr-zone[data-sr-area="opp"] #sr-opp-avatar {
    margin: 0 auto;
    order: 1;
  }
  /* TRADE + COMBAT on the right side of the opp HUD. Order: 2 puts
     them after AUTHORITY (-1), NAME (0), AVATAR (1). No auto margins
     needed — the AVATAR's auto-margin already pushes everything after
     it to the right edge. */
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-gold,
  .sr-zone[data-sr-area="opp"] section.sr-section .sr-chip-damage {
    order: 2;
  }
  /* The legacy .sr-resource-chips rule (styles.css:~421) sets
     `margin-top: 0.4rem` to gap the chip row away from the section h2.
     With the h2 now hidden inside the gold box, that margin grows the
     flex line by ~6.4px on the top side, which shifts the chip box
     +3.2px below the gold box's vertical centre (the HUD inspector
     measured this exactly). Zero the margin so the chip container
     pins to the box centre. */
  .sr-zone[data-sr-area="opp"] #sr-opp-resource-chips,
  .sr-zone[data-sr-area="you"] #sr-resource-chips {
    margin-top: 0;
  }

  /* Both authority panels carry a gold frame so the AUTHORITY /
     COMBAT / TRADE HUD reads as the command surface for each
     player. The chip palette inside (Health blue, Damage red, Gold
     yellow) is unchanged — the frame only adds a thin gold border,
     outer + inset gold glow, and gold-tinted section heading.
     `padding-block: 0` overrides the `var(--sr-section-py)` vertical
     padding inherited from `section.sr-section` so the gold frame
     hugs the chip row vertically; negative margins on the
     trade-row-facing edge eat the `var(--sr-section-gap)` grid gap
     so the frame sits flush against the trade row. */
  .sr-zone[data-sr-area="opp"] section.sr-section,
  .sr-zone[data-sr-area="you"] section.sr-section {
    border: 1px solid #e8d040;
    border-radius: 6px;
    box-shadow:
      0 0 8px rgba(232, 208, 64, 0.25),
      inset 0 0 6px rgba(232, 208, 64, 0.08);
    padding-inline: 0.5rem;
    padding-block: 0;
  }
  /* Pull each HUD zone flush against the trade row by eating the
     grid `gap: var(--sr-section-gap)` between the HUD row and the
     trade row from each side. Negative margin MUST live on the zone
     (grid item), NOT on the section inside it — `.sr-zone[data-sr-area]`
     declares `overflow: hidden` (~line 2531) which would clip the
     same margin if it were applied to the inner section. Grid items
     respect margins (the negative margin shifts the item's position
     within its grid track) even when their `overflow: hidden`
     prevents their *children* from extending past the zone box. */
  .sr-zone[data-sr-area="opp"] {
    margin-bottom: calc(-1 * var(--sr-section-gap));
  }
  .sr-zone[data-sr-area="you"] {
    margin-top: calc(-1 * var(--sr-section-gap));
  }
  .sr-zone[data-sr-area="opp"] section.sr-section h2,
  .sr-zone[data-sr-area="you"] section.sr-section h2 {
    color: #e8d040;
    letter-spacing: 0.18em;
  }


  /* [REQ-343] [REQ-349] [REQ-350] Mobile portrait hardening — drop
     visual chrome that eats vertical space, shrink the info pip so
     it stays tappable but doesn't dominate a thumbnail card. Body
     lock + #sr-game-area height: 100dvh + the [hidden] override are
     all promoted to the global block under REQ-350; this block only
     carries mobile-specific tweaks. `--sr-chrome-h` shrinks here
     because the body padding, h1, and section labels collapse to
     near-nothing — the card-row fr-fractions can claim more of the
     viewport. */
  @media (max-width: 480px) {
    :root {
      /* Bumped 160 → 260 to reserve vertical budget for the stagebtn
         row, which is now pinned at min-height: 90px so the action
         button has obvious tap real estate. The card-w token
         subtracts this reserve from 100dvh before dividing across
         the 4 card-row tracks — if the reserve undershoots, card-w
         oversizes, the card rows over-claim height, and the
         stagebtn row gets pushed past the viewport bottom and
         clipped by body { overflow: hidden }. */
      --sr-chrome-h: 260px;
    }
    body { padding: 2px 4px; }
    body > h1, body > a.home-link { display: none; }
    .sr-banner { display: none; }
    section.sr-section h2 { display: none; }
    .sr-card-info-btn { width: 14px; height: 14px; line-height: 12px; font-size: 0.6rem; }
    /* [REQ-378] Mobile portrait keeps the 2-col grid so the player's
       deck + discard stay visually adjacent to the rows they belong
       to (`youdiscard` right of `youplay`, `youdeck` right of `hand`)
       — same placement as the desktop layout, just at the smaller
       responsive --sr-card-w. Replaces the REQ-377 single-column
       fallback that turned each pile into its own full-width row
       above the row it should have been beside. The `auto` second
       column consumes one card-width; the four card-rows keep their
       `minmax(0, 1fr)` fractions so the play surface stays no-scroll
       under 100dvh. */
    /* [REQ-384] [REQ-385] Mobile portrait mirrors the desktop 3-column
       shape with the new opphand row inserted between opp authority
       and opp in-play. */
    #sr-game-area {
      grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
      grid-template-rows:
        auto                /* row 1: top */
        auto                /* row 2: opp hand row */
        calc(var(--sr-card-h) + 6px) /* row 3: opp in-play — card-h + 6px clearance (REQ-716) */
        minmax(0, 1fr)      /* row 4: oppfiller — absorbs freed 1fr space */
        auto                /* row 5: opp authority HUD */
        minmax(0, 1fr)      /* row 6: trade row */
        auto                /* row 7: you authority HUD */
        minmax(0, 1fr)      /* row 8: youfiller — symmetric */
        calc(var(--sr-card-h) + 6px) /* row 9: your in-play — card-h + 6px clearance (REQ-716) */
        auto                /* row 10: your hand */
        auto;               /* row 11: action button row */
      grid-template-areas:
        "top         top         top"
        "oppdeck     opphand     oppbases"
        "oppdiscard  oppplay     oppbases"
        "oppfiller   oppfiller   oppbases"
        "opp         opp         opp"
        "trade       trade       trade"
        "you         you         you"
        "yourbases   youfiller   youfiller"
        "yourbases   youplay     youdiscard"
        "yourbases   hand        youdeck"
        "stagebtn    stagebtn    stagebtn";
    }
    /* [REQ-378] [REQ-394] Player youdeck bottom-pins so the deck
       card edge meets the hand cards (REQ-393 / REQ-385). REQ-394
       split the paired rule — oppdeck moved to align-self: start
       (top of row 3, mirror of player bottom-of-row-7) inside the
       opp-mirror block below. */
    .sr-zone[data-sr-area="youdeck"] .sr-pile-tile-mount {
      align-self: end;
    }
    /* [REQ-350] Collapsible logs eat vertical chrome; on phones the
       turn counter is enough. Hide the log details entirely; the
       card-detail modal (REQ-327) is the read affordance. */
    .sr-log-details { display: none; }
  }

  /* [REQ-343] Desktop portrait — narrow tall window. Cards relax to
     a comfortable mid-size so 5 trade slots use the available width
     without cramping. */
  @media (orientation: portrait) and (min-width: 700px) {
    :root {
      --sr-card-w: clamp(80px, 11vw, 130px);
      --sr-card-h: clamp(108px, 14.9vw, 176px);
    }
  }

  /* [REQ-343] [REQ-350] Mobile landscape — tightest case (e.g.
     812x375). Hide page chrome (h1, banner, home-link, section
     labels) and pivot to a 2-col grid: opponent left, you right,
     full-width trade row + hand + actions. The card-row count
     becomes 3 (oppplay/youplay paired + trade + hand). Cards derive
     their token from the same 2D-aware :root math; --sr-chrome-h
     shrinks because chrome rows are minimal here. */
  @media (orientation: landscape) and (max-height: 500px) {
    :root {
      --sr-chrome-h: 100px;
      /* In landscape the card-row count is 3, not 4 — retune the
         token's height-budget divisor to use 3 instead of the
         default 4. We override --sr-card-w with a tighter min(). */
      --sr-card-w: min(
        calc((100vw - var(--sr-pad-x) * 2 - var(--sr-card-gap) * 7) / 8),
        calc((100dvh - var(--sr-chrome-h)) / 3 / 1.353)
      );
    }
    body { padding: 2px 4px; }
    body > h1, a.home-link, .sr-banner, section.sr-section h2 { display: none; }
    .sr-log-details { display: none; }
    #sr-opp-pile-counts, #sr-your-pile-counts { display: none; }
    .sr-zone[data-sr-area="opp"] section.sr-section:nth-of-type(2),
    .sr-zone[data-sr-area="you"] section.sr-section:first-of-type {
      display: none;
    }
    /* [REQ-380] Mobile landscape — bases get their own row above each
       in-play row; the legacy actions row is gone (staged button
       folds into the YOUR · AUTHORITY zone via flex). */
    #sr-game-area {
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      grid-template-rows:
        auto              /* top */
        auto              /* opp + you stats share row */
        auto              /* oppbases + yourbases share row */
        minmax(0, 1fr)    /* oppplay / youplay paired */
        minmax(0, 1fr)    /* trade */
        minmax(0, 1fr)    /* hand */
        auto;             /* action button */
      grid-template-areas:
        "top        top"
        "opp        you"
        "oppbases   yourbases"
        "oppplay    youplay"
        "trade      trade"
        "hand       hand"
        "stagebtn   stagebtn";
    }
  }

  /* [REQ-343] [REQ-350] Desktop portrait — narrow tall window. Cards
     can be readable here since the height budget is generous. */
  @media (orientation: portrait) and (min-width: 700px) and (min-height: 700px) {
    :root {
      --sr-chrome-h: 260px;
    }
  }

  /* [REQ-343] [REQ-350] Desktop landscape — sidebar layout. Logs +
     actions ride the right rail so the play area gets full vertical
     reach. The play column splits 4 card-row-fractions inside the
     viewport height; chrome rows top/bottom of the play column
     collapse to thin authority strips. */
  @media (min-width: 1024px) and (min-height: 600px) {
    :root {
      --sr-chrome-h: 120px;
    }
    /* [REQ-380] Desktop landscape — bases get their own rows above each
       in-play row; the legacy `actions` rail is gone (staged button
       folds into the YOUR · AUTHORITY zone via flex). The right rail
       carries the log panel (`top`) and then mirrors `you` so the
       chips + staged button stretch the full width on the bottom row. */
    #sr-game-area {
      grid-template-columns: minmax(0, 1fr) 220px;
      grid-template-rows:
        auto              /* opp authority */
        auto              /* oppbases */
        minmax(0, 1fr)    /* oppplay */
        minmax(0, 1fr)    /* trade */
        auto              /* yourbases */
        minmax(0, 1fr)    /* youplay */
        minmax(0, 1fr)    /* hand */
        auto              /* you authority */
        auto;             /* action button */
      grid-template-areas:
        "opp        top"
        "oppbases   top"
        "oppplay    top"
        "trade      top"
        "yourbases  you"
        "youplay    you"
        "hand       you"
        "you        you"
        "stagebtn   stagebtn";
    }
  }

  /* [REQ-369] Phase 10 redesign Chunk 11 — fullscreen polish. The
     menu's Fullscreen toggle (REQ-366) calls
     document.documentElement.requestFullscreen(); under fullscreen
     the browser chrome is gone, so --sr-chrome-h can shrink to ~40px
     (just enough for the hamburger button + a thin top margin) and
     the card token reclaims the freed pixels. The rule duplicates
     under :-webkit-full-screen so older Safari (≤17) renders the
     same way. */
  :fullscreen #sr-game-area {
    --sr-chrome-h: 40px;
  }
  :-webkit-full-screen #sr-game-area {
    --sr-chrome-h: 40px;
  }
  /* [REQ-369] Under fullscreen, the body padding can also shrink so
     the page edges don't waste pixels. Both spellings paired so
     older Safari matches modern Chromium / Firefox behaviour. */
  :fullscreen,
  :-webkit-full-screen {
    padding: 0 !important;
  }

  /* [REQ-372] Phase 10 redesign follow-up — visual attack-mode
     highlight. When the local player has combat to spend AND it's
     their turn, boot.js toggles `body.sr-can-attack`. CSS then
     paints a pulsing red outline + box-shadow on every opponent
     outpost so the click affordance for REQ-360's
     click-to-attack-base path reads at a glance. */
  @keyframes sr-activate-pulse {
    0%, 100% { outline-color: rgba(232, 240, 32, 0.9); }
    50%       { outline-color: rgba(232, 240, 32, 0.3); }
  }

  @keyframes sr-attack-pulse {
    0%, 100% { outline-color: rgba(232, 64, 64, 0.95); }
    50%       { outline-color: rgba(232, 64, 64, 0.55); }
  }
  /* [REQ-392] Per-base glow gate — paint only on .sr-can-attack-this
     stamped by renderBases (combat ≥ defense AND outpost-first rule
     allows). Replaces REQ-372's blanket .sr-outpost selector so a
     2-combat player no longer sees a 4-defense outpost glowing red
     when they can't destroy it. */
  body.sr-can-attack [data-sr-area="oppplay"] .sr-can-attack-this,
  body.sr-can-attack #sr-their-bases .sr-can-attack-this {
    outline: 3px solid rgba(232, 64, 64, 0.95);
    outline-offset: 3px;
    animation: sr-attack-pulse 1s ease-in-out infinite;
    cursor: pointer;
  }

  /* [REQ-372] [REQ-392] Vestibular safety — disable the infinite pulse under
     prefers-reduced-motion. The static outline still reads, just
     no pulse. WCAG 2.1 SC 2.3.3. */
  @media (prefers-reduced-motion: reduce) {
    body.sr-can-attack [data-sr-area="oppplay"] .sr-can-attack-this,
    body.sr-can-attack #sr-their-bases .sr-can-attack-this {
      animation: none;
    }
  }

  /* [REQ-405] [REQ-318] SN holo + tap-to-copy. */
  @keyframes sr-copy-holo-pulse {
    0%,100% { box-shadow: 0 0 8px 1px rgba(64,232,240,.45); }
    50%     { box-shadow: 0 0 18px 3px rgba(64,232,240,.85); }
  }
  .sr-inplay-card.sr-copy-of { animation: sr-copy-holo-pulse 1.6s ease-in-out infinite; }
  .sr-inplay-card.sr-copy-of:hover,
  .sr-inplay-card.sr-copy-of:focus,
  .sr-inplay-card.sr-copy-of:active {
    box-shadow: 0 0 22px 4px rgba(120,240,255,.95);
  }
  .sr-inplay-card.sr-copy-pick-target {
    outline: 2px dashed rgba(120,240,255,.85);
    outline-offset: 2px;
  }
  .sr-inplay-card.sr-copy-pick-target:hover {
    outline-color: #fff;
    box-shadow: 0 0 18px 3px rgba(120,240,255,.9);
  }
  @media (prefers-reduced-motion: reduce) {
    .sr-inplay-card.sr-copy-of { animation: none; }
  }


