/* @file star_realms/styles.css
 * @project KAINYNE Website
 * @description STAR REALMS page styles — extracted from index.html.
 *   Cache-bust: bump ?v=N in index.html when changing this file.
 * @module UI/StarRealms/Styles
 * @requirements REQ-005, REQ-203, REQ-212, REQ-249, REQ-252, REQ-313, REQ-317, REQ-327, REQ-328, REQ-329, REQ-334, REQ-336, REQ-343, REQ-349, REQ-350, REQ-376, REQ-407, REQ-408, REQ-409, REQ-419, REQ-426
 */

  * { box-sizing: border-box; margin: 0; padding: 0; }
  /* [REQ-350] Strict no-scroll fit — every responsive value reads
     from one token block. Card token is 2D-aware: picks the smaller
     of width-budget-per-card vs height-budget-per-row-as-width so
     cards always fit BOTH axes. `--sr-chrome-h` reserves the height
     for non-card rows (top + opp-stats + you-stats + actions) and is
     retuned per viewport. The 4 card-rows (oppplay / trade / youplay
     / hand) split the remaining height evenly via grid fr-units. */
  :root {
    --sr-card-gap: clamp(2px, 0.5vw, 8px);
    --sr-pad-y: clamp(2px, 0.6vh, 12px);
    --sr-pad-x: clamp(4px, 1.2vw, 14px);
    --sr-section-py: clamp(1px, 0.3vh, 6px);
    --sr-section-gap: clamp(2px, 0.4vh, 6px);
    --sr-h1-size: clamp(0.9rem, 2.6vw, 3rem);
    --sr-h2-size: clamp(0.55rem, 1.6vw, 0.7rem);
    --sr-card-name-size: clamp(0.5rem, 1.3vw, 0.85rem);
    --sr-card-effect-size: clamp(0.45rem, 1.1vw, 0.68rem);
    /* [REQ-350] Reserved height for non-card rows; per-viewport
       overrides retune this. */
    --sr-chrome-h: 220px;
    /* [REQ-350] Card token derives from BOTH viewport dimensions and
       the row count so cards fit both axes. 8 trade-row tiles is the
       widest row; 4 card-rows is the tallest stack. Aspect ratio
       1:1.353 preserved (170/230 of REQ-343 reference cards). */
    --sr-card-w: min(
      calc((100vw - var(--sr-pad-x) * 2 - var(--sr-card-gap) * 7) / 8),
      calc((100dvh - var(--sr-chrome-h)) / 4 / 1.353)
    );
    --sr-card-h: calc(var(--sr-card-w) * 1.353);
  }
  html { min-height: 100dvh; background: #0f0f0f; }
  body {
    background: #0f0f0f;
    color: #f0f0f0;
    font-family: monospace;
    min-height: 100vh;
    min-height: 100dvh;
    padding: var(--sr-pad-y) var(--sr-pad-x);
    line-height: 1.45;
  }
  /* [REQ-350] [PTR-048] Lock the body to 100dvh + no-scroll while the
     game area is visible, on every viewport class. The :has() selector
     keys on the runtime [hidden] attribute on #sr-game-area, so the
     lobby state (PTR-048's #sr-game-area[hidden] { display: none }
     contract) leaves body default-scrollable for the lobby form. The
     game area's own grid template + card tokens guarantee fit; this
     rule prevents any stray overflow from leaking onto body. */
  body:has(#sr-game-area:not([hidden])) {
    overflow: hidden;
    height: 100dvh;
    min-height: 0;
  }
  a.home-link {
    display: inline-block;
    margin-bottom: 1.5rem;
    color: #e8f020;
    text-decoration: none;
    border: 1px solid #e8f020;
    padding: 0.35rem 0.6rem;
  }
  a.home-link:hover { background: #e8f020; color: #0f0f0f; }
  /* [REQ-366] Phase 10 redesign — hide the legacy header strip
     (Home link + STAR REALMS h1 + turn banner). They stay in the
     DOM for SEO + a11y; the new top-right hamburger menu carries
     all the same affordances. */
  .sr-page-chrome { display: none; }
  /* [REQ-366] Top-right hamburger button. Fixed-position so it stays
     above the in-game grid even on narrow viewports; sits above the
     dialog backdrop and is hidden when the dialog is open via the
     [open]+button rule below. */
  #sr-menu-btn {
    position: fixed;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 50;
    background: rgba(0, 0, 0, 0.6);
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 4px;
    padding: 0.25rem 0.55rem;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
  }
  #sr-menu-btn:hover, #sr-menu-btn:focus {
    background: #e8f020;
    color: #0f0f0f;
    outline: none;
  }
  #sr-menu[open] {
    border: 1px solid #e8f020;
    background: #14161b;
    color: #e8e9ef;
    padding: 0.75rem;
    border-radius: 6px;
    min-width: 18rem;
    max-width: 90vw;
    max-height: 80dvh;
    overflow-y: auto;
  }
  #sr-menu::backdrop { background: rgba(0, 0, 0, 0.5); }
  .sr-menu-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.5rem;
    border-bottom: 1px dashed #2a2d38;
    padding-bottom: 0.4rem;
  }
  .sr-menu-turn {
    font-weight: 700;
    color: #e8f020;
    letter-spacing: 0.05em;
  }
  #sr-menu-close {
    background: transparent;
    color: #e8e9ef;
    border: 1px solid #2a2d38;
    border-radius: 4px;
    width: 1.6rem;
    height: 1.6rem;
    line-height: 1;
    font-size: 1.1rem;
    cursor: pointer;
  }
  #sr-menu-close:hover, #sr-menu-close:focus {
    border-color: #e8f020;
    color: #e8f020;
    outline: none;
  }
  .sr-menu-actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.6rem;
  }
  #sr-menu-fullscreen,
  .sr-menu-home {
    display: inline-block;
    background: transparent;
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 4px;
    padding: 0.3rem 0.6rem;
    font-size: 0.85rem;
    cursor: pointer;
    text-decoration: none;
    font-family: monospace;
  }
  #sr-menu-fullscreen:hover, .sr-menu-home:hover,
  #sr-menu-fullscreen:focus, .sr-menu-home:focus {
    background: #e8f020;
    color: #0f0f0f;
    outline: none;
  }
  .sr-menu-log {
    margin-top: 0.5rem;
    border-top: 1px dashed #2a2d38;
    padding-top: 0.4rem;
  }
  .sr-menu-log > summary {
    cursor: pointer;
    font-weight: 600;
    padding: 0.2rem 0;
    color: #cfd6e4;
  }
  h1 {
    /* [REQ-343] Title shrinks to a chip on phones; full size on desktop. */
    font-size: var(--sr-h1-size);
    color: #e8f020;
    letter-spacing: 0.08em;
    margin-bottom: 0.15rem;
  }
  .sr-banner {
    border: 1px solid #e8f020;
    color: #e8f020;
    padding: 0.75rem 1rem;
    max-width: 720px;
    margin: 1rem 0 1.5rem;
    font-size: 0.85rem;
  }
  section.sr-section {
    /* [REQ-343] 960px cap removed — the #sr-game-area grid now owns
       layout width, so individual sections must not clamp themselves
       narrower than the grid cell they sit in. */
    border-top: 1px dashed #2a2d38;
    padding: var(--sr-section-py) 0;
    min-width: 0;
  }
  /* [REQ-412] [PTR-068] Opp sections paint the dashed separator on the
     BOTTOM edge so it hugs the trade row (mirror of player border-top). */
  .sr-zone[data-sr-area="opp"] section.sr-section,
  .sr-zone[data-sr-area="opphand"] section.sr-section,
  .sr-zone[data-sr-area="oppplay"] section.sr-section,
  .sr-zone[data-sr-area="oppbases"] section.sr-section {
    border-top: 0;
    border-bottom: 1px dashed #2a2d38;
  }
  section.sr-section h2 {
    /* [REQ-343] Section labels shrink with the viewport. */
    font-size: var(--sr-h2-size);
    letter-spacing: 0.18em;
    color: #888;
    margin-bottom: 0.25rem;
  }
  /* [REQ-350] Card rows are flex containers without min-height; the
     #sr-game-area grid owns row height via fr-units. nowrap keeps
     each row on a single line so the grid row height is enough — if
     the row's contents would wrap, the page would scroll. The
     2D-aware --sr-card-w token guarantees content fits. */
  /* [REQ-368] Phase 10 redesign Chunk 10 — hand overflow scroller.
     When the hand exceeds the visible width (>6 cards), boot.js
     reveals the chevron buttons (otherwise they stay hidden via
     the [hidden] attribute). The wrapper is position:relative so
     the chevrons absolutely position over the row edges; the inner
     #sr-hand row scrolls horizontally with scroll-snap so the
     chevron-clicked nudge lands cleanly on a card boundary. */
  .sr-row-scroller {
    position: relative;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: thin;
    -webkit-overflow-scrolling: touch;
    width: 100%;
  }
  .sr-row-scroller > .sr-row {
    /* The inner row keeps its existing flex layout; scroll-snap
       targets the row's children so each card is a snap point. */
    scroll-snap-type: x mandatory;
  }
  .sr-row-scroller > .sr-row > * {
    scroll-snap-align: start;
    flex-shrink: 0;
  }
  .sr-row-chevron {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 4;
    width: 1.6rem;
    height: 2.2rem;
    background: rgba(0, 0, 0, 0.7);
    color: #e8f020;
    border: 1px solid #e8f020;
    border-radius: 4px;
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
  }
  .sr-row-chevron:hover, .sr-row-chevron:focus {
    background: #e8f020;
    color: #0f0f0f;
    outline: none;
  }
  .sr-row-chevron-prev { left: 0; }
  .sr-row-chevron-next { right: 0; }
  .sr-row-chevron[hidden] { display: none; }
  .sr-row {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--sr-card-gap);
    min-width: 0;
    min-height: 0;
    align-items: flex-start;
  }
  .sr-authority {
    padding: 0.35rem 0.8rem;
    border: 1px solid #2a2d38;
    border-radius: 2px;
    margin-right: 0.5rem;
    color: #f0f0f0;
    font-size: 0.9rem;
  }
  .sr-authority-p1 { border-color: #40b4e8; color: #40b4e8; }
  .sr-authority-p2 { border-color: #e84040; color: #e84040; }
  /* [REQ-339] Player-name label chip rendered next to each authority
     chip so both sides know who is Player 1 vs Player 2. Mirrors the
     authority chip's per-seat palette so the row reads as a single
     coloured unit. */
  .sr-player-label {
    padding: 0.35rem 0.8rem;
    border: 1px solid #2a2d38;
    border-radius: 2px;
    margin-right: 0.5rem;
    font-size: 0.85rem;
    font-weight: 600;
  }
  .sr-player-label-p1 { border-color: #40b4e8; color: #40b4e8; }
  .sr-player-label-p2 { border-color: #e84040; color: #e84040; }
  .sr-pool {
    display: inline-block;
    padding: 0.35rem 0.8rem;
    border: 1px solid #2a2d38;
    border-radius: 2px;
    font-size: 0.9rem;
  }
  .sr-pool-trade { color: #e8f020; border-color: #e8f020; }
  .sr-pool-combat { color: #ff8040; border-color: #ff8040; }
  /* [REQ-363] Phase 10 redesign — three-chip resource panel (Gold /
     Damage / Health) painted next to the legacy authority + pools
     line. Each chip is a small block carrying a label + value
     stacked vertically; the per-colour modifiers paint borders,
     text, and a subtle glow so the three resources read as
     distinct surfaces at a glance. */
  /* [REQ-370] Phase 10 redesign follow-up — drop the legacy
     player-side authority + pools text readout now that the resource
     chips (Gold/Damage/Health, REQ-363) display the same info in a
     faster-to-scan colored panel. The mounts stay in the DOM so the
     existing renderYourAuthority + renderPools renderers keep
     painting into them (avoiding a render-time NPE on null nodes);
     only the visual layer is suppressed. The opponent-side mounts
     (#sr-opp-authority + #sr-opp-pools) stay visible since no
     opponent chips ship yet. */
  /* [REQ-371] Phase 10 redesign bugfix — symmetric hide for the
     opponent's legacy text readout now that REQ-371 ships opponent
     resource chips alongside the player's. */
  #sr-your-authority,
  #sr-your-pools,
  #sr-opp-authority,
  #sr-opp-pools { display: none; }
  .sr-resource-chips {
    display: flex;
    gap: 0.4rem;
    margin-top: 0.4rem;
    flex-wrap: wrap;
  }
  .sr-chip {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 0.3rem 0.6rem;
    border: 2px solid;
    border-radius: 4px;
    min-width: 3rem;
    line-height: 1.1;
    background: rgba(0, 0, 0, 0.35);
    font-weight: 600;
  }
  .sr-chip-label {
    font-size: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.85;
  }
  .sr-chip-value {
    font-size: 1.1rem;
    font-weight: 700;
  }
  .sr-chip-gold {
    border-color: #e8d040;
    color: #e8d040;
    box-shadow: 0 0 6px rgba(232, 208, 64, 0.25);
  }
  .sr-chip-damage {
    border-color: #e84040;
    color: #e84040;
    box-shadow: 0 0 6px rgba(232, 64, 64, 0.25);
  }
  .sr-chip-health {
    border-color: #40b4e8;
    color: #40b4e8;
    box-shadow: 0 0 6px rgba(64, 180, 232, 0.25);
  }
  /* [REQ-336] Authority + Trade/Combat chips share a single row inside
     each `OPPONENT · AUTHORITY` / `YOUR · AUTHORITY` section so each
     player's full resource state reads at a glance. flex-wrap keeps
     the chips legible on narrow viewports. */
  .sr-authority-line {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
  }
  /* [REQ-328] Visible pile counts (deck / discard / hand). One row
     per player; the OPP row uses the existing P2-red palette + the
     YOU row uses the P1-blue so the row identity matches the
     authority chips. Chips sit in a flex-wrap row so a narrow
     viewport stacks them cleanly. */
  .sr-pile-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    margin: 0.25rem 0;
    font-size: 0.85rem;
  }
  .sr-pile-row-opp { color: #e84040; }
  .sr-pile-row-you { color: #40b4e8; }
  .sr-pile-label {
    font-size: 0.7rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    min-width: 5.5rem;
    color: currentColor;
    opacity: 0.85;
  }
  .sr-pile-chip {
    display: inline-block;
    padding: 0.2rem 0.55rem;
    border: 1px solid currentColor;
    border-radius: 2px;
    font-size: 0.78rem;
    color: currentColor;
    background: rgba(0, 0, 0, 0.25);
  }
  /* [REQ-329] Discard chip is a clickable trigger that opens the
     pile-viewer modal. Hover/focus boost matches REQ-327's
     `.sr-card-info-btn` so the affordance reads as interactive. */
  .sr-pile-chip-clickable {
    cursor: pointer;
    transition: background 0.12s, box-shadow 0.12s;
  }
  .sr-pile-chip-clickable:hover,
  .sr-pile-chip-clickable:focus {
    background: rgba(255, 255, 255, 0.08);
    box-shadow: 0 0 8px currentColor;
    outline: 1px dotted currentColor;
    outline-offset: 1px;
  }
  /* [REQ-329] Discard-pile viewer modal. Native <dialog> element;
     wider than `.sr-card-detail` so a longer pile lays out two
     columns of items on desktop. ::backdrop dims the page behind. */
  .sr-pile-viewer {
    background: #0a0716;
    color: #f0f0f0;
    border: 2px solid #2a2d38;
    border-radius: 6px;
    padding: 1.25rem 1.5rem;
    max-width: 540px;
    width: 92%;
    max-height: 80vh;
    overflow-y: auto;
    font-family: monospace;
    box-shadow: 0 0 32px rgba(0, 0, 0, 0.6);
  }
  .sr-pile-viewer::backdrop {
    background: rgba(0, 0, 0, 0.65);
  }
  .sr-pile-viewer-close {
    position: absolute;
    top: 0.5rem;
    right: 0.6rem;
    background: transparent;
    border: none;
    color: #888;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.2rem 0.5rem;
  }
  .sr-pile-viewer-close:hover { color: #f0f0f0; }
  .sr-pile-viewer-title {
    font-size: 1.05rem;
    margin-bottom: 0.85rem;
    letter-spacing: 0.06em;
    color: #e8f020;
  }
  .sr-pile-viewer-empty {
    color: #888;
    font-style: italic;
    font-size: 0.85rem;
    padding: 0.5rem 0;
  }
  .sr-pile-viewer-list {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
  }
  .sr-pile-viewer-item {
    border-left: 3px solid currentColor;
    padding: 0.45rem 0.7rem;
    background: rgba(255, 255, 255, 0.03);
    border-radius: 2px;
    cursor: pointer;
    transition: background 0.12s;
  }
  .sr-pile-viewer-item:hover,
  .sr-pile-viewer-item:focus {
    background: rgba(255, 255, 255, 0.07);
    outline: 1px dotted currentColor;
    outline-offset: 1px;
  }
  .sr-pile-viewer-head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: baseline;
    margin-bottom: 0.2rem;
  }
  .sr-pile-viewer-name {
    font-weight: bold;
    font-size: 0.9rem;
  }
  .sr-pile-viewer-meta {
    color: #b0b0b0;
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  .sr-pile-viewer-effects {
    color: #d0d0d0;
    font-size: 0.78rem;
    line-height: 1.35;
  }
  .sr-pile-viewer-effect {
    display: flex;
    align-items: baseline;
    gap: 0.3rem;
    margin: 0.1rem 0;
  }
  @media (max-width: 600px) {
    .sr-pile-viewer {
      max-width: none;
      width: 95%;
      padding: 1rem 1.1rem;
    }
  }
  .sr-hand-card, .sr-trade-slot {
    background: #17171d;
    border: 1px solid #2a2d38;
    color: #f0f0f0;
    font-family: monospace;
    padding: 0.4rem 0.7rem;
    cursor: pointer;
    font-size: 0.8rem;
    border-radius: 2px;
  }
  .sr-hand-card:not([disabled]):hover,
  .sr-trade-slot:not([disabled]):hover {
    border-color: #e8f020;
    color: #e8f020;
    opacity: 1;
  }
  .sr-hand-card[disabled], .sr-trade-slot[disabled] {
    cursor: not-allowed;
    opacity: 0.5;
  }
  .sr-trade-explorer { color: #b0b0b0; }
  .sr-inplay-card, .sr-base {
    background: #17171d;
    border: 1px solid #2a2d38;
    padding: 0.35rem 0.7rem;
    border-radius: 2px;
    font-size: 0.8rem;
    cursor: pointer;
  }
  .sr-inplay-card:hover { border-color: #ff8040; }
  .sr-outpost { border-color: #e84040; }
  /* [REQ-362] Phase 10 redesign — full-card faction tint. The
     mapping (SE yellow, TF blue, Blob green, MC red, neutral slate)
     aligns with sr-visuals.js and the retail Star Realms artwork,
     and supersedes the prior border-only accent. The gradient tilts
     from the faction colour at the top-left to a darker derivative
     bottom-right so the corner glyphs (REQ-358) stay legible against
     the lighter region. The 3px border-left remains as a secondary
     edge cue for users who keep cards close together; both the
     border colour and the background are sourced from the same
     mapping so the two never disagree.
     [PR-#520] Blob and Machine Cult border-left + gradient values
     are swapped relative to the original Phase 10 ship so this
     stylesheet agrees with sr-visuals.js (which has always painted
     Blob green and MC red on the inline art panels) and with the
     retail box. Pre-fix the two layers fought: CSS painted Blob
     red while the inline art painted Blob green. */
  .sr-faction-star_empire {
    border-left: 3px solid #e8d040;
    background: linear-gradient(135deg, #4a3f0c 0%, #1a160a 100%);
  }
  .sr-faction-trade_federation {
    border-left: 3px solid #40b4e8;
    background: linear-gradient(135deg, #0c2e4a 0%, #0a1018 100%);
  }
  .sr-faction-blob {
    border-left: 3px solid #40e8a0;
    background: linear-gradient(135deg, #0c4a2e 0%, #0a1812 100%);
  }
  .sr-faction-machine_cult {
    border-left: 3px solid #e84040;
    background: linear-gradient(135deg, #4a0c0c 0%, #1a0a0a 100%);
  }
  .sr-faction-neutral {
    border-left: 3px solid #566275;
    background: linear-gradient(135deg, #1c1f28 0%, #0c0d12 100%);
  }
  /* [REQ-362] Phase 10 redesign — 4-corner card face. REQ-358 emitted
     `.sr-card-faction` / `.sr-card-cost` / `.sr-card-name` /
     `.sr-card-type` slots on every tile; this rule absolutely
     positions them inside the card frame. Parent tiles already
     declare `position: relative` (existing rule), so the corners
     anchor to the tile's own bounding box. */
  /* [PTR-052] On narrow cards (e.g. portrait phones with the small
     `--sr-card-w` token) the faction acronym + cost stamps were
     bleeding into each other — the user reported "no regular spacing
     between those two stamps. usually they are right next to each
     other and on some cards there is one space". Without a per-corner
     `max-width`, long names (e.g. `[NEU]`) plus a 2-digit cost (e.g.
     `(10)`) absolutely-positioned at left:0.4rem / right:0.4rem
     can overlap horizontally when the card is narrower than their
     combined intrinsic width. Cap each at 45% of the card width and
     pin `white-space: nowrap` so they can't wrap into a 2-line stack
     either; the residual gap of >=10% of card width keeps them
     visually anchored to opposite corners on every viewport. */
  .sr-card-faction {
    position: absolute;
    top: 0.25rem;
    left: 0.4rem;
    max-width: 45%;
    white-space: nowrap;
    overflow: hidden;
    font-size: var(--sr-card-name-size);
    font-weight: 700;
    letter-spacing: 0.03em;
    pointer-events: none;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
  }
  .sr-card-cost {
    position: absolute;
    top: 0.25rem;
    right: 0.4rem;
    max-width: 45%;
    white-space: nowrap;
    overflow: hidden;
    text-align: right;
    font-size: var(--sr-card-name-size);
    font-weight: 700;
    pointer-events: none;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
  }
  .sr-card-name {
    position: absolute;
    top: 50%;
    left: 0.3rem;
    right: 0.3rem;
    transform: translateY(-50%);
    font-size: var(--sr-card-name-size);
    font-weight: 600;
    text-align: center;
    line-height: 1.15;
    pointer-events: none;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
  }
  .sr-card-type {
    position: absolute;
    bottom: 0.25rem;
    left: 50%;
    transform: translateX(-50%);
    font-size: var(--sr-card-name-size);
    font-weight: 700;
    pointer-events: none;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
    opacity: 0.85;
  }
  /* [REQ-362] Retire the legacy text rows REQ-358 left in place for
     back-compat. The 4-corner face now carries the same info — head
     line / meta line / effect bullets are hidden so the corners own
     the visual space. The DOM emit stays in `buildCardBody` so the
     `el.title` (full card text on hover) keeps working. */
  /* [REQ-374] Phase 10 follow-up — retire in-tile effect bullets so
     the 4-corner face owns the tile's visual space at every viewport.
     Inspect overlays (card-detail modal + pile-viewer) keep their own
     wrappers (.sr-card-detail-effect, .sr-pile-viewer-effect) and
     remain visible. */
  .sr-effects,
  .sr-effect-line,
  .sr-card-head,
  .sr-card-meta,
  .sr-card-effect {
    /* [REQ-371] Bumped to !important so later orphan rules with the
       same selector + specificity can't silently re-enable display
       (REQ-362 originally shipped without !important; the orphans
       at the bottom of the file overrode it via cascade order). */
    display: none !important;
  }
  .sr-hand-empty, .sr-inplay-empty, .sr-bases-empty {
    color: #666;
    font-style: italic;
    font-size: 0.8rem;
  }
  .sr-ally-fired { outline: 1px dotted #e8f020; }
  /* [REQ-245] Phase 2c — activated-base button. Inline child of .sr-base. */
  .sr-base-activate {
    background: transparent;
    border: 1px solid #e8f020;
    color: #e8f020;
    font-family: monospace;
    font-size: 0.7rem;
    padding: 0.1rem 0.4rem;
    margin-left: 0.4rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-base-activate:hover:not([disabled]) { background: #e8f020; color: #0f0f0f; }
  .sr-base-activate-fired,
  .sr-base-activate[disabled] {
    border-color: #555;
    color: #555;
    cursor: not-allowed;
  }
  /* [REQ-317] [REQ-395] Pending-choice prompt. Pre-fix this was an
     inline panel (`position: static`) sitting in document flow — the
     comment cited "not a true overlay to avoid CSP / focus-trap
     complexity". REQ-395 promotes it to a true fixed overlay so
     opening / closing the picker no longer reflows #sr-game-area:
     the play surface stays put + the picker floats above the bottom
     HUD chip bar. Anchored bottom-center (`left: 50%` +
     `translateX(-50%)`); `bottom: 64px + safe-area-inset-bottom`
     clears the YOU HUD (TRADE/COMBAT/AUTHORITY chips + PLAY ALL)
     AND the iOS home-indicator. The hand row (data-sr-area="hand",
     grid row 7) sits ABOVE the HUD so it stays uncovered. z-index:
     50 lifts it above #sr-game-area zones (default z=0); native
     <dialog> modals (win modal, pile-viewer, card-detail) sit in
     the top-layer above any z-index per the HTML spec, so this
     panel never covers them. 0.92 alpha background + box-shadow
     separate the panel visually from the play surface without
     blocking the hand row from view. */
  /* [PTR-051] The previous bottom-anchored prompt
     (`bottom: calc(safe-area + 64px); max-height: 32vh`) covered grid
     row 7 (`hand`) on portrait viewports — the user reported being
     unable to tap their hand cards while the scrap_hand_or_discard
     picker was open. Re-anchor to TOP of the viewport (above the OPP
     authority strip, which is read-only during the player's turn) so
     the entire hand row stays tappable. Wider `max-height: 40vh` is
     fine here because the hand is no longer below the prompt. */
  .sr-choice-prompt {
    position: fixed;
    left: 50%;
    top: calc(env(safe-area-inset-top, 0px) + 8px);
    bottom: auto;
    transform: translateX(-50%);
    width: min(94vw, 520px);
    max-height: 40vh;
    overflow-y: auto;
    z-index: 50;
    margin: 0;
    padding: 0.5rem 0.85rem;
    border: 2px solid #e8f020;
    border-radius: 4px;
    background: rgba(10, 7, 22, 0.92);
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.55);
  }
  .sr-choice-prompt[hidden] { display: none; }
  .sr-choice-heading {
    font-size: 0.8rem;
    color: #e8f020;
    letter-spacing: 0.08em;
    /* [REQ-396] heading-to-options gap tightened 0.6rem → 0.35rem. */
    margin-bottom: 0.35rem;
    text-transform: uppercase;
  }
  .sr-choice-option {
    display: inline-block;
    /* [REQ-396] tighter button paddings + margins so the picker fits
       in 32vh without scrolling. Saves ~8px per button. */
    margin: 0.15rem 0.3rem 0.15rem 0;
    padding: 0.35rem 0.7rem;
    background: transparent;
    border: 1px solid #e8f020;
    color: #e8f020;
    font-family: monospace;
    font-size: 0.78rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-choice-option:hover { background: #e8f020; color: #0f0f0f; }
  /* [REQ-339] Multi-select chip + Confirm button styling for the
     Recycling Station discard picker. Toggle state surfaced via the
     `.sr-choice-multi-selected` class set by the boot click delegate
     so users see which cards are marked before confirming. */
  .sr-choice-multi {
    display: inline-block;
    margin: 0.25rem 0.4rem 0.25rem 0;
    padding: 0.5rem 0.9rem;
    background: transparent;
    border: 1px dashed #e8f020;
    color: #e8f020;
    font-family: monospace;
    font-size: 0.85rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-choice-multi:hover { background: rgba(232, 240, 32, 0.18); }
  .sr-choice-multi-selected {
    background: #e8f020;
    color: #0f0f0f;
    border-style: solid;
  }
  .sr-choice-confirm {
    display: inline-block;
    margin: 0.5rem 0 0.25rem;
    padding: 0.5rem 1.1rem;
    background: #2a402a;
    border: 1px solid #5fc05f;
    color: #5fc05f;
    font-family: monospace;
    font-size: 0.9rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-choice-confirm:hover { background: #5fc05f; color: #0f0f0f; }
  /* [REQ-391] [PTR-061] Physical pick-mode visuals — the user taps a
     hand / trade-row tile to mark a scrap target, then taps Confirm.
     Two visual layers: a dashed outline on EVERY legal target while
     pick mode is active (so the user sees what's tappable), and a
     solid yellow outline on the SELECTED target (so the user sees
     which one they chose). Clicking a different target moves the
     yellow outline; the boot also keeps the .sr-pick-status chip in
     sync. PTR-061 fixed the hand selector — the original REQ-391 rule
     targeted #sr-your-hand which doesn't exist (the hand element id
     is #sr-hand — see index.html), so the dashed-orange + yellow
     outlines never painted on hand cards and the user had no visual
     feedback that hand-card taps were registering as picks. The
     #sr-trade-row branch was correct and is unchanged. */
  body.sr-pick-mode-scrap-hand-discard #sr-hand button[data-card-id],
  body.sr-pick-mode-trade-scrap #sr-trade-row button[data-slot-idx]:not([data-slot-idx="explorer"]),
  body.sr-pick-mode-discard-then-draw #sr-hand button[data-card-id],
  /* [REQ-419] [PTR-075] The youdiscard branch is gated on the
     parent `.sr-zone[data-sr-area="youdiscard"][data-sr-pick-target="1"]`
     ancestor so the dashed-orange pick-target outline stops
     painting whenever `applyPickModeBodyClasses` skips youdiscard
     from the live-zones list (i.e. when the discard is empty —
     no card to scrap means no legal target). */
  body.sr-pick-mode-scrap-hand-discard
    .sr-zone[data-sr-area="youdiscard"][data-sr-pick-target="1"]
    [data-pile-target="discard"][data-pile-owner="you"] {
    outline: 2px dashed #ff8040;
    outline-offset: -2px;
  }
  body.sr-pick-mode-scrap-hand-discard #sr-hand button[data-sr-pick-selected="1"],
  body.sr-pick-mode-trade-scrap #sr-trade-row button[data-sr-pick-selected="1"],
  body.sr-pick-mode-discard-then-draw #sr-hand button[data-sr-pick-selected="1"],
  body.sr-pick-mode-scrap-hand-discard
    .sr-zone[data-sr-area="youdiscard"][data-sr-pick-target="1"]
    [data-pile-target="discard"][data-pile-owner="you"][data-sr-pick-selected="1"] {
    outline: 3px solid #e8f020;
    outline-offset: -3px;
  }
  /* [REQ-407] [PTR-063] Trade-row scrap-pick visual polish — eligible
     non-Explorer slots get a small brightness/saturation boost so they
     read as ACTIVE against the global gray-out applied to the rest of
     the board. The Explorer slot is not in pc.targets (engine excludes
     it from scrap targets), so it inherits the dim from the global
     non-target rule below. */
  body.sr-pick-mode-trade-scrap #sr-trade-row button[data-slot-idx]:not([data-slot-idx="explorer"]) {
    filter: brightness(1.1) saturate(1.15);
  }
  /* [REQ-409] [PTR-065] Cross-prompt targeting consistency. While any
     pendingChoice is open, every .sr-zone NOT marked as the active
     target zone (via data-sr-pick-target="1" set by
     applyPickModeBodyClasses in boot.js) is dimmed and made
     non-interactive. The zone(s) flagged as the legal target retain
     their normal colour + pointer-events so the player knows where
     to tap. The choice prompt itself sits outside .sr-zone and is
     unaffected. */
  body.sr-pick-mode-active #sr-game-area .sr-zone:not([data-sr-pick-target="1"]) {
    filter: grayscale(0.6) brightness(0.65);
    pointer-events: none;
  }
  /* [REQ-419] [PTR-075] Opponent-turn gray-out for non-button tiles.
     `setClickable(interactive)` (boot.js render()) only flips the
     `disabled` attribute on real `<button>` elements, so the scrap
     pile (rendered as `<div role="button">` by `_buildPileTile`)
     and the explorer slot (whose `.sr-trade-explorer` accent wins
     over the default disabled-opacity rule) stayed full-bright
     while the rest of the board was visibly gated. The
     `sr-not-my-turn` body class is toggled in render() against the
     existing `interactive` flag so the dim flips synchronously
     with `setClickable`. Visual-only — pile-viewer click delegates
     remain wired so the scrap pile + explorer stay inspectable on
     the opponent's turn (mirror of REQ-334 / REQ-410). */
  /* [REQ-424] [PTR-078] Extend the PTR-075 opp-turn dim to the
     remaining player-owned non-button tiles — bases under
     `#sr-your-bases` and the YOU discard tile — which are rendered as
     `<div role="button">` by `renderBases` / `_buildPileTile` and so
     never pick up `[disabled]` from `setClickable()`. Visual-only:
     base `data-base-idx` and discard pile-viewer click delegates stay
     wired so the player can still inspect on the opponent's turn
     (mirror of the scrap pile + explorer contract). Opp-side bases
     (`#sr-their-bases`) and the opp discard tile
     (`.sr-discard-pile-tile-opp`) are intentionally NOT included —
     they belong to the active player on opp turn. */
  /* [REQ-427] [PTR-080] Extend the same dim to `#sr-hand
     button[data-card-id]` because under PTR-080 the hand row stays
     `disabled = false` on the opponent's turn (so the REQ-388 drag
     controller's pointerdown can fire) — which means the UA :disabled
     dim no longer applies to hand tiles, leaving them full-bright
     while the rest of the player's side is grayed. The on-drop / play
     self-gates keep release-on-target a no-op, so the visual cue is
     the only thing that needed restoring. */
  body.sr-not-my-turn #sr-game-area .sr-scrap-pile-tile,
  body.sr-not-my-turn #sr-trade-row button[data-slot-idx="explorer"],
  body.sr-not-my-turn #sr-your-bases .sr-base,
  body.sr-not-my-turn .sr-discard-pile-tile-you,
  body.sr-not-my-turn #sr-hand button[data-card-id] {
    filter: grayscale(0.6) brightness(0.65);
    opacity: 0.85;
  }
  .sr-pick-status {
    display: block;
    margin: 0.4rem 0;
    padding: 0.3rem 0.6rem;
    background: #1a1a1a;
    border: 1px solid #5fc05f;
    color: #c0c0c0;
    font-family: monospace;
    font-size: 0.85rem;
    border-radius: 2px;
  }
  /* [REQ-408] [PTR-064] Live "Selected: N / max" read-out for the
     discard_then_draw field-tap picker. Same shape as .sr-pick-status
     so the prompt panel reads consistently across pick kinds. */
  .sr-pick-counter {
    display: block;
    margin: 0.4rem 0;
    padding: 0.3rem 0.6rem;
    background: #1a1a1a;
    border: 1px solid #5fc05f;
    color: #c0c0c0;
    font-family: monospace;
    font-size: 0.85rem;
    border-radius: 2px;
  }
  .sr-pick-confirm {
    display: inline-block;
    margin: 0.5rem 0.4rem 0.25rem 0;
    padding: 0.5rem 1.1rem;
    background: #2a402a;
    border: 1px solid #5fc05f;
    color: #5fc05f;
    font-family: monospace;
    font-size: 0.9rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-pick-confirm:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
  .sr-pick-confirm:hover:not(:disabled) {
    background: #5fc05f;
    color: #0f0f0f;
  }
  .sr-pick-none {
    display: inline-block;
    margin: 0.5rem 0 0.25rem;
    padding: 0.5rem 1.1rem;
    background: #1a1a1a;
    border: 1px solid #c08080;
    color: #c08080;
    font-family: monospace;
    font-size: 0.9rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-pick-none:hover { background: #c08080; color: #0f0f0f; }
  /* [REQ-313] Card-readability styles. `.sr-card-head` is the bold
     name + cost line. `.sr-effects` is the inline ability list shown
     under every card so the player can read the rules without
     hovering. `.sr-effect-line` is one bullet. The hover `title`
     attribute on each card surfaces the same text as a native
     browser tooltip. Keeps trade-row / hand / base / inplay buttons
     auto-sized so longer text wraps instead of clipping. */
  .sr-trade-slot, .sr-hand-card { text-align: left; vertical-align: top; }
  /* [REQ-371] Orphan .sr-card-head re-style removed — REQ-362 retired
     the legacy text rows; this block was overriding the display:none
     hide via cascade order and leaking the legacy text onto the
     redesigned 4-corner card faces. */
  .sr-effects {
    margin-top: 0.15rem;
    color: #b0b0b0;
    /* [REQ-343] Card effect text scales with viewport. */
    font-size: var(--sr-card-effect-size);
    line-height: 1.15;
    overflow: hidden;
  }
  /* [REQ-212] Phase 1b — effect lines now lay out as
     [prefix?] [icon] [text]. The icons use `currentColor` so they
     pick up the tile's faction-tinted text colour set inline by
     applyTileStyle. `align-items: baseline` keeps the SVG vertically
     centred against the first line of wrapped text. */
  .sr-effect-line {
    white-space: normal;
    display: flex;
    align-items: baseline;
    gap: 0.3rem;
    margin: 0.1rem 0;
  }
  .sr-effect-icon, .sr-effect-prefix {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    width: 14px;
    height: 14px;
    line-height: 1;
    opacity: 0.85;
  }
  .sr-effect-prefix { opacity: 0.55; }
  .sr-effect-text { flex: 1 1 auto; }
  /* [REQ-212] Phase 1b — procedural art panel: a faction-tinted
     starfield + nebula generated by sr-visuals.js's `artPanelStyle`
     and a centred ship/base/outpost silhouette. The panel sits at
     the top of every card tile (hand, trade row, in-play, base) and
     is ~46 px tall so it doesn't dwarf the readable name + effects.
     Negative top margin pulls it flush with the tile's inner edge
     past the existing button padding. */
  .sr-art-panel {
    position: relative;
    /* [REQ-343] Art panel scales so the readable name+effects keep
       proportional space on shrunk cards. */
    height: clamp(18px, 4.5vh, 46px);
    margin: -0.4rem -0.7rem 0.4rem -0.7rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    overflow: hidden;
    border-radius: 2px 2px 0 0;
  }
  .sr-inplay-card .sr-art-panel,
  .sr-base .sr-art-panel {
    margin: -0.35rem -0.7rem 0.35rem -0.7rem;
  }
  .sr-art-silhouette {
    position: absolute;
    inset: 6px 18%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: currentColor;
  }
  .sr-art-silhouette svg { width: 100%; height: 100%; }
  /* [REQ-327] Phase 2 — info trigger that opens the detail-card
     modal. Small "?" pip in the top-right of every card tile.
     Inherits the tile's faction-tinted color so it reads against
     the nebula art panel; bumps to full-opacity on hover/focus.
     Positioned absolutely so it overlays the art panel without
     reflowing the tile content. The parent tile sets
     `position: relative` below; without it the pip would escape
     the tile bounds. */
  .sr-trade-slot, .sr-hand-card, .sr-inplay-card, .sr-base { position: relative; }
  /* [REQ-364] Phase 10 redesign — the `?` info pip is retired. The
     whole card tile is now the tap target (single = view modal,
     double = action). The legacy positioning + hover rules below
     stay for diff cleanliness; this `display: none !important`
     override (cascading on the same selector) forces them inert
     so any stale call site that emits the span before the next
     polish chunk can't ship a visible regression. */
  .sr-card-info-btn { display: none !important; }
  .sr-card-info-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 20px;
    height: 20px;
    line-height: 18px;
    text-align: center;
    border-radius: 50%;
    border: 1px solid currentColor;
    background: rgba(0, 0, 0, 0.45);
    color: currentColor;
    font-size: 0.75rem;
    font-weight: bold;
    cursor: pointer;
    opacity: 0.6;
    user-select: none;
    z-index: 1;
  }
  .sr-card-info-btn:hover,
  .sr-card-info-btn:focus {
    opacity: 1;
    outline: 1px dotted currentColor;
    outline-offset: 1px;
  }
  /* [REQ-327] Phase 2 — detail-card modal. Native <dialog> styling.
     The faction-tinted border + glow are applied inline by
     populateDetail (matches the tile palette). ::backdrop dims the
     page behind the modal so the user's eye lands on the card.
     Mobile: full-width on viewports ≤ 600px. */
  .sr-card-detail {
    background: #0a0716;
    color: #f0f0f0;
    border: 2px solid #2a2d38;
    border-radius: 6px;
    padding: 1.25rem 1.5rem;
    max-width: 480px;
    width: 90%;
    font-family: monospace;
    box-shadow: 0 0 32px rgba(0, 0, 0, 0.6);
  }
  .sr-card-detail::backdrop {
    background: rgba(0, 0, 0, 0.65);
  }
  .sr-card-detail-close {
    position: absolute;
    top: 0.5rem;
    right: 0.6rem;
    background: transparent;
    border: none;
    color: #888;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.2rem 0.5rem;
  }
  .sr-card-detail-close:hover { color: #f0f0f0; }
  .sr-card-detail-title {
    font-size: 1.15rem;
    margin-bottom: 0.3rem;
    letter-spacing: 0.04em;
  }
  .sr-card-detail-meta {
    color: #b0b0b0;
    font-size: 0.8rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-bottom: 0.85rem;
  }
  .sr-card-detail-art {
    height: 160px;
    border-radius: 3px;
    margin-bottom: 0.85rem;
    border: 1px solid rgba(255, 255, 255, 0.08);
    overflow: hidden;
    position: relative;
  }
  .sr-card-detail-silhouette {
    position: absolute;
    inset: 18px 22%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: currentColor;
    opacity: 0.85;
  }
  .sr-card-detail-silhouette svg { width: 100%; height: 100%; }
  .sr-card-detail-effects {
    color: #d0d0d0;
    font-size: 0.85rem;
    line-height: 1.45;
  }
  .sr-card-detail-effect {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    margin: 0.35rem 0;
    padding: 0.25rem 0;
    border-bottom: 1px dashed rgba(255, 255, 255, 0.05);
  }
  .sr-card-detail-effect:last-child { border-bottom: none; }
  @media (max-width: 600px) {
    .sr-card-detail {
      max-width: none;
      width: 95%;
      padding: 1rem 1.1rem;
    }
    .sr-card-detail-art { height: 130px; }
  }
  .sr-trade-slot, .sr-hand-card, .sr-inplay-card, .sr-base {
    /* [REQ-343] Cards size from a single token so trade row + hand +
       in-play + bases all scale together. The 1:1.353 ratio matches
       the original 170x230 art proportions. */
    width: var(--sr-card-w);
    height: var(--sr-card-h);
    padding: clamp(2px, 0.6vw, 8px);
    display: inline-flex;
    flex-direction: column;
    margin: 0.2rem 0.3rem 0.2rem 0;
    overflow: hidden;
  }
  button#sr-end-turn {
    background: transparent;
    border: 1px solid #e8f020;
    color: #e8f020;
    padding: 0.5rem 1rem;
    font-family: monospace;
    font-size: 0.8rem;
    letter-spacing: 0.1em;
    cursor: pointer;
  }
  button#sr-end-turn:hover:not([disabled]) { background: #e8f020; color: #0f0f0f; }
  button#sr-end-turn[disabled] { cursor: not-allowed; opacity: 0.4; }
  /* [REQ-334] Phase 9 layout — action row groups Play All / Attack /
     End Turn next to the active player's pools so the human reads
     resources + controls in one glance. The buttons mirror the
     existing `#sr-end-turn` palette: green for Play All (it pushes
     the player's tempo), red for Attack (combat damage), yellow
     for End Turn (wraps the turn). */
  /* [REQ-367] Phase 10 redesign — staged action button. One button
     cycles Play All -> Attack -> End Turn (one press per stage);
     the live label is set by render() via UI.computeStageAction.
     Legacy three-button row stays in the DOM but hides visually
     under .sr-legacy-action-buttons so a follow-up can drop them
     once the staged button has soaked. */
  button#sr-stage-btn {
    background: #0f0f0f;
    color: #e8f020;
    border: 2px solid #e8f020;
    border-radius: 6px;
    padding: 0.55rem 1.4rem;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    cursor: pointer;
    text-transform: uppercase;
    transition: background 0.12s, color 0.12s, transform 0.05s;
  }
  button#sr-stage-btn:hover:not([disabled]),
  button#sr-stage-btn:focus:not([disabled]) {
    background: #e8f020;
    color: #0f0f0f;
    outline: none;
  }
  button#sr-stage-btn:active:not([disabled]) { transform: scale(0.97); }
  button#sr-stage-btn[disabled] { cursor: not-allowed; opacity: 0.4; }
  .sr-legacy-action-buttons { display: none; }
  .sr-action-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
  }
  button#sr-play-all {
    background: transparent;
    border: 1px solid #40e8a0;
    color: #40e8a0;
    padding: 0.5rem 1rem;
    font-family: monospace;
    font-size: 0.8rem;
    letter-spacing: 0.1em;
    cursor: pointer;
  }
  button#sr-play-all:hover:not([disabled]) { background: #40e8a0; color: #0f0f0f; }
  button#sr-play-all[disabled] { cursor: not-allowed; opacity: 0.4; }
  button#sr-attack {
    background: transparent;
    border: 1px solid #ff8040;
    color: #ff8040;
    padding: 0.5rem 1rem;
    font-family: monospace;
    font-size: 0.8rem;
    letter-spacing: 0.1em;
    cursor: pointer;
  }
  button#sr-attack:hover:not([disabled]) { background: #ff8040; color: #0f0f0f; }
  button#sr-attack[disabled] { cursor: not-allowed; opacity: 0.4; }
  /* [REQ-334] Turn counter — large yellow chip so the player always
     knows which turn they're on at a glance. */
  .sr-turn-counter {
    display: inline-block;
    padding: 0.4rem 0.9rem;
    border: 1px solid #e8f020;
    color: #e8f020;
    font-size: 1rem;
    letter-spacing: 0.18em;
    border-radius: 2px;
  }
  /* [REQ-334] Game Log + Error Log panels — fixed-height scrollable
     monospace areas. The error variant tints text red so visible
     failures stand out from regular action history. */
  .sr-log-panel {
    max-height: 8rem;
    overflow-y: auto;
    border: 1px dashed #2a2d38;
    padding: 0.4rem 0.6rem;
    background: rgba(0, 0, 0, 0.25);
    font-family: monospace;
    font-size: 0.8rem;
    line-height: 1.4;
  }
  .sr-log-panel-error { border-color: #a33; color: #f88; }
  .sr-log-line { padding: 0.05rem 0; }
  .sr-log-empty { color: #666; font-style: italic; }
  /* [REQ-349] Trade-deck and player-deck pile tiles — non-interactive
     card-sized count tiles (deck contents are face-down by rule). The
     legacy small `.sr-trade-deck-chip` / `.sr-scrap-pile-chip` chip
     styles have been removed; both the trade deck and the scrap pile
     now render as card-sized `.sr-pile-tile` elements inside the
     trade row. */
  .sr-trade-deck-tile {
    border-color: #888;
    color: #888;
    cursor: default;
  }
  .sr-deck-pile-tile-opp { border-color: #e84040; color: #e84040; cursor: default; }
  .sr-deck-pile-tile-you { border-color: #40b4e8; color: #40b4e8; cursor: default; }
  /* [REQ-376] New Game lives inside the hamburger menu; the
     `.sr-menu-actions` flex row owns its spacing via `gap`, so the
     legacy footer margins are dropped. The visual treatment matches
     the neighbouring Toggle fullscreen / Home buttons (transparent
     bg, monospace text) — same chromeless-on-dark look. */
  button#sr-new-game {
    background: transparent;
    border: 1px solid #888;
    color: #888;
    padding: 0.3rem 0.6rem;
    font-family: monospace;
    font-size: 0.85rem;
    cursor: pointer;
    border-radius: 4px;
  }
  button#sr-new-game:hover { border-color: #f0f0f0; color: #f0f0f0; }

  /* [REQ-249] Phase 3a-ii — lobby panels. Default-hidden via the
     `hidden` HTML attribute; the inline boot toggles visibility per
     role click. Spacing matches .sr-section so the lobby reads as a
     sibling of the game board, not a foreign overlay. */
  .sr-lobby-section {
    border-top: 1px dashed #2a2d38;
    padding: 0.85rem 0;
    max-width: 720px;
  }
  .sr-lobby-section[hidden] { display: none; }
  .sr-lobby-section label {
    display: inline-block;
    font-family: monospace;
    color: #888;
    font-size: 0.7rem;
    letter-spacing: 0.18em;
    margin-bottom: 0.3rem;
    text-transform: uppercase;
  }
  .sr-lobby-section input[type="text"] {
    background: #17171d;
    border: 1px solid #2a2d38;
    color: #f0f0f0;
    font-family: monospace;
    font-size: 0.95rem;
    padding: 0.45rem 0.7rem;
    border-radius: 2px;
    outline: none;
    width: 100%;
    max-width: 320px;
  }
  .sr-lobby-section input[type="text"]:focus {
    border-color: #e8f020;
  }
  .sr-role-btn {
    display: inline-block;
    margin: 0.4rem 0.6rem 0.4rem 0;
    background: transparent;
    border: 1px solid #2a2d38;
    color: #f0f0f0;
    font-family: monospace;
    font-size: 0.85rem;
    padding: 0.6rem 1rem;
    border-radius: 2px;
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
  }
  .sr-role-btn:not([disabled]):hover { border-color: #e8f020; color: #e8f020; }
  .sr-role-btn[disabled] { cursor: not-allowed; opacity: 0.45; }
  .sr-role-primary {
    border-color: #e8f020;
    color: #e8f020;
  }
  .sr-role-primary:not([disabled]):hover {
    background: #e8f020;
    color: #0f0f0f;
  }
  .sr-back-btn {
    display: inline-block;
    margin-top: 0.8rem;
    background: transparent;
    border: 1px solid #888;
    color: #888;
    font-family: monospace;
    font-size: 0.75rem;
    padding: 0.4rem 0.8rem;
    border-radius: 2px;
    cursor: pointer;
  }
  .sr-back-btn:hover { border-color: #f0f0f0; color: #f0f0f0; }
  .sr-soon-note {
    color: #888;
    font-size: 0.85rem;
    line-height: 1.5;
    margin: 0.4rem 0 0 0;
  }
  /* [REQ-252] Phase 3a-iii-C — room-code display. Oversized
     monospace chip so the host can read it across a room. */
  .sr-room-code {
    display: inline-block;
    font-family: monospace;
    font-size: 2.2rem;
    letter-spacing: 0.4em;
    color: #e8f020;
    padding: 0.4rem 0.8rem 0.4rem 1.2rem;
    border: 1px solid #e8f020;
    border-radius: 3px;
    background: rgba(232, 240, 32, 0.08);
    margin: 0.4rem 0;
  }
  #sr-join-code {
    font-family: monospace;
    font-size: 1.3rem;
    text-transform: uppercase;
    letter-spacing: 0.3em;
    max-width: 160px;
    padding: 0.5rem 0.75rem;
    border: 1px solid #2a2d38;
    border-radius: 2px;
    background: #17171d;
    color: #e8f020;
    outline: none;
    text-align: center;
    margin-right: 0.5rem;
  }
  #sr-join-code:focus { border-color: #e8f020; }

  /* [Card layout] Meta line — second row in every card tile;
     shows Ship/Base + cost + defense so field cards carry the
     same information set as trade-row cards. */
  /* [REQ-371] Orphan .sr-card-meta re-style removed — REQ-362 retired
     the legacy text rows; this block was overriding the display:none
     hide via cascade order. */
  /* [REQ-350] Trade row holds 8 card-sized columns: TRADE DECK tile
     + 5 trade slots (or empty placeholders) + Explorer + SCRAP tile.
     The 2D-aware `--sr-card-w` token from :root guarantees 8 columns
     fit the viewport width on every device class — no overflow-x
     needed. `min-width: 0` on the columns lets them honour the
     token's computed width without expanding past their flex/grid
     allocation. */
  /* [REQ-382] [REQ-383] [REQ-384] Trade row holds three groups
     (TRADE DECK, `.sr-trade-row-slots` wrapping the 5 slots + Explorer,
     SCRAP) centred horizontally via `justify-content: center`. REQ-384
     swaps `align-items: start` for `center` so the 8 tiles vertically
     centre within the trade row's grid-cell height — they sit in the
     optical middle of the row instead of hugging the top edge. */
  #sr-trade-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: var(--sr-card-gap);
    min-width: 0;
  }
  /* [REQ-382] Inner wrapper for the 5 trade slots + Explorer — kept as
     a 6-column grid so the buyable cards stay aligned as a unit
     regardless of how many slots are filled. */
  .sr-trade-row-slots {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, var(--sr-card-w)));
    gap: var(--sr-card-gap);
    align-items: start;
    min-width: 0;
  }
  /* [REQ-384] Bases live in dedicated 1-card-wide vertical column
     zones on the side of the play surface (player LEFT, opp RIGHT
     via grid-template-areas above). The inner row flips from
     horizontal flex to vertical flex so up to 3 base tiles stack
     downward at standard --sr-card-w / --sr-card-h size. The
     min-height on the zone wrapper reserves enough vertical space
     in the grid cell for the 3-card stack at standard size. */
  #sr-their-bases,
  #sr-your-bases {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sr-card-gap);
    width: var(--sr-card-w);
  }
  /* [REQ-386] Opp bases stack from the BOTTOM of the column upward
     so played bases huddle immediately above the trade row regardless
     of fill level. Player bases stay top-down (flex-start) so they
     hug the trade row from below — each player's played bases are
     adjacent to the trade row. */
  #sr-their-bases {
    justify-content: flex-end;
  }
  #sr-your-bases {
    justify-content: flex-start;
  }
  /* [REQ-404] When opp has no bases, the (no bases) placeholder span
     must hug the BOTTOM of the 3-card-tall #sr-their-bases column so
     it sits immediately above the trade row — mirror of the player's
     (no bases) span which naturally sits at the TOP of #sr-your-bases
     immediately below the trade row. The column's
     `justify-content: flex-end` (line 1386) only positions flex items
     once the line is sized; on the empty span (single inline child)
     `margin-top: auto` is what actually pushes it down through the
     remaining vertical space. Mirror rule on the player side is a
     no-op (margin-top: 0 is already the default for flex-start) so
     it is omitted to avoid carrying a redundant declaration. */
  #sr-their-bases > .sr-bases-empty {
    margin-top: auto;
  }
  /* [REQ-391] min-height MUST live on the INNER flex container, not
     the .sr-zone wrapper. Pre-fix the rule sat on the grandparent
     (.sr-zone[data-sr-area="oppbases"]); but column-flex `align-items:
     stretch` only stretches the cross-axis (width), so the inner
     flex container shrinks to its content's height regardless of the
     wrapper's reservation. With 1 base played the inner container
     was 1-card-tall and `justify-content: flex-end` had zero distance
     to push the card downward → opp bases rendered at the TOP of the
     column instead of immediately above the trade row. Moving the
     min-height to #sr-their-bases / #sr-your-bases gives flex-end a
     3-card-tall column to position the cards at the bottom = adjacent
     to the trade row from above. The grid cell auto-sizes around
     this inner min-height so REQ-380's grid-template-areas row
     reservation is unchanged. */
  #sr-their-bases,
  #sr-your-bases {
    min-height: calc(3 * var(--sr-card-h) + 2 * var(--sr-card-gap));
    /* [REQ-392] Pin max-height to a definite 3-card calc so 4+ bases
       reliably scroll via overflow-y: auto in every layout. With the
       previous `max-height: 100%`, in `auto`-row landscape layouts
       (styles.css:2329, 2374, 2377) the inner's max-content grew
       with base count, the auto track stretched, the 1fr play /
       trade / hand rows lost their 100dvh share, and their cards
       were clipped by `#sr-game-area`'s overflow: hidden. */
    max-height: calc(3 * var(--sr-card-h) + 2 * var(--sr-card-gap));
    overflow-y: auto;
    /* [REQ-397] Lock to vertical scroll only. Without this, sub-pixel
       width drift from section padding could produce a horizontal
       scrollbar on viewports where the auto LEFT/RIGHT grid column
       widens slightly under content pressure — user reported player
       bases scrolling horizontally with 4+ played. */
    overflow-x: hidden;
  }
  /* [REQ-426] flex-shrink: 0 on bases inside the column zones forces a
     4th base to push the column into vertical scroll (REQ-392's
     overflow-y: auto) instead of compressing the existing 3 — without
     this lock the cards inherit flex-shrink: 1, the flex column
     squashes every tile to fit the 3-card max-height, and the user
     sees four squashed bases instead of three full-size bases plus a
     scrollbar. Mirrors REQ-389 which pinned in-play ships against
     horizontal flex compression. */
  #sr-your-bases > .sr-base,
  #sr-their-bases > .sr-base {
    flex-shrink: 0;
  }
  /* [REQ-384] [REQ-412] Pile-tile mounts hug the right edge of their
     grid cell (auto column track can otherwise leave slack). REQ-411
     added oppdeck + oppdiscard — both moved to the RIGHT column. */
  .sr-zone[data-sr-area="youdiscard"] .sr-pile-tile-mount,
  .sr-zone[data-sr-area="youdeck"] .sr-pile-tile-mount,
  .sr-zone[data-sr-area="oppdiscard"] .sr-pile-tile-mount,
  .sr-zone[data-sr-area="oppdeck"] .sr-pile-tile-mount {
    justify-self: end;
  }
  /* [REQ-393] Player hand cell strips its section padding + gap so
     the cards stop being pushed below the (mobile-hidden) h2, and
     the youdeck pile-tile bottom-pins so it sits at the same
     vertical baseline as the hand cards. Pre-fix the hand cards
     rendered offset downward by section.sr-section's vertical
     padding while the deck tile sat at the top of its bare cell —
     they shared a grid row but not a baseline. flex-end on the
     section pushes cards to the bottom of the cell on viewports
     where the h2 is still visible (>480px landscape, etc.) so
     desktop users see the same alignment. */
  .sr-zone[data-sr-area="hand"] section.sr-section {
    padding: 0;
    gap: 0;
    justify-content: flex-end;
  }
  .sr-zone[data-sr-area="youdeck"] .sr-pile-tile-mount {
    align-self: end;
  }
  /* [REQ-385] Hand cards align right inside the centre column so the
     hand starts adjacent to the deck pile in the right column instead
     of spreading from the left edge. The .sr-row-scroller wrapper
     justifies its children right too so the chevron + scroll affordance
     stays aligned with the cards. */
  #sr-hand {
    justify-content: flex-end;
  }
  .sr-row-scroller[data-sr-row="hand"] {
    justify-content: flex-end;
  }
  /* [REQ-385] Trade zone vertically + horizontally centres its content
     so the 8-card trade row sits in the optical middle of the trade
     grid cell — visible vertical centring between the opp and player
     play areas. */
  .sr-zone[data-sr-area="trade"] {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* [REQ-385] Hide the DISCARD overlay label on face-up discard tiles
     so the top-card art reads cleanly without the word "DISCARD"
     printed across it. Empty (face-down) discard tiles keep their
     `.sr-pile-tile-head` label so the player still knows which tile
     is the discard pile. */
  .sr-pile-tile-faceup.sr-discard-pile-tile .sr-pile-tile-overlay-label {
    display: none;
  }
  /* [REQ-385] [REQ-386] Bases with an unfired activated_* ability
     glow yellow (REQ-385). REQ-386 retired the small ⚡ Activate
     button + thunder-bolt — the glow is the only visual cue, and
     double-tap on the card body fires the ability via REQ-378's
     singleOrDoubleTap delegate. Per user feedback "the glow is fine
     how it is" — no outline / inner ring. */
  .sr-base.sr-base-can-activate {
    box-shadow: 0 0 8px #e8f020;
  }
  /* [REQ-387] Ship + base tiles whose scrap_for_* ability is still
     firable get the same yellow glow REQ-385 set on activated bases.
     Bases with BOTH activated_* and scrap_for_* (Blob Wheel, Trading
     Post, Barter World) carry both classes; once the activated fires
     (.sr-base-can-activate is dropped) the .sr-can-activate keeps the
     glow on so the player remembers they can still scrap the base
     via the REQ-387 multi-ability picker. */
  .sr-inplay-card.sr-can-activate,
  .sr-base.sr-can-activate {
    box-shadow: 0 0 8px #e8f020;
  }
  /* [REQ-396] [PTR-060] Trade-row affordability glow — saturated
     gold halo on slots whose card.cost <= local viewer's trade pool.
     play-ui.js renderTradeRow ALSO stamps the same boxShadow inline
     after buildCardBody (because applyTileStyle writes a faction-
     palette inline boxShadow that beats this class rule in the
     cascade); this rule is the back-stop for any future call site
     that doesn't fight an inline style. The class is flipped on
     every renderBoard() call, so the glow updates whenever the
     trade pool moves (play / buy / scrap). */
  .sr-trade-slot.sr-can-afford {
    box-shadow: 0 0 18px 2px #ffd700, 0 0 6px #ffd700, inset 0 0 0 2px #ffd700;
  }
  /* [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-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;
  }
  .sr-dragging {
    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;
  }
  .sr-zone[data-sr-area="oppbases"] {
    align-self: end;
  }
  .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 the card is absolutely positioned by `renderCardTile`. */
  .sr-pile-tile-faceup {
    display: block;
    padding: 0;
    text-align: left;
  }
  .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;
  }
  .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;
  }
  /* [REQ-397] Opp in-play cards bottom-pin in row 4 so they share a
     horizontal baseline with the opp discard pile-tile (REQ-395
     align-self: end) and the bottom of the opp bases column (REQ-394
     align-self: end on the wrapper). Pre-fix the section's default
     justify-content: flex-start top-pinned the cards inside row 4 —
     they sat far from the trade row and didn't align with the discard
     or first opp base. Mirror of player youplay's natural top-pinning
     in row 6 (cards just below trade row from the player's POV). */
  .sr-zone[data-sr-area="oppplay"] > section.sr-section {
    justify-content: flex-end;
  }
  .sr-zone[data-sr-area="oppplay"] > section.sr-section:last-child,
  .sr-zone[data-sr-area="youplay"] > section.sr-section:last-child {
    flex: 0 1 auto;
  }
  /* [REQ-404] When opp has nothing in play, the (none) placeholder span
     inside #sr-their-in-play (a .sr-row flex container) sits at the TOP
     of the row by default (.sr-row's `align-items: flex-start` at line
     269). The user reported this as a mirror break — player's (none)
     sits at the TOP of #sr-your-in-play immediately below the trade
     row, but opp's (none) was floating mid-cell instead of hugging the
     bottom edge of row 4 (just above the trade row). REQ-400's audit
     called this out explicitly: REQ-397's `justify-content: flex-end`
     on the section only takes effect once cards' fixed dimensions
     stretch the section vertically; on the empty-state span it was a
     visual no-op. Re-anchor the row's cross-axis alignment to flex-end
     so the (none) span hugs the bottom edge of the row whose height
     is dictated by the section's flex-end pin. */
  #sr-their-in-play {
    align-items: flex-end;
  }
  /* [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-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                /* top */
      auto                /* opp authority row */
      auto                /* opp hand row (card backs, max 5) */
      minmax(0, 1fr)      /* opp in-play */
      minmax(0, 1fr)      /* trade row */
      minmax(0, 1fr)      /* your in-play */
      minmax(0, 1fr)      /* your hand */
      auto;               /* you (authority chips + staged button) */
    /* [REQ-394] [REQ-412] Opp half horizontally-reflects the player
       half — analogous zones share a column (REQ-411 superseded
       REQ-394's 180° point-symmetric layout). */
    grid-template-areas:
      "top         top         top"
      "opp         opp         opp"
      "oppdeck     opphand     oppbases"
      "oppdiscard  oppplay     oppbases"
      "trade       trade       trade"
      "yourbases   youplay     youdiscard"
      "yourbases   hand        youdeck"
      "you         you         you";
    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; }
  /* [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; }
  /* [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;
  }
  #sr-your-in-play .sr-inplay-card,
  #sr-their-in-play .sr-inplay-card {
    flex-shrink: 0;
  }
  /* [REQ-380] YOUR · AUTHORITY zone: chips on the left, staged action
     button pinned to the right edge so the button shares the row with
     the resource state instead of stealing its own grid row. */
  .sr-zone[data-sr-area="you"] section.sr-you-section {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
  }
  .sr-zone[data-sr-area="you"] section.sr-you-section > h2 {
    flex: 0 0 100%;
    margin: 0;
  }
  .sr-zone[data-sr-area="you"] section.sr-you-section > #sr-stage-btn {
    margin-left: auto;
  }

  /* [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 {
      --sr-chrome-h: 160px;
    }
    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                /* top */
        auto                /* opp authority */
        auto                /* opp hand row */
        minmax(0, 1fr)      /* opp in-play */
        minmax(0, 1fr)      /* trade row */
        minmax(0, 1fr)      /* your in-play */
        minmax(0, 1fr)      /* your hand */
        auto;               /* you (chips + staged button) */
      /* [REQ-394] [REQ-412] Mobile portrait mirrors the desktop
         layout — same column for analogous opp/player zones. */
      grid-template-areas:
        "top         top         top"
        "opp         opp         opp"
        "oppdeck     opphand     oppbases"
        "oppdiscard  oppplay     oppbases"
        "trade       trade       trade"
        "yourbases   youplay     youdiscard"
        "yourbases   hand        youdeck"
        "you         you         you";
    }
    /* [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 */
      grid-template-areas:
        "top        top"
        "opp        you"
        "oppbases   yourbases"
        "oppplay    youplay"
        "trade      trade"
        "hand       hand";
    }
  }

  /* [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 + staged button */
      grid-template-areas:
        "opp        top"
        "oppbases   top"
        "oppplay    top"
        "trade      top"
        "yourbases  you"
        "youplay    you"
        "hand       you"
        "you        you";
    }
  }

  /* [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-attack-pulse {
    0%, 100% {
      outline-color: rgba(232, 64, 64, 0.95);
      box-shadow: 0 0 12px rgba(232, 64, 64, 0.55);
    }
    50% {
      outline-color: rgba(232, 64, 64, 0.45);
      box-shadow: 0 0 4px rgba(232, 64, 64, 0.3);
    }
  }
  /* [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: 2px solid rgba(232, 64, 64, 0.95);
    outline-offset: 2px;
    animation: sr-attack-pulse 1.4s 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;
    cursor: pointer;
  }
  .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; }
  }
