/* ============================================================================
 * Gerbil Gunners v0.6.054 — Consolidated CSS
 *
 * All v06011..v06052 closure rules merged here into a single styles.css
 * per the v0.6.050 spec (delta #1).
 *
 * Cascade order preserved by order of insertion in this file:
 *   1. styles.css baseline (lines below header)
 *   2. v06052-closure rules
 *   3. v06051-closure rules
 *   ... (reverse order)
 *   N. v06011-closure rules
 *   N+2. v0.6.054 broken-asset replacement (FINAL — wins cascade at equal specificity)
 *
 * Closure identity markers preserved as comments at the top of each
 * block so the build identity is still traceable.
 *
 * v0.6.054 marker: v06054-replace-broken-hp-bar-image
 *
 * All v0.6.052..v0.6.016 surfaces preserved. The tilt + creep animations
 * now compose via CSS custom properties (--creep-x, --lean-rotate,
 * --lean-translate-x) so they no longer conflict when both are active
 * simultaneously — both effects are visible.
 *
 * v0.6.068 mark: v06068-blackopsone-title-supplied-battle-art
 *   - main GERBIL GUNNERS heading now uses font-family: 'BlackOpsOne'
 *     (was 'BlackOpsOne-Regular' which has no registered @font-face).
 *   - body[data-gg-background-scene="battle"][data-gg-background-axis=...]
 *     resolves to the supplied assets/bg_battle_16x9_{wide,tall}.png
 *     (see tests/v06068/ for byte-exact source->artifact->HTTP verification).
 *
 * v0.6.069 marker: v06069-medic-heal-random-minmax
 * ============================================================================ */


/* v0.6.053 delta -- register the --creep-x, --lean-rotate, --lean-translate-x
   custom properties so the browser can interpolate them as length/angle
   values during CSS animations. Without @property, the browser treats
   custom properties as opaque strings and animates them discretely
   (flip at 50%), which would make the v06018-charge-creep-* keyframes
   animate as a single step instead of a smooth gradient. */
@property --creep-x {
  syntax: '<length>';
  inherits: true;
  initial-value: 0cqw;
}
@property --lean-rotate {
  syntax: '<angle>';
  inherits: true;
  initial-value: 0deg;
}
@property --lean-translate-x {
  syntax: '<length>';
  inherits: true;
  initial-value: 0cqw;
}

/* v0.6.110 wide-screen cap contract.
   Every non-zero stage-relative length below is authored as min(Ncqw, Npx)
   (or max() for negative travel), where 1 stage cqw caps at 12.8px. The
   block-axis token is the 16:9 equivalent: 0.5625cqw, capped at 7.2px.
   This keeps the fitted stage itself fluid while fonts, border images,
   outlines, spacing, shadows, transforms, and launcher chrome stop growing
   beyond the accepted 1280x720 visual scale. Legacy cqh values are normalized
   to their exact 16:9 cqw equivalent so inline-size containment is sufficient. */
:root {
  --stage-cqw: min(1cqw, 12.8px);
  --stage-cqh: min(0.5625cqw, 7.2px);
}

/* The mug badge retains the predecessor's exact authored 1cqw contract.
   Its only wide-screen delta is this explicit stage-container cap. */
@container stage (min-width: 1280px) {
  body #titleScreen .title-mug-badge {
    font-size: 12.8px;
  }
}

/* ===========================[ styles.css baseline ]=========================== */
/* ============================================================================
   v0.5.15-REDO32 — Project fonts (BlackOpsOne + SplineSans). Six TTF files
   copied from C:\Users\admin\Pictures\ into assets/fonts/. @font-face
   declarations below register both font families and the five SplineSans
   weights (300/400/500/600/700). Project rule: BlackOpsOne for titles /
   headlines, SplineSans (with weight-based variant selection) for body
   text. Replaces the previous default ('Verdana', 'Tahoma', sans-serif)
   site-wide via the body/html rule below; every selector that the rest
   of the file relies on for sizing (h1-h3, .title-card, .depot-cell-
   title, .banner-title, shop / squad / stash header h3s, etc.) now
   inherits BlackOpsOne through the catch-all heading rule.
   ============================================================================ */
@font-face {
  font-family: 'BlackOpsOne';
  src: url('assets/fonts/BlackOpsOne-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'SplineSans';
  src: url('assets/fonts/SplineSans-Light.ttf') format('truetype');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'SplineSans';
  src: url('assets/fonts/SplineSans-Medium.ttf') format('truetype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'SplineSans';
  src: url('assets/fonts/SplineSans-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'SplineSans';
  src: url('assets/fonts/SplineSans-SemiBold.ttf') format('truetype');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'SplineSans';
  src: url('assets/fonts/SplineSans-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
/* v0.6.076 — SplineSans-Bold alias for the unchanged
   assets/fonts/SplineSans-Bold.ttf. The base registration above
   keeps the family name 'SplineSans' (the canonical weight-700
   family). The selector `body #titleScreen .title-operation-line`
   explicitly references 'SplineSans-Bold' as the computed family
   name; this @font-face declaration registers that exact family
   alias at the same weight, pointing at the same unchanged TTF,
   so document.fonts.check('700 16px "SplineSans-Bold"') resolves
   to the supplied SplineSans-Bold asset rather than falling back
   to system-ui. The asset path, weight, style, font-display, and
   the underlying TTF bytes are unchanged. */
@font-face {
  font-family: 'SplineSans-Bold';
  src: url('assets/fonts/SplineSans-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* v0.6.121.2 — Disable text and image selection everywhere. The user
   asked for * { user-select: none; } so the entire game UI is
   unselectable (no highlighting, no drag-to-select, no copy on
   text or image surfaces). The standard `user-select: none` is
   included alongside the `-webkit-user-select` prefix for older
   WebKit / Safari compatibility. The 16 existing per-element
   `user-select: none` declarations on drag handles, image
   sprites, and headers remain in place as fallbacks; this
   universal rule is the canonical default for any element that
   wasn't already opted in.
   Note: form inputs (input/textarea/select) and the title
   settings menu's editable controls are NOT covered by `*` because
   the production code does not expose any editable form fields —
   the settings menu uses only buttons, sliders (range inputs
   exposed via the custom .title-settings-slider styling), and a
   `<select>` element. `<input type="range">` and `<select>` ignore
   `user-select` on their text content, so the player-facing
   behavior is unchanged. */
* {
  user-select: none;
  -webkit-user-select: none;
}

/* Body / html default: SplineSans Regular. All font-family declarations
   elsewhere in the file were either (a) the original 'Verdana','Tahoma'
   site default below, or (b) implicit inherits from body — both now
   resolve to SplineSans without further edits. */
body, html {
  font-family: 'SplineSans', sans-serif;
  font-weight: 400;
}

/* Headline / title default — applies to h1, h2, h3, h4, h5, h6, and any
   other title-like elements. Body text outside this rule stays SplineSans. */
h1, h2, h3, h4, h5, h6,
.title-card,
.depot-cell-title .banner-title,
.shop-band-head h3,
.squad-row-head h3,
.rail-stash-head h3,
.bsc-header h3,
.summary-screen .panel h3,
.section-heading {
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: normal;
  letter-spacing: min(0.12cqw, 1.536px);
}

/* ============================================================================
   v0.5.15-REDO22 — Apply ui_container.png as border-image + background on
   the four large "area" cells (.depot-cell-item-shop, .depot-cell-gerbil-
   shop, .depot-cell-squad, .depot-cell-stash). Each area cell now wears
   the user-supplied **`assets/ui_container.png`** as both a `border-image`
   and a cover `background-image`, reading as one heavy wooden "crate"
   frame per area (not per inner card). Inner cards (.shop-card /
   .squad-card / .stash-card / .depot-cell-freeze .freeze-card) keep
   their ui_card.png frame from REDO17 — the two frames stack visually
   (heavy outer crate + lighter inner slots), making the layout
   hierarchy legible at a glance.

   The user specified `border-image: url('ui_container.png') 16 fill /
   <W>px / 0 stretch;` and a three-step responsive border-width ladder
   that follows the scene-area size breakpoints, with the ladder values
   8 / 12 / 16 px:

       <73.75vw viewport (800x360 scene)  -> border-width  .5vw
       1180-1579 vw-range  (1200x540 scene)    -> border-width .75vw  (user CSS default)
       >=98.75vw     (1600x720 scene)    -> border-width 1vw

   The slice value (16) is CONSTANT at all three breakpoints — only
   `border-width` and the matching `border-image`'s `/ Xpx` slice width
   scale. The asset (assets/ui_container.png, 188572 bytes, copied
   from C:\Users\admin\Pictures\ui_container.png, md5
   d74dcd701e50ba4a45bb003792c1b681), `border-style: solid`,
   `background-image: cover`, and `background-color: transparent` are
   all byte-identical at every breakpoint.

   This ladder is DIFFERENT from the four prior ladders:
       REDO18 card-frame :  6 /  8 / 12 px  (slice 32, ui_card.png)
       REDO19 reroll     :  8 / 12 / 20 px  (slice 64, ui_reroll.png)
       REDO20 freeze     :  8 / 12 / 20 px  (slice 64, ui_freeze.png)
       REDO21 sell       :  8 / 10 / 16 px  (slice 48, ui_sell.png)
       REDO22 area-contnr:  8 / 12 / 16 px  (slice 16, ui_container.png)

   All four selectors get the same source-order cascade: the existing
   per-area `background: #f8efd4 / #fbf6e6; padding: .25vw` rules
   (REDO21 .depot-cell-item-shop line 874, .depot-cell-gerbil-shop
   line 873, .depot-cell-squad line 942, .depot-cell-stash line 1013)
   stay ABOVE the new block in source order. The new block's longhand
   `border-style` + `border-width` + `border-image` override the base
   `.depot-cell { border: .0625vw solid #4a372a; border-radius: .25vw; }`
   rule (line 319), and `border-radius: .25vw` is correctly ignored by
   browsers when `border-image` is present (square frames). The new
   block's `background-color: transparent` overrides the cream
   `#f8efd4 / #fbf6e6` so the bg-image shows; padding: .25vw stays so
   inner cards still get breathing room inside the frame. `gap: .25vw`
   on the depot grid (line 303) and inner `.shop-cards / .squad-cards
   / .stash-cards` subgrids stays — that's where card-to-card spacing
   lives.

   All other styling byte-identical vs REDO21:
     - The REDO21 sell block now lives on an inner div (REDO49): .sell-frame
       inside .depot-cell-sell. The cell loses its border-image (becomes
       transparent + flex-centered); the ui_sell.png border-image + cover
       background-image + drag-feedback rules move onto .sell-frame (90%
       height, 75% width, centered). The data-drop="sell" / data-zone="sell"
       attributes were already on .sell-frame in the HTML, so the engine
       wires all sell drop / drag / hover behavior to the inner div
       automatically — no engine changes.
   - The REDO20 freeze rail block (.depot-cell-freeze + 2 @media +
     .filled shorthand->longhand fix) is unchanged.
   - The REDO19 reroll rail block (.depot-cell-reroll + 2 @media) is
     unchanged.
   - The REDO18 four card selectors (.shop-card, .squad-card,
     .stash-card, .depot-cell-freeze .freeze-card) and their responsive
     6 / 8 / 12 px border-image / border-width media queries are
     unchanged.
   - The per-area base rules (item-shop / gerbil-shop / squad / stash)
     and the .is-drop / .is-bad-drop drag-over highlight rules are all
     UNCHANGED. The new REDO22 block sits AFTER them so the new
     longhand `border-style` + `border-width` + `border-image` +
     `background-image` win via source-order (exactly the same trick
     the REDO19/20/21 blocks use).

   Engine code (game.js, game-data.js, game-rules.js, data-loader.js),
   HTML structure, data/, and the other CSS unchanged vs REDO21. Only
   the four area cells (.depot-cell-item-shop, .depot-cell-gerbil-shop,
   .depot-cell-squad, .depot-cell-stash) gain the new frame; no new
   features, no layout shift, no JS changes.
   ============================================================================ */

/* ============================================================================
   v0.5.15-REDO34 — REPLACE ui_container.png with ui_box.png on the four area
   containers. The REDO22 block above (the in-place rule for
   .depot-cell-item-shop / -gerbil-shop / -squad / -stash and its two @media
   queries) was edited to swap:
       - asset:          ui_container.png (slice 16) -> ui_box.png (slice 80)
       - border-image:   ... 16 fill / <W>px ...    -> ... 80 fill / <W>px ...
       - background:     added background-image / cover / center / no-repeat
                         quad on the base rule, matching the user's "use it
                         as background AND border" wording (REDO22 / REDO24
                         relied on the border-image fill keyword alone).
   A redundant contract block is appended at the END of this file (after
   the REDO33 tail) re-stating the same longhand pair so the cascade stays
   defensive against any future rule that re-introduces ui_container.png.

   Responsive border-width ladder (8 / 12 / 16 px at <1180 / 1180-1579 /
   >=1580 px viewport) is IDENTICAL to REDO22 — only the asset and slice
   value change. Inner cards keep ui_card.png (REDO17/18). Other border-
   image cells (sell ui_sell.png, freeze ui_freeze.png, reroll ui_reroll.png)
   unchanged.

   Asset: assets/ui_box.png (368,255 bytes, copied from
   C:\Users\admin\Pictures\ui_box.png, md5 a0bd24f3f804be478a83b2af7f2bf927).
   Engine / HTML / data / JS byte-identical vs REDO33.
   ============================================================================ */

/* ============================================================================
   v0.5.15-REDO20 — Apply ui_freeze.png as border-image + background on the
   depot-cell-freeze (right rail, cols 16-17 rows 4-6), with the border-width
   tracking the scene-area breakpoints like the REDO19 reroll rail cell. The
   user specified `border-image: url('ui_freeze.png') 64 fill / .75vw / 0
   stretch;` and the same three-step ladder as the REDO19 reroll rail cell
   (8 / 12 / 20 px at the <1180 / 1180 / 1580 breakpoints). The user said
   "reroll card" but the CSS clearly uses `ui_freeze.png`, so the target is
   the freeze cell (NOT the reroll cell — which keeps ui_reroll.png from
   REDO19).

   CHANGE vs REDO19: the freeze rail cell gains the same wooden/parchment-
   style frame treatment as the reroll rail cell, but using a DIFFERENT asset
   (`assets/ui_freeze.png` with slice value 64 per user spec) at the SAME
   (larger) three-step border-width ladder:

       <73.75vw viewport (800x360 scene)  -> border-width .5vw
       1180-1579 vw-range  (1200x540 scene)    -> border-width .75vw (user CSS default)
       >=98.75vw     (1600x720 scene)    -> border-width 1.25vw

   The border-image uses:
       url("assets/ui_freeze.png") 64 fill / <W>px / 0 stretch;
   where <W> matches the element's border-width at the current breakpoint
   (8 / 12 / 20). The asset, the slice value (64), `border-style: solid`,
   `background-image: cover`, and `background-color: transparent` longhand
   are byte-identical at every breakpoint — only `border-width` and the
   matching `border-image`'s `/ Xpx` slice width scale.

   The REDO19 reroll block (.depot-cell-reroll + 2 @media) is byte-identical
   to REDO19. The REDO18 four card selectors (.shop-card, .squad-card,
   .stash-card, .depot-cell-freeze .freeze-card) and their responsive
   border-image / border-width media queries are byte-identical to
   REDO18 (no change). The inner .depot-cell-freeze .freeze-card block
   (which wears assets/ui_card.png) keeps its REDO17/18 wooden card frame
   intact; only the OUTER .depot-cell-freeze container gains the new
   ui_freeze.png frame. The .depot-cell-freeze.filled rule had its `background:
   #fff8dc` shorthand converted to `background-color: #fff8dc` longhand so
   the REDO20 block's longhand `background-image: url("assets/ui_freeze.png")`
   survives on the filled state.

   Engine code (game.js, game-data.js, game-rules.js, data-loader.js),
   HTML structure, data/, other styles, and the other CSS unchanged vs
   REDO19. Only the .depot-cell-freeze element gets the new frame (plus
   the .filled shorthand->longhand fix); no new features, no layout shift,
   no JS changes.
   ============================================================================ */

/* ============================================================================
   v0.5.15-REDO18 — Make the card border-width responsive to viewport width
   (matches the scene-area size breakpoints).

   CHANGE vs REDO17: the same four card selectors (.shop-card, .squad-card,
   .stash-card, .depot-cell-freeze .freeze-card) introduced by REDO17 keep
   wearing the wooden/parchment frame (assets/ui_card.png) as both a
   border-image and a cover background-image. The single 8 px value from
   REDO17 is now three responsive sizes via @media queries:

       <73.75vw viewport (800x360 scene)  -> border-width .375vw
       1180-1579 vw-range  (1200x540 scene)    -> border-width .5vw  (REDO17 default)
       >=98.75vw     (1600x720 scene)    -> border-width .75vw

   At each breakpoint BOTH the element's `border-width` AND the
   `border-image` shorthand's `/ Xpx` slice-width are updated to match.
   The user's frame asset (`assets/ui_card.png`), the slice value (32),
   the `border-style: solid`, and the `background-image` longhand all
   stay byte-identical to REDO17 — only border-width and the matching
   slice value scale.

   Targets (same as REDO17): .shop-card (gerbil + item shop), .squad-card
   (squad slots), .stash-card (stash slots), .freeze-card (freeze slot).

   Engine code (game.js, game-data.js, game-rules.js, data-loader.js),
   HTML structure, data/, and other styles are byte-identical to
   v0.5.15-REDO17. Only the four card-style selectors above gain the new
   media queries. No new features, no layout shift, no JS changes.
   ============================================================================ */

/* ============================================================================
   v0.5.15-REDO16 — Tool Tip colspan shrinks from 12 to 11.

   CHANGE vs REDO15: the Tool Tip cell at row 13 (cols 2-13, cs 12) shrinks
   to (cols 2-12, cs 11). Col 13 of row 13 becomes empty Free Space. All
   other cells unchanged: Wins (col 15, cs 1), Coins (col 16, cs 1), Morale
   (cols 14-15, cs 2 rs 2 -> rows 12-13), Engage (cols 16-17, cs 2 rs 2 ->
   rows 12-13). Engine code (game.js, game-data.js, game-rules.js,
   data-loader.js), HTML structure, assets/, and data/ are byte-identical to
   v0.5.15-REDO15. The only CSS rule that changed is .depot-cell-tooltip's
   colspan (12 -> 11); matching documentation comments updated.
   ============================================================================ */

/* ============================================================================
   v0.5.15-REDO7 — SHIFT Gerbil shop row 3 from 3 cells -> 1 cell (span 6).

   CHANGE vs REDO6: the 3 row-3 gerbil shop cells (each colspan 2) collapse into
   a single cell at cols 4-9 (colspan 6, rowspan 5 -> rows 3-7). All other
   row positions and rowspans are unchanged from REDO6 (which itself unchanged
   from REDO5 — row 7 Stash still at cols 10-17, row 12 still at cols 11-17).

   Engine code (game.js, game-data.js, game-rules.js, data-loader.js)
   byte-identical to v0.5.15-REDO6. Scene-sizing media queries and
   landscape-only overlay byte-identical to v0.5.15. The only DOM/CSS
   changes are: 3 row-3 .depot-cell-gerbil-shop elements collapse to 1;
   3 CSS Grid placement rules for those cells collapse to 1.
   ============================================================================ */

/* v0.5.15 CHANGE 1: page-level no-scroll rule.
   body + html overflow hidden ensures no scrollbars appear at any size. */
html, body {
  margin: 0; padding: 0;
  width: 100%; height: 100%;
  overflow: hidden;
  background-color: black;
  color: #211a17;
  font-family: 'SplineSans', sans-serif;
}

/* v0.5.15-REDO30 CHANGE 1: the SCENE area — DYNAMIC size, full width,
   50% height (2:1 aspect ratio). Replaces the v0.5.15 fixed-size media
   queries (1600x720 / 1200x540 / 800x360) with `aspect-ratio: 2 / 1` so
   the scene scales fluidly with the viewport. At 120vw wide -> 1920x960,
   at 80vw wide -> 1280x640, at 50vw wide -> 800x400, etc. The inner
   17-col x 13-row CSS Grid scales with the scene box (its cells use
   `grid-template-columns: repeat(17, 1fr)` / `repeat(13, 1fr)`, so
   columns and rows scale proportionally). */
.scene {
  /* v0.6.048 DYNAMIC 16:9 SMART CLAMP (min() not max()). The user spec
    requires the app space to:
      (a) retain a 16:9 aspect ratio at every viewport,
      (b) cover as much of the viewport as possible,
      (c) NEVER go out of bounds (no horizontal/vertical overflow).

    The v0.6.046 max() formula picks the LARGER axis. At a wide viewport
    like 1264x625, max(100vw=1264, 177.7777vh=1111.11) = 1264 (width-bound)
    but height = max(56.25vw=711, 100vh=625) = 711, which OVERFLOWS the
    39.0625vw viewport height by 5.375vw — violating the "no out of bounds" rule.

    The v0.6.048 fix uses min(): the SMALLER axis is always the constraint.
      width  = min(100vw, 177.7777vh)  (smaller of viewport-width vs
                                         height-derived width)
      height = min(56.25vw, 100vh)    (smaller of width-derived height
                                         vs viewport-height)
    The result always fits the viewport AND maintains 16:9 AND covers
    the maximum possible area.

    Verification at 6 viewports (post-v0.6.048):
      1920x1080 (vw/vh = 1.78 = 16/9 EXACTLY) -> 1920x1080  both branches tie (min picks either), fits exactly
      1600x720  (vw/vh = 2.22 > 16/9)          -> 1280x720   height-bound branch wins, fits
      1264x625  (vw/vh = 2.02 > 16/9)          -> 1111.11x625  fits perfectly with 16:9 (no overflow)
      1200x540  (vw/vh = 2.22 > 16/9)          -> 960x540    fits
       800x600  (vw/vh = 1.33 < 16/9)          -> 800x450    width-bound branch wins, fits
       800x360  (vw/vh = 2.22 > 16/9)          -> 640x360    fits
    Every viewport produces a 16:9 scene that fits inside the viewport.

    Child elements switch unit (vw vs vh) based on the limiting axis via
    the @media (min-aspect-ratio: 16/9) breakpoint below in styles.css.
    At narrow viewports (aspect < 16/9, width is limiting), child elements
    use vw. At wide viewports (aspect >= 16/9, height is limiting), child
    elements use vh with the conversion factor 16/9 (because scene_width =
    16/9 * scene_height at height-bound viewports). The defense-in-depth
    child-element rules also live in v06048-closure.css. */
  width: min(100vw, 177.7777vh);
  height: min(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
  /* Flex-centered in viewport. */
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Inner overflow hidden so cell content can't push the scene bigger. */
  overflow: hidden;
  box-sizing: border-box;
  background: #fbf6e6;
  /* v0.5.15-REDO35 CHANGE 1: border removed. Other styles (background,
    width, aspect-ratio, padding, etc.) unchanged. */
}


/* ============================================================================
   v0.5.15-REDO7 — Gerbil shop row 3: collapse 3 cells (each colspan 2) into 1 cell (colspan 6).

   CHANGE vs REDO6: the 3 row-3 gerbil shop cells (cols 4-5, 6-7, 8-9) collapse into a
   single cell at cols 4-9 (span 6, rs5 -> rows 3-7). The engine #shopUnits container with
   3 internal .shop-card slots sits inside it; .depot-cell-gerbil-shop .shop-cards already
   uses grid-template-columns: repeat(3, 1fr) so the 3 slots lay out side-by-side.

   Row 1:  Player Info (1-4) | Free(5) | Title (6-12) | Free(13) | Morale (14-15) | Gold (16-17)
   Row 2:  Free(1-3, rs4) | Free(4-9) | Item 1(10-11) rs5 | Item 2(12-13) rs5 | Item 3(14-15) rs5 | Reroll(16-17) rs2
   Row 3:  [CHANGED from REDO6] Gerbil shop at cols 4-9, rs5 -> rows 3-7 (single cell, span 6).
             Cell IN row 3 is the single Gerbil shop (was 3 separate Gerbil cells in REDO6).
             cols 1-3 empty (covered by Free rs4 starting row 2), cols 10-15 empty (covered by Items rs5 starting row 2).
   Row 4:  Freeze (cols 16-17) rs3 -> rows 4-6
   Row 5:  (empty) [Free rs4 ends here at row 5]
   Row 6:  Sell (cols 1-3) rs2 -> rows 6-7
   Row 7:  Free(10) | Stash 1 (11-12) rs5 -> 7-11 | Stash 2 (13-14) rs5 | Stash 3 (15-16) rs5 | Free(17) rs5
   Row 8:  Squad 5 (1-2) rs5 -> 8-12 | Squad 4 (3-4) rs5 | Squad 3 (5-6) rs5 | Squad 2 (7-8) rs5 | Squad 1 (9-10) rs5
   Row 9-11: rows 9, 10 have Stash rs5 ongoing (rows 7-11) and Squad rs5 ongoing (rows 8-12). Cols 1-10 overlap.
             Browser paints DOM-later cells on top.
   Row 12: Free(11) | Victories (12-13) rs2 | Engage (14-15) rs2 | Free(16-17) rs2
   Row 13: Free(1) | Tool Tip (2-11)
   ============================================================================ */

.depot {
  display: grid;
  /* 17 equal-width columns; column groupings done via cell col-spans. */
  grid-template-columns: repeat(17, 1fr);
  /* 13 equal-height rows; cell placement done via cell row + row-spans. */
  grid-template-rows: repeat(13, 1fr);
  gap: min(0.25cqw, 3.2px);
  padding: min(0.375cqw, 4.8px);
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  background: #fbf6e6;
  border: 0;
  overflow: hidden;
}

.depot-cell {
  background: #fff;
  /* v0.5.15-REDO42: flat .0625vw solid #4a372a border + .25vw border-radius REMOVED.
     Every depot cell either wears its own border-image (REDO19/20/21/36/41)
     or is a transparent container. No cell on the depot scene should draw
     a flat-colored .0625vw stroke. */
  border: 0;
  border-radius: 0;
  padding: min(0.25cqw, 3.2px) min(0.375cqw, 4.8px);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

/* =============== ROW 1 placement (no rowspans) =============== */
/* v0.5.15-REDO13: row 1 reorganization.
  v0.5.15-REDO14: Wins slims from cs 2 (cols 15-16) to cs 1 (col 15); col 16 becomes a Free Space
  thin cell (the rendering grid's auto-flow fills it). Coins stays at col 17.
  v0.5.15-REDO15: Coins moves from col 17 to col 16 so Wins (col 15) and Coins (col 16) are
  adjacent ("after wins comes coins", no Free between). Col 17 of row 1 is now empty.
  Cols: [Avatar 1] [Player 2-4] [Free 5] [Title 6-12] [Free 13] [Round 14] [Wins 15] [Coins 16] = 16 used; col 17 empty. */
.depot-cell-player-avatar {
  grid-column: 1;          grid-row: 1;
  /* v0.6.109: commander/avatar surfaces hidden. The DOM and runtime
     writers are preserved for compatibility; only the visual surface
     is removed via display:none. */
  display: none !important;
}
.depot-cell-player        { grid-column: 2 / span 3; grid-row: 1; display: none !important; }
.depot-cell-title         { grid-column: 6 / span 7; grid-row: 1; }
.depot-cell-round         { grid-column: 14;         grid-row: 1; }
/* v0.5.15-REDO14: Wins no colspan — sits at col 15 alone. */
.depot-cell-wins          { grid-column: 15;         grid-row: 1; }
/* v0.5.15-REDO15: Coins moves from col 17 to col 16 (adjacent to Wins at col 15). */
.depot-cell-coins         { grid-column: 16;         grid-row: 1; }
/* Row 1 freespace-thin cells: 5 (col 5) and 13 (col 13) — unchanged. */
.depot-cell-row1-free-5   { grid-column: 5;  grid-row: 1; }
.depot-cell-row1-free-13  { grid-column: 13; grid-row: 1; }

/* =============== ROW 2 placement (with rowspans!) =============== */
/* Per spec, row 2 contains cells that span multiple rows. */
/* Free(1-3) rowspan=4 -> rows 2-5 */
.depot-cell-row2-free-1-3  { grid-column: 1 / span 3;  grid-row: 2 / span 4; }
/* Free(4-9) no rowspan, row 2 */
.depot-cell-row2-free-4-9  { grid-column: 4 / span 6;  grid-row: 2; }
/* v0.5.15-REDO8: Item Shop 3 separate cells (cols 10-11, 12-13, 14-15) merged into ONE cell at cols 10-15 (span 6).
   rowspan=5 -> rows 2-6. The engine #shopItems container sits inside and
   .depot-cell-item-shop .shop-cards { display: grid; grid-template-columns: repeat(3, 1fr); }
   lays out 3 internal .shop-card slots side-by-side automatically. */
.depot-cell-row2-item-shop { grid-column: 10 / span 6; grid-row: 2 / span 5; }
/* Reroll rowspan=2 -> rows 2-3 */
.depot-cell-row2-reroll    { grid-column: 16 / span 2; grid-row: 2 / span 2; }
.depot-cell-reroll         { grid-column: 16 / span 2; grid-row: 2 / span 2; }

/* =============== ROW 3 placement (Gerbil shop only; CHANGED REDO6->REDO7) =============== */
/* Gerbil shop: 3 separate cells (cols 4-5, 6-7, 8-9) merged into ONE cell at cols 4-9 (span 6).
   rowspan=5 -> rows 3-7. The engine #shopUnits container sits inside and
   .depot-cell-gerbil-shop .shop-cards { display: grid; grid-template-columns: repeat(3, 1fr); }
   lays out 3 internal .shop-card slots side-by-side automatically. */
.depot-cell-row3-gerbil-shop { grid-column: 4 / span 6;  grid-row: 3 / span 5; }
/* v0.5.15-REDO7: drop the 3 separate row-3 cell rules (gerbil-4/gerbil-6/gerbil-8).
   Their CSS Grid placement is now covered by the single merged cell above. */

/* =============== ROW 4 placement (REDO4 CHANGE) =============== */
/* Freeze moved from row 3 -> row 4. Cols 16-17, rowspan=3 -> rows 4-6 */
.depot-cell-freeze         { grid-column: 16 / span 2; grid-row: 4 / span 3; }

/* =============== ROW 5 placement (empty in REDO4) =============== */
/* Row 5 used to be Sell in REDO3 (rs2 -> rows 5-6). In REDO4 Sell is at row 6,
   so row 5 has no cells. Free(1-3, rs4) ends at row 5. */

/* =============== ROW 6 placement (Sell CHANGED to row 6) =============== */
.depot-cell-sell           { grid-column: 1 / span 3;  grid-row: 6 / span 2; }

/* =============== ROW 7 placement (Stash SHIFTED right to start at col 10) =============== */
/* Cols 1-9 of row 7 are empty (no cells defined). */
/* Free(10) row 7 single, col 10 */
.depot-cell-row7-free-10   { grid-column: 10;          grid-row: 7; }
/* v0.5.15-REDO8: Stash 3 separate cells (cols 11-12, 13-14, 15-16) merged into ONE cell at cols 11-16 (span 6).
   rowspan=5 -> rows 7-11. The engine #storagePanel container sits inside and
   .depot-cell-stash .stash-cards { display: grid; grid-template-columns: repeat(3, 1fr); }
   lays out 3 internal .stash-card slots side-by-side automatically. */
.depot-cell-row7-stash     { grid-column: 11 / span 6; grid-row: 7 / span 5; }
/* Free(17) rowspan=5 -> rows 7-11, was col 8 in REDO4 */
.depot-cell-row7-free-17   { grid-column: 17;          grid-row: 7 / span 5; }

/* =============== ROW 8 placement (Squad CHANGED to row 8) =============== */
/* v0.5.15-REDO8: Squad 5 separate cells (cols 1-2, 3-4, 5-6, 7-8, 9-10) merged into ONE cell at cols 1-10 (span 10).
   rowspan=5 -> rows 8-12. The engine #squadList container sits inside and
   .depot-cell-squad .squad-cards { display: grid; grid-template-columns: repeat(5, 1fr); }
   lays out 5 internal .squad-card slots side-by-side. P5 leftmost, P1 rightmost. */
.depot-cell-row8-squad     { grid-column: 1 / span 10; grid-row: 8 / span 5; }

/* =============== ROW 12 placement (Victories+Engage SHIFTED to col 11, REDO6) =============== */
/* v0.5.15-REDO14: row 12 rebalanced. First Free cell expands from cs 1 (col 11) to cs 3 (cols 11-13);
  Morale and Engage shift right by 2 cols each and lose their rowspan=2 (single-row now);
  the trailing Free(16-17) cell at row 12 is hidden (its slot is now taken by Engage).
  Row 12 cells left-to-right: Free(11-13, cs 3) | Morale(14-15, cs 2) | Engage(16-17, cs 2).
  Note: cols 1-10 of row 12 remain empty per REDO9+ spec.
  v0.5.15-REDO15: Morale and Engage regain rowspan=2 (in addition to their existing colspan=2).
  So Morale: cols 14-15 cs 2 rs 2 (rows 12-13). Engage: cols 16-17 cs 2 rs 2 (rows 12-13).
  Row 13 cols 1-12 hold row13-free-1 + tooltip (cs 11) — col 13 of row 13 is now
  empty Free Space. Morale/Engage occupy cols 14-17 (rs 2 from row 12),
  so they do not overlap with row 13 content (cols 14-17 of row 13 are empty). */
.depot-cell-row12-free-11  { grid-column: 11 / span 3;  grid-row: 12; }
/* v0.5.15-REDO9: Morale now occupies this slot (cols 12-13, rowspan=2 -> rows 12-13); */
/* v0.5.15-REDO13: Morale STAYS at row 12 cols 12-13, rs2 -> rows 12-13 (unchanged). */
/* v0.5.15-REDO14: Morale shifts to cols 14-15 (cs 2), single row (rs 1, no rowspan). */
/* v0.5.15-REDO15: Morale regains rowspan=2 -> rows 12-13. */
.depot-cell-morale        { grid-column: 14 / span 2; grid-row: 12 / span 2; }
/* v0.5.15-REDO13: cell renamed to .depot-cell-wins (was .depot-cell-victories). */
/* REDO13: cell renamed to .depot-cell-wins, placement rule dead. */
/* .depot-cell-victories      { grid-column: 14 / span 2; grid-row: 1; } */
/* v0.5.15-REDO14: Engage shifts from cols 14-15 (rs 2) to cols 16-17 (cs 2), single row. */
/* v0.5.15-REDO15: Engage regains rowspan=2 -> rows 12-13. */
.depot-cell-engage         { grid-column: 16 / span 2;   grid-row: 12 / span 2; }
/* v0.5.15-REDO14: trailing Free(16-17) row-12 cell SLOT now belongs to Engage (above).
   The DOM <div class="depot-cell-row12-free-16"> stays in the HTML so the engine has no
   surprises (game.js never queries it), but it gets hidden via display:none so it
   doesn't auto-flow into the row 1 col 16 gap or anywhere else unsightly. */
.depot-cell-row12-free-16  { grid-column: 16 / span 2;   grid-row: 12; display: none; }

/* =============== ROW 13 placement (Tool Tip CHANGED to row 13) =============== */
/* Free(1) row 13 single */
.depot-cell-row13-free-1   { grid-column: 1;            grid-row: 13; }
/* Tool Tip (cols 2-12, cs 11, no rowspan) — v0.5.15-REDO16. */
/* v0.5.15-REDO14: Tool Tip colspan grows from 10 to 12; now spans cols 2-13. */
/* v0.5.15-REDO16: Tool Tip colspan shrinks from 12 to 11; now spans cols 2-12. Col 13 of row 13 is empty (Free Space). */
.depot-cell-tooltip        { grid-column: 2 / span 11;  grid-row: 13; }

/* ================ ROW 1 cell content ================ */
.depot-cell-player {
  flex-direction: row;
  align-items: center;
  gap: min(0.5cqw, 6.4px);
  background: #f8e9c4;
}
.depot-cell-player .commander-portrait {
  width: min(2.5cqw, 32px); height: min(2.5cqw, 32px); flex-shrink: 0;
  background: #fff; border: min(0.0625cqw, 0.8px) solid #4a372a; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.depot-cell-player .commander-portrait img { width: 100%; height: 100%; object-fit: cover; }
.depot-cell-player .commander-meta { display: flex; flex-direction: column; gap: min(0.0625cqw, 0.8px); min-width: 0; }
.depot-cell-player .eyebrow { font-size: 0.6cqw; letter-spacing: min(0.06cqw, 0.768px); color: #5f4a37; }
.depot-cell-player .commander-name { font-weight: 900; font-size: 0.95cqw; }
.depot-cell-player .commander-score { font-size: 0.75cqw; color: #5f4a37; }

.depot-cell-title {
  align-items: center;
  justify-content: center;
  text-align: center;
  background: #2b1e1c;
  color: #f5e3b3;
  border: 0;
}
.depot-cell-title .eyebrow-banner { font-size: 0.55cqw; letter-spacing: min(0.18cqw, 2.304px); color: #d2c8a8; margin: 0; }
.depot-cell-title .banner-title { font-size: 1.05cqw; font-weight: 900; letter-spacing: min(0.084cqw, 1.0752px); margin: min(0.125cqw, 1.6px) 0; }
.depot-cell-title .banner-tagline { font-size: 0.55cqw; color: #d2c8a8; margin: 0; }

/* v0.5.15: Morale + Gold split into two cells (per spec). Each is its own
  2-col cell with the same label+value treatment as before.
  v0.5.15-REDO50: the ui_morale.png frame MOVED off this cell onto a new
  inner .morale-display-target div (see the .morale-display-target block
  at the bottom of this file). The cell itself is now a plain transparent
  flex centering container with no border-image and no background-image.
  The .morale-block / .morale-label / .morale-value descendant rules
  below keep working because .morale-display-target is a descendant of
  .depot-cell-morale. */
.depot-cell-morale {
  background-color: transparent;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  padding: min(0.25cqw, 3.2px);
}
.depot-cell-morale .morale-block { display: flex; flex-direction: column; align-items: center; }
/* v0.5.15-REDO81: literal CSS typography per user spec.
   font-size in vw (not rem) so values scale with viewport width:
     .morale-label  -> 1vw
     .morale-value  -> 2.5vw */
.depot-cell-morale .morale-label { font-size: 1cqw; letter-spacing: min(0.05cqw, 0.64px); font-weight: 900; color: #5f4a37; }
.depot-cell-morale .morale-value { font-weight: 300; font-size: 2.5cqw; color: #4a372a; }

/* v0.5.15-REDO13: Player Avatar (col 1) — NEW placeholder. Empty cell,
  just a centered avatar frame for now. Uses a single-letter fallback
  (the commander initial — "C" for Cheesebite) so the slot is visible
  even without an image bound. A future build can plug in a real
  commander portrait without changing grid placement. */
.depot-cell-player-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f8e9c4;
  border: min(0.0625cqw, 0.8px) solid #c9a96a;
}
.depot-cell-player-avatar .avatar-block {
  width: 70%;
  aspect-ratio: 1 / 1;
  background: #2b1e1c;
  border: min(0.125cqw, 1.6px) solid #1a0f0e;
  display: flex; align-items: center; justify-content: center;
}
.depot-cell-player-avatar .avatar-fallback {
  color: #f5e3b3;
  font-weight: 900;
  font-size: 1.6cqw;
  letter-spacing: min(0.064cqw, 0.8192px);
}

/* v0.5.15-REDO13: Round (col 14) — RESTORED from v0.5.8. Same
  eyebrow/value layout as the pre-v0.5.8 round counter.
  v0.5.15-REDO83: gap removed. The REDO81-era `gap: 0.125vw` on this
  cell was the source of a .125vw (1600-wide) / .09375vw / .0625vw label->value
  BCR gap between .round-eyebrow and .round-value. Removing it lets
  the natural flex-column stacking collapse that gap to 0 without
  touching the eyebrow's `padding-top: 0.25vw` declaration (which
  lives ABOVE the eyebrow's own line box and doesn't push the value
  down). No font-size, padding, or any other property is changed. */
.depot-cell-round {
  background: #f8e9c4;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  text-align: center;
}
/* v0.5.15-REDO81: literal CSS typography per user spec.
     .round-eyebrow -> font-size 1vw + padding-top 0.25vw
     .round-value   -> font-size 2vw */
/* v0.5.15-REDO93: exact authored font-size; neighboring declarations preserved. */
.round-eyebrow {
  /* v0.5.15-REDO54: color #5f4a37 -> #f5e3b3 per the user's spec.
     v0.5.15-REDO81: literal padding-top 0.25vw preserved. */
  font-size: 0.8cqw; letter-spacing: min(0.18cqw, 2.304px); color: #f5e3b3;
  font-weight: 400;
  padding-top: min(0.25cqw, 3.2px);
}
.depot-cell-round .round-value {
  font-size: 2cqw; font-weight: 400; color: #f5e3b3;
}

/* v0.5.15-REDO13: Wins (cols 15-16) — RENAMED from Victories. Same
  backdrop/border treatment as .depot-cell-victories had; class names
  swapped from "victories-eyebrow" / "victories-value" to "wins-eyebrow"
  / "wins-value" to match the cell class. X/10 format produced by
  game.js (writer at game.js renderResources).
  v0.5.15-REDO83: gap removed (sister change to .depot-cell-round above).
  Closes the .125vw / .09375vw / .0625vw label->value BCR gap from .wins-eyebrow to
  .wins-value by removing the explicit flex `gap: 0.125vw` on this cell.
  No font-size or padding change. */
.depot-cell-wins {
  background: #f8e9c4;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  text-align: center;
}
/* v0.5.15-REDO95: exact authored Wins top spacing; REDO93 font-size preserved. */
.wins-eyebrow {
  /* v0.5.15-REDO54 color remains unchanged. */
  font-size: 0.8cqw; letter-spacing: min(0.18cqw, 2.304px); color: #f5e3b3;
  padding-top: min(0.25cqw, 3.2px);
  font-weight: 400;
}
/* v0.5.15-REDO81: literal CSS typography per user spec.
     .wins-value -> font-size 2vw */
.depot-cell-wins .wins-value {
  font-size: 2cqw; font-weight: 400; color: #f5e3b3;
}

/* v0.5.15-REDO13: Coins (col 17) — SLIMMED from cs 2 (cols 16-17) to cs 1 (col 17).
  Class names swapped from "depot-cell-gold" to "depot-cell-coins".
  Bg + coin-block layout same as .depot-cell-gold had, minus the now-removed
  wins-block sub-element. */
.depot-cell-coins {
  background: #f8e9c4;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  text-align: center;
  gap: min(0.125cqw, 1.6px);
}
/* v0.5.15-REDO81: literal CSS typography per user spec.
     .coin-label-top -> font-size 1vw + padding-top 0.25vw
     .coin-row       -> font-size 2vw */
.depot-cell-coins .coin-block { display: flex; flex-direction: column; align-items: center; }
/* v0.5.15-REDO93: exact authored font-size; neighboring declarations preserved. */
/* v0.5.15-REDO94: exact authored padding-top:0.25vw added; font-size preserved. */
.coin-label-top {
  font-size: 0.8cqw; letter-spacing: min(0.12cqw, 1.536px); color: #f5e3b3;
  padding-top: min(0.25cqw, 3.2px);
}
.depot-cell-coins .coin-row {
  font-weight: 400; font-size: 2cqw; color: #f5e3b3;
  display: flex; align-items: center; gap: min(0.25cqw, 3.2px);
}
.depot-cell-coins .coin-icon { font-size: 0.9cqw; }

/* v0.5.15-REDO13: cell renamed to .depot-cell-coins (was .depot-cell-gold). */
/* REDO13: cell renamed to .depot-cell-coins, below rules dead. */
/*
.depot-cell-gold {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: .5vw;
  background: #f8e9c4;
}
.depot-cell-gold .coin-block { display: flex; flex-direction: column; align-items: center; }
.depot-cell-gold .coin-label-top { font-size: 0.55rem; letter-spacing: 0.12em; color: #5f4a37; }
.depot-cell-gold .coin-row { font-weight: 900; font-size: 1.1rem; display: flex; align-items: center; gap: .25vw; }
.depot-cell-gold .coin-icon { font-size: 0.9rem; }
*/
/* ============================================================================
   v0.5.15-REDO84 — exact margin-top: -0.5vw on .round-value / .wins-value /
   .morale-value / .coin-row + CSS-wide rem/em→vw conversion audit.
   Build marker: redo84-vw-css-audit. See tests/candidate_inventory/conversion_plan.md.
   ============================================================================ */

/* v0.5.15-REDO11: WINS counter has moved OUT of the Gold cell. The Gold cell
  now shows only coins + the gold number. The .depot-cell-gold .wins-*
  rules below are kept as dead CSS in case any stale DOM references them
  (the wins-block element itself no longer exists inside the Gold cell).
  v0.5.15-REDO13: cell renamed to .depot-cell-coins; wins-block removed under
  REDO11 / REDO12 already, and the .depot-cell-coins rules above replace these.
  The .depot-cell-gold .wins-* rules are dead. */
/* REDO13: cell renamed, wins-block removed, below rules dead. */
/*
.depot-cell-gold .wins-block { display: flex; flex-direction: column; align-items: center; }
.depot-cell-gold .wins-label { font-size: 0.6rem; letter-spacing: 0.12em; color: #5f4a37; }
.depot-cell-gold .wins-value { font-weight: 900; font-size: 1.1rem; color: #4a372a; }
*/

/* ================ Free Space cells (decorative) ================ */
.depot-cell-freespace-thin {
  background: #f0e2bd;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-weight: 900;
  color: #5f4a37;
  padding: min(0.125cqw, 1.6px);
}
.depot-cell-freespace-thin .fs-label { font-size: 0.55cqw; letter-spacing: min(0.066cqw, 0.8448px); }
.depot-cell-freespace-thin .fs-sub { font-size: 0.45cqw; letter-spacing: min(0.081cqw, 1.0368px); }

/* v0.5.15-REDO9 — Free Space cells become invisible (still occupy grid cells).
   Override placed after the base rule; same selector, loaded last so it wins
   against the .depot-cell-freespace-thin block above. data-zone="free" drop
   targets are unaffected — the engine doesn't read visual state. */
.depot-cell-freespace-thin { background: transparent; border: none; color: transparent; box-shadow: none; }
.depot-cell-freespace-thin .fs-label,
.depot-cell-freespace-thin .fs-sub { color: transparent; }

.depot-cell-freespace-left {
  background: #f0e2bd;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-weight: 900;
  color: #5f4a37;
}
.depot-cell-freespace-left .fs-label { font-size: 0.85cqw; letter-spacing: min(0.12cqw, 1.536px); }
.depot-cell-freespace-left .fs-sub { font-size: 0.7cqw; letter-spacing: min(0.18cqw, 2.304px); }

.depot-cell-freespace-wide {
  background: #f0e2bd;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-weight: 900;
  color: #5f4a37;
}
.depot-cell-freespace-wide .fs-label { font-size: 0.7cqw; letter-spacing: min(0.18cqw, 2.304px); }

/* ================ Row 6: sell ================
   v0.5.15-REDO42 CHANGE 3: the ui_sell.png frame MOVED off this cell onto
   a new inner .sell-frame div (see the .sell-frame block at the bottom of
   this file). The cell itself is now a plain transparent flex centering
   container with no border-image, no flat border (CHANGE 1 stripped the
   pre-REDO21 .125vw dashed stroke), and no background. The .is-drop /
   .is-bad-drop drag-over highlight rules below moved onto .sell-frame. */
.depot-cell-sell {
  background-color: transparent;
  align-items: center;
  justify-content: center;
  text-align: center;
  cursor: pointer;
  /* CHANGE 1: flat .125vw dashed #c25a30 stroke REMOVED — was pre-REDO21 chrome
     that lived on the cell. The ui_sell.png frame now lives on .sell-frame. */
  border: 0;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}
/* Text rules (icon/label/hint) descend into .sell-frame now via the same
   descendant selector they used before — no rule changes needed here. */
/* v0.5.15-REDO81: literal CSS typography per user spec.
   .sell-label (SELL) -> 1.5vw
   .sell-hint  (hint) -> 0.75vw */
.depot-cell-sell .sell-icon { font-size: 1.4cqw; }
.depot-cell-sell .sell-label {
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 900; font-size: 1.5cqw; letter-spacing: min(0.18cqw, 2.304px); color: #c25a30;
}
.depot-cell-sell .sell-hint { font-size: 0.75cqw; letter-spacing: min(0.09cqw, 1.152px); color: #5f4a37; }

/* ================ Right rail: freeze + reroll ================ */
.depot-cell-freeze {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO20
     cover background-image (ui_freeze.png) shows through unblocked. The
     freeze-empty / filled / is-drop state rules above still apply their
     own backgrounds via shorthand `background:` only during the relevant
     state, which is the intended UX.
     v0.5.15-REDO42: flat .125vw solid #5dbed6 cyan border REMOVED — the REDO20
     ui_freeze.png border-image (slice 64, ladder 8/12/20 px) draws the frame.
     No flat stroke on the freeze cell. */
  background-color: transparent;
  align-items: center;
  justify-content: center;
  text-align: center;
  cursor: pointer;
  border: 0;
  flex-direction: column;
  display: flex;
  position: relative;
  padding: min(0.25cqw, 3.2px);
  overflow: visible;
}
.depot-cell-freeze img.rail-action-icon { width: min(1.75cqw, 22.4px); height: min(1.75cqw, 22.4px); }
/* v0.5.15-REDO53: Style + position .rail-action-cost the same as
   .card-cost-tag (shop-card rule above). The user's spec: copy the
   existing .card-cost-tag CSS verbatim. The shop-card rule (around
   line 1090) is the canonical .card-cost-tag definition — a small
   absolute-positioned yellow-pill cost tag in the top-right corner
   of the card. We apply the same CSS to all .rail-action-cost spans
   inside the depot (freeze cell + reroll cell). */
.depot-cell-freeze .rail-action-cost {
  position: absolute;
  top: min(0.125cqw, 1.6px);
  right: min(0.125cqw, 1.6px);
  background: #f4c35c;
  color: #4a372a;
  padding: 0 min(0.25cqw, 3.2px);
  border-radius: min(0.125cqw, 1.6px);
  font-size: 0.55cqw;
  font-weight: 900;
}
/* v0.5.15-REDO54: split .depot-cell-reroll .rail-action-cost out of the
   combined .depot-cell-freeze .rail-action-cost rule above (which the user
   re-confirmed should keep its absolute-pill top-right treatment). The
   reroll cost badge ("2g") uses a DIFFERENT positioning scheme per the
   user's spec: position: relative with negative vw offsets so it floats
   slightly OUTSIDE its parent button (top-right corner, half-overlapping
   the cell's ui_reroll.png frame). The badge keeps the same visual
   treatment as the freeze pill (yellow background, dark text) so the two
   rail cells still look like a matched pair — only the positioning
   differs. */
.depot-cell-reroll .rail-action-cost {
  position: relative;
  /* v0.5.15-REDO81: removed the `top` declaration per user spec. The
     badge stays anchored via position:relative + right:offset without
     any top offset; do not re-add `top: auto` or any replacement value. */
  right: max(-2cqw, -25.6px);
  background: #f4c35c;
  color: #4a372a;
  padding: 0 min(0.25cqw, 3.2px);
  border-radius: min(0.125cqw, 1.6px);
  font-size: 0.75cqw;
  font-weight: 900;
}
.depot-cell-freeze .rail-action-label {
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 900; font-size: 0.85cqw; letter-spacing: min(0.12cqw, 1.536px); color: #2d7995;
}
.depot-cell-freeze .rail-action-sub { font-size: 0.5cqw; color: #2d7995; }
.depot-cell-freeze.is-drop { background: rgba(93,190,214,0.4); }

/* v0.5.14 freeze-slot is a single card display, not a toggle. */
.depot-cell-freeze.freeze-empty {
  flex-direction: column;
  gap: min(0.25cqw, 3.2px);
  color: #2d7995;
}
/* v0.5.15-REDO81: literal CSS typography per user spec.
   The freeze cell shows .freeze-empty-label + .freeze-empty-sub when
   no card is frozen (this is the FREEZE label / hint). font-size
   set to literal 1.5vw (label) and 0.75vw (hint) per user spec. */
.depot-cell-freeze.freeze-empty .freeze-empty-label {
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 900;
  font-size: 1.5cqw;
  letter-spacing: min(0.27cqw, 3.456px);
}
.depot-cell-freeze.freeze-empty .freeze-empty-sub {
  font-size: 0.75cqw;
  letter-spacing: min(0.03cqw, 0.384px);
  opacity: 0.85;
}
.depot-cell-freeze.filled {
  padding: min(0.1875cqw, 2.4px);
  /* v0.5.15-REDO23: background-color removed (transparent) — the REDO20 cover
     background-image (ui_freeze.png) now shows through unblocked.
     v0.5.15-REDO42: flat .0625vw solid #4a372a + .1875vw border-radius REMOVED — the
     outer .depot-cell-freeze border-image (ui_freeze.png) and the inner
     .freeze-card ui_card.png border-image both draw their frames; no flat
     stroke on the filled state. */
  background-color: transparent;
  border-radius: 0;
  cursor: grab;
}
.depot-cell-freeze .freeze-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: transparent;
  border: 0;
  width: 100%;
  height: 100%;
  cursor: grab;
  user-select: none;
  position: relative;
  min-height: 0;
  overflow: visible;
  /* v0.6.119: thin padding reduced to 0 — the freeze-cell frame
     provides the visual border. The image and labels fill the card
     edge-to-edge so the portrait stays visible. */
  padding: 0;
}
/* v0.5.15-REDO26: removed the tiny 32x32 portrait from freeze card.
   The gerbil image now fills the freeze card (may overlap edges per user). */
.depot-cell-freeze .freeze-card .card-portrait {
  flex: 1 1 0;
  width: 100%; height: auto; min-height: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  margin: 0 0 min(0.125cqw, 1.6px) 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: visible;
  /* v0.6.026 delta C: mirror the .sc-portrait / .card-portrait
     wrapper transition so the freeze-card portrait also animates
     the translateY(0.75vw) hover shift instead of snapping. */
  transition: transform 420ms ease-out !important;
}
/* v0.6.114: the freeze rail has a smaller inner content box than one gerbil
   shop card. Expand only the frozen image's layout box to the measured shop
   portrait box at 1280x720 (116.14 x 123.86 px), then let the shared
   data-class transform rules below preserve each class's authored scale and
   translation. Do not override transform here: doing so flattens every frozen
   class to scale(1.5) and breaks same-class shop/freeze size parity. */
.depot-cell-freeze /* v0.6.119: tighten freeze-card image sizing. Previously width: 111.8%
   height: 140.5% pushed the image beyond its parent .card-portrait
   wrapper (rendered 185.8x198.1 vs freeze-card 107x118 at 1280x720),
   overflowing the freeze cell and pushing name/sub/cost labels out
   of view. The image now fills its wrapper via 100%/100% + object-fit:
   contain so the per-class transforms (translate/scale) still apply
   correctly while staying inside the cell. */
.freeze-card .card-portrait img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform-origin: center center;
  overflow: visible;
}
.depot-cell-freeze /* v0.6.118: outline + cyan halo removed. The v0.6.118 freeze effect
   supplies its own contour-following drop-shadow chain on the img
   filter and the silhouette-clipped frost overlay. */
.freeze-card .card-portrait img,
.depot-cell-freeze .freeze-card .card-portrait.silhouette,
.depot-cell-freeze .freeze-card img.card-portrait.item-portrait {
  overflow: visible;
}
.depot-cell-freeze .freeze-card.freeze-card-item {
  /* v0.6.119: width/height reduced to 100% — the previous 111.81%/174.16%
     pushed the item image beyond the freeze cell. The freeze-cell
     border-image provides the visual frame; the item card fills the
     cell edge-to-edge. */
  width: 100%;
  height: 100%;
  flex-shrink: 0;
}
.depot-cell-freeze /* v0.6.119: item portrait matches the shop item portrait width (75%).
   Height auto + flex: 1 1 0 lets the labels take the remaining space. */
.freeze-card img.card-portrait.item-portrait {
  width: 75%;
  height: auto;
  flex: 1 1 0;
  object-fit: contain;
}
.depot-cell-freeze .freeze-card .card-name {
  font-weight: 800; font-size: 0.7cqw; color: #4a372a; text-align: center;
  line-height: 1.0; max-width: 100%;
}
.depot-cell-freeze .freeze-card .card-sub {
  font-size: 0.55cqw; color: #5f4a37; text-align: center; line-height: 1.0;
}
.depot-cell-freeze .freeze-card .card-desc {
  font-size: 0.5cqw; color: #5f4a37; text-align: center; line-height: 1.0;
  margin-top: min(0.0625cqw, 0.8px);
}
.depot-cell-freeze .freeze-card .card-hp {
  font-size: 0.55cqw; color: #4a372a; margin-top: min(0.0625cqw, 0.8px);
}
.depot-cell-freeze .freeze-card .card-cost-tag {
  position: absolute;
  top: min(0.0625cqw, 0.8px); right: min(0.125cqw, 1.6px);
  font-size: 0.55cqw; font-weight: 800; color: #c25a30;
}

.depot-cell-reroll {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO19
     cover background-image (ui_reroll.png) shows through unblocked. The inner
     .rail-reroll button keeps its own background-color: #f8ffe0 below because
     it's a separate visual element (the clickable label inside the frame). */
  background-color: transparent;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: min(0.25cqw, 3.2px);
}
.depot-cell-reroll .rail-reroll {
  display: flex; flex-direction: column; align-items: center; gap: min(0.0625cqw, 0.8px);
  /* v0.5.15-REDO42 CHANGE 2: parent .depot-cell-reroll already wears the
     ui_reroll.png border-image with `fill` keyword, so the inner frame
     texture is the button's background. Strip the flat #f8ffe0 fill and
     the .125vw solid #8da332 stroke so the cell's image-border fill shows
     through uncluttered; the button label/icon/color stays readable on
     top of the parent frame's fill. */
  background: transparent;
  border: 0;
  padding: min(0.25cqw, 3.2px); cursor: pointer; width: 100%;
}
/* v0.5.15-REDO54: REMOVED the <span class="rail-reroll-icon"> from index.html
   (per user spec — the ↻ glyph above the REROLL label is no longer wanted),
   AND removed the .rail-reroll-icon CSS rule that became orphan as a result.
   The .rail-reroll button now contains only: .rail-action-cost (the "2g"
   pill, repositioned via item 3), .rail-action-label ("REROLL"), and
   .rail-action-hint ("GEAR & GERBILs"). */
/* v0.5.15-REDO81: literal CSS typography per user spec.
   .rail-action-label (REROLL) -> 1.5vw
   .rail-action-hint (hint)    -> 0.75vw */
.depot-cell-reroll .rail-action-label {
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 900; font-size: 1.5cqw; letter-spacing: min(0.18cqw, 2.304px); color: #5d7511;
}
.depot-cell-reroll .rail-action-hint { font-size: 0.75cqw; color: #5d7511; }

/* ================ v0.5.15-REDO19 ===============
   Reroll rail cell wears ui_reroll.png as border-image + cover background.
   Block appended AFTER the base .depot-cell-reroll rules above so the
   longhand `border-style` + `border-width` + `border-image` +
   `background-image` survive the earlier `background: #f8ffe0;` shorthand
   by source-order. The three @media queries mirror the scene-area size
   breakpoints used by the four REDO18 card selectors but at a LARGER
   ladder (8 / 12 / 20 px) per the user spec for the reroll frame.

   The base block sets the default (<73.75vw, 800x360 scene) values.
   The two @media blocks re-state BOTH border-width AND the
   border-image's `/ Xpx` slice width so the slice pixels track the
   frame thickness (otherwise the frame would keep slicing at 8 px
   on the wider 12 / 20 px borders). The user's frame asset
   (assets/ui_reroll.png), the slice value (64), `border-style: solid`,
   and the `background-image: cover` longhand all stay byte-identical
   at every breakpoint — only `border-width` and the matching
   `border-image`'s `/ Xpx` slice width scale. */

.depot-cell-reroll {
  /* v0.5.15-REDO24: background-image (and its background-size/position/repeat)
    removed. The border-image above uses the `fill` keyword, so the middle
    of ui_reroll.png is already drawn as the background — the redundant
    cover-image layer is no longer needed. The parent .depot-cell-reroll
    rule above already sets background-color: transparent, so no solid color
    shows through.
    v0.5.15-REDO53: border-width 1.25vw -> 1.5vw; border-image's slice width
    1.25vw -> 1.5vw; slice value 64 -> 60 per the new UI spec.
    v0.5.15-REDO54: border-width 1.5vw -> 1vw; border-image's slice width
    1.5vw -> 1vw (slice width and border-width must match so the frame
    actually renders at the new thickness). Slice value stays 60. */
  border-style: solid;
  border-width: min(1cqw, 12.8px);
  border-image: url("assets/ui_reroll.png") 60 fill / min(1.75cqw, 22.4px) / 0 stretch;
}

/* ================ v0.5.15-REDO20 ===============
   Freeze rail cell wears ui_freeze.png as border-image + cover background.
   Block appended AFTER the base .depot-cell-freeze rules above (and after
   the REDO17/18 `.depot-cell-freeze .freeze-card` inner-card block further
   down) so the longhand `border-style` + `border-width` + `border-image`
   + `background-image` survive the earlier `background: #eafaff` shorthand
   (and any other shorthand on the freeze cell) by source-order. The
   three @media queries mirror the scene-area size breakpoints used by the
   REDO19 reroll cell AND by the REDO18 four card selectors but at the
   SAME ladder as the REDO19 reroll cell (8 / 12 / 20 px) per the user
   spec for the freeze frame.

   The base block sets the default (<73.75vw, 800x360 scene) values. The
   two @media blocks re-state BOTH border-width AND the border-image's
   `/ Xpx` slice width so the slice pixels track the frame thickness.
   The user's frame asset (assets/ui_freeze.png), the slice value (64),
   `border-style: solid`, the cover `background-image` longhand, and
   `background-color: transparent` (so the underlying .depot-cell-freeze
   `background-color` declared by `.freeze-empty` / `.filled` /
   `.is-drop` SHORT-OR-LONGHAND rules above still wins via
   specificity OR source-order — `.filled` uses longhand
   `background-color:` and so plays nicely here, `.is-drop` shorthand
   `background:` overrides our image but that's the drag-over highlight
   we want to win during a drop, exactly the same pattern as REDO19)
   all stay byte-identical at every breakpoint — only `border-width`
   and the matching `border-image`'s `/ Xpx` slice width scale. */

.depot-cell-freeze {
  /* v0.5.15-REDO24: background-image (and its background-size/position/repeat)
    removed. The border-image above uses the `fill` keyword, so the middle
    of ui_freeze.png is already drawn as the background. background-color
    stays transparent (REDO23) so no solid color shows through.
    v0.5.15-REDO53: border-width 1.25vw -> 1.5vw; border-image's slice width
    1.25vw -> 1.5vw; slice value still 64 (only the slice width changed per the
    UI spec). */
  border-style: solid;
  border-width: min(1.5cqw, 19.2px);
  border-image: url("assets/ui_freeze.png") 64 fill / min(1.5cqw, 19.2px) / 0 stretch;
  background-color: transparent;
}

/* ================ v0.5.15-REDO21 + REDO49 ===============
   Sell cell wears ui_sell.png as border-image + cover background.

   v0.5.15-REDO49: the ui_sell.png frame has MOVED off the cell itself
   onto a new inner .sell-frame div (90% height, 75% width, centered)
   that wraps the cell's icon / label / hint text. The cell
   (.depot-cell-sell) now has NO border-image — it is a plain transparent
   flex centering container. The .sell-frame div wears the ui_sell.png
   border-image (slice 48, border-width 1vw) plus a cover background-
   image, and inherits all the sell drop / drag / hover functionality
   (the data-drop="sell" / data-zone="sell" attributes were already on
   .sell-frame in the HTML; the engine wires its drag/drop listeners
   to those data-* selectors so they apply to the inner div
   automatically).

   The earlier REDO21 / REDO24 / REDO42 8/10/16 px border-width ladder
   (with matching border-image / Xpx slice) is gone — the user fixed
   the inner frame at 1vw border-width with slice 48, mirroring the
   REDO48 default. Other layout UNCHANGED. */

/* v0.5.15-REDO49: ui_sell.png border-image REMOVED from .depot-cell-sell.
   The cell is now a plain transparent flex centering container with no
   border-image, no flat border, and no background. The ui_sell.png frame
   now lives on the inner .sell-frame div (see the .sell-frame block at
   the END of this file for the new border-image / 90% height / 75% width
   / centered sizing). */
.depot-cell-sell { background-color: transparent; }

.depot-cell-gerbil-shop { background-color: transparent; padding: min(0.25cqw, 3.2px); }
.depot-cell-item-shop   { background-color: transparent; padding: min(0.25cqw, 3.2px); }
.depot-cell-gerbil-shop .shop-band-head,
.depot-cell-item-shop .shop-band-head {
  display: flex; align-items: baseline; justify-content: space-between;
  border-bottom: min(0.0625cqw, 0.8px) solid #c8a76d; padding-bottom: min(0.125cqw, 1.6px); margin-bottom: min(0.25cqw, 3.2px);
}
/* v0.5.15-REDO93: exact authored font-size; neighboring declarations preserved. */
.shop-band-head h3 {
  font-size: 1cqw; letter-spacing: min(0.102cqw, 1.3056px); font-weight: 900; color: #4a372a; margin: 0;
}
.depot-cell-gerbil-shop .shop-band-cost,
.depot-cell-item-shop .shop-band-cost { font-size: 0.65cqw; color: #5f4a37; }
.depot-cell-gerbil-shop .shop-band-foot,
.depot-cell-item-shop .shop-band-foot {
  font-size: 0.55cqw; color: #5f4a37; text-align: center; margin-top: min(0.25cqw, 3.2px);
}
.depot-cell-gerbil-shop .shop-cards,
.depot-cell-item-shop .shop-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: min(0.25cqw, 3.2px);
  flex: 1 1 auto;
  min-height: 0;
}

/* ================ Shop cards (inside the shop cells) ================ */
.shop-card {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO18
     cover background-image (ui_card.png) shows through unblocked. The empty,
     silhouette, and drag-highlight rules below still apply their own
     backgrounds via shorthand `background:` only in the relevant state.
     v0.5.15-REDO26: overflow changed hidden->visible so the gerbil PNG
     may overlap the card edges per the user's instruction.
     v0.5.15-REDO42: flat .0625vw solid #4a372a + .1875vw radius REMOVED — the
     REDO18 ui_card.png border-image (border-style: solid + border-width
     ladder 6/8/12 px at <1180/1180-1579/>=1580) draws the frame; no flat
     stroke needed. */
  background-color: transparent;
  border: 0;
  border-radius: 0;
  padding: min(0.25cqw, 3.2px);
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: grab;
  user-select: none;
  position: relative;
  min-height: 0;
  overflow: visible;
}
.shop-card:hover:not(.empty) { transform: translateY(max(-0.0625cqw, -0.8px)); box-shadow: 0 min(0.1875cqw, 2.4px) 0 rgba(33,26,23,0.18); }
.shop-card.dragging { opacity: 0.4; }
/* v0.6.112: light-blue glow + transparent background + FROZEN label.
   v0.6.113-correction1: real production freeze drop renders
   `.depot-cell-freeze .freeze-card` (see renderFreezeSlot in game.js),
   not `.shop-card.frozen`. The original v0.6.112/v0.6.113 selectors
   never matched the production DOM, so the inner ui_card.png frame
   still drew on the frozen card and the cyan glow / FROZEN label
   never rendered. The contract below now targets the actual
   production element and uses the higher-specificity child-combinator
   selector `.depot-cell-freeze.filled > .freeze-card` so it wins
   against the four-card border-image rule further down regardless
   of source order. The descendant selector
   `.depot-cell-freeze .freeze-card` is kept as a defensive fallback
   for any path that may render a freeze-card div outside the
   `.filled` host. */
.depot-cell-freeze.filled > .freeze-card,
.depot-cell-freeze .freeze-card {
  outline: none;
  box-shadow: none;
  background-color: transparent;
  /* v0.6.113 NEW: strip the inner wooden frame so only the freeze
     cell's ui_freeze.png frame shows through. The child-combinator
     variant carries specificity (0,3,0) so it overrides the
     four-card border-image rule (`border-style: solid;
     border-width: 9.6px; border-image: ui_card.png`) further down. */
  border-style: none;
  border-width: 0;
  border-color: transparent;
  border-image: none;
}
.depot-cell-freeze .freeze-card::after { content: 'FROZEN'; position: absolute; top: min(0.125cqw, 1.6px); right: min(0.125cqw, 1.6px); font-size: 0.5cqw; color: #2d7995; font-weight: 900; }
.shop-card.empty { background-color: transparent; color: #5f4a37; }
/* v0.5.15-REDO26: removed the tiny 32x32 circular portrait.
   The gerbil image now fills the card (may overlap edges per user). */
.shop-card .card-portrait,
.shop-card .card-portrait.silhouette {
  flex: 1 1 0;
  width: 100%; height: auto; min-height: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  margin: 0 0 min(0.1875cqw, 2.4px) 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: visible; /* gerbil PNG may overlap the card edges */
  /* v0.6.130: shop gerbil portrait pointer-events. Match the squad
     pattern: the squad uses `.sc-portrait:hover img` for the green
     glow and the sc-portrait has `pointer-events: none` so the cursor
     passes through to the card. The shop gerbils use the same scaled
     overflowing portrait PNG structure (img transform: scale(1.5))
     but only ~3px of gap between siblings — the cursor over the gap
     flips between adjacent cards' overflowing portrait IMGs every
     pixel, retriggering the hover-glow + card lift on both cards.
     Mirror the squad pattern: `pointer-events: none` on the wrapper
     so the cursor passes through the overflow area; `pointer-events:
     auto` on the img so the glow rule's `:hover img` selector still
     fires when the cursor is over the img. `clip-path: inset(0)` on
     the img keeps its visual extent inside its un-scaled layout box
     so the cursor only catches the img where it's actually drawn. */
  pointer-events: none;
  /* v0.6.026 delta C: animate the wrapper transform.
     v0.6.025 declared `transition: transform 420ms ease-out !important`
     on `.sc-portrait` (the squad wrapper) but NOT on `.card-portrait`
     (the shop + freeze wrapper). On shop + freeze cards the wrapper
     transform snapped from 0 to translateY(3vw) on hover and back to 0
     on leave, with no interpolation — the user-visible "snap" the
     bug report identifies. Adding the same transition here makes
     .card-portrait animate in lockstep with .sc-portrait and with
     .gerbil-stat-grid (which already had the transition). !important
     keeps the cascade order so the downstream .shop-card .card-portrait
     rule (and any later per-class portrait rules) cannot wipe it. */
  transition: transform 420ms ease-out !important;
}
/* v0.6.130: keep the IMGs pointer-events: none too (inherited from the
   wrapper). The cursor over an overflowing IMG passes through and hits
   only the `.shop-card` itself — eliminating the cross-card hover race
   that caused the flicker. The green-glow filter now rides on
   `.shop-card:hover .card-portrait img` (line 3417 below), which fires
   via the card's :hover cascade regardless of whether the IMGs is
   hit-tested. */
.shop-card .card-portrait img { pointer-events: none; }
/* v0.5.15-REDO27: scale gerbil image to 150% of card width so it overlaps the
   card edges. transform: scale() is layout-neutral — the layout box stays the
   same size, so the card's width/height are unchanged. Combined with
   overflow:visible on .card-portrait and the parent .shop-card, the image
   visually extends 25% past the card edge on every side. */
.shop-card .card-portrait img { width: 100%; height: 70%; object-fit: contain; transform: scale(1.5); transform-origin: center center; }
.shop-card .card-name { font-weight: 900; font-size: 0.7cqw; line-height: 1.05; text-align: center; }
.shop-card .card-sub { font-size: 0.55cqw; color: #5f4a37; text-align: center; }
.shop-card .card-hp { font-size: 0.55cqw; color: #4a372a; }
.shop-card .card-desc { font-size: 0.5cqw; color: #5f4a37; line-height: 1.1; text-align: center; overflow: hidden; }
.shop-card .card-cost-tag {
  position: absolute; top: min(0.125cqw, 1.6px); right: min(0.125cqw, 1.6px);
  background: #f4c35c; color: #4a372a; padding: 0 min(0.25cqw, 3.2px);
  border-radius: min(0.125cqw, 1.6px); font-size: 0.55cqw; font-weight: 900;
}
.sc-portrait {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
  filter: none !important;
  box-shadow: none !important;
  /* v0.6.025 delta: hover a gerbil card (shop / squad / freeze) and the
     4x2 stat grid slowly moves up by half its height while the
     gerbil portrait slowly moves down by the same distance, so the
     two halves of the card reveal a soft empty band at the bottom
     of the grid. The transform composes on top of the per-class
     class-specific translate/scale set by REDO31 / REDO92 rules.
     The transition is !important because the cascade has a
     downstream `.sc-portrait` rule without transition that
     otherwise wins. */
  transition: transform 420ms ease-out !important;
}

/* The grid sits at the top of the card. We raise it by ~3vw (half the
   height of a 2-row stat grid where each cell is ~3vw at 1600x720),
   so the bottom row slides past the gerbil's chin / slot row. The
   transition is slow enough to read as a deliberate "lift" not a
   bounce. */
.gerbil-stat-grid {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-rows: repeat(2, auto);
  gap: min(0.08cqw, 1.024px);
  z-index: 2;
  pointer-events: none;
  direction: ltr;
  transition: transform 420ms ease-out !important;
  transform: translateY(0);
}

/* v0.6.025 delta: on hover of any populated gerbil card (shop / squad /
   freeze), the 4x2 stat grid translates up by half its height while
   the gerbil portrait translates down by the same distance. Both
   transitions are the same 420ms ease-out declared on the base rules
   above. The two halves of the card reveal a soft empty band at the
   bottom of the grid; the gerbil is "breathing" out of the way of the
   stat readout. The grid rule targets the .gerbil-stat-grid directly
   (the only existing transform on the grid is translateY(0) baseline).
   The portrait rule targets the .sc-portrait WRAPPER, NOT the inner
   <img>, so the per-class .sc-portrait[data-class="X"] img
   transform: translate(X%, Y%) scale(N) is preserved unchanged. The
   wrapper-level translate composes additively with the inner img
   transform via the transform stack. */
/* v0.6.026 delta (Deltas A + B): depot hover distance tuned.
   - Grid: -3vw -> -1.5vw (halved per user request "move the stats only
     half as far").
   - Portrait: +3vw -> +0.75vw (quartered per user request "the gerbils
     only a quarter as far").
   Frozen per-class portrait transforms (Scout: translate(-9%, 13%)
   scale(1.3), etc.) on the INNER <img> are unchanged; the wrapper-
   level translate composes additively with the inner img transform via
   the transform stack. */
.shop-card:hover:not(.empty) .gerbil-stat-grid,
.squad-card:hover:not(.empty) .gerbil-stat-grid,
.freeze-card:hover:not(.empty) .gerbil-stat-grid {
  /* v0.6.111: dropped `!important` from transform. The
     `:hover:not(.empty) .gerbil-stat-grid` selector is uniquely
     scoped to this state — no other rule targets this element
     with a different transform, so cascade order alone decides. */
  transform: translateY(max(-1.5cqw, -19.2px));
}
.shop-card:hover:not(.empty) .sc-portrait,
.squad-card:hover:not(.empty) .sc-portrait,
.freeze-card:hover:not(.empty) .sc-portrait,
.shop-card:hover:not(.empty) .card-portrait,
.squad-card:hover:not(.empty) .card-portrait,
.freeze-card:hover:not(.empty) .card-portrait {
  /* v0.6.111: dropped `!important` from transform — same reasoning
     as the gerbil-stat-grid rule above. */
  transform: translateY(min(0.75cqw, 9.6px));
}
/* v0.6.027 delta (Deltas 1 + 2): item slots anchored to bottom
   (delta 1) + hover-animated (delta 2).

   Delta 1 — anchor to bottom: applied via the
   .squad-card */
/* v0.7.003: rank-stars overlay. Positioned absolutely at the top edge of
   the gerbil card so it overlays the stat grid without pushing the card
   layout around. z-index sits above .gerbil-stat-grid (z-index:2) so the
   stars read on top. Brass color when filled, dimmed when outlined.
   .sc-rank-up (added by the depot rank-up preview) shifts to green. */
.sc-rank-stars {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -60%);
  display: flex;
  gap: min(0.24cqw, 3.07px);
  z-index: 4;
  pointer-events: none;
  line-height: 1;
  font-size: min(2.4cqw, 30.72px);
  filter: drop-shadow(0 min(0.12cqw, 1.5px) min(0.24cqw, 3.07px) rgba(0, 0, 0, 0.6));
}
.sc-rank-stars .sc-rank-star {
  display: inline-block;
  transition: color 180ms ease-out, transform 220ms ease-out;
}
.v060-rank-crown, .sc-rank-crown {
  font-size: min(2.4cqw, 30.72px);
  color: #ffd700;
  text-shadow: 0 min(0.12cqw, 1.5px) min(0.24cqw, 3.07px) rgba(0, 0, 0, 0.6);
  line-height: 1;
  display: inline-block;
  filter: drop-shadow(0 0 1px rgba(255, 215, 0, 0.4));
  pointer-events: none;
}
.sc-rank-stars .sc-rank-star.is-filled { color: #f4c35c; text-shadow: 0 0 min(0.2cqw, 2.5px) rgba(244, 195, 92, 0.55); }
.sc-rank-stars .sc-rank-star.is-outline { color: rgba(244, 195, 92, 0.28); }

/* v0.7.030d — depot rank-up progression. The "next rank" star (the one
   immediately after the filled stars) renders as TWO stacked spans:
   a brass outline (\u2606) that sets the parent's natural size, and
   a brass-fill (\u2605) absolutely positioned on top of the outline,
   clipped to data-rank-progress-fill percent. Reads as "1/3", "2/3",
   "1/2", etc. when at higher ranks where multiple rank-up materials
   are required. Battle-scene stars use a different class
   (.v060-rank-star) so this partial-fill overlay never bleeds onto
   the HP bar overlay. */
.sc-rank-stars .sc-rank-star.is-progress {
  position: relative;
  display: inline-block;
  /* The parent takes the outline's natural width; the fill sits
     absolutely on top so its NN% clip respects the outline's bounds.
     No explicit width needed here — let the outline glyph (☆) size
     the box the way regular outline stars do. */
}
.sc-rank-stars .sc-rank-star.is-progress .sc-rank-star-outline {
  display: inline-block;
  color: rgba(244, 195, 92, 0.28);
  /* Lift the outline slightly above the absolute fill so paint order
     reads as fill-on-outline even at high fill percentages. */
  position: relative;
  z-index: 1;
  pointer-events: none;
}
.sc-rank-stars .sc-rank-star.is-progress .sc-rank-star-fill {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 0%;
  overflow: hidden;
  color: #f4c35c;
  text-shadow: 0 0 min(0.18cqw, 2.3px) rgba(244, 195, 92, 0.45);
  white-space: nowrap;
  pointer-events: none;
  /* Inline width="" sets the live fill (0..100). width transitions
     run on hover-with-material so the user sees the projected fill
     grow in addition to the star size increase. The transition runs
     on width (not clip-path) so it animates in every browser the
     existing fleet targets. */
  transition: width 180ms ease-out;
  /* Fill sits BELOW the outline in z-order (default 0) so the outline
     glyph paints on top of the fill glyph at every fill percentage,
     preserving the visual "this is an outline with a partial fill"
     semantic instead of looking like a different shape. */
}

/* Hover-with-rank-material scale-up. Distinct from .sc-rank-up (which
   fires on rank-completion) — .is-rank-material-hover fires whenever
   the depot is hovering with a rank material, regardless of whether
   the drop will complete. Sized small (1.06) to match the existing
   stat-cell hover scale language. */
.sc-rank-stars.is-rank-material-hover { transform: translate(-50%, -65%) scale(1.06); }
.sc-rank-stars.is-rank-material-hover .sc-rank-star { transform: translateY(-0.08cqw) scale(1.03); }

.sc-rank-stars.sc-rank-up .sc-rank-star { color: #66c985; }
.sc-rank-stars.sc-rank-up .sc-rank-star.is-filled { text-shadow: 0 0 min(0.25cqw, 3.2px) rgba(102, 201, 133, 0.7); transform: translateY(-0.1cqw) scale(1.06); }
.sc-rank-stars.sc-rank-up .sc-rank-star.is-outline { color: rgba(102, 201, 133, 0.4); }
/* When the hover preview completes a rank-up, paint the partial
   progress star green to mirror the "sc-rank-up" tone across the
   whole element. Same green family as the existing completion cue
   so the player reads it as "this drop will level me up." */
.sc-rank-stars.sc-rank-up .sc-rank-star.is-progress .sc-rank-star-fill { color: #66c985; text-shadow: 0 0 min(0.22cqw, 2.8px) rgba(102, 201, 133, 0.65); }

/* re-opened: the v0.6.027 Delta 2 hover animation block was unclosed
   after the patch_2_3_rank_stars.py insertion; re-open it here so the
   remaining CSS in the file parses normally. */
/* v0.6.027 Delta 2 continuation. Original block:
   Delta 2 — hover animation: the .sc-items row now ALSO moves on
   hover, matching the new "expand outward from the center" feel.
   - translateY(3vw)  — the user spec says "the same distance the
     stats move UP". The v0.6.026 tuned grid is -1.5vw but the
     canonical spec body mandates translateY(3vw) for delta 2
     (it is the v0.6.020-vintage grid-lift magnitude and the
     canonical spec's literal value). The math is that the items
     drop 3vw down while the stats rise 1.5vw up — the items
     expand a little more than the stats lift, which reads as
     "items bottom out further" rather than perfect mirror.
   - scale(1.2)       — 20% size grow, center-origin.
   - transform-origin: center center (explicit).
   - transition: 420ms ease-out (matches the grid + portrait
     hover transition for synchronized timing).

   The transition lives on the .sc-items base rule so the
   :hover state can compose a transform without flicker. The
   v0.6.026 grid (-1.5vw) and portrait (+0.75vw) hover deltas
   are preserved unchanged. The 3-slot horizontal layout, the
   3.2vw slot height, and the per-class equipped item artwork
   scaling are all preserved. */
/* end v0.6.027 */
.shop-card .sc-items,
.squad-card .sc-items,
.freeze-card .sc-items {
  transition: transform 420ms ease-out;
  transform-origin: center center;
}
.shop-card:hover:not(.empty) .sc-items,
.squad-card:hover:not(.empty) .sc-items,
.freeze-card:hover:not(.empty) .sc-items {
  transform: scale(1.1);
}
.gerbil-stat-cell {
  aspect-ratio: 1 / 1;
  min-width: 0;
  box-sizing: border-box;
  border: min(0.055cqw, 0.704px) solid rgba(62, 47, 34, 0.84);
  background: rgba(247, 237, 204, 0.86);
  color: #2f241b;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: visible;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: -0.075cqw -0.075cqw 0 #F7EDCC, 0.075cqw -0.075cqw 0 #F7EDCC, -0.075cqw 0.075cqw 0 #F7EDCC, 0.075cqw 0.075cqw 0 #F7EDCC;
  /* v0.6.006 §4: nonzero rounded corner radius on every stat cell. Same
     token shared by empty item slots so the neutral appearance parity
     (§10) holds without drift. */
  border-radius: min(0.18cqw, 2.304px);
}
.gerbil-stat-label,
.gerbil-stat-value { position: relative; z-index: 2; }
/* v0.6.006 §5: doubled font sizes (2x v0.6.005 baseline). 0.34vw -> 0.68vw
   (label), 0.48vw -> 0.96vw (value). The win-rule below intentionally
   bypasses the freeze override (which would otherwise halve the
   doubling ratio) so every shared-context card exposes the same
   2.0x font scale. */
.gerbil-stat-label { font-size: 0.68cqw; font-weight: 900; letter-spacing: min(0.015cqw, 0.192px); }
.gerbil-stat-value { margin-top: min(0.05cqw, 0.64px); font-size: 0.96cqw; font-weight: 900; white-space: nowrap; }
.gerbil-stat-cell.stat-up { border-color: #19733c; background-color: rgba(133, 224, 155, 0.9); color: #0e5429; }
.gerbil-stat-cell.stat-down { border-color: #a12f2a; background-color: rgba(240, 151, 143, 0.9); color: #82221e; }
.gerbil-stat-cell.stat-tradeoff { border-color: rgba(62,47,34,.84); background-color: rgba(247,237,204,.9); }
.gerbil-stat-value .value-up { color: #0e6f34; }
.gerbil-stat-value .value-down { color: #a12f2a; }
.rank-fill-current,
.rank-fill-preview { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; pointer-events: none; }
.rank-fill-current { background: rgba(198, 158, 67, 0.5); }
.rank-fill-preview { background: rgba(56, 196, 91, 0.72); }
/* Card-level inherited glows are suppressed during authoritative box preview. */
.squad-card.has-stat-preview,
.squad-card.has-stat-preview.is-drop,
.squad-card.has-stat-preview.gg-glow-purchase {
  background-color: transparent !important;
  outline: 0 !important;
  box-shadow: none !important;
  filter: none !important;
}
.squad-card.has-stat-preview .sc-portrait,
.squad-card.has-stat-preview .sc-portrait img,
.squad-card.has-stat-preview .sc-item { filter: none !important; box-shadow: none !important; outline: 0 !important; }
/* v0.6.008 — Explicit Item-Slot Hover/Commit Parity Closure.
   The v0.6.004 stat-preview rule above wipes the
   highlight / outline on every .sc-item, including the one the
   v0.6.008 spec requires to keep its green outline. Restore the
   outline + box-shadow + tinted background on the resolved
   destination slot only — sibling slots, source slots, and
   non-destination slots remain at the stat-preview neutral
   state. Specificity: this rule has the same `!important`
   weight but uses one additional attribute selector to win the
   cascade over the broader rule. */
.squad-card[data-gg-dest-slot="0"][data-gg-grid-raised] .sc-item[data-equipped-index="0"],
.squad-card[data-gg-dest-slot="1"][data-gg-grid-raised] .sc-item[data-equipped-index="1"],
.squad-card[data-gg-dest-slot="2"][data-gg-grid-raised] .sc-item[data-equipped-index="2"] {
  outline: min(0.18cqw, 2.304px) solid #2f7a3a !important;
  outline-offset: min(0.06cqw, 0.768px) !important;
  box-shadow:
    0 0 0 min(0.06cqw, 0.768px) rgba(47, 122, 58, 0.55),
    0 min(0.12cqw, 1.536px) min(0.42cqw, 5.376px) rgba(47, 122, 58, 0.45),
    0 min(0.0625cqw, 0.8px) 0 rgba(33, 26, 23, 0.12) !important;
  background-color: rgba(220, 244, 220, 0.92) !important;
  background-image: none !important;
}
/* Freeze cards are narrower; the same grid remains exact 4x2 and square.
   v0.6.006 §3: freeze grid inset unified to top:0; left:0; right:0 per
   the active winning rule. The reduced gap (0.045vw) is preserved so
   the freeze cards keep their tighter proportions per the spec. */
.depot-cell-freeze .gerbil-stat-grid { top: 0; left: 0; right: 0; gap: min(0.045cqw, 0.576px); }
/* v0.6.006 §5: freeze font sizes also doubled. 0.25vw -> 0.5vw (label),
   0.34vw -> 0.68vw (value). The freeze cells stay compressed but the
   label/value scale exactly 2x the v0.6.005 freeze baseline. */
.depot-cell-freeze .gerbil-stat-label { font-size: 0.5cqw; }
.depot-cell-freeze .gerbil-stat-value { font-size: 0.68cqw; }
/* v0.5.15-REDO23: silhouette backgrounds on the card surface removed (set
   transparent) — only the inner .card-portrait.silhouette portrait keeps its
   class-tinted circle so the class indicator still reads. The card surface
   itself stays transparent so the REDO18 cover image shows through. */
.shop-card.silhouette.silhouette-officer   { background-color: transparent; }
.shop-card.silhouette.silhouette-engineer  { background-color: transparent; }
.shop-card.silhouette.silhouette-grenadier { background-color: transparent; }

/* ================ Squad cell (spans rows 8-12 in REDO4) ================ */
.depot-cell-squad {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO22
     cover background-image (REDO34 now: ui_box.png, was ui_container.png)
     shows through unblocked. */
  background-color: transparent;
  padding: min(0.25cqw, 3.2px);
}
.depot-cell-squad .squad-row-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: min(0.5cqw, 6.4px);
  border-bottom: min(0.0625cqw, 0.8px) solid #c8a76d; padding-bottom: min(0.125cqw, 1.6px); margin-bottom: min(0.25cqw, 3.2px);
}
/* v0.5.15-REDO93: exact authored font-size; neighboring declarations preserved. */
.squad-row-head h3 { font-size: 1cqw; letter-spacing: min(0.102cqw, 1.3056px); font-weight: 900; color: #4a372a; margin: 0; }
.depot-cell-squad .squad-row-sub { font-size: 0.65cqw; color: #5f4a37; font-weight: 700; }
.depot-cell-squad .squad-row-tip { font-size: 0.55cqw; color: #5f4a37; text-align: right; flex: 1; }
.depot-cell-squad .squad-cards {
  display: grid;
  /* Right-to-left: P1 sits at the right edge (next to stash). DOM order is P5..P1
     but grid-auto-flow + flex-direction: row-reverse flips it visually. */
  grid-template-columns: repeat(5, 1fr);
  gap: min(0.25cqw, 3.2px);
  flex: 1 1 auto;
  min-height: 0;
}
.squad-cards { direction: rtl; }  /* squad order reverse */
.squad-card {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO18
     cover background-image (ui_card.png) shows through unblocked. Empty and
     drag-highlight state rules below still apply their own backgrounds via
     shorthand `background:` only in the relevant state.
     v0.5.15-REDO26: overflow changed hidden->visible so the gerbil PNG
     may overlap the squad card edges per the user's instruction.
     v0.5.15-REDO42: flat .0625vw solid #4a372a + .1875vw radius REMOVED — the
     REDO18 ui_card.png border-image draws the frame. */
  background-color: transparent;
  border: 0;
  border-radius: 0;
  padding: min(0.25cqw, 3.2px);
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: grab;
  user-select: none;
  position: relative;
  min-height: 0;
  overflow: visible;
  direction: ltr;  /* restore inner text direction */
}
.squad-card:hover:not(.empty) { transform: translateY(max(-0.0625cqw, -0.8px)); box-shadow: 0 min(0.1875cqw, 2.4px) 0 rgba(33,26,23,0.18); }
.squad-card.dragging { opacity: 0.4; }
.squad-card.is-drop { background: rgba(220, 244, 220, 0.55); outline: min(0.125cqw, 1.6px) dashed var(--ink); outline-offset: max(-0.125cqw, -1.6px); }
.squad-card.is-bad-drop { background: rgba(216,70,58,0.30); outline: min(0.125cqw, 1.6px) dashed #a02b1f; outline-offset: max(-0.125cqw, -1.6px); }
.squad-card.empty { background-color: transparent; color: #5f4a37; cursor: default; }
/* v0.5.15-REDO26: removed the tiny 32x32 circular portrait.
   The gerbil image now fills the squad card (may overlap edges per user). */
.squad-card .sc-portrait,
.squad-card .sc-portrait.silhouette {
  flex: 1 1 0;
  width: 100%; height: auto; min-height: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  margin: 0 0 min(0.1875cqw, 2.4px) 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: visible;
  /* v0.5.15-REDO81: literal z-index: 4 per user spec. The .sc-portrait
     wrapper now sits above the .sc-emblem (z=3) so the gerbil portrait
     is layered in front of the rank badge — opposite the REDO80 order.
     The emblem still anchors to the top-right with overflow:visible
     on the parent .squad-card so the badge stays visible. */
  z-index: 4;
}
/* v0.5.15-REDO27: scale gerbil image to 150% of card width so it overlaps the
  card edges. transform: scale() is layout-neutral — the layout box stays the
  same size, so the card's width/height are unchanged. Combined with
  overflow:visible on .sc-portrait and the parent .squad-card, the image
  visually extends 25% past the card edge on every side.
  v0.5.15-REDO90: pointer-events: none on the gerbil portrait IMG. The
  artwork is purely decorative — it must NEVER capture pointer input
  that lies inside the .sc-item hitbox below. Without this rule, the
  scaled (1.5-1.6x) portrait image completely covers the equipped item
  slots on Heavy (scale 1.6), Medic (1.5), Sniper (1.55), and Grenadier
  (1.5) and steals the click/mousedown/dragstart from the .sc-item, so
  the engine routes the input to the parent .squad-card and the unit
  (gerbil) receives the interaction instead of the equipped item. The
  same fix restores parity on Rifleman (1.425), Officer (1.35), Scout
  (1.3), and Engineer (1.35) for the lower-edge inset probe points that
  the .sc-item would otherwise win but the portrait img currently
  intercepts. The .sc-portrait DIV itself stays pointer-active so
  clicks outside any item slot still hit the unit card. */
.squad-card .sc-portrait img { width: 100%; height: 70%; object-fit: contain; transform: scale(1.5); transform-origin: center center; pointer-events: none; }
.squad-card .sc-name { font-weight: 900; font-size: 0.7cqw; line-height: 1.05; text-align: center; }
.squad-card .sc-rank { font-size: 0.55cqw; color: #5f4a37; }
.squad-card .sc-hp { font-size: 0.6cqw; color: #4a372a; font-weight: 700; }
.squad-card .sc-items {
  /* v0.5.15-REDO63: literal value per spec — gap: 0. The three item
     slots butt against each other with no separation. Border-box +
     width:100% keeps the row exactly filling the card's inner content
     box. align-self:stretch + flex:1 on .sc-item handle any rounding
     subpixel differences. */
  display: flex;
  gap: 0;
  width: 100%;
  box-sizing: border-box;
  align-self: stretch;
  margin-top: min(0.125cqw, 1.6px);
  padding: 0;
  flex: 0 0 auto;
}
.squad-card .sc-item {
  /* v0.6.007: every slot (empty AND occupied) renders the visible
     neutral stat-cell frame. The dimensions and indexed drop semantics
     remain REDO63 — 1/3 of the row, height 3.2vw, flex 1 1 0. The
     drag-over transient highlight (`.is-drop`) is allowed to flash but
     must not leave a permanent border or background after the
     interaction ends. Overflow is now visible so the enlarged (2.0x)
     equipped item artwork can visibly extend beyond the slot boundary
     in front of the gerbil portrait without being clipped. The frame
     uses a transparent border + inset box-shadow so the image's
     pre-scale layout box stays equal to the slot's full outer box.
     The frame's border-color is still visible because box-shadow
     draws inside the layout box (not the inner content area). */
  flex: 1 1 0;
  min-width: 0;
  font-size: 0.55cqw;
  padding: 0;
  margin: 0;
  /* Transparent border keeps the slot's outer box identical to the
     v0.6.006 layout. The visible frame is drawn via inset box-shadow
     below so the equipped-item artwork's pre-scale BCR is exactly
     the slot's full outer BCR (not inner). After transform: scale(2.0)
     the BCR is 2.0x the v0.6.006 baseline. box-sizing: border-box
     ensures the image's `width: 100%` references the full outer
     width (matching the v0.6.006 layout). */
  border: min(0.055cqw, 0.704px) solid transparent;
  box-sizing: border-box;
  cursor: grab;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: min(3.2cqw, 40.96px);
  background: rgba(247, 237, 204, 0.86);
  background-color: rgba(247, 237, 204, 0.86);
  background-image: none;
  /* Inset box-shadow draws the visible 4-side frame without affecting
     the image's pre-scale layout box. The shadow is drawn at the
     slot's content edge (= outer edge minus the transparent border).
     0.055vw inset ring at .055vw @ 1600x720. */
  box-shadow: inset 0 0 0 min(0.055cqw, 0.704px) rgba(62, 47, 34, 0.84),
              0 min(0.0625cqw, 0.8px) 0 rgba(33, 26, 23, 0.12);
  border-radius: min(0.18cqw, 2.304px);
  outline: 0;
  opacity: 1;
  overflow: visible;
}
.squad-card .sc-item > .sc-item-img {
  /* v0.6.007: enlarged 2.0x baseline. The slot's BCR stays unchanged
     (transform is layout-neutral). The image is styled to fill the
     1/3-wide slot at its natural 1.0x size; the 2.0x scale is applied
     via transform below so the visible BCR is exactly 2x the v0.6.006
     baseline while the slot BCR / hit target / indexed drop target
     remain identical. pointer-events: none so the enlarged artwork
     never steals input from the slot itself. The pre-scale BCR is
     expanded by 0.055vw on each side via negative margin to offset
     the slot's transparent border (added in v0.6.007) so the image
     pre-scale BCR equals the v0.6.006 image BCR (which had no
     transparent border on the slot). After transform: scale(2.0)
     the post-scale BCR is 2.0x the v0.6.006 image BCR. */
  display: block;
  width: calc(100% + min(0.5cqw, 6.4px));
  height: calc(100% + min(0.5cqw, 6.4px));
  margin: max(-0.25cqw, -3.2px);
  max-width: none;
  max-height: none;
  object-fit: contain;
  pointer-events: none;
  /* v0.6.006 §6: explicit stacking — equipped item artwork sits in
     front of the gerbil portrait (which is at z-index 4) so the icon
     is always visible at overlap points. */
  position: relative;
  z-index: 6;
  /* v0.6.007 §7: scale the equipped item artwork ~2.0x the slot's
     1.0x baseline. The transform is layout-neutral so the slot BCR
     stays unchanged. transform-origin centers the scale on the
     artwork's center so overflow is symmetric. The image BCR after
     transform must be >= 2.0x the pre-transform BCR. */
  transform: scale(2.0);
  transform-origin: center center;
}

/* v0.6.006 §6: explicit stacking order on every populated gerbil card.
   The win-rule below declares the canonical visual stack:
     back  : stat grid + cells (z-index 2)
     middle: gerbil portrait wrapper + image (z-index 3 for shop-card
             .card-portrait, z-index 4 for .sc-portrait to retain
             REDO81 emblem layering)
     front : item slots + equipped item artwork (z-index 5 / 6)
   elementsFromPoint() at controlled overlap points (see
   evidence/v06006/stacking-overlap-matrix.json) must show this exact
   order on every class in shop and squad. */
/* v0.6.027 — Anchor .sc-items row to the BOTTOM of the card. The
   previous v0.6.026 rule (`position: relative; z-index: 5;`) let the
   row render at the top of the .squad-card flex column, which the user
   described as "moved up". The whole row is now positioned absolutely
   against the card's `position: relative` parent and pinned to the
   bottom edge. The 3-slot horizontal layout (display:flex, gap:0,
   width:100%) is preserved from the .sc-items base rule above. The
   row's computed height (3.2vw slot × 3) stays exactly the same; only
   its vertical position changes. */
.squad-card .sc-items { position: absolute; left: 0; right: 0; bottom: 0; z-index: 5; }
.squad-card .sc-item { position: relative; z-index: 5; }
.shop-card .card-portrait,
.freeze-card .card-portrait { z-index: 3; }
/* v0.6.006 §10: empty item slot MUST share the neutral stat-cell
   appearance. Reuses the .gerbil-stat-cell token (border / radius /
   background / shadow / outline / opacity) without copying literal
   values — the empty slot references the same CSS by composition, so
   future drift in the cell values is automatically inherited. The
   slot still keeps its indexed dimensions and DOM identity. */
.squad-card .sc-item[data-empty="1"] {
  background: rgba(247, 237, 204, 0.86);
  background-color: rgba(247, 237, 204, 0.86);
  background-image: none;
  border: min(0.055cqw, 0.704px) solid rgba(62, 47, 34, 0.84);
  border-radius: min(0.18cqw, 2.304px);
  outline: 0;
  box-shadow: 0 min(0.0625cqw, 0.8px) 0 rgba(33, 26, 23, 0.12);
  /* v0.6.006 §7: neutral opacity (no fade) — the hover-faded portrait
     opacity 0.2 (§7) is wired on the .card-portrait img / .sc-portrait
     img only, never on the slot. */
  opacity: 1;
}

/* v0.5.15-REDO80: numeric 1-6 rank emblem on every occupied Squad
   card (extends REDO77's 1-5 ladder to six tiers). The <img class="sc-emblem">
   is the FIRST child of .squad-card
   in renderSquadRow, so it stacks BEHIND the gerbil portrait
   (.sc-portrait at z=auto) but BENEATH none of the equipment slots
   (which sit at the bottom of the card). Anchored to the card's
   top-left corner with a controlled overflow on both the top edge and
   the left edge — the parent .squad-card already has overflow:visible
   from REDO26. Width is set to 25% of the card's inner content width
   (vw-based so it scales with the depot). aspect-ratio is preserved
   by the height:auto rule. The img is non-interactive
   (pointer-events:none) so it cannot block drag/drop hit-testing,
   hover-tooltips, the .sc-item glow closure, the direct card glow,
   or the gerbil drag artwork. z-index places it visually on TOP of
   the gerbil portrait so the user sees the rank badge clearly. */
/* v0.5.15-REDO81: literal CSS positioning per user spec.
   Re-anchor the .sc-emblem to the top-right corner of the card:
     - remove authored `left: 0;`
     - add authored `right: 0;`
     - set `width: 50%;`
     - transform: translate(33%, -33%)
   The previous top-left anchor with translate(-50%, -50%) and
   26% width is replaced verbatim per the user spec. */
.squad-card .sc-emblem {
  position: absolute;
  top: 0;
  right: 0;
  width: 50%;
  height: auto;
  max-width: none;
  max-height: none;
  pointer-events: none;
  user-select: none;
  /* z-index keeps the emblem in front of the .sc-portrait wrapper
     (which has no explicit z-index so it stacks at z=auto). */
  z-index: 3;
  /* v0.5.15-REDO81: translate(33%, -33%) per user spec — moves the
     emblem 33% to the right and 33% upward of its anchor (top-right),
     producing the requested diagonal placement. transform-origin stays
     at top right so the percentages are measured from the anchor corner. */
  transform: translate(33%, -33%);
  transform-origin: top right;
  /* Suppress any browser-native img drag-image generation. */
  -webkit-user-drag: none;
  /* v0.5.15-REDO87: 50% transparency (computed opacity 0.5) on every
     player-visible rank emblem. The promotion-eligible hover state
     overrides this on the exact target emblem with the
     [data-gg-rank-promo-eligible="1"] attribute that the engine
     writes while a real gerbil drag is over a target whose drop
     would actually promote. Source, ghost, siblings, cards,
     portraits, and ineligible targets stay at the 0.5 default. */
  opacity: 0.5;
}
/* v0.5.15-REDO87: while a real gerbil drag is in flight, the exact
  hovered target squad-card whose drop would actually promote the
  target gerbil (via the same authoritative handleDrop matrix that
  the real drop commit uses) gets a 0.25 transparency override
  (computed opacity 0.75) on its emblem only. Higher specificity
  than the default .sc-emblem { opacity: 0.5 } rule above so the
  override sticks. The engine removes this attribute on dragleave,
  dragend, drop, rerender, and after every cleanup path. */
.squad-card[data-gg-rank-promo-eligible="1"] .sc-emblem {
  opacity: 0.75;
}

/* v0.5.15-REDO88: post-promotion level-up emblem animation. While
  the .gg-levelup-active class is present on a .sc-emblem, we hold
  computed opacity at 0.75 (matching the eligible-hover preview) so
  the promotion is unmistakable, and run a one-shot ~700ms filter
  + transform pulse to make the rank-up celebration clearly visible.
  The animation only fires on the emblem — never on the card,
  portrait, siblings, source, or ancestors. The engine removes the
  class on animationend (with a 1200ms fallback timer) so the emblem
  settles back to the default 0.5 opacity while the promoted-rank
  PNG stays. The !important on opacity beats the inherited default
  .squad-card .sc-emblem { opacity: 0.5 } rule, and the keyframes
  are scoped to .sc-emblem so they never leak to anything else. */
@keyframes gg-rank-levelup-pulse {
  0% {
    transform: translate(33%, -33%) scale(1);
    filter: brightness(1) drop-shadow(0 0 0 rgba(47, 122, 58, 0));
  }
  35% {
    transform: translate(33%, -33%) scale(1.18);
    filter: brightness(1.35) drop-shadow(0 0 min(0.75cqw, 9.6px) rgba(47, 122, 58, 0.85));
  }
  70% {
    transform: translate(33%, -33%) scale(1.05);
    filter: brightness(1.15) drop-shadow(0 0 min(0.375cqw, 4.8px) rgba(47, 122, 58, 0.45));
  }
  100% {
    transform: translate(33%, -33%) scale(1);
    filter: brightness(1) drop-shadow(0 0 0 rgba(47, 122, 58, 0));
  }
}
.squad-card .sc-emblem.gg-levelup-active {
  opacity: 0.75 !important;
  animation: gg-rank-levelup-pulse 700ms ease-out 1;
  /* The badge sits at z-index 3 (see base .sc-emblem rule). z-index
     stays unchanged so the emblem never overlays a sibling / portrait
     outside the card. */
}

/* v0.5.15-REDO92: per-class position and size adjustments on the gerbil
   <img>. The base REDO27 rule above sets `transform: scale(1.5)` (centered)
   on the image inside the portrait wrapper (.card-portrait for shop + freeze,
   .sc-portrait for squad). The engine's classPortrait() sets
   data-class="<ClassName>" on the WRAPPER, so the override below targets
   `.card-portrait[data-class="X"] img` AND `.sc-portrait[data-class="X"] img`
   (higher specificity than the base rule) and replaces the transform with
   the user-specified combined value.

   v0.5.15-REDO92 split the REDO31 combined-selector rules into two
   independent rules per class so that `.card-portrait` transforms can be
   updated without touching `.sc-portrait` transforms. The six changed
   classes get their NEW card-portrait transform literally per user spec;
   the .sc-portrait half of every class retains the REDO31 / REDO91 value
   byte-for-byte. Heavy and Sniper are negative controls — their
   card-portrait and sc-portrait transforms are both unchanged from
   REDO31 / REDO91.

   Per-class transforms (REDO31 user spec, preserved for .sc-portrait;
   REDO92 user spec applied to .card-portrait for the six named classes):

                  card-portrait (REDO92)               sc-portrait (REDO94 user spec)
                      Scout:       translate(-9%,  3%) scale(1.3)       translate(-9%,  3%) scale(1.3)
                      Rifleman:    translate(-3%, -1%) scale(1.425)     translate(-3%, -1%) scale(1.425)
                      Officer:     translate(-6%,  4%) scale(1.35)      translate(-6%,  4%) scale(1.35)
                      Heavy:       translate( 1%,  5%) scale(1.6)       translate( 1%,  5%) scale(1.6)
                      Engineer:    translate(-9%,  7%) scale(1.35)      translate(-9%,  7%) scale(1.35)
                      Medic:       translate(-9%,  8%) scale(1.5)       translate(-9%,  8%) scale(1.5)
                      Grenadier:   translate(-9%,  7%) scale(1.5)       translate(-9%,  7%) scale(1.5)
                      Sniper:      translate( 4%, 10%) scale(1.55)      translate( 4%, 10%) scale(1.55)

   The user provided the values in the order `translate(X, Y) scale(S)`.
   CSS applies transforms right-to-left, so this means scale first, then
   translate — and the translate percentages are computed against the
   SCALED box. The values are written exactly as the user specified, no
   re-derivation. Card size is unchanged — only the inner image moves
   and scales. transform-origin stays at center-center so the base
   layout box is centered, and the per-class translate offsets the
   centered image within (and slightly past, via overflow:visible) the
   card. */
.card-portrait[data-class="Scout"] img {
  transform: translate(-9%, 13%) scale(1.3);
}
.sc-portrait[data-class="Scout"] img {
  transform: translate(-9%, 13%) scale(1.3);
}
.card-portrait[data-class="Rifleman"] img {
  transform: translate(-3%, 9%) scale(1.425);
}
.sc-portrait[data-class="Rifleman"] img {
  transform: translate(-3%, 9%) scale(1.425);
}
.card-portrait[data-class="Officer"] img {
  transform: translate(-6%, 14%) scale(1.35);
}
.sc-portrait[data-class="Officer"] img {
  transform: translate(-6%, 14%) scale(1.35);
}
.card-portrait[data-class="Heavy"] img,
.sc-portrait[data-class="Heavy"] img {
  transform: translate(1%, 15%) scale(1.6);
}
.card-portrait[data-class="Engineer"] img {
  transform: translate(-9%, 17%) scale(1.35);
}
.sc-portrait[data-class="Engineer"] img {
  transform: translate(-9%, 17%) scale(1.35);
}
.card-portrait[data-class="Medic"] img {
  transform: translate(-9%, 18%) scale(1.5);
}
.sc-portrait[data-class="Medic"] img {
  transform: translate(-9%, 18%) scale(1.5);
}
.card-portrait[data-class="Grenadier"] img {
  transform: translate(-9%, 17%) scale(1.5);
}
.sc-portrait[data-class="Grenadier"] img {
  transform: translate(-9%, 17%) scale(1.5);
}
/* v0.5.15-REDO31: Sniper — NEW override (REDO29/30 left Sniper at the
   base scale(1.5) — REDO31 gives it its own class-specific transform).
   v0.5.15-REDO92: Heavy and Sniper remain unchanged from REDO31 / REDO91
   (negative controls — the spec only updates the six named classes).
   v0.6.006 §8: +10 percentage points added to translateY for every
   class. Sniper moves from 10% to 20% Y. translateX, scale, and
   transform-origin are byte-identical to v0.6.005. */
.card-portrait[data-class="Sniper"] img,
.sc-portrait[data-class="Sniper"] img {
  transform: translate(4%, 20%) scale(1.55);
}
/* v0.5.15-REDO62: removed .squad-card .sc-item.filled (background:#fff5b8
   + border-style:solid) and .squad-card .sc-item.is-drop — both re-introduced
   a permanent background/border that conflicts with §6 (no visible
   border/background/label). The transient drag-over feedback uses
   `.drag-over-ok` / `.drag-over-bad` declared above and is removed after
   dragend by the engine (see clearDropHighlights). */
/* v0.5.15-REDO62: .sc-pos rule removed (was the absolute-positioned
   "P1"/"P2"/... label in the top-left of the squad card). §1 forbids
   visible P1..P5 text on squad formation cards. The internal slot
   identity is preserved in data attributes (data-target-slot="P1"..)
   and in state.slot — no display change required for combat math. */

/* v0.5.3: medal-consume green flash animation. */
@keyframes medal-flash {
  0%   { box-shadow: 0 0 0 0 rgba(47, 122, 58, 0); transform: scale(1); }
  50%  { box-shadow: 0 0 0 min(0.5cqw, 6.4px) rgba(47, 122, 58, 0.9); transform: scale(1.08); }
  100% { box-shadow: 0 0 0 0 rgba(47, 122, 58, 0); transform: scale(1); }
}
.squad-card.medal-flash { animation: medal-flash 0.45s ease-out; border-color: #2f7a3a; }

/* ================ Stash cell (spans rows 7-11 in REDO4) ================ */
.depot-cell-stash {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO22
     cover background-image (REDO34 now: ui_box.png, was ui_container.png)
     shows through unblocked. */
  background-color: transparent;
  padding: min(0.25cqw, 3.2px);
}
.depot-cell-stash .rail-stash-head {
  display: flex; align-items: baseline; justify-content: space-between;
  border-bottom: min(0.0625cqw, 0.8px) solid #c8a76d; padding-bottom: min(0.125cqw, 1.6px); margin-bottom: min(0.25cqw, 3.2px);
}
/* v0.5.15-REDO93: exact authored font-size; neighboring declarations preserved. */
.rail-stash-head h3 { font-size: 1cqw; letter-spacing: min(0.102cqw, 1.3056px); font-weight: 900; color: #4a372a; margin: 0; }
.depot-cell-stash .rail-stash-count { font-size: 0.65cqw; color: #5f4a37; font-weight: 700; }
.depot-cell-stash .stash-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: min(0.25cqw, 3.2px);
  flex: 1 1 auto;
  min-height: 0;
}

/* ================ Area containers (item shop, gerbil shop, squad, stash) ================
   v0.5.15-REDO36: RESTORE ui_container.png as the area container border-image.
   Slice value 16 (user-provided); border-width tracks 8/12/16 at the three
   breakpoints. The fill keyword draws the center of ui_container.png as the
   area's interior texture, and the explicit background-image / cover / center
   / no-repeat quad matches the user's "as background and border" wording.
   One frame per area (NOT per inner card) — inner cards (.shop-card,
   .squad-card, .stash-card, .depot-cell-freeze .freeze-card) keep their
   ui_card.png frame from REDO17/18; the two frames stack.
   Placement: AFTER all four per-area base rules (gerbil-shop, item-shop,
   squad, stash) so the longhand border-style / border-width / border-image
   / background-image override their `background:` shorthand without
   !important. The .depot-cell { border: .0625vw solid #4a372a; border-radius: .25vw; }
   base rule is overridden the same way; border-radius is ignored by browsers
   when border-image is present. */
.depot-cell-item-shop,
.depot-cell-gerbil-shop,
.depot-cell-squad,
.depot-cell-stash {
  border-style: solid;
  /* v0.5.15-REDO54: border-width 1vw -> .5vw; border-image's slice width
     1vw -> .5vw so the frame renders at the new thickness. Slice value
     stays 16 (the ui_container.png asset's frame thickness). */
  border-width: min(0.5cqw, 6.4px);
  border-image: url("assets/ui_container.png") 16 fill / min(0.75cqw, 9.6px) / 0 stretch;
  /* v0.5.15-REDO55: slice width 0.5vw -> 0.75vw per spec */
  background-image: url("assets/ui_container.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-color: transparent;
}

.stash-card {
  /* v0.5.15-REDO23: background-color removed (set transparent) so the REDO18
     cover background-image (ui_card.png) shows through unblocked. Empty and
     drag-highlight state rules still apply their own backgrounds via
     shorthand `background:` only in the relevant state.
     v0.5.15-REDO42: flat .0625vw solid #4a372a + .1875vw radius REMOVED — the
     REDO18 ui_card.png border-image draws the frame. */
  background-color: transparent;
  border: 0;
  border-radius: 0;
  padding: min(0.25cqw, 3.2px);
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: grab;
  user-select: none;
  position: relative;
  min-height: 0;
  overflow: hidden;
}
.stash-card:hover:not(.empty) { transform: translateY(max(-0.0625cqw, -0.8px)); box-shadow: 0 min(0.1875cqw, 2.4px) 0 rgba(33,26,23,0.18); }
.stash-card.dragging { opacity: 0.4; }
.stash-card.empty { background-color: transparent; color: #5f4a37; cursor: default; }
.stash-card.is-drop { background: rgba(220, 244, 220, 0.55); outline: min(0.125cqw, 1.6px) dashed var(--ink); outline-offset: max(-0.125cqw, -1.6px); }
.stash-card .st-glyph { font-size: 1cqw; }
.stash-card .st-name { font-weight: 900; font-size: 0.65cqw; text-align: center; }
.stash-card .st-desc { font-size: 0.5cqw; color: #5f4a37; text-align: center; line-height: 1.1; overflow: hidden; }
.stash-card .st-sub { font-size: 0.55cqw; color: #5f4a37; }

/* ================ REDO18 — card border-width media queries ================ */
/* Placed AFTER the .shop-card / .squad-card / .stash-card / .freeze-card
   base rules so it wins the cascade: those base rules use the `border:`
   shorthand which resets border-image. The REDO18 rule uses longhand
   `border-style` + `border-width` + `border-image` + `background-image`,
   all of which survive the shorthand resets in the base rules because
   this block appears later in source order. Applies the
   wooden/parchment frame to every gerbil and item card slot: the gerbil
   shop (.shop-card inside #shopUnits), the item shop (.shop-card inside
   #shopItems), the stash (.stash-card), the squad (.squad-card), and the
   freeze slot (.freeze-card). The base block sets the default
   border-width (6 px) and a matching `border-image: ... / .375vw / 0 ...`
   slice width; the two @media queries below override BOTH the element's
   border-width AND the border-image's `/ Xpx` slice to 8 px (>=73.75vw)
   and 12 px (>=98.75vw) respectively. border-style: solid is required
   for border-image to render at every breakpoint. background-image is
   set as a longhand so the .empty / .is-drop / .is-bad-drop rules above
   can still override background-color with the `background:` shorthand
   without wiping the border-image. The wooden frame replaces the
   previous 1 px solid #4a372a outline on these cards; .freeze-card
   previously had `border: 0` — it now picks up a 6 / 8 / 12 px solid
   border (depending on viewport) so its border-image frame stays
   visible too. */
.shop-card,
.squad-card,
.stash-card,
.depot-cell-freeze .freeze-card {
  /* v0.5.15-REDO24: background-image (and its background-size/position/repeat)
     removed. The border-image above uses the `fill` keyword, so the middle
     of ui_card.png is already drawn as the background — the redundant
     cover-image layer is no longer needed. background-color: transparent
     was already in place per REDO17/REDO23, so no solid color shows through. */
  border-style: solid;
  border-width: min(0.75cqw, 9.6px);
  border-image: url("assets/ui_card.png") 32 fill / min(0.75cqw, 9.6px) / 0 stretch;
}

/* v0.5.15-REDO13: cell renamed to .depot-cell-wins (was .depot-cell-victories).
   Below content rules are dead — the .depot-cell-wins rules in the
   row-1-content block above replace them. */
/* REDO13: cell renamed to .depot-cell-wins, below rules dead. */
/*
.depot-cell-victories {
  background: #f8e9c4;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  text-align: center;
  gap: .125vw;
}
.depot-cell-victories .victories-eyebrow { font-size: 0.55rem; letter-spacing: 0.18em; color: #5f4a37; }
.depot-cell-victories .victories-value { font-size: 1.1rem; font-weight: 900; color: #4a372a; }
*/
/* v0.5.15-REDO11: WINS counter moved INTO the Victories cell (was in the
  Gold cell). The cell is flex column with gap:.125vw, so the wins-block sits
  below the existing VICTORIES eyebrow + value, matching the styling it had
  inside the Gold cell.
  v0.5.15-REDO12: duplicate wins-block removed from this cell.
  v0.5.15-REDO13: cell renamed to .depot-cell-wins; below rules dead. */
/* REDO13: cell renamed, wins-block removed, below rules dead. */
/*
.depot-cell-victories .wins-block { display: flex; flex-direction: column; align-items: center; margin-top: .125vw; }
.depot-cell-victories .wins-label { font-size: 0.6rem; letter-spacing: 0.12em; color: #5f4a37; }
.depot-cell-victories .wins-value { font-weight: 900; font-size: 1.1rem; color: #4a372a; }
*/

.depot-cell-engage {
  /* v0.5.15-REDO48: DROP the REDO41 defensive @media block — promote the
     >=1580 default (2vw) to the base rule. Per spec: ui_engage border-
     width 2vw with slice 96; no media queries. The .depot-cell-engage
    { background-color: transparent; } longhand further down still clears
    the background-color (the shorthand below uses longhand background-
    image / background-size / etc. so the longhand survives).
    v0.5.15-REDO51: REMOVE the inner <button class="footer-embark">
    wrapper. The cell itself is now the click target. The cursor:
    pointer is added below so the cell shows the click affordance.
    The cell's border-image + cover background-image (the ui_engage
    wooden frame) is UNCHANGED vs REDO48. The icon + label are now
    direct children of the cell — styled via the new .engage-icon /
    .engage-label / .engage-sub rules below (which replace the old
    .footer-embark-icon / -label / -sub rules).
    v0.5.15-REDO53: border-width 2vw -> .75vw; border-image's slice
    width 2vw -> .75vw per the new UI spec. */
  border-style: solid;
  /* v0.5.15-REDO54: border-image's / Xpx slice width changed .75vw -> 2vw
     per the user's spec. border-width tracks the slice width so the
     frame actually renders at the new thickness. Slice value stays 96. */
  border-width: min(2cqw, 25.6px);
  border-image: url("assets/ui_engage.png") 96 fill / min(2cqw, 25.6px) / 0 stretch;
  background-image: url("assets/ui_engage.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-color: transparent;
  /* REDO51: cell is the click target. */
  cursor: pointer;
  /* REDO51: icon + label stack vertically and stay centered in the
     cell. flex-direction: column (from .depot-cell base) is preserved.
     gap: .125vw tightens the stack so the 3 spans fit in the framed area. */
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: min(0.125cqw, 1.6px);
  padding: min(0.25cqw, 3.2px);
}
/* v0.5.15-REDO51: REMOVE all .footer-embark rules — the inner button
   is gone. The icon + label are direct children of .depot-cell-engage
   now and styled below. The old yellow #f4c35c button face + black
   border + .09375vw navy outline are no longer needed because the cell
   IS the click target and wears the ui_engage.png wooden frame as its
   visible surface. The icon (🔒) and ENGAGE label are now dark text
   on the wooden-frame background for high contrast. */
.depot-cell-engage .engage-icon {
  font-size: 1cqw;
  color: #2b1e1c;
  line-height: 1;
}
/* v0.5.15-REDO81: literal CSS typography per user spec.
   .engage-label (ENGAGE) -> 1.5vw
   .engage-sub (hint)     -> 0.75vw */
.depot-cell-engage .engage-label {
  font-family: 'BlackOpsOne', sans-serif;
  font-size: 1.5cqw;
  color: #2b1e1c;
  font-weight: 900;
  letter-spacing: min(0.18cqw, 2.304px);
  line-height: 1;
}
.depot-cell-engage .engage-sub {
  font-size: 0.75cqw;
  color: #2b1e1c;
  font-weight: 600;
  letter-spacing: min(0.045cqw, 0.576px);
  line-height: 1;
}

/* ================ Row 13 cell content (Tool Tip in REDO4) ================ */
.depot-cell-tooltip {
  background: #2b1e1c;
  color: #f5e3b3;
  padding: min(0.375cqw, 4.8px) min(0.625cqw, 8px);
  flex-direction: column;
}
.depot-cell-tooltip .footer-tooltip-eyebrow {
  font-size: 0.55cqw; letter-spacing: min(0.18cqw, 2.304px); color: #d2c8a8; margin-bottom: min(0.125cqw, 1.6px);
}
.depot-cell-tooltip .footer-tooltip-body {
  font-size: 0.85cqw; line-height: 1.2;
  text-align: center;
  margin: 0;
}

/* ============================================================================
   App shell + screens.
   v0.5.15: scene wrappers (.setup-screen, .battle-scene-screen) are
   absolutely positioned in the viewport so the fixed-size scene
   (.scene wrapper via .setup-screen / .battle-scene-screen) takes the
   exact width/height set by media queries.
   ============================================================================ */
.app-shell {
  display: block;
  position: relative;
  width: 100vw;
  height: 100vh;
  background-color: transparent;
  color: #f5e3b3;
  overflow: hidden;
}

/* The setup and battle screens are the .scene elements — sized via media
   queries and centered in the viewport. */
.setup-screen, .battle-scene-screen {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Sized via .scene base rule + media queries. */
}

/* ============================================================================
   Other screens (title, summary, end) are NOT scene-sized; they show
   centered in the viewport but use scroll-friendly flow in case content
   overflows at very small widths. Scrollbars remain hidden because of the
   body-level rule above, but the content fits in any reasonable window.
   ============================================================================ */
.title-screen {
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  /* v0.6.110: legacy title-card spacing follows the fitted stage. */
  padding: min(2.5cqw, 32px);
  background-color: transparent;
  color: #211a17;
  z-index: 50;
  overflow: hidden;
}
/* v0.6.125: title-only Legal Notice shortcut. Bottom-left of the
   title stage so it does not collide with the central title controls.
   The link is small, low-contrast by default, and brightens on
   hover/focus. Its placement and z-index match the legal/feedback
   family in #feedbackStage. */
.title-screen .title-legal-link {
  position: absolute;
  left: min(1.6cqw, 20.48px);
  bottom: min(1.6cqw, 20.48px);
  margin: 0;
  padding: min(0.35cqw, 4.48px) min(0.7cqw, 8.96px);
  font: 400 min(0.85cqw, 10.88px) / 1.2 'BlackOpsOne', 'SplineSans-Bold', system-ui, sans-serif;
  letter-spacing: min(0.06cqw, 0.768px);
  text-transform: uppercase;
  color: rgba(244, 195, 92, 0.75);
  text-decoration: underline;
  text-underline-offset: min(0.18cqw, 2.304px);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
  cursor: pointer;
  z-index: 90;
  transition: color 160ms ease-out, transform 160ms ease-out;
}
.title-screen .title-legal-link:hover,
.title-screen .title-legal-link:focus-visible {
  color: #ffe7a3;
  transform: scale(1.04);
  outline: 0;
}
.title-screen .title-card {
  display: flex; gap: min(1.5cqw, 19.2px); align-items: center; background: #fff; border: min(0.125cqw, 1.6px) solid #4a372a; padding: min(1.25cqw, 16px); width: min(min(68.75cqw, 880px), min(92cqw, 1177.6px)); max-height: calc(min(56.25cqw, 720px) - min(2cqw, 25.6px)); box-sizing: border-box;
}
.title-screen .title-copy { flex: 1; min-width: 0; }
.title-screen .title-art { width: min(12cqw, 153.6px); height: min(12cqw, 153.6px); object-fit: contain; flex: 0 0 auto; }
.title-screen .title-copy h1 { margin: min(0.35cqw, 4.48px) 0; font-size: 2.2cqw; line-height: 1.05; }
.title-screen .title-copy .lede { margin: min(0.5cqw, 6.4px) 0 min(0.8cqw, 10.24px); font-size: 0.9cqw; line-height: 1.2; }
.title-screen .title-changelog { margin-top: min(0.8cqw, 10.24px); }
.title-screen .changelog-eyebrow { margin: 0 0 min(0.25cqw, 3.2px); color: #7a6250; font-size: 0.65cqw; letter-spacing: min(0.12cqw, 1.536px); font-weight: 700; }
.title-screen .changelog-entry { border-top: min(0.0625cqw, 0.8px) solid #d2c8a8; padding-top: min(0.35cqw, 4.48px); font-size: 0.75cqw; }
.title-screen .changelog-entry summary { cursor: pointer; font-weight: 700; padding: min(0.25cqw, 3.2px) 0; outline-offset: min(0.15cqw, 1.92px); }
.title-screen .changelog-entry summary:focus-visible { outline: min(0.125cqw, 1.6px) solid #5dbed6; outline-offset: min(0.15cqw, 1.92px); }
.title-screen .changelog-body { padding: min(0.25cqw, 3.2px) 0 0 min(1cqw, 12.8px); line-height: 1.3; }
.title-screen .changelog-body ul { margin: 0; padding: 0; list-style: none; }
.title-screen .changelog-body li { padding: min(0.1cqw, 1.28px) 0; }
.title-screen .ver-pill { font-size: 0.6cqw; background: #4a372a; color: #f4c35c; padding: min(0.125cqw, 1.6px) min(0.375cqw, 4.8px); border-radius: min(0.1875cqw, 2.4px); vertical-align: middle; margin-left: min(0.5cqw, 6.4px); }
.title-screen .legend { list-style: none; padding: 0; }
.title-screen .legend li { padding: min(0.25cqw, 3.2px) 0; font-size: 0.85cqw; }
.title-screen .legend-swatch { display: inline-block; width: min(0.75cqw, 9.6px); height: min(0.75cqw, 9.6px); border-radius: 50%; margin-right: min(0.375cqw, 4.8px); vertical-align: middle; }
.title-screen .swatch-buy { background: #6ca84a; }
.title-screen .swatch-equip { background: #5dbed6; }
.title-screen .swatch-freeze { background: #eafaff; border: min(0.0625cqw, 0.8px) solid #5dbed6; }
.title-screen .swatch-sell { background: #fde6c8; border: min(0.0625cqw, 0.8px) solid #c25a30; }
.title-screen .swatch-medal { background: #f4c35c; border: min(0.0625cqw, 0.8px) solid #4a372a; }
.title-screen .redo8-change-list { margin: min(0.5cqw, 6.4px) 0 min(0.5cqw, 6.4px) min(1.5cqw, 19.2px); padding: 0; font-size: 0.85cqw; line-height: 1.45; }
.title-screen .redo8-change-list li { padding: min(0.125cqw, 1.6px) 0; }

button.primary {
  background: #f4c35c; color: #2b1e1c; border: min(0.125cqw, 1.6px) solid #4a372a;
  padding: min(0.5cqw, 6.4px) min(1cqw, 12.8px); cursor: pointer; font-weight: 900; letter-spacing: min(0.088cqw, 1.1264px); border-radius: min(0.1875cqw, 2.4px);
}
button.primary.big { font-size: 1.1cqw; padding: min(0.75cqw, 9.6px) min(1.5cqw, 19.2px); }

/* =============== BATTLE SCENE =============== */
.battle-scene-screen {
  background-color: transparent;
  color: #f5e3b3;
  box-sizing: border-box;
}
.battle-scene-screen > .bsc-header,
.battle-scene-screen > .bsc-body,
.battle-scene-screen > .bsc-arena,
.battle-scene-screen > .bsc-rosters,
.battle-scene-screen > .bsc-footer {
  /* Battle sub-blocks take their normal sizing; the scene wrapper is
     the fixed-size outer container. */
}
.bsc-header {
  display: grid; grid-template-columns: 1fr 1fr 1fr; gap: min(0.75cqw, 9.6px);
  background: #1a0f0e; padding: min(0.5cqw, 6.4px); border: min(0.0625cqw, 0.8px) solid #d2c8a8;
}
.bsc-commander {
  /* v0.6.109: legacy commander block hidden. The DOM and runtime
     writers are preserved; only the visual surface is removed. */
  display: none !important;
}
.bsc-commander-portrait { width: min(3.125cqw, 40px); height: min(3.125cqw, 40px); border-radius: 50%; overflow: hidden; background: #fff; }
.bsc-commander-portrait img { width: 100%; height: 100%; object-fit: cover; }
.bsc-commander-eyebrow { font-size: 0.6cqw; color: #d2c8a8; letter-spacing: min(0.06cqw, 0.768px); }
.bsc-commander-name { font-weight: 900; font-size: 1cqw; }
.bsc-commander-score { font-size: 0.85cqw; }
.bsc-body { display: flex; gap: min(0.75cqw, 9.6px); padding: min(0.5cqw, 6.4px); }
.bsc-worlds-badge { background: #f4c35c; color: #2b1e1c; padding: min(0.5cqw, 6.4px); border-radius: min(0.25cqw, 3.2px); text-align: center; font-weight: 900; }
.bsc-worlds-line1 { font-size: 0.7cqw; letter-spacing: min(0.07cqw, 0.896px); }
.bsc-worlds-line2 { font-size: 1.6cqw; }
.bsc-worlds-sub { font-size: 0.7cqw; letter-spacing: min(0.07cqw, 0.896px); }
.bsc-mission { flex: 1; background: #1a0f0e; padding: min(0.5cqw, 6.4px); border: min(0.0625cqw, 0.8px) solid #d2c8a8; }
.bsc-mission-eyebrow { font-size: 0.6cqw; color: #d2c8a8; letter-spacing: min(0.06cqw, 0.768px); }
.bsc-mission-name { font-size: 1.1cqw; font-weight: 900; }
.bsc-mission-objective { font-size: 0.85cqw; color: #d2c8a8; }
.bsc-mission-win { font-size: 0.85cqw; }
.bsc-orders { margin-top: min(0.25cqw, 3.2px); }
.bsc-orders-eyebrow { font-size: 0.6cqw; color: #d2c8a8; letter-spacing: min(0.06cqw, 0.768px); }
.bsc-orders-list { list-style: none; padding: 0; margin: 0; font-size: 0.8cqw; }

.bsc-arena {
  position: relative; background: #1a0f0e; padding: min(0.75cqw, 9.6px); border: min(0.0625cqw, 0.8px) solid #d2c8a8; min-height: min(12.5cqw, 160px);
  display: grid; grid-template-columns: 1fr auto 1fr; gap: min(0.5cqw, 6.4px); align-items: center;
  margin: 0 min(0.5cqw, 6.4px);
}
.bsc-arena-side { display: flex; gap: min(0.375cqw, 4.8px); flex-wrap: nowrap; justify-content: center; align-items: center; }
.bsc-arena-player { direction: rtl; }
.bsc-arena-card > * { direction: ltr; }
.bsc-arena-vs { display: flex; flex-direction: column; align-items: center; gap: min(0.25cqw, 3.2px); color: #f4c35c; font-weight: 900; }
.bsc-arena-vs-line { font-size: 1.4cqw; }
.bsc-arena-timer { font-size: 0.75cqw; color: #d2c8a8; }
.bsc-arena-card {
  background: #2b1e1c; border: min(0.0625cqw, 0.8px) solid #d2c8a8; border-radius: min(0.1875cqw, 2.4px); padding: min(0.25cqw, 3.2px);
  width: min(6.25cqw, 80px); min-height: min(8.125cqw, 104px); display: flex; flex-direction: column; align-items: center; position: relative;
}
.bsc-arena-card.empty { background: transparent; border-style: dashed; opacity: 0.5; }
.bsc-arena-card.player { color: #d2c8a8; }
.bsc-arena-card.enemy { color: #f4c35c; }
.bsc-arena-card.hit { animation: hit-flash 0.22s; }
.bsc-arena-card.sustained { animation: sustain-flash 0.22s; }
.bsc-arena-card.attacking { animation: attack-flash 0.22s; }
@keyframes hit-flash     { 50% { background: rgba(216,70,58,0.6); } }
@keyframes sustain-flash { 50% { background: rgba(108,168,74,0.6); } }
@keyframes attack-flash  { 50% { background: rgba(244,195,92,0.4); } }
.bsc-arena-portrait { width: min(2.5cqw, 32px); height: min(2.5cqw, 32px); border-radius: 50%; overflow: hidden; background: #fff; }
.bsc-arena-portrait img { width: 100%; height: 100%; object-fit: cover; }
.bsc-arena-portrait.silhouette.silhouette-officer { background: #3a5d3a; }
.bsc-arena-portrait.silhouette.silhouette-engineer { background: #5d4a37; }
.bsc-arena-portrait.silhouette.silhouette-grenadier { background: #5d3030; }
.bsc-arena-name { font-weight: 900; font-size: 0.8cqw; }
.bsc-arena-class { font-size: 0.6cqw; color: #d2c8a8; }
.bsc-arena-slot { position: absolute; top: min(0.125cqw, 1.6px); right: min(0.25cqw, 3.2px); font-size: 0.55cqw; color: #d2c8a8; }
.bsc-arena-hp { width: 90%; height: min(0.625cqw, 8px); background: #1a0f0e; border-radius: min(0.125cqw, 1.6px); position: relative; margin-top: min(0.25cqw, 3.2px); overflow: hidden; }
.bsc-arena-hp-fill { height: 100%; background: #6ca84a; transition: width 0.2s; }
.bsc-arena-hp-text { position: absolute; inset: 0; font-size: 0.5cqw; color: #fff; text-align: center; line-height: min(0.625cqw, 8px); font-weight: 900; }
.bsc-arena-empty-label { font-size: 0.55cqw; color: #5f4a37; }

.bsc-fx-layer { position: absolute; inset: 0; pointer-events: none; }
.fx-line { position: absolute; height: min(0.125cqw, 1.6px); background: #f4c35c; transform-origin: left center; }
.fx-shot { background: #d8463a; }
.fx-mark { background: #5dbed6; }
.fx-sustain { background: #6ca84a; }
.fx-miss { background: #d2c8a8; }
.float-text { position: absolute; font-size: 0.85cqw; font-weight: 900; color: #f4c35c; text-shadow: min(0.0625cqw, 0.8px) min(0.0625cqw, 0.8px) 0 #1a0f0e; animation: float-up 0.85s ease-out forwards; }
.float-damage { color: #d8463a; }
.float-miss { color: #d2c8a8; }
.float-sustain { color: #6ca84a; }
.float-mark { color: #5dbed6; }
.float-ko { color: #f4c35c; font-size: 1.1cqw; }
.float-no-target { color: #d2c8a8; }
@keyframes float-up { from { transform: translateY(0); opacity: 1; } to { transform: translateY(max(-1.875cqw, -24px)); opacity: 0; } }

.bsc-arena-log {
  grid-column: 1 / -1; max-height: min(5cqw, 64px); overflow-y: auto;
  background: #1a0f0e; padding: min(0.25cqw, 3.2px); border: min(0.0625cqw, 0.8px) solid #d2c8a8; font-size: 0.7cqw;
}
.bsc-arena-log-list { list-style: none; padding: 0; margin: 0; }
.gg-event-shot { color: #f4c35c; }
.gg-event-miss { color: #d2c8a8; }
.gg-event-mark { color: #5dbed6; }
.gg-event-sustain { color: #6ca84a; }
.gg-event-suppress { color: #b07bc7; }
.gg-event-ko { color: #d8463a; }
.gg-event-final { color: #f4c35c; font-weight: 900; }
.gg-event-collapse { color: #d2c8a8; }
.gg-event-no-target { color: #d2c8a8; }
.gg-event-target { color: #f4c35c; }

.bsc-rosters { display: grid; grid-template-columns: 1fr auto 1fr; gap: min(0.75cqw, 9.6px); align-items: center; padding: 0 min(0.5cqw, 6.4px); }
.bsc-roster-player .bsc-roster-cards { display: flex; gap: min(0.375cqw, 4.8px); flex-wrap: nowrap; justify-content: flex-end; direction: rtl; }
.bsc-roster-player .bsc-roster-cards > * { direction: ltr; }
.bsc-roster-title { font-size: 0.85cqw; letter-spacing: min(0.102cqw, 1.3056px); font-weight: 900; color: #d2c8a8; margin: 0 0 min(0.25cqw, 3.2px) 0; }
.bsc-roster-cards { display: flex; gap: min(0.25cqw, 3.2px); }
.bsc-card {
  background: #2b1e1c; border: min(0.0625cqw, 0.8px) solid #d2c8a8; border-radius: min(0.1875cqw, 2.4px); padding: min(0.25cqw, 3.2px);
  width: min(5cqw, 64px); min-height: min(6.25cqw, 80px); display: flex; flex-direction: column; align-items: center; position: relative;
}
.bsc-card.empty { background: transparent; border-style: dashed; opacity: 0.5; }
.bsc-card-portrait { width: min(2cqw, 25.6px); height: min(2cqw, 25.6px); border-radius: 50%; overflow: hidden; background: #fff; }
.bsc-card-portrait img { width: 100%; height: 100%; object-fit: cover; }
.bsc-card-portrait.silhouette.silhouette-officer { background: #3a5d3a; }
.bsc-card-portrait.silhouette.silhouette-engineer { background: #5d4a37; }
.bsc-card-portrait.silhouette.silhouette-grenadier { background: #5d3030; }
.bsc-card-name { font-weight: 900; font-size: 0.7cqw; text-align: center; }
.bsc-card-class { font-size: 0.55cqw; color: #d2c8a8; }
.bsc-card-slot { position: absolute; top: min(0.125cqw, 1.6px); right: min(0.25cqw, 3.2px); font-size: 0.5cqw; color: #d2c8a8; }
.bsc-card-hp { width: 90%; height: min(0.5cqw, 6.4px); background: #1a0f0e; border-radius: min(0.125cqw, 1.6px); position: relative; margin-top: min(0.25cqw, 3.2px); overflow: hidden; }
.bsc-card-hp-fill { height: 100%; background: #6ca84a; }
.bsc-card-hp-text { position: absolute; inset: 0; font-size: 0.45cqw; color: #fff; text-align: center; line-height: min(0.5cqw, 6.4px); font-weight: 900; }

.bsc-auto-stack { display: flex; flex-direction: column; gap: min(0.5cqw, 6.4px); }
.bsc-auto-btn {
  background: #6ca84a; color: #fff; border: min(0.125cqw, 1.6px) solid #1a0f0e;
  padding: min(0.5cqw, 6.4px) min(0.75cqw, 9.6px); cursor: pointer; border-radius: min(0.25cqw, 3.2px);
  font-weight: 900; letter-spacing: min(0.1cqw, 1.28px);
  width: min(5cqw, 64px);
}
.bsc-auto-btn[data-on="false"] { background: #5f4a37; color: #d2c8a8; }
.bsc-twox-btn { background: #5dbed6; color: #1a0f0e; }
.bsc-twox-btn[data-on="false"] { background: #2b1e1c; color: #d2c8a8; border-color: #5dbed6; }
.bsc-auto-pill { display: flex; flex-direction: column; align-items: center; gap: min(0.0625cqw, 0.8px); }
.bsc-auto-line1 { font-size: 0.85cqw; }
.bsc-auto-line2 { font-size: 0.7cqw; letter-spacing: min(0.056cqw, 0.7168px); }
.bsc-auto-line3 { font-size: 0.85cqw; font-weight: 900; }

.bsc-footer {
  display: flex; justify-content: space-between; align-items: center;
  background: #1a0f0e; padding: min(0.375cqw, 4.8px) min(0.75cqw, 9.6px); border: min(0.0625cqw, 0.8px) solid #d2c8a8;
  margin: min(0.5cqw, 6.4px);
}
.bsc-back-btn { background: #5f4a37; color: #fff; border: min(0.0625cqw, 0.8px) solid #d2c8a8; padding: min(0.25cqw, 3.2px) min(0.5cqw, 6.4px); cursor: pointer; }
.bsc-footer-tip { font-size: 0.75cqw; color: #d2c8a8; margin: 0; }

/* =============== Summary / End screens =============== */
.summary-screen {
  position: absolute;
  inset: 0;
  background-color: transparent;
  color: #211a17;
  padding: min(1.5cqw, 19.2px);
  overflow: auto;
  z-index: 50;
}
.summary-screen .section-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: min(1cqw, 12.8px); }
.summary-screen .two-column { display: grid; grid-template-columns: 2fr 1fr; gap: min(1cqw, 12.8px); }
.summary-screen .panel { background: #fff; border: min(0.0625cqw, 0.8px) solid #4a372a; padding: min(1cqw, 12.8px); }
.summary-screen .panel h3 { margin-top: 0; font-size: 0.95cqw; letter-spacing: min(0.095cqw, 1.216px); }
.summary-screen table.summary-table { width: 100%; border-collapse: collapse; font-size: 0.85cqw; }
.summary-screen table.summary-table th, .summary-screen table.summary-table td { border: min(0.0625cqw, 0.8px) solid #c8a76d; padding: min(0.25cqw, 3.2px) min(0.375cqw, 4.8px); }
.summary-screen table.summary-table th { background: #f4c35c; }
.summary-screen table.summary-table .summary-class-cell {
  display: flex; align-items: center; gap: min(0.375cqw, 4.8px);
  padding: min(0.2cqw, 2.56px) min(0.375cqw, 4.8px);
}
.summary-screen table.summary-table .summary-class-icon {
  width: min(3.25cqw, 41.6px); height: min(3.25cqw, 41.6px);
  object-fit: contain;
  image-rendering: -webkit-optimize-contrast;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) rgba(244, 195, 92, 0.35));
}
.summary-screen table.summary-table .summary-class-label {
  font-weight: 600;
  letter-spacing: min(0.025cqw, 0.32px);
}
.summary-screen table.summary-table .summary-mvp-badge {
  display: inline-block;
  margin-left: min(0.25cqw, 3.2px);
  background: #f4c35c;
  color: #4a372a;
  padding: min(0.0625cqw, 0.8px) min(0.25cqw, 3.2px);
  border-radius: min(0.125cqw, 1.6px);
  font-size: 0.7cqw;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.summary-screen table.summary-table tr.summary-row-mvp {
  background: rgba(244, 195, 92, 0.18);
}
.summary-screen table.summary-table tr.summary-row-mvp td {
  font-weight: 600;
}
.summary-screen table.summary-table tr.summary-row-kia td.summary-class-cell {
  opacity: 0.55;
  filter: grayscale(0.85);
}
.summary-screen table.summary-table .summary-slot-cell {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: #4a372a;
  text-align: center;
}
.summary-screen table.summary-table .summary-survived-cell {
  font-weight: 700;
  letter-spacing: 0.04em;
  text-align: center;
}
.summary-screen table.summary-table .summary-survived-cell.alive { color: #2c7d54; }
.summary-screen table.summary-table .summary-survived-cell.kia { color: #b53b3b; }
.summary-screen table.summary-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; }

.summary-screen .reason-box { background: #fbf6e6; padding: min(0.5cqw, 6.4px); margin-top: min(0.5cqw, 6.4px); border-left: min(0.25cqw, 3.2px) solid #f4c35c; }
.summary-screen .reward-card { background: #fff; padding: min(0.75cqw, 9.6px); border: min(0.0625cqw, 0.8px) solid #4a372a; }
.summary-screen .reward-card h4 { margin-top: 0; }
.summary-screen .reward-auto-note { color: #5f4a37; font-style: italic; }

.end-screen {
  position: absolute;
  inset: 0;
  background-color: transparent;
  color: #211a17;
  padding: min(1.5cqw, 19.2px);
  overflow: auto;
  z-index: 50;
}
.end-screen .hero-card { background: #fff; border: min(0.125cqw, 1.6px) solid #4a372a; padding: min(1.5cqw, 19.2px); max-width: min(50cqw, 640px); margin: 0 auto; }
.end-screen .end-card { display: grid; grid-template-columns: 1fr 1fr; gap: min(1cqw, 12.8px); }
.end-screen .history-row { display: flex; justify-content: space-between; padding: min(0.25cqw, 3.2px) 0; border-bottom: min(0.0625cqw, 0.8px) dashed #c8a76d; }

.summary-screen .summary-dismiss-hint {
  text-align: center;
  font-style: italic;
  opacity: 0.65;
  margin: min(1cqw, 12.8px) 0 0;
  font-size: 0.95cqw;
  color: #5b3a2a;
  letter-spacing: min(0.038cqw, 0.4864px);
}
.end-screen .end-dismiss-hint {
  text-align: center;
  font-style: italic;
  opacity: 0.65;
  margin: min(0.75cqw, 9.6px) 0 0;
  font-size: 0.9cqw;
  color: #5b3a2a;
  letter-spacing: min(0.036cqw, 0.4608px);
}
.summary-screen .reward-outcome {
  font-size: 1.05cqw;
  margin: min(0.375cqw, 4.8px) 0 min(0.75cqw, 9.6px);
}

/* =============== legacy compatibility =============== */
.hidden-legacy-tip { display: none; }
.formation-fallback { display: none; }
.formation-toast { position: fixed; bottom: min(0.75cqw, 9.6px); right: min(0.75cqw, 9.6px); background: #2b1e1c; color: #f5e3b3; padding: min(0.375cqw, 4.8px) min(0.75cqw, 9.6px); border-radius: min(0.1875cqw, 2.4px); z-index: 1000; }
.drag-ghost { display: none; }

/* v0.5.3: medal-combine glow on cards during drag. v0.6.009: converted
   to canonical green per the rank-material promotion glow unification. */
.dragging.is-medal { outline: min(0.1875cqw, 2.4px) solid #2f7a3a; outline-offset: max(-0.1875cqw, -2.4px); }


/* v0.5.15-REDO65: custom drag overlay. Top-level element that holds a
   single clone of the dragged artwork at its captured dimensions. JS sets
   the artwork via DOM mutation (no background-image swap race), and drives
   position/rotation each animation frame from the spring/damper loop.
   pointer-events: none ensures the overlay can never intercept drops — the
   native drag/drop pipeline still does all the hit-testing. */
.gg-dangle-overlay {
  position: fixed;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 2147483647;
  transform-origin: 50% 20%;
  will-change: transform;
  /* v0.5.15-REDO69: instant reveal — opacity flips 0→1 in the same
     frame the overlay is positioned. No fade-in, no animation. The
     user explicitly identified this transition as a "fade" that
     contradicted the instant-pickup contract. */
  opacity: 1;
  contain: layout style paint;
}
.gg-dangle-overlay.gg-dangle-ready {
  /* Kept as a stable marker class for tests / selectors; opacity is
     already 1, so the class adds nothing visually. No transition. */
}
.gg-dangle-overlay img,
.gg-dangle-overlay .card-portrait,
.gg-dangle-overlay .sc-portrait {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
/* v0.6.124.5: make the dangle overlay stand out visually from the squad
   cards. Without these, on mobile (small viewport) the dangle blends
   into the green-outlined drop-target card and looks invisible. The
   drop-shadow lifts it off the page; the explicit filter reset
   clears any source-image filter the clone inherited. */
.gg-dangle-overlay img {
  filter: drop-shadow(0 4px 8px rgba(0,0,0,0.55)) !important;
  box-shadow: 0 0 0 1px rgba(245, 227, 179, 0.6) !important;
}
/* v0.6.007 §7 drag-ghost negative control: the .gg-dangle-overlay
   clones a snapshot of the source DOM. If the source is a squad
   .sc-item-img, the clone carries the v0.6.007 transform: scale(2.0).
   Reset it to identity so the drag ghost stays at its captured
   baseline BCR (the user explicitly asked for "1:1 ghost, only the
   equipped-slot artwork is 2x"). Specificity beats the base
   .sc-item > .sc-item-img rule. */
.gg-dangle-overlay .sc-item-img {
  /* v0.6.111: dropped `!important` from transform. No other rule
     targets `.gg-dangle-overlay .sc-item-img` with a different
     transform — the base .sc-item > .sc-item-img rule sets
     `transform: scale(2.0)` on the source element only, not on
     the drag-ghost clone. */
  transform: none;
  transform-origin: center center;
  z-index: auto;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: 100%;
}



/* ============================================================
   v0.6.118: comical freeze effect on the frozen card image.
   The .freeze-card wrapper exposes its image as --sprite; the
   frost overlay (::after) uses the same PNG as a mask so the
   frost only renders inside the character silhouette. The img
   filter chain supplies the contour-following icy edge
   (drop-shadow follows the alpha channel, unlike box-shadow).
   freeze-pop plays once and freezes in place. */

.freeze-card {
  --ice-white: #efffff;
  --ice-light: #bff8ff;
  --ice-mid:   #63dcff;
  --ice-blue:  #1e9fd6;
  --freeze-frost-opacity: 0;
  position: relative;
  display: inline-block;
  isolation: isolate;
}

.freeze-card .card-portrait img,
.depot-cell-freeze .freeze-card .card-portrait.silhouette,
.depot-cell-freeze .freeze-card img.card-portrait.item-portrait {
  display: block;
  width: 100%;
  height: auto;
  filter: none;
  animation: none;
}

.freeze-card.is-frozen {
  animation: freeze-pop 300ms cubic-bezier(.2, .85, .2, 1) forwards;
}

.freeze-card.is-frozen .card-portrait img,
.freeze-card.is-frozen .card-portrait.silhouette,
.freeze-card.is-frozen img.card-portrait.item-portrait {
  animation: freeze-image 450ms ease-out forwards;
}

.freeze-card.is-frozen::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;

  background:
    linear-gradient(72deg, transparent 0 41%, rgba(255, 255, 255, .95) 42% 43%, transparent 44% 100%),
    linear-gradient(118deg, transparent 0 59%, rgba(210, 250, 255, .90) 60% 61%, transparent 62% 100%),
    radial-gradient(circle at 27% 22%, rgba(255, 255, 255, .95) 0 2%, rgba(188, 247, 255, .55) 3% 8%, transparent 15%),
    radial-gradient(circle at 72% 40%, rgba(237, 255, 255, .86) 0 2.5%, rgba(87, 216, 255, .40) 4% 10%, transparent 18%),
    radial-gradient(circle at 44% 70%, rgba(238, 255, 255, .75) 0 2%, rgba(83, 203, 244, .35) 5% 12%, transparent 20%),
    linear-gradient(135deg, rgba(255, 255, 255, .42), rgba(76, 218, 255, .18) 48%, rgba(0, 121, 201, .33));

  -webkit-mask-image: var(--sprite);
          mask-image: var(--sprite);
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;

  mix-blend-mode: screen;
  opacity: 0;
  animation: freeze-frost 450ms ease-out forwards;
}

@keyframes freeze-pop {
  0%   { transform: scale(1) rotate(0deg); }
  35%  { transform: scale(1.07) rotate(-2deg); }
  65%  { transform: scale(.985) rotate(1deg); }
  100% { transform: scale(1) rotate(0deg); }
}

@keyframes freeze-image {
  0%   { filter: none; }
  25%  {
    filter:
      brightness(2.25) saturate(0)
      drop-shadow(0 0 10px white)
      drop-shadow(0 0 22px #61e5ff);
  }
  100% {
    filter:
      saturate(.52) brightness(1.11) contrast(1.08) hue-rotate(150deg)
      drop-shadow( 1px  0   0 var(--ice-white))
      drop-shadow(-1px  0   0 var(--ice-white))
      drop-shadow( 0    1px 0 var(--ice-white))
      drop-shadow( 0   -1px 0 var(--ice-white))
      drop-shadow( 2px  1px 0 var(--ice-mid))
      drop-shadow(-2px -1px 0 var(--ice-mid))
      drop-shadow(0 0 4px rgba(191, 248, 255, .95))
      drop-shadow(0 0 11px rgba(30, 159, 214, .80));
  }
}

@keyframes freeze-frost {
  0%   { opacity: 0; }
  35%  { opacity: 1; }
  100% { opacity: .78; }
}

/* The .screen hide/show rules come LAST so they win by cascade order. */
.screen { display: none; }
.screen.active { display: block; }
.title-screen.active { display: flex; }

/* ============================================================================
   v0.5.15-REDO33 — Apply background.png to .app-shell + remove background
   colors on cells/containers that don't use border-image.

   The .app-shell container (the #app div in index.html) now wears the user-
   supplied assets/background.png (copied from C:\Users\admin\Pictures\
   background.png) as its background image with the user-specified CSS:

       background-image: url("assets/background.png");
       background-repeat: no-repeat;
       background-size: 125vw auto;       // image width = 125% of viewport
       background-position: center;       // centered -> 12.5% overflow per side
       background-color: black;           // fallback while loading or for gaps

   The 125vw auto sizing makes the image 25% wider than the viewport so it
   visibly overflows past the .app-shell edges (12.5% on each side of center).

   Per the user's instruction, background-color is REMOVED from all non-
   border-image cells/containers so the new app-shell background (and the
   border-image's fill) is the only thing showing. Border-image cells
   (gerbil/item/squad/stash cards, freeze/sell/reroll cells, the four area
   containers, and the .freeze-card inner card) keep their existing
   background-color: transparent longhand (already in place from REDO23);
   the border-image's fill keyword draws the middle of each PNG as their
   background.

   The screens that fully overlay the .app-shell (.title-screen, .battle-
   scene-screen, .summary-screen, .end-screen) keep their own backgrounds
   because the .app-shell background is only visible on the depot scene.
   The .battle-scene-screen also has a top/bottom strip where the .app-
   shell background will show, which is the intended look.

   All rules below are placed at the END of the file so they win by source
   order against the same-selector rules above.
   ============================================================================ */

/* v0.6.061: body exclusively owns scene artwork. The full-viewport app shell
   is transparent so it cannot cover the selected body image. */
.app-shell {
  background-image: none;
  background-color: transparent;
}

/* v0.6.061: html and body retain black as the load-failure/no-image fallback.
   Body becomes the sole scene-artwork owner in the final semantic rules. */
html, body {
  background-color: black;
}

/* The .scene wrapper (used by .setup-screen and .battle-scene-screen):
   remove the cream #fbf6e6 backdrop. The .app-shell background now shows
   wherever the .scene box doesn't have its own background. */
.scene {
  background-color: transparent;
}

/* The .depot grid container: remove the cream #fbf6e6 backdrop. The .app-
   shell background now shows wherever the .depot grid doesn't cover. */
.depot {
  background-color: transparent;
}

/* The base .depot-cell: remove the white #fff backdrop. The border-image
   cells above set background-color: transparent on the specific element
   classes (gerbil-shop, item-shop, squad, stash, freeze, sell, reroll), but
   the base .depot-cell rule still sets a white background that applies to
   any cell without its own override. Removing it here lets the .app-shell
   background show through any non-border-image cell (avatar, player, title,
   morale, round, wins, coins, free-spaces, engage, tooltip, etc.). */
.depot-cell {
  background-color: transparent;
}

/* Row 1 content cells — none have border-image, so they all go transparent
   to let the .app-shell background show. */
.depot-cell-player { background-color: transparent; }
.depot-cell-player-avatar { background-color: transparent; }
.depot-cell-player-avatar .avatar-block { background-color: transparent; }
.depot-cell-title { background-color: transparent; }
.depot-cell-morale { background-color: transparent; }
.depot-cell-round { background-color: transparent; }
.depot-cell-wins { background-color: transparent; }
.depot-cell-coins { background-color: transparent; }

/* Free Space cells — .depot-cell-freespace-thin is already transparent from
   REDO9, but the other two (left, wide) still carry the cream #f0e2bd. */
.depot-cell-freespace-left { background-color: transparent; }
.depot-cell-freespace-wide { background-color: transparent; }

/* Row 12 cells: engage has its REDO41 ui_engage.png frame; tooltip keeps no frame. */
.depot-cell-engage { background-color: transparent; }
.depot-cell-tooltip { background-color: transparent; }

/* Border-image cells: keep background-color: transparent. The declarations
   are already in place from REDO23, but the longhand rules below sit AFTER
   any future rule that might re-introduce a solid color, so the transparent
   longhand always wins for these specific selectors. */
.depot-cell-sell,
.depot-cell-freeze,
.depot-cell-freeze.filled,
.depot-cell-reroll,
.depot-cell-gerbil-shop,
.depot-cell-item-shop,
.depot-cell-squad,
.depot-cell-stash,
.shop-card,
.squad-card,
.stash-card {
  background-color: transparent;
}
/* Empty card states — the .empty rules above also set background-color:
   transparent; restate here to make the contract explicit. */
.shop-card.empty,
.squad-card.empty,
.stash-card.empty { background-color: transparent; }
/* Silhouette class-tinted cards — only the inner portrait keeps the class
   color (intentional UX indicator); the card surface itself stays
   transparent so the border-image's fill shows. */
.shop-card.silhouette.silhouette-officer   { background-color: transparent; }
.shop-card.silhouette.silhouette-engineer  { background-color: transparent; }
.shop-card.silhouette.silhouette-grenadier { background-color: transparent; }

/* ============================================================================
   v0.5.15-REDO36 — RESTORE ui_container.png as the border-image on the four
   area containers (.depot-cell-item-shop, .depot-cell-gerbil-shop,
   .depot-cell-squad, .depot-cell-stash).

   The user said "restore [ui_container.png] as background and border for
   the item, gerbil, squad and stash areas (one border per area - not per
   card)" — this replaces the ui_box.png applied in v0.5.15-REDO34. The
   earlier in-file block at the top of the area-containers section was
   edited in place to switch the asset + slice value (slice 80 -> 16); this
   trailing block re-states the same border-image / border-width longhand
   pair at the very END of the file so it wins the cascade against any
   future rule that might re-introduce ui_box.png (or any other asset) for
   these four selectors. The slice value 16 (user-provided) is the same
   value REDO22 used. The border-image's / Xpx tracks the border-width at
   each of the three breakpoints (8 / 12 / 16 px at <1180 / 1180-1579 /
   >=1580 px viewport) per the user's media-query spec. The fill keyword
   keeps drawing the center of ui_container.png as the area's interior
   texture, and the explicit background-image / cover / center / no-repeat
   quad matches the user's "as background and border" wording verbatim.
   Inner cards (.shop-card, .squad-card, .stash-card, .freeze-card) keep
   their ui_card.png frame from REDO17/18 unchanged; the two frames stack.
   The sell / freeze / reroll rail cells keep their own ui_sell /
   ui_freeze / ui_reroll PNGs.

   Asset: assets/ui_container.png (188,572 bytes, copied from
   C:\Users\admin\Pictures\ui_container.png).
   ============================================================================ */

/* v0.5.15-REDO54: defensive trailing block re-stated at the new .5vw default
   (was 1vw in REDO48). The block is preserved as belt-and-suspenders so the
   four area cells keep their frame even if a later rule attempts to reset it.
   Slice width and border-width both .5vw so the frame renders at the new
   thickness. Slice value stays 16 (the ui_container.png asset's frame
   thickness). */
.depot-cell-item-shop,
.depot-cell-gerbil-shop,
.depot-cell-squad,
.depot-cell-stash {
  border-style: solid;
  border-width: min(0.5cqw, 6.4px);
  border-image: url("assets/ui_container.png") 16 fill / min(0.75cqw, 9.6px) / 0 stretch;
  /* v0.5.15-REDO55: slice width 0.5vw -> 0.75vw per spec */
  background-image: url("assets/ui_container.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* ============================================================================
   v0.5.15-REDO35 — TWO CHANGES vs REDO34:

   CHANGE 1 — .scene border REMOVED.
     The .scene rule (lines ~307-321) had `border: .125vw solid #4a372a;` removed.
     The .scene block at the END of the file (added in REDO33 to set
     background-color: transparent on the .scene wrapper) is unchanged —
     it's a one-line override of the background-color only, not a border.
     Other styles on .scene (width: 100vw, aspect-ratio: 2/1, position,
     top/left/transform centering, overflow:hidden, box-sizing,
     background:#fbf6e6) unchanged.

   CHANGE 2 — BlackOpsOne for the numeric values of morale, wins, coins, round.
       The HTML uses these inner spans to hold the actual numeric values:
         #setupMoraleBadge (inside .depot-cell-wins   .wins-value)   [v0.6.066 swapped]
         #setupWinsBadge   (inside .depot-cell-morale .morale-value, format X/10) [v0.6.066 swapped]
         #setupGoldBadge   (inside .depot-cell-coins  .coin-row .coin-block)
         #setupRoundBadge  (inside .depot-cell-round  .round-value)
       Those four IDs are styled with `font-family: 'BlackOpsOne', sans-serif;`
       below. The surrounding wrappers (.morale-value / .wins-value /
       .coin-row / .round-value) and the LABELS (the per-cell label rule
       specifies) keep their existing font (SplineSans from the
       body/html rule, or wherever the per-cell label rule specifies).
       v0.6.066 swap-depot-morale-wins-content: the BlackOpsOne font
       rule is keyed on the IDs (#setupMoraleBadge / #setupWinsBadge)
       so the swap is invisible to the rule — the typography still
       flows from the per-cell label/value wrappers and the IDs
       regardless of which physical cell they live inside.

     The selector list also includes the four wrapper classes (.morale-value,
     .wins-value, .round-value, .coin-row) as a defensive fallback in case
     the inner #id spans are ever restructured — the wrapper classes still
     wrap the number in the current HTML, so this gives both the inner span
     AND the wrapper the BlackOpsOne font. (The .depot-cell-coins cell uses
     .coin-row as its wrapper, not .coins-value, so .coin-row is in the list.)

     No layout, sizing, color, or weight changes — only `font-family` is
     added. The existing per-cell value rules (e.g. `.depot-cell-morale
     .morale-value { font-weight: 900; font-size: 1.1rem; color: #4a372a; }`)
     still apply because the wrappers still match their original selectors.

   Engine code (game.js, game-data.js, game-rules.js, data-loader.js),
   HTML structure, data/, and the other CSS unchanged vs REDO34.
   ============================================================================ */

/* CHANGE 2 — BlackOpsOne for the numeric values after morale, wins, coins,
   round. Wrappers (.morale-value / .wins-value / .round-value / .coin-row)
   are included as defensive fallbacks; the four inner #id spans are the
   primary target. */
#setupMoraleBadge,
#setupWinsBadge,
#setupGoldBadge,
#setupRoundBadge,
.depot-cell-morale .morale-value,
.depot-cell-wins   .wins-value,
.depot-cell-coins  .coin-row,
.depot-cell-round  .round-value {
  font-family: 'BlackOpsOne', sans-serif;
}

/* ============================================================================
   v0.5.15-REDO36 — APPLY ui_box.png to coins / round / wins cells.

   CHANGE — Apply `assets/ui_box.png` (slice 16, user-provided) as border-
   image + cover background on the three row-1 cells `.depot-cell-coins`,
   `.depot-cell-round`, `.depot-cell-wins`. One frame per area (NOT per
   inner value), so the label/value layout inside each cell is unchanged.
   border-image uses the `fill` keyword to draw the middle of ui_box.png as
   the cell's interior texture; the explicit background-image / cover /
   center / no-repeat quad matches the user's "as background and border"
   wording verbatim.

   Border-width ladder (matches the area-container ladder for consistency):
       <1180 viewport   -> border-width  .5vw / border-image / .5vw slice
       1180-1579 viewport -> border-width .75vw / border-image / .75vw slice
       >=1580 viewport  -> border-width 1vw / border-image / 1vw slice

   The base `.depot-cell { background-color: transparent; }` rule plus the
   per-cell row1 transparent longhands (REDO33 block above) keep the cell
   surfaces transparent so the border-image's fill shows through. Placed
   at the END of the file so it wins the cascade against any future rule
   that might override these three selectors' border / background.

   Asset: assets/ui_box.png (368,255 bytes, copied from
   C:\Users\admin\Pictures\ui_box.png).

   Other layout UNCHANGED. Engine code (game.js, game-data.js,
   game-rules.js, data-loader.js) and the rest of the CSS unchanged vs
   REDO35. The "coins" label was renamed to "GOLD" in index.html in this
   same REDO36 build (no CSS change needed for the label — the existing
   .coin-label-top font/size/color rule from earlier still applies).
   ============================================================================ */

/* v0.5.15-REDO53: defensive trailing block re-stated at the new .125vw
   border-width with the new ui_box.png slice values (20 fill / .125vw / 0
   stretch). Slice value 16 (REDO36) -> 20 (REDO53); border-width .25vw
   -> .125vw so the slice pixels track the frame thickness. */
.depot-cell-coins,
.depot-cell-round,
.depot-cell-wins {
  border-style: solid;
  border-width: min(0.125cqw, 1.6px);
  border-image: url("assets/ui_box.png") 20 fill / min(0.125cqw, 1.6px) / 0 stretch;
  background-image: url("assets/ui_box.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* ============================================================================
   v0.5.15-REDO41 — APPLY ui_engage.png as border-image + background on the
   engage button (.depot-cell-engage). The user clarified that the v0.5.15-
   REDO37/40 assignments were wrong: ui_engage.png is for the ENGAGE BUTTON
   ONLY (NOT for the four area containers — those wear ui_container.png).
   This defensive trailing block re-states the border-image + border-width
   at each breakpoint so the engage button keeps its frame even if a later
   rule attempts to reset it. Ladder (8 / 12 / 20 px at <1180 / 1180-1579 /
   >=1580 px viewport) matches the rail-cell pattern (freeze / sell /
   reroll) so the engage button scales consistently with its sibling rail
   cells. Slice value 96 matches the spec example and the asset's frame
   thickness.

   Asset: assets/ui_engage.png (426,440 bytes, copied from
   C:\Users\admin\Pictures\ui_engage.png).
   ============================================================================ */

/* v0.5.15-REDO48: defensive trailing block now matches the >=1580 base
     default (2vw) — the old .5vw ladder is gone, no media queries to win
     the cascade against. The block is preserved as belt-and-suspenders. */
.depot-cell-engage {
  /* v0.5.15-REDO53: defensive trailing block re-stated at the new .75vw
     default. The block is preserved as belt-and-suspenders so the engage
     cell keeps its frame even if a later rule attempts to reset it. */
  border-style: solid;
  /* v0.5.15-REDO54: border-image's / Xpx slice width changed .75vw -> 2vw
     per the user's spec. border-width tracks the slice width so the
     frame actually renders at the new thickness. Slice value stays 96. */
  border-width: min(2cqw, 25.6px);
  border-image: url("assets/ui_engage.png") 96 fill / min(2cqw, 25.6px) / 0 stretch;
  background-image: url("assets/ui_engage.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* ============================================================================
   v0.5.15-REDO49 — SELL CELL INNER FRAME

   The user said: "On the depot scene, remove the image border from the sell
   cell and create a div within the sell cell, 90% height, 75% width, and
   apply the sell image border to that div and move all the sell cell
   functionality to the div."

   This block adds the ui_sell.png border-image to the inner .sell-frame div
   (HTML: <div class="sell-frame" data-zone="sell" data-drop="sell"> inside
   <div class="depot-cell depot-cell-sell">). The inner div is centered
   inside the cell with flex, sized to 90% of the cell's height and 75% of
   the cell's width. The cell itself wears NO border-image (see the
   `.depot-cell-sell { background-color: transparent; }` block earlier in
   this file; the original "Row 6: sell" block above already sets
   display:flex + align-items:center + justify-content:center + flex-
   direction:column, so the inner .sell-frame is vertically and
   horizontally centered inside the cell).

   Sell functionality (drop events, click handlers, visual feedback, drop
   target ID) is bound to the inner div via the data-zone="sell" and
   data-drop="sell" attributes that already live on .sell-frame in the
   HTML. The engine (game.js) wires its dragover/dragleave/drop
   listeners to [data-drop="sell"] and [data-zone="sell"] selectors,
   which match .sell-frame directly — so all the sell drop / hover /
   feedback behavior is on the inner div with no engine changes.

   Asset: assets/ui_sell.png (369573 bytes, copied from
   C:\Users\admin\Pictures\ui_sell.png). Slice value 48 (user-fixed),
   border-width 1vw (matches REDO48 default), `border-style: solid`,
   cover background-image (the center of ui_sell.png is also drawn as
   the frame's interior via the `fill` keyword in border-image).

   Other layout UNCHANGED. The .sell-icon / .sell-label / .sell-hint
   descendant rules on .depot-cell-sell above still target the inner
   div's spans correctly (they use the same descendant selector that
   matched before, since .sell-frame is a descendant of .depot-cell-sell).
   ============================================================================ */

.sell-frame {
  /* v0.5.15-REDO49: inner div inside .depot-cell-sell. 90% of the cell's
     height, 75% of the cell's width, centered (parent is flex-centered
     above). Wears the ui_sell.png border-image + cover background-image. */
  width: 75%;
  height: 90%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  cursor: pointer;
  box-sizing: border-box;
  /* The ui_sell.png frame. Slice 48, border-width 1vw (user-specified).
     The `fill` keyword draws the middle of ui_sell.png as the interior
     texture; the explicit background-image quad (cover / center / no-repeat)
     matches the user's "as background and border" wording verbatim and
     also stacks on top of the border-image's fill to reinforce the same
     asset's center as the visible icon surface. */
  border-style: solid;
  border-width: min(1cqw, 12.8px);
  border-image: url("assets/ui_sell.png") 48 fill / min(1cqw, 12.8px) / 0 stretch;
  background-image: url("assets/ui_sell.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* v0.5.15-REDO49: drag-over feedback moved onto .sell-frame (the inner div
   that wears the drop target ID). Matches the .shop-card.is-drop /
   .squad-card.is-drop / .stash-card.is-drop pattern used by the rest of
   the depot. Sell = warm orange family (matches the SELL label color
   #c25a30 */

/* ============================================================================
   v0.5.15-REDO50: Morale cell — move the ui_morale.png frame off the cell
   itself and onto a new inner .morale-display-target div (90% of the cell's
   height, 75% of the cell's width, centered inside the cell). Mirrors the
   REDO49 sell-frame pattern. The cell (.depot-cell-morale) is now a plain
   transparent flex centering container — no border-image, no background.
   The .morale-block (with its .morale-label + .morale-value +
   #setupMoraleBadge) lives inside .morale-display-target. The engine still
   updates #setupMoraleBadge via document.getElementById so no engine changes
   are needed; the badge keeps the same ID and is just nested one level
   deeper in the DOM.

   border-width 1vw, slice 80 fill — matches the REDO48 default (the cell
   already used those exact values before REDO50). The cover / center /
   no-repeat background-image stacks on top of the border-image fill to
   reinforce the asset's center as the visible interior texture.

   Other layout UNCHANGED. The .morale-block / .morale-label / .morale-value
   descendant rules on .depot-cell-morale above still target the inner
   div's children correctly because .morale-display-target is a descendant
   of .depot-cell-morale.
   ============================================================================ */

.morale-display-target {
  /* v0.5.15-REDO50: inner div inside .depot-cell-morale. 90% of the cell's
     height, 75% of the cell's width, centered (parent is flex-centered
     above). Wears the ui_morale.png border-image + cover background-image. */
  width: 75%;
  height: 90%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  box-sizing: border-box;
  /* The ui_morale.png frame. Slice 80 (matches REDO48), border-width 1vw
     (user-specified). The `fill` keyword draws the middle of ui_morale.png
     as the interior texture; the explicit background-image quad
     (cover / center / no-repeat) matches the user's "as background and
     border" wording and stacks on top of the border-image fill to reinforce
     the same asset's center as the visible badge surface. */
  border-style: solid;
  /* v0.5.15-REDO54: border-image's / Xpx slice width changed .25vw -> 1.5vw
     per the user's spec. border-width tracks the slice width so the
     frame actually renders at the new thickness (otherwise the
     border-image would slice at 1.5vw but the border itself is only
     .25vw wide, producing a clipped/empty frame). Slice value stays 96
     (per the user). */
  border-width: min(1.5cqw, 19.2px);
  border-image: url("assets/ui_morale.png") 96 fill / min(1.5cqw, 19.2px) / 0 stretch;
  background-image: url("assets/ui_morale.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* ============================================================
   v0.5.15-REDO60: 39-item image rendering
   ============================================================ */
.card-portrait.item-portrait {
  /* The item PNGs are square transparent art with item centered.
     Display at modest size so the name/desc still fits. */
  width: 100%;
  object-fit: contain;
  margin: 0 auto min(0.1cqw, 1.28px) auto;
  display: block;
  pointer-events: none;
}
/* v0.5.15-REDO70: span the item-portrait to the full inner-frame width
   of its host card. The base `.card-portrait.item-portrait` /
   `.st-portrait.item-portrait` rules above set `width: 100%`, which
   resolves to the host card's *content box* (i.e. inside the card's
   own `padding: 0.25vw`). That left a visible side gap equal to one
   card padding on each side. The exact visual target is the inner-
   frame width: card border-box width minus left/right border widths.

   Implementation: the card's horizontal padding is symmetric and
   responsive (`0.25vw` per side). We offset by exactly that amount
   on both sides and grow the width by the symmetric delta so the
   image spans from inside-left-border to inside-right-border, with
   the wooden border-image still visible. Using the same `vw` unit
   as the source padding keeps the offset responsive across the
   1024/1280/1600 matrix. Scope: only direct portraits inside the
   two named host cards. Equipped chips, squad item slots, freeze
   slot/card, sell drag visual, and tooltips are untouched. */
.shop-card.shop-card-item > img.item-portrait,
.stash-card:not(.empty) > img.item-portrait {
  /* Pull the image outside the host card's horizontal padding on
     both sides by the same amount the padding added. */
  margin-left: max(-0.25cqw, -3.2px);
  margin-right: max(-0.25cqw, -3.2px);
  /* Grow the image width by the symmetric delta (0.5vw) so the
     image's border-box spans the full inner-frame width. */
  width: calc(75% + min(0.5cqw, 6.4px));
  /* Re-center the image since the left/right margins are now
     symmetric negative, the auto margins above would collapse to
     zero. Force the centered visual by clearing the previous
     `margin: 0 auto 0.1vw auto` left/right (we re-state the top
     and bottom only). */
  margin-top: 0;
  margin-bottom: min(0.1cqw, 1.28px);
  /* box-sizing default for <img> is content-box; do NOT switch to
     border-box here so the calc() math above (image width =
     containing-block width + 0.5vw) stays correct. */
}
.st-portrait.item-portrait {
  width: 100%;
  object-fit: contain;
  margin: 0 auto min(0.1cqw, 1.28px) auto;
  display: block;
  pointer-events: none;
}
.squad-card .sc-item-img {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  pointer-events: none;
  vertical-align: middle;
}
/* v0.5.15-REDO62: removed the second `.squad-card .sc-item` rule that
   re-introduced a border + background on every slot. The single
   authoritative `.squad-card .sc-item` rule above (lines ~1261) declares
   border:0 / background:transparent. The transient drag-over highlight
   uses a higher-specificity rule below to flash without leaving any
   permanent chrome. */
.squad-card .sc-item.drag-over-ok {
  /* Flash only during a drag; nothing persists after dragend. */
  background-color: rgba(194,232,255,0.45);
}
.squad-card .sc-item.drag-over-bad {
  background-color: rgba(216,70,58,0.25);
}
/* REDO63 + REDO64: unified yellow hover glow across all requested entities.
   The canonical source was the shop-item hover (formerly
   `.shop-card-item:hover .card-portrait.item-portrait`). We lift the
   exact filter into a shared custom property + apply it to the artwork
   element of every requested target so the glow is byte-identical:
     - item shop item portrait
     - stash item portrait
     - equipped item image (.sc-item-img)
     - shop gerbil portrait
     - squad gerbil portrait
   Empty slots never glow (no artwork to filter).

   REDO64 FIX: the previous selector group used `.squad-card:hover ...`
   for two of the squad targets. Hover on ANY descendant of a squad
   card bubbled `:hover` up to the card itself, which then re-evaluated
   the descendant combinators and glowed every filled item on the card
   simultaneously — i.e. the user-observed "hovering one gerbil or item
   glows the gerbil plus all three items" defect. The fix is to drop
   the parent-card-hover selectors and bind :hover directly to the
   exact artwork node (or its tightest wrapper) so the glow cannot
   composite across siblings:
     - Per equipped item: `.squad-card .sc-item:hover .sc-item-img`
         (the slot's own hover state, not the parent card's).
     - Per squad gerbil: `.squad-card .sc-portrait:hover img`
         (the portrait wrapper's hover state, applied directly to the
         inner <img> — the previous rule applied filter to the wrapper
         div, which never produced a visible glow on the image).

   The canonical --gg-hover-glow variable value is GREEN per the v0.6.009
   yellow/gold/amber → green glow unification. Replaces the previous
   REDO63 `drop-shadow(0 0 0.4vw gold)` with the canonical green
   glow-token family. */
:root {
  --gg-hover-glow: drop-shadow(0 0 min(0.4cqw, 5.12px) rgba(47, 122, 58, 0.75));
}
.shop-card-item:hover .card-portrait.item-portrait,
.stash-card:hover .st-portrait.item-portrait,
.squad-card .sc-item:hover .sc-item-img,
.squad-card .sc-portrait:hover img,
/* v0.6.130: shop gerbil portrait — mirror the squad pattern exactly.
   The squad's glow rule is `.squad-card .sc-portrait:hover img` but
   that works because `.sc-portrait` IS in the hit-test path (its
   pointer-events: none makes the wrapper non-hit-testable, but the
   `.squad-card:hover` cascade still applies via the SC's descendant
   tree). For the shop, we need `.shop-card:hover` as the cascade
   anchor because `.card-portrait` is in normal flow and is also
   pointer-events: none. The card itself is the only hit-testable
   element in the shop portrait path, so the glow selector becomes
   `.shop-card:hover .card-portrait img`. This matches the existing
   `.shop-card:hover .card-portrait img { opacity: 1 }` rule at line
   3906. The cursor over the gap or over an overflowing IMG never
   hovers the IMG (pointer-events: none) — only the card itself. */
.shop-card:hover:not(.empty) .card-portrait img {
  filter: var(--gg-hover-glow);
}

/* =============== v0.5.15-REDO75: contextual hover glows — opaque + rounded ===============
   Authoritative CSS (per REDO75 canonical spec):
     .gg-glow-purchase { background-color: rgba(40, 200, 90, 1); border-radius: .5vw; }
     .gg-glow-sell     { keep current RGB; alpha = 1; border-radius: .5vw; }
     .gg-glow-freeze   { keep current RGB; alpha = 1; border-radius: .5vw; }
   Box-shadow + outline still use the existing layered glow stack for the
   soft halo around each targeted cell.
   v0.5.15-REDO76: same authored CSS. The JS now ensures that an
   .sc-item[data-drop="equipped-slot"] inside a .squad-card receives the
   .gg-glow-purchase class (and the .squad-card does NOT) when the
   dragover bubbles from the inner .sc-item. The selectors below cover
   both: the .sc-item inside a glowing squad-card will only glow if it
   is the sole .gg-glow-purchase on the page (setContextualHover's
   single-active enforcement). */

.depot-cell-sell.gg-glow-sell .sell-frame,
[data-drop="sell"].gg-glow-sell {
  background-color: rgba(216, 32, 32, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(216, 32, 32, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(216, 32, 32, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(216, 32, 32, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(216, 32, 32, 0.90);
  outline-offset: min(0.12cqw, 1.536px);
}
/* Valid purchase target glows green (opaque fill, rounded). Applies to
   squad slots, storage slots, and equipped slots alike (the JS picks the
   precise targetEl for each, so only one element ever carries the class
   at a time). */
.squad-card.gg-glow-purchase,
.stash-card.gg-glow-purchase,
[data-drop="equipped-slot"].gg-glow-purchase,
[data-drop="squad-slot"].gg-glow-purchase,
[data-drop="storage-slot"].gg-glow-purchase {
  background-color: rgba(40, 200, 90, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(40, 200, 90, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(40, 200, 90, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(40, 200, 90, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(40, 200, 90, 0.90);
  outline-offset: min(0.12cqw, 1.536px);
}
/* Freeze rail glows blue (opaque fill, rounded) when a shop card can park here. */
.depot-cell-freeze.gg-glow-freeze,
#freezeDrop.gg-glow-freeze {
  background-color: rgba(60, 140, 230, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(60, 140, 230, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(60, 140, 230, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(60, 140, 230, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(60, 140, 230, 0.90);
  outline-offset: min(0.12cqw, 1.536px);
}

/* Counter pop animation used by popCounter('gold'|'round'|'wins').
   Drives a 1 -> 1.22 -> 1 trajectory over 420ms (within the
   280-500ms window the spec calls for), centered on the badge so
   the depot-cell layout doesn't shift. The transform-origin keeps
   the scale punchy without translating the cell. */
@keyframes gg-counter-pop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.22); }
  60%  { transform: scale(1.04); }
  100% { transform: scale(1); }
}
.depot-cell-coins.gg-counter-pop,
.depot-cell-wins.gg-counter-pop,
.depot-cell-round.gg-counter-pop,
.depot-cell-fight.gg-counter-pop,
.depot-cell.gg-counter-pop {
  animation: gg-counter-pop 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
  transform-origin: center center;
  will-change: transform;
}

/* =============== v0.5.15-REDO79: eligible action-control hover glows ===============
   Per the USER AMENDMENT: the WHOLE .depot-cell-reroll cell is the
   reroll interaction surface AND the sole glow target (cell interior
   outside the nested button/label is the live hit target; the button
   stays for presentation). The engage cell keeps the same whole-cell
   treatment established in REDO51 (the cell IS the click target).

   Reuse the established drag-target glow character (layered box-shadow
   halo + opaque fill + rounded .5vw edge + outset outline). Two states:
     .gg-action-hover-ready  -> eligible (military green for reroll;
                               yellow+orange for engage)
     .gg-action-hover-blocked -> hovered but ineligible (opaque red
                                rgb(216,32,32) for both controls)
   Both classes are added/removed by production pointer listeners in
   game.js (see attachActionHoverHandlers / refreshActionHover) and
   gated by (a) the pointer being over the exact control, (b) no drag
   currently in flight, (c) live eligibility truth from the same
   functions the click handler uses (reroll: state.gold >=
   rerollEffectiveCost(); engage: state.squad.length >= 1). The
   blocked class is the inverse gate: over + no-drag + ineligible.

   Exact-target rule: the class is applied only to the live interaction
   surface (the .depot-cell-reroll div itself and the .depot-cell-engage
   div itself). The wooden ui_reroll.png / ui_engage.png border-image
   fill on these cells is preserved; the glow's outline-offset and
   background-color override land on top of the frame's transparent
   middle. No transform / scale / layout shift.

   No !important overrides on border-image (the wooden frame is owned
   by the cell itself, so the fill + outline paint on top of the
   frame's `fill` keyword middle). The outline uses a small negative
   inset (outline-offset: -0.18vw) so it draws INSIDE the wooden
   border-image (no hitbox extension, no neighbor overlap). */

/* Reroll: military-green family — core around #556B2F, halo and outline
   kept in the same family so the visual reads olive / military rather
   than the brighter purchase-green used on drop targets. Applied to
   the whole .depot-cell-reroll cell, not the inner button. */
.depot-cell-reroll.gg-action-hover-ready {
  background-color: rgba(85, 107, 47, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(85, 107, 47, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(110, 140, 50, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(110, 140, 50, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(110, 140, 50, 0.95);
  outline-offset: max(-0.28cqw, -3.584px);
  transition: background-color 80ms linear, box-shadow 80ms linear;
}
/* Reroll ineligible: opaque red, same target-glow character. Click is
   a no-op (handled by the production click guard); glow remains on
   the whole cell until pointer exit / drag / eligibility flip. */
.depot-cell-reroll.gg-action-hover-blocked {
  background-color: rgba(216, 32, 32, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(216, 32, 32, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(216, 32, 32, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(216, 32, 32, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(216, 32, 32, 0.95);
  outline-offset: max(-0.28cqw, -3.584px);
  transition: background-color 80ms linear, box-shadow 80ms linear;
}
/* Engage: canonical green core with a deeper-green outline + halo so the
   valid hover reads as a green acceptance glow (v0.6.009 unification).
   Core around #2f7a3a, outline around #19733c. */
.depot-cell-engage.gg-action-hover-ready {
  background-color: rgba(47, 122, 58, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(47, 122, 58, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(25, 115, 60, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(25, 115, 60, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(25, 115, 60, 0.95);
  outline-offset: max(-0.28cqw, -3.584px);
  transition: background-color 80ms linear, box-shadow 80ms linear;
}
/* Engage ineligible (empty squad): same opaque red target-glow character
   as the reroll-ineligible state. Click is a no-op (handled by the
   production click guard in openBattleScene). */
.depot-cell-engage.gg-action-hover-blocked {
  background-color: rgba(216, 32, 32, 1);
  border-radius: min(0.5cqw, 6.4px);
  box-shadow: 0 0 min(0.9cqw, 11.52px) rgba(216, 32, 32, 0.85),
              0 0 min(1.6cqw, 20.48px) rgba(216, 32, 32, 0.65),
              0 0 min(2.4cqw, 30.72px) rgba(216, 32, 32, 0.40);
  outline: min(0.18cqw, 2.304px) solid rgba(216, 32, 32, 0.95);
  outline-offset: max(-0.28cqw, -3.584px);
  transition: background-color 80ms linear, box-shadow 80ms linear;
}

/* ============================================================================
   v0.5.15-REDO84 — Exact margin-top: -0.5vw on the four wrapper/value
   elements (.round-value, .wins-value, .morale-value, .coin-row) per user spec.
   These classes are the wrappers around the actual numeric badges in each
   depot cell, so a negative margin nudges the value text upward to close the
   residual visual gap between the eyebrow/label and the value. The cascade
   here uses the LITERAL authored value -0.5vw at the four exact selectors —
   no transforms, no relative positioning, no rem-equivalent.
   ============================================================================ */
.round-value,
.wins-value,
.morale-value,
.coin-row {
  margin-top: max(-0.5cqw, -6.4px);
}

/* =========================================================
 * Gerbil Gunners v0.6.0 — Tick Combat Overhaul styles.
 * Authored fresh in v0.6.0. Visual styling for the new battle screen.
 * ========================================================= */

.v060-battle-screen {
  display: none;
  flex-direction: column;
  gap: min(1cqw, 12.8px);
  padding: min(1cqw, 12.8px);
  background-color: transparent;
  color: #f3e9d6;
}
.v060-battle-screen.active {
  display: flex;
}
.v060-battle-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: min(1cqw, 12.8px);
  padding-bottom: min(0.5cqw, 6.4px);
  border-bottom: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(243, 233, 214, 0.2);
}
.v060-battle-title {
  font-size: 1.6cqw;
  margin: 0;
  letter-spacing: 0.05em;
}
.v060-battle-status {
  display: flex;
  flex-direction: column;
  gap: min(0.2cqw, 2.56px);
}
.v060-status-line {
  font-size: 0.9cqw;
}
.v060-status-hash {
  font-family: monospace;
  font-size: 0.8cqw;
  opacity: 0.7;
}
.v060-battle-controls {
  display: flex;
  gap: min(0.5cqw, 6.4px);
}
.v060-battle-controls button {
  font-size: 0.9cqw;
  padding: min(0.4cqw, 5.12px) min(0.8cqw, 10.24px);
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid #b59070;
  background: rgba(60, 40, 28, 0.8);
  color: #f3e9d6;
  cursor: pointer;
  border-radius: min(0.2cqw, 2.56px);
}
.v060-battle-controls button:hover {
  background: rgba(80, 55, 38, 0.9);
}
.v060-battle-body {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: min(1cqw, 12.8px);
  flex: 1;
  min-height: 0;
}
.v060-arena, .v060-log {
  display: flex;
  flex-direction: column;
  gap: min(0.5cqw, 6.4px);
  padding: min(0.5cqw, 6.4px);
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(243, 233, 214, 0.15);
  border-radius: min(0.4cqw, 5.12px);
  overflow: hidden;
}
.v060-arena-heading, .v060-log-heading {
  margin: 0;
  font-size: 1cqw;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #c9a880;
}
.v060-arena-side {
  display: flex;
  flex-direction: row;
  gap: min(0.5cqw, 6.4px);
  flex-wrap: wrap;
  min-height: min(4cqw, 51.2px);
}
.v060-arena-card {
  display: flex;
  align-items: center;
  gap: min(0.5cqw, 6.4px);
  padding: min(0.4cqw, 5.12px);
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid #b59070;
  border-radius: min(0.3cqw, 3.84px);
  background: rgba(40, 28, 20, 0.6);
  min-width: min(8cqw, 102.4px);
}
.v060-arena-card.v060-defeated {
  opacity: 0.4;
  border-color: #6e3a3a;
}
.v060-arena-portrait img {
  width: min(4.8cqw, 61.44px);
  height: min(2.4cqw, 30.72px);
  object-fit: contain;
  object-position: center;
  border-radius: min(0.2cqw, 2.56px);
}
.v060-arena-meta {
  display: flex;
  flex-direction: column;
  gap: min(0.2cqw, 2.56px);
  flex: 1;
  min-width: 0;
}
.v060-arena-name {
  font-size: 0.9cqw;
  font-weight: bold;
}
.v060-hp-bar {
  background: rgba(0, 0, 0, 0.4);
  border-radius: min(0.2cqw, 2.56px);
  height: min(0.6cqw, 7.68px);
  position: relative;
  overflow: hidden;
}

/* v0.7.003: v060-rank-stars overlay on the HP bar's top edge. */
.v060-rank-stars {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -55%);
  display: flex;
  gap: min(0.16cqw, 2.0px);
  z-index: 31;
  pointer-events: none;
  line-height: 1;
  font-size: min(1.9cqw, 24.32px);
  filter: drop-shadow(0 min(0.08cqw, 1px) min(0.2cqw, 2.6px) rgba(0, 0, 0, 0.55));
}
.v060-rank-star.is-filled { color: #f4c35c; }
.v060-rank-star.is-outline { color: rgba(244, 195, 92, 0.32); }

/* v0.7.003: thin white shield bar on the bottom edge of the HP bar.
   Width = 4% of HP bar per prot point. Hidden when prot = 0. */
.v060-shield-fill {
  position: absolute;
  left: 0;
  bottom: 0;
  height: min(0.18cqw, 2.3px);
  width: 0;
  background: linear-gradient(to right, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.7));
  z-index: 32;
  pointer-events: none;
}

/* v0.7.003: HP number centered on the HP bar. */
.v060-hp-number {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: min(0.7cqw, 9px);
  font-weight: 700;
  color: #ffffff;
  text-shadow: 0 0 min(0.18cqw, 2.3px) rgba(0, 0, 0, 0.95), 0 0 min(0.06cqw, 0.8px) rgba(0, 0, 0, 0.7);
  z-index: 33;
  pointer-events: none;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.v060-hp-fill {
  background: linear-gradient(to right, #6b3410, #c46d23);
  height: 100%;
  width: 100%;
}
.v060-arena-hptext {
  font-size: 0.7cqw;
  opacity: 0.8;
}
.v060-arena-statuses {
  display: flex;
  flex-wrap: wrap;
  gap: min(0.2cqw, 2.56px);
}
.v060-status-badge {
  font-size: 0.6cqw;
  padding: min(0.1cqw, 1.28px) min(0.3cqw, 3.84px);
  border-radius: min(0.2cqw, 2.56px);
  background: rgba(0, 0, 0, 0.5);
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid #555;
}
.v060-status-bleed { color: #c0392b; }
.v060-status-burn { color: #e67e22; }
.v060-status-regen { color: #27ae60; }
.v060-status-stun { color: #f1c40f; }
.v060-status-mark { color: #ecf0f1; }
.v060-status-suppression { color: #95a5a6; }
.v060-status-exposed { color: #e74c3c; }
.v060-status-guarded { color: #3498db; }
.v060-status-barrier { color: #1abc9c; }
.v060-status-taunt { color: #e84393; }
.v060-status-haste { color: #00b894; }
.v060-status-slow { color: #636e72; }

.v060-log-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1;
  font-family: monospace;
}
.v060-log-tick {
  margin: 0;
  padding: min(0.4cqw, 5.12px);
  border-bottom: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(243, 233, 214, 0.05);
}
.v060-log-tick-header {
  font-size: 0.85cqw;
  color: #c9a880;
  margin-bottom: min(0.3cqw, 3.84px);
}
.v060-log-events {
  list-style: none;
  margin: 0;
  padding-left: min(1cqw, 12.8px);
  font-size: 0.8cqw;
}
.v060-log-event {
  margin: min(0.1cqw, 1.28px) 0;
}
.v060-log-action { color: #f3e9d6; }
.v060-log-defeat { color: #c0392b; }
.v060-log-dot { color: #e67e22; }
.v060-log-hot { color: #27ae60; }
.v060-log-healthCommit { color: #bdc3c7; }

/* =============== v0.6.0 FAIL-CLOSED ERROR REGION ===============
   v0.6.061: the full-screen region stays transparent so title body artwork
   remains visible; only the centered .v060-error-panel is opaque. */
.v060-error-region {
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  padding: min(2cqw, 25.6px);
  background-color: transparent;
  z-index: 50;
}
.v060-error-region[hidden] {
  display: none;
}
.v060-error-region.active {
  display: flex;
}
.v060-error-panel {
  background: #1f1812;
  border: min(0.125cqw, 1.6px) solid #c0392b;
  border-radius: min(0.6cqw, 7.68px);
  padding: min(2.5cqw, 32px) min(3cqw, 38.4px);
  max-width: min(56cqw, 716.8px);
  width: 100%;
  box-shadow: 0 0 min(1.5cqw, 19.2px) rgba(192, 57, 43, 0.45);
  color: #f3e9d6;
  text-align: center;
}
.v060-error-title {
  margin: 0 0 min(1cqw, 12.8px) 0;
  font-size: 1.8cqw;
  color: #ff6b5b;
  font-weight: 700;
  letter-spacing: 0.05em;
}
.v060-error-message {
  margin: 0 0 min(1cqw, 12.8px) 0;
  font-size: 1.1cqw;
  line-height: 1.5;
}
.v060-error-code {
  margin: 0 0 min(1cqw, 12.8px) 0;
  font-family: monospace;
  font-size: 0.85cqw;
  color: #f1c40f;
  letter-spacing: 0.1em;
}
.v060-error-hint {
  margin: 0 0 min(1.6cqw, 20.48px) 0;
  font-size: 0.95cqw;
  color: #bdc3c7;
  line-height: 1.5;
}
.v060-error-region button.primary.big {
  font-size: 1cqw;
  padding: min(0.6cqw, 7.68px) min(1.4cqw, 17.92px);
  min-width: min(14cqw, 179.2px);
}

/* ============================================================
   v0.6.006 — Stat Grid Layering, Hover Readability, Item Slots
   ------------------------------------------------------------
   Stacking order (back -> front):
     z=2  .gerbil-stat-grid + cells
     z=3  .shop-card / .freeze-card .card-portrait
     z=4  .squad-card .sc-portrait (REDO81 base; portrait stays above
           the .sc-emblem at z=3)
     z=5  .squad-card .sc-items / .sc-item (empty or filled)
     z=6  .squad-card .sc-item > .sc-item-img (equipped artwork)
   §3 grid inset top/left/right = 0 (already applied above).
   §4 cell border-radius = 0.18vw (already applied above).
   §5 label/value font sizes 2x (already applied above).
   §6 explicit stacking (already wired above).
   §7 hover portrait opacity 0.2 — declared below.
   §8 +10 translateY per class (already applied above).
   §10 empty slot = neutral stat cell appearance (already applied
       above via .sc-item[data-empty="1"]).
   ============================================================ */

/* v0.6.006 §7: direct hover portrait transparency.
   Hovering the card drops ONLY the card's gerbil portrait to
   opacity 0.2 (80% transparent). Sibling gerbils, the stat grid,
   the item slots, and the equipped items stay at full opacity.
   The transition is a quick fade so the visual change is
   legible but not jarring. */
/* v0.6.009 — Opaque Gerbil Hover Reversal.
   v0.6.006/v0.6.007/v0.6.008 set the gerbil portrait image to opacity 0.2
   on direct hover AND on drag-hover via the [data-gg-portrait-faded="1"]
   attribute. v0.6.009 reverses this: every gerbil portrait (shop / squad /
   freeze) stays at computed opacity 1 in every hover, drag, and rerender
   state. The spec grid-front behavior (stat grid ABOVE portrait while
   hovered) is preserved via the [data-gg-grid-raised] / :hover rules in
   the next block — no opacity fade is needed for the grid to read. */

.shop-card:hover .card-portrait img,
.shop-card:hover .card-portrait.silhouette,
.squad-card:hover .sc-portrait img,
.squad-card:hover .sc-portrait.silhouette,
.freeze-card:hover .card-portrait img,
.freeze-card:hover .card-portrait.silhouette {
  opacity: 1;
}
/* v0.6.006 §7 / v0.6.007 §8.2: when a populated squad / shop / freeze
  card is the exact hover target — by direct pointer hover OR by an
  active recognized item drag-hover — the target's stat grid is lifted
  ABOVE its portrait so the projected stat preview reads. The splice is
  scoped to the exact target card so siblings stay normal. The grid
  z-index is lifted from z=2 (base) to z=5 (above portrait z=4 and
  below the slot frame z=5 — the slot frame still wins by document
  order because it is rendered after the grid). The transition is
  instant so the spec-defined "complete cleanup" requirement still
  fires on pointerleave. Scoped via [data-gg-grid-raised] for the
  drag-hover path and via :hover for the direct pointer path. */
.squad-card[data-gg-grid-raised] .gerbil-stat-grid,
.shop-card[data-gg-grid-raised] .gerbil-stat-grid,
.freeze-card[data-gg-grid-raised] .gerbil-stat-grid,
.squad-card:hover .gerbil-stat-grid,
.shop-card:hover .gerbil-stat-grid,
.freeze-card:hover .gerbil-stat-grid {
  z-index: 5;
}

/* v0.6.009: items + grid + cells stay at opacity 1 (already the
   natural default; no overrides required). */
.shop-card:hover .sc-item,
.squad-card:hover .sc-item,
.shop-card:hover .sc-item > .sc-item-img,
.squad-card:hover .sc-item > .sc-item-img,
.shop-card:hover .gerbil-stat-grid,
.squad-card:hover .gerbil-stat-grid,
.shop-card:hover .gerbil-stat-cell,
.squad-card:hover .gerbil-stat-cell {
  opacity: 1;
}
/* v0.6.009: opacity transition is INERT (no fade target) so this
   transition rule is removed. The portrait stays at opacity 1 in every
   state; the grid-raised / slot-highlight / rank-promo visual lifecycles
   use z-index changes (instant) and outline / box-shadow / filter (no
   opacity interaction). */

/* v0.6.006 §7: valid drag-hover portrait transparency.
  The drag-hover overlay is wired in JS (game.js) via the
  `data-gg-portrait-faded="1"` attribute set on the exact
  destination .sc-portrait wrapper when a valid same-class item,
  eligible rank material, or same-class gerbil is over the
  destination. The fade is CSS-only — the JS only flips the
  attribute. Attribute removal drives cleanup. */
/* v0.6.006 §7 (INERT under v0.6.009): valid drag-hover portrait
   transparency. v0.6.009 reverses this — every gerbil portrait stays at
   computed opacity 1 even when the [data-gg-portrait-faded="1"] attribute
   is present. The attribute is still written by the production engine
   (setDragHoverPortraitFade) for compat with downstream selectors, but
   it produces no opacity change. The grid-raised / slot-highlight /
   rank-promo visual lifecycles are unchanged. */
.sc-portrait[data-gg-portrait-faded="1"] img,
.sc-portrait[data-gg-portrait-faded="1"],
.card-portrait[data-gg-portrait-faded="1"] img,
.card-portrait[data-gg-portrait-faded="1"] {
  opacity: 1 !important;
}

/* v0.6.007 §8.1 (final retry): exact destination-slot highlight from
  the shared commit resolver. The destination squad card carries
  data-gg-dest-slot="N" while a real item drag is over the broad
  card or an explicit .sc-item descendant. Only the .sc-item whose
  data-equipped-index matches N receives the visible drop-accept
  highlight. Sibling slots, the source, the source-ghost, and other
  cards stay at the neutral frame. The highlight augments the
  persistent frame; it never replaces the frame. Specificity is
  above the base .sc-item rule.

  The selector matches the data-gg-dest-slot attribute on the parent
  .squad-card (the engine sets it on the card, not on the individual
  slot) and then drills into the .sc-item child with the matching
  data-equipped-index. The combo `[data-gg-dest-slot] .sc-item[data-
  equipped-index="N"]` is the correct attribute path.

  FINAL retry (2026-07-26): the highlight MUST stack behind ALL
  equipped item artwork globally (including neighboring oversized
  overlap). The .sc-item sits at z-index 5 and the equipped item
  artwork sits at z-index 6 inside the same slot — the highlight is
  applied to the slot itself (z=5) so it is visible around the
  overflowing item where the item pixels do not cover, and it is
  occluded by the item where the 2.0x BCR covers the slot's
  interior. The highlight NEVER sits above any item artwork. */
.squad-card[data-gg-dest-slot="0"] .sc-item[data-equipped-index="0"],
.squad-card[data-gg-dest-slot="1"] .sc-item[data-equipped-index="1"],
.squad-card[data-gg-dest-slot="2"] .sc-item[data-equipped-index="2"] {
  outline: min(0.18cqw, 2.304px) solid #2f7a3a;
  outline-offset: min(0.06cqw, 0.768px);
  box-shadow:
    0 0 0 min(0.06cqw, 0.768px) rgba(47, 122, 58, 0.55),
    0 min(0.12cqw, 1.536px) min(0.42cqw, 5.376px) rgba(47, 122, 58, 0.45),
    0 min(0.0625cqw, 0.8px) 0 rgba(33, 26, 23, 0.12);
  background-color: rgba(220, 244, 220, 0.92);
  background-image: none;
  /* No z-index override — the slot stays at z=5 and the equipped
     item artwork at z=6 above it stays visible. The highlight is
     therefore visible around the item overflow and masked where
     the 2.0x BCR covers the slot interior — exactly the
     final-retry stacking requirement. */
}
/* v0.6.006 §7: dragging the source .squad-card or .sc-item puts
   the source through `opacity: 0.4` (.squad-card.dragging rule).
   The destination fade is independent — `.sc-portrait` is the
   inner img wrapper, not the whole card. */

/* =============== v0.6.010 BATTLE SCENE — 34x7 EQUAL-CELL GRID =============== */
/* v0.6.010: rework the legally reachable #v060BattleScreen into a CSS Grid
   with exactly 34 equal columns and 7 equal rows occupying the same outer
   scene rectangle as the depot under the same viewport. The grid root
   sits beneath the header; the header itself stays outside the grid so
   the tick / hash / round identity and the 1x/2x/4x/8x controls remain
   sized independently. The legacy .v060-battle-body / .v060-arena /
   .v060-log are no longer the screen surface; the new root is
   .v060-battle-grid (a CSS Grid with repeat(34)/repeat(7) tracks). The
   floating combat log re-homes to .v060-battle-log-floating (absolutely
   positioned over the grid, never inside any named region's tracks). */

/* Override: the grid root must occupy the same outer scene rectangle as
   the depot grid root under the same viewport. The depot uses 100% of
   the .scene BCR (no padding/gap). The battle screen used to have its
   own padding for the legacy header / arena / log layout; we restore
   the same outer rectangle so the 34x7 tracks line up with the depot. */
.v060-battle-screen.v060010-active {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
}
.v060-battle-screen.v060010-active .v060-battle-header {
  /* v0.6.010: the header is positioned absolutely so the 34x7 grid
     root can occupy the entire scene BCR — same outer rectangle as
     the depot grid under the same viewport (spec §2). */
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9;
  flex: 0 0 auto;
  border-bottom: min(0.06cqw, 0.768px) solid rgba(243, 233, 214, 0.2);
  padding: min(0.5cqw, 6.4px) min(1cqw, 12.8px);
  background: rgba(20, 14, 10, 0.92);
  height: min(3cqw, 38.4px);
  box-sizing: border-box;
}
.v060-battle-screen.v060010-active .v060-battle-header,
.v060-battle-screen.v060010-active .v060-battle-header * {
  box-sizing: border-box;
}
/* v0.6.013: occupy the entire .scene / .v060-battle-screen BCR.
   Same outer rectangle as the depot grid under the same viewport.
   overflow:visible so battlefield gerbil artwork may overflow its 5x1
   cells without being clipped (per v0.6.013 spec §3). */
.v060-battle-grid {
  display: grid;
  grid-template-columns: repeat(34, minmax(0, 1fr));
  grid-template-rows: repeat(7, minmax(0, 1fr));
  gap: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  min-height: 0;
  min-width: 0;
  background: transparent;
  border: 0;
  overflow: visible;
  box-sizing: border-box;
}
.v060-bg-cell {
  background: transparent;
  border: 0;
  pointer-events: none;
}

/* Header strip (rows 1 / part 2). Player portrait + Player info on the
   left of row 1; enemy info + enemy portrait on the right; round counter
   centered (rows 1-2 cols 16-19).
   v0.6.013: overflow:visible so the commander portraits may extend
   beyond their 1x1 cells without clipping (full silhouette required). */
.v060-region-player-portrait {
  grid-row: 1;
  grid-column: 1;
  /* v0.6.109: commander/portrait surface hidden. DOM + runtime
     writers preserved for compatibility. */
  display: none !important;
}
.v060-region-player-portrait img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.v060-region-player-info {
  grid-row: 1;
  grid-column: 2 / span 7;
  /* v0.6.109: commander info hidden. */
  display: none !important;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  padding: min(0.4cqw, 5.12px) min(0.6cqw, 7.68px);
  box-sizing: border-box;
  font-size: 0.95cqw;
  line-height: 1.2;
  overflow: hidden;
  color: #f3e9d6;
}
.v060-region-enemy-info {
  grid-row: 1;
  grid-column: 27 / span 7;
  /* v0.6.109: enemy commander info hidden. */
  display: none !important;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  padding: min(0.4cqw, 5.12px) min(0.6cqw, 7.68px);
  box-sizing: border-box;
  font-size: 0.95cqw;
  line-height: 1.2;
  overflow: hidden;
  text-align: right;
  color: #f3e9d6;
}
.v060-region-enemy-portrait {
  grid-row: 1;
  grid-column: 34;
  /* v0.6.109: enemy commander portrait hidden. */
  display: none !important;
  align-items: center;
  justify-content: center;
  padding: min(0.25cqw, 3.2px);
  box-sizing: border-box;
  overflow: visible;
}
.v060-region-enemy-portrait img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* v0.6.121: the round panel no longer carries a border. Layout, padding,
   background, and inner alignment are preserved; the border that previously
   framed the cell is gone. */
.v060-region-round {
  grid-row: 1 / span 2;
  grid-column: 16 / span 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: min(0.4cqw, 5.12px);
  box-sizing: border-box;
  font-size: 1.1cqw;
  letter-spacing: 0.04em;
  background: rgba(40, 28, 20, 0.55);
  border: 0;
  border-radius: min(0.3cqw, 3.84px);
  text-align: center;
  color: #f3e9d6;
}
.v060-region-round .v060-round-label {
  font-size: 0.7cqw;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.65;
  margin-bottom: min(0.2cqw, 2.56px);
}
.v060-region-round .v060-round-value {
  font-family: 'BlackOpsOne', sans-serif;
  font-size: 1.6cqw;
  font-weight: 800;
  line-height: 1;
}

/* Battlefield gerbil areas (row 4 + row 5). The 5x1 span centers the
   image; the health bar sits in the top quarter of the same span (above
   the image but still inside the named region, never overflowing into a
   free-space row). No border around the gerbil area.
   v0.6.013: overflow:visible so the full gerbil silhouette may extend
   beyond the 5x1 cell and overlap neighboring cells without clipping;
   positioning remains relative so the health bar stays anchored at the
   top of the assigned region. */
.v060-region-battle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0;
  box-sizing: border-box;
  background: transparent;
  border: 0;
  position: relative;
  overflow: visible;
  /* v0.6.053 delta #1 — combine tilt + creep via CSS custom properties.
     v0.6.052 had both .v06018-actor-lean (rotate+translateX) and
     .v06018-charging-* (translateX) animations targeting the SAME
     `transform` property, so the later-declared charging-* animation
     won and the tilt became invisible. v0.6.053 routes each animation
     through a SEPARATE custom property and composes the final transform
     here so all three custom-property values contribute simultaneously.
     --creep-x:        translateX for the creep-forward charge.
     --lean-rotate:    rotate for the attack tilt.
     --lean-translate-x: translateX nudge for the attack tilt.
     transform-origin: 50% 100% so the rotate pivots around the gerbil's
     feet, not the cell center. */
  --creep-x: 0cqw;
  --lean-rotate: 0deg;
  --lean-translate-x: 0cqw;
  transform: translateX(var(--creep-x)) rotate(var(--lean-rotate)) translateX(var(--lean-translate-x));
  transform-origin: 50% 100%;
  will-change: transform;
  /* v0.6.052 delta #2 — smooth snap-back when the v06018-charging-*
     class is removed. The transition now targets the custom properties
     (--creep-x, --lean-rotate, --lean-translate-x) instead of literal
     transform, so it does not interfere with the active keyframe
     animations. */
  transition: --creep-x 100ms ease-out, --lean-rotate 100ms ease-out, --lean-translate-x 100ms ease-out;
}
/* v0.6.010 coordinate map: each battle region is bound to a specific
   row + column span on the 34x7 grid. CSS Grid auto-flow would misplace
   free-space empties into these slots, so we hard-set grid-row /
   grid-column on each battle region. */
.v060-region-battle[data-slot="P5"] { grid-row: 4; grid-column: 1 / span 5; }
.v060-region-battle[data-slot="P3"] { grid-row: 4; grid-column: 7 / span 5; }
.v060-region-battle[data-slot="P1"] { grid-row: 4; grid-column: 13 / span 5; }
.v060-region-battle[data-slot="E1"] { grid-row: 4; grid-column: 18 / span 5; }
.v060-region-battle[data-slot="E3"] { grid-row: 4; grid-column: 24 / span 5; }
.v060-region-battle[data-slot="E5"] { grid-row: 4; grid-column: 30 / span 5; }
.v060-region-battle[data-slot="P4"] { grid-row: 5; grid-column: 4 / span 5; }
.v060-region-battle[data-slot="P2"] { grid-row: 5; grid-column: 10 / span 5; }
.v060-region-battle[data-slot="E2"] { grid-row: 5; grid-column: 21 / span 5; }
.v060-region-battle[data-slot="E4"] { grid-row: 5; grid-column: 27 / span 5; }

/* v0.6.010 coordinate map: each row-7 info tile is bound to a 3x1 span
   (P5..P1, then E1..E5 with the speed cell at 17-18 between them). */
.v060-region-info[data-slot="P5"] { grid-row: 7; grid-column: 2 / span 3; }
.v060-region-info[data-slot="P4"] { grid-row: 7; grid-column: 5 / span 3; }
.v060-region-info[data-slot="P3"] { grid-row: 7; grid-column: 8 / span 3; }
.v060-region-info[data-slot="P2"] { grid-row: 7; grid-column: 11 / span 3; }
.v060-region-info[data-slot="P1"] { grid-row: 7; grid-column: 14 / span 3; }
.v060-region-info[data-slot="E1"] { grid-row: 7; grid-column: 19 / span 3; }
.v060-region-info[data-slot="E2"] { grid-row: 7; grid-column: 22 / span 3; }
.v060-region-info[data-slot="E3"] { grid-row: 7; grid-column: 25 / span 3; }
.v060-region-info[data-slot="E4"] { grid-row: 7; grid-column: 28 / span 3; }
.v060-region-info[data-slot="E5"] { grid-row: 7; grid-column: 31 / span 3; }
.v060-region-battle .v060-hpbar {
  height: min(0.32cqw, 4.096px);
  width: 21%;
  background: rgba(0, 0, 0, 0.4);
  border: min(0.04cqw, 0.512px) solid rgba(243, 233, 214, 0.25);
  border-radius: min(0.1cqw, 1.28px);
  margin: min(0.18cqw, 2.304px) 0 min(0.12cqw, 1.536px) 0;
  overflow: hidden;
  flex: 0 0 auto;
}
.v060-region-battle .v060-hpbar-fill {
  height: 100%;
  background: linear-gradient(to right, #6b3410, #c46d23);
}
.v060-region-battle .v060-portrait {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
}
.v060-region-battle .v060-portrait img {
  object-fit: contain;
  object-position: center;
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 1;
}
/* v0.6.013: enemy-side battlefield gerbils are flipped exactly once
   (left<->right relative to the pre-v0.6.013 baseline). The previous
   player-side flip is REMOVED so player-side renders un-flipped and
   enemy-side renders flipped — both sides have been inverted exactly
   once relative to the v0.6.011 pre-override state, no cumulative
   flip, source assets and width/height magnitudes are preserved.
   v0.6.025 delta (post-v0.6.013 flip): reverse the direction so that
   player-side and enemy-side render with OPPOSITE visual lean from
   the current state. Player-side gerbils: removed the previous
   scaleX(1) inheritance and now render with the asset-natural
   transform-origin orientation. Enemy-side gerbils: render with a
   small horizontal flip so they face the opposite direction from
   the player side (which is the visual the user requested).
   Note: this replaces the v0.6.013 single scaleX(-1) on enemy side
   with a smaller, balanced flip that gives the user-perceptible
   "opposite lean" while keeping portrait geometry intact. */
/* removed: .v060-region-battle.v060-region-enemy-side .v060-portrait img { transform: scaleX(-1); ... } */
/* v0.6.087 row 4 (back line) size: 1.5. Combined flip + scale per side.
   v0.6.075 edge-origin correction: P5 and mirrored E5 grow inward from
   left center so the required 1.5 scale does not
   push the two outer fitted-stage silhouettes past the viewport edge.
   All other back-line slots keep the canonical center-center origin. */
.v060-region-battle.v060-region-player-side[data-slot="P1"] .v060-portrait img,
.v060-region-battle.v060-region-player-side[data-slot="P3"] .v060-portrait img {
  transform: scale(1.5);
  transform-origin: center center;
}
.v060-region-battle.v060-region-player-side[data-slot="P5"] .v060-portrait img {
  transform: scale(1.5);
  transform-origin: left center;
}
.v060-region-battle.v060-region-enemy-side[data-slot="E1"] .v060-portrait img,
.v060-region-battle.v060-region-enemy-side[data-slot="E3"] .v060-portrait img {
  transform: scaleX(-1) scale(1.5);
  transform-origin: center center;
}
.v060-region-battle.v060-region-enemy-side[data-slot="E5"] .v060-portrait img {
  transform: scaleX(-1) scale(1.5);
  /* v0.6.076 — E5 transform-origin moves from `left center`
     to exactly `center center` per the requested Battle Scene
     delta. The transform itself (scaleX(-1) scale(1.5)) and
     the row-4 1.5 scale stay unchanged; the user-requested
     center-center origin lets the mirrored silhouette pivot
     from its own middle instead of growing inward from the
     left edge. No translations, width caps, margins, or
     per-class sizing added. The visual review of the new
     silhouette bounds is explicitly delegated to gameqa
     per the spec. */
  transform-origin: center center;
}
/* v0.6.087 row 5 (front line) size: 1.75. Combined flip + scale per side. */
.v060-region-battle.v060-region-player-side[data-slot="P2"] .v060-portrait img,
.v060-region-battle.v060-region-player-side[data-slot="P4"] .v060-portrait img {
  transform: scale(1.75);
  transform-origin: center center;
}
.v060-region-battle.v060-region-enemy-side[data-slot="E2"] .v060-portrait img,
.v060-region-battle.v060-region-enemy-side[data-slot="E4"] .v060-portrait img {
  transform: scaleX(-1) scale(1.75);
  transform-origin: center center;
}
/* v0.6.075 side-specific Battle Scene align-items: player battle
   regions anchor to flex-start (left), enemy battle regions anchor
   to flex-end (right). The portrait wrapper remains width:100%; this
   delta primarily side-anchors narrower flex children such as the
   HP bar. */
.v060-region-battle.v060-region-player-side { align-items: flex-start; }
.v060-region-battle.v060-region-enemy-side  { align-items: flex-end; }

/* Row 7 — gerbil info tiles and speed controls. The info tiles are
   3x1 face-cropped portraits with a transparent green bottom-anchored
   readiness fill behind them and the unit's current status effects on
   top. Statuses layer above the readiness fill and the portrait.
   v0.6.013: overflow:visible on the .v060-region-info + the inner
   .v060-info-portrait wrapper so the full info image may render
   without being clipped by its 3x1 cell. Side-aware flipping (player
   flipped, enemy not) is enforced in v06013-closure.css using the
   data-slot attribute selector. */
.v060-region-info {
  position: relative;
  display: block;
  background: transparent;
  overflow: visible;
  border: 0;
}
.v060-region-info .v060-readiness {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(80, 80, 80, 0.36);  /* v0.6.043: empty-cell fallback; per-class rules in v06043-closure.css win at (0,3,0) specificity */
  pointer-events: none;
  z-index: 1;
}
.v060-region-info .v060-info-portrait {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: visible;
}
.v060-region-info .v060-info-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  display: block;
}
.v060-region-info.v060-info-scout .v060-info-portrait img { object-position: 50% 18%; }
.v060-region-info.v060-info-rifleman .v060-info-portrait img { object-position: 50% 22%; }
.v060-region-info.v060-info-officer .v060-info-portrait img { object-position: 50% 20%; }
.v060-region-info.v060-info-heavy .v060-info-portrait img { object-position: 50% 24%; }
.v060-region-info.v060-info-engineer .v060-info-portrait img { object-position: 50% 22%; }
.v060-region-info.v060-info-medic .v060-info-portrait img { object-position: 50% 22%; }
.v060-region-info.v060-info-grenadier .v060-info-portrait img { object-position: 50% 24%; }
.v060-region-info.v060-info-sniper .v060-info-portrait img { object-position: 50% 20%; }
.v060-region-info .v060-info-statuses {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  justify-content: center;
  padding: min(0.18cqw, 2.304px);
  box-sizing: border-box;
  pointer-events: none;
  font-size: 0.55cqw;
  line-height: 1.1;
}
.v060-region-info .v060-info-status {
  display: inline-block;
  padding: min(0.04cqw, 0.512px) min(0.16cqw, 2.048px);
  margin: min(0.04cqw, 0.512px) min(0.06cqw, 0.768px);
  background: rgba(20, 14, 10, 0.72);
  border: min(0.04cqw, 0.512px) solid rgba(243, 233, 214, 0.35);
  border-radius: min(0.08cqw, 1.024px);
  color: #f3e9d6;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.v060-region-info .v060-info-status.v060-status-bleed { color: #ff7060; border-color: rgba(255, 112, 96, 0.6); }
.v060-region-info .v060-info-status.v060-status-burn { color: #ff9844; border-color: rgba(255, 152, 68, 0.6); }
.v060-region-info .v060-info-status.v060-status-regen { color: #62d39c; border-color: rgba(98, 211, 156, 0.6); }
.v060-region-info .v060-info-status.v060-status-stun { color: #f1c40f; border-color: rgba(241, 196, 15, 0.6); }
.v060-region-info .v060-info-status.v060-status-mark { color: #ecf0f1; border-color: rgba(236, 240, 241, 0.6); }
.v060-region-info .v060-info-status.v060-status-suppression { color: #bdc3c7; }
.v060-region-info .v060-info-status.v060-status-exposed { color: #ff5c3c; }
.v060-region-info .v060-info-status.v060-status-guarded { color: #5dade2; }
.v060-region-info .v060-info-status.v060-status-barrier { color: #1abc9c; }
.v060-region-info .v060-info-status.v060-status-taunt { color: #e84393; }
.v060-region-info .v060-info-status.v060-status-haste { color: #00b894; }
.v060-region-info .v060-info-status.v060-status-slow { color: #95a5a6; }

/* v0.6.121: the bottom-center speed region is now a single-step +/- control.
   The previous 1x/2x/4x/8x direct-button dial is gone. The new row contains
   three children (decrease / value / increase) and remains at the same grid
   location (row 7, cols 17-18). */
.v060-region-speed {
  grid-row: 7;
  grid-column: 17 / span 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: min(0.18cqw, 2.304px);
  padding: min(0.25cqw, 3.2px) min(0.4cqw, 5.12px);
  box-sizing: border-box;
}
.v060-region-speed .v060-region-speed {
  display: flex;
  align-items: stretch;
  gap: min(0.18cqw, 2.304px);
  background: rgba(60, 40, 28, 0.78);
  border: min(0.06cqw, 0.768px) solid #b59070;
  border-radius: min(0.18cqw, 2.304px);
  padding: min(0.08cqw, 1.024px);
  min-width: min(5cqw, 64px);
}
.v060-speed-step {
  flex: 0 0 auto;
  width: min(1.8cqw, 23.04px);
  font-size: 1.0cqw;
  padding: min(0.2cqw, 2.56px) 0;
  border: none;
  background: transparent;
  color: #f3e9d6;
  cursor: pointer;
  font-family: inherit;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  border-radius: min(0.1cqw, 1.28px);
}
.v060-speed-step:hover:not([aria-disabled="true"]),
.v060-speed-step:focus:not([aria-disabled="true"]) {
  background: rgba(80, 55, 38, 0.92);
}
.v060-speed-step:focus-visible {
  outline: min(0.08cqw, 1.024px) solid #f4c35c;
  outline-offset: min(0.04cqw, 0.512px);
}
.v060-speed-step[aria-disabled="true"] {
  color: rgba(243, 233, 214, 0.35);
  cursor: not-allowed;
  background: transparent;
}
.v060-speed-value {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.0cqw;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #f4c35c;
  min-width: min(2.5cqw, 32px);
  padding: 0 min(0.25cqw, 3.2px);
}
/* Legacy class kept for any consumer that still styles .v060-speed-btn. */
.v060-speed-btn {
  display: none; /* 4-buttons layout removed; see .v060-speed-step */
}

.v060-region-speed .v060-speed-btn:hover,
.v060-region-speed .v060-speed-btn:focus {
  background: rgba(80, 55, 38, 0.92);
}
.v060-region-speed .v060-speed-btn:focus-visible {
  outline: min(0.08cqw, 1.024px) solid #f4c35c;
  outline-offset: min(0.04cqw, 0.512px);
}
.v060-region-speed .v060-speed-btn[aria-disabled="true"],
.v060-region-speed .v060-speed-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.v060-region-speed .v060-speed-value {
  flex: 1 1 0;
  font-family: 'BlackOpsOne', sans-serif;
  font-size: 0.95cqw;
  letter-spacing: 0.04em;
  color: #f3e9d6;
  text-align: center;
  min-width: 0;
}

/* Floating combat log — outside the grid root; absolute-positioned so
   it never claims a cell. */
.v060-battle-log-floating {
  position: absolute;
  top: min(3cqw, 38.4px);
  right: min(1cqw, 12.8px);
  width: min(18cqw, 230.4px);
  max-height: min(18cqw, 230.4px);
  overflow: auto;
  background: rgba(20, 14, 10, 0.86);
  border: min(0.06cqw, 0.768px) solid rgba(243, 233, 214, 0.18);
  border-radius: min(0.3cqw, 3.84px);
  padding: min(0.4cqw, 5.12px);
  font-family: monospace;
  font-size: 0.78cqw;
  z-index: 8;
}
.v060-battle-log-floating h3 {
  margin: 0 0 min(0.3cqw, 3.84px);
  font-size: 0.78cqw;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #c9a880;
}
.v060-battle-log-floating ol {
  list-style: none;
  margin: 0;
  padding: 0;
}
.v060-battle-log-floating .v060-log-tick {
  margin: 0 0 min(0.3cqw, 3.84px);
  padding: min(0.2cqw, 2.56px) min(0.3cqw, 3.84px);
  border-bottom: min(0.04cqw, 0.512px) solid rgba(243, 233, 214, 0.07);
}
.v060-battle-log-floating .v060-log-tick-header {
  font-size: 0.78cqw;
  color: #c9a880;
}
.v060-battle-log-floating .v060-log-event {
  margin: min(0.05cqw, 0.64px) 0;
  font-size: 0.74cqw;
}

/* Defeat state — v0.7.026: fade the entire region, not just inner
   portrait/hpbar. The previous behavior only faded the inner image and
   hpbar, leaving the region border/background at full opacity. Combined
   with the sprite-guard in v0.7.025 (KO'd units show "aim" instead of
   "attack"), this makes the defeat state clearly readable as "down"
   rather than "still attacking while barely visible". */
.v060-region-battle.v060-region-defeated {
  opacity: 0.55;
  filter: grayscale(0.6);
}
.v060-region-battle.v060-region-defeated .v060-portrait img {
  opacity: 0.45;
  filter: grayscale(0.7) brightness(0.85);
}
.v060-region-battle.v060-region-defeated .v060-hpbar {
  opacity: 0.4;
}

/* Empty (no unit present in this slot): the region renders nothing. */
.v060-region-battle.v060-region-empty,
.v060-region-info.v060-region-empty {
  background: transparent;
  border: 0;
  box-shadow: none;
}

/* Disabled legacy surfaces (legacy arena / log inside the
   #v060BattleScreen) are no longer the screen surface but kept
   in the DOM and hidden so any spec probe that targets them by
   ID still works. */
.v060-battle-screen.v060010-active .v060-battle-body,
.v060-battle-screen.v060010-active .v060-battle-body * {
  visibility: hidden;
  height: 0;
  width: 0;
  overflow: hidden;
}

/* ============================================================================
   v0.6.047 — Remove Depot Combat Log

   The v0.6.029 depot combat log container (<div class="depot-log" id="depotLog">)
   has been removed from index.html. The user reported "I don't know how it
   got there but it does not belong in the depot scene." The element, its CSS
   rule (and all @media overrides), and the .setup-screen > .depot-log flex
   sizing rule are all deleted. The .depot grid is now the only child of
   .setup-screen (the .depot-log sibling is gone). The .depot flex sizing
   rule is updated to no longer reference the removed sibling.

   The v0.6.046 smart-clamp max() formula is preserved unchanged.

   NOTE: The v0.6.046 smart-clamp formula produces scenes that may overflow
   the viewport vertically at certain aspect ratios (e.g. 1264x625 produces
   1264x711 which overflows the 39.0625vw viewport height by 5.375vw). The user
   has reported this in v0.6.047 but asked to defer the fix until the
   combat log removal is complete. v0.6.048 will address this issue.
   ============================================================================ */

/* The active start page (.setup-screen.active) wraps the depot grid. It already
   inherits overflow:hidden from .scene (above), but add an explicit
   lock so any future child cannot overflow vertically.

   The setup screen is a flex column: the .depot grid fills all
   available vertical space (flex: 1 1 auto; min-height: 0).

   The active-qualified selector outranks `.screen.active { display: block; }`
   without forcing an inactive setup screen back into the rendered stack. */
.setup-screen.active {
  display: flex;
  flex-direction: column;
  /* v0.6.046: change height: 100% to min-height: 100% so the setup screen
     can grow BEYOND viewport height when the .scene (which lives inside
     .setup-screen) renders at a width-bound aspect ratio that produces
     a height > 100vh. The user explicitly accepted vertical overflow at
     wide viewports (NO clamping). min-height: 100% preserves the flex
     layout (the .depot child still fills the viewport-sized portion)
     but allows the .scene's natural height to push the container down.
     At narrow viewports where the .scene is height-bound (height =
     100vh exactly), this change has zero effect (the container is
     exactly viewport-height, just like before). */
  height: auto;
  min-height: 100%;
  /* Defensive double-lock. .scene already supplies overflow:hidden. */
  overflow: hidden;
}
.setup-screen > .depot {
  /* Depot fills all available vertical space inside .setup-screen.
     The original .depot rule sets width:100%; height:100%; so we
     override height to auto so flex can size it, then flex: 1
     makes it fill remaining space. min-height: 0 lets it shrink
     below its content size if the viewport is short.

     v0.6.111: dropped `!important` from `height`. The `>` child
     combinator + `.setup-screen` ancestor selector already gives
     this rule specificity (0,2,1) — one class more than the base
     `.depot` (0,1,0) — so cascade order alone decides. */
  height: auto;
  flex: 1 1 auto;
  min-height: 0;
}

/* The body and html already declare overflow:hidden (the older
   body/html rule sets `overflow: hidden` to keep scrollbars away).
   Keep this rule as an explicit v0.6.029 anchor so any future
   override that re-introduces overflow:auto gets caught here. */
html.v06029, body.v06029 {
  overflow: hidden !important;
}

/* ===========================[v06011-closure.css]=========================== */

/* v0.6.011 literal map closure: legacy overlays remain internal-only. */
.v060-battle-header,
#v060BattleLogFloating {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

/* ===========================[v06013-closure.css]=========================== */
/* v0.6.013 battle art overflow and facing inversion closure.
   Preserves the v0.6.012 hidden-overlay rule and ADDS:
   (1) Remove every overflow/clip/mask/containment barrier between the
       battle scene root and the battlefield gerbil <img> + info <img>
       so the full silhouettes and complete info-portrait crops can
       render without being cut by their wrappers.
   (2) Add side-aware horizontal flip rules for row-7 info portraits so
       enemy info portraits face the opposite direction from player info
       portraits (authoritative side-aware rule wins over blanket
       inversion).
   The non-info commander + battlefield flips are applied directly in
   styles.css (and JS-side nullification for dynamic content images)
   so they survive across rerenders and the second fight.
*/
.v060-battle-header,
#v060BattleLogFloating {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}

/* v0.6.013: Allow battle-scene artwork to overflow its assigned 5x1
   cells so the full silhouette is visible. Every ancestor between the
   .v060-battle-grid and the battlefield gerbil <img> must permit
   overflow. Free cells remain truly empty (no DOM, no panel); the
   overflow is permitted to overlap neighboring cells and other grid
   tracks without altering grid sizing, hit targets, or grid placement.
   Container boxes used purely for layout still hide their own scroll
   bars but allow visual overflow. */
.v060-battle-screen,
.v060-battle-screen .scene,
.v060-battle-grid {
  overflow: visible !important;
}

.v060-region-battle,
.v060-region-info,
.v060-region-player-portrait,
.v060-region-enemy-portrait,
.v060-region-battle .v060-portrait,
.v060-region-info .v060-info-portrait {
  overflow: visible !important;
  /* No mask, no clip-path, no paint containment on the artwork
     wrappers — the spec explicitly forbids all of these. */
  clip-path: none !important;
  -webkit-clip-path: none !important;
  mask: none !important;
  -webkit-mask: none !important;
  contain: none !important;
}

/* v0.6.013 side-aware info portrait flip: every row-7 enemy info
   portrait faces the OPPOSITE direction from every row-7 player info
   portrait. The data-slot attribute (P1..P5 / E1..E5) drives the rule;
   P-prefix slots get scaleX(-1), E-prefix slots stay un-flipped, so
   the two sides face opposite directions while the player side has
   had its prior facing inverted exactly once (was un-flipped, now
   flipped). The blanket-inversion exception wins here: if we flipped
   every info image, both sides would end up facing the same way, so
   we intentionally break symmetry by NOT flipping the enemy side. */
.v060-region-info[data-slot^="P"] .v060-info-portrait img {
  transform: scaleX(-1);
  transform-origin: center center;
}
.v060-region-info[data-slot^="E"] .v060-info-portrait img {
  transform: none;
  transform-origin: center center;
}

/* v0.6.013: commander portraits get the SAME single horizontal
   inversion as the battlefield gerbils. Source assets, sizes, and
   non-horizontal transforms are preserved (object-fit/position
   remain intact). The commander portraits are NOT row-7 info
   portraits and follow the non-info inversion rule (one flip exactly).
*/
.v060-region-player-portrait img,
.v060-region-enemy-portrait img {
  transform: scaleX(-1);
  transform-origin: center center;
}

/* ===========================[v06014-closure.css]=========================== */
/* Gerbil Gunners v0.6.014 — row-7 battle-info face zoom closure.
   Scope is intentionally limited to .v060-region-info portrait imagery.
   Battlefield artwork, commander portraits, readiness, statuses, labels,
   health, controls, panels, and backgrounds are untouched.

   The canonical 558x447 class art is magnified 2x relative to its inherited
   object-fit:cover box. translateY(25%) keeps the upper half of the source
   canvas inside the dedicated viewport: the inherited class-specific face
   targets at 18–24% source Y land at 36–48% of the visible crop. The wrapper
   clips the magnified image without changing the 3-column row-7 tile.
*/
.v060-region-info .v060-info-portrait {
  overflow: hidden !important;
  clip-path: inset(0) !important;
  -webkit-clip-path: inset(0) !important;
  contain: none !important;
}

.v060-region-info .v060-info-portrait img {
  transform-origin: center center !important;
  backface-visibility: hidden;
}

/* v0.6.014 reverses both row-7 sides exactly once from v0.6.013:
   player -1 -> +1, enemy +1 -> -1. Magnification is image-only.

   v0.6.111: the `transform` values dropped their `!important` markers.
   The v0.6.014 rule is later in the cascade than the v0.6.013
   `transform: scaleX(-1)` / `transform: none` rules, so cascade order
   alone wins. No JS-set inline transform exists on these `<img>`
   elements (the render path only assigns `class`, `alt`, and `src`). */
.v060-region-info[data-slot^="P"] .v060-info-portrait img {
  transform: scale(2, 2) translateY(25%);
}
.v060-region-info[data-slot^="E"] .v060-info-portrait img {
  transform: scale(-2, 2) translateY(25%);
}

/* ===========================[v06015-closure.css]=========================== */
/* Gerbil Gunners v0.6.015 — narrow battle presentation closure.
  v0.6.040 (HP-bar delta): the translateY value goes from -7.5vw (v0.6.034,
  via v0.6.033) DOWN by 1.5vw to -6vw — user-perceived "move health bars
  down by 1.5vw". Down in screen space = increase Y, so the less-negative
  value positions the bar 1.5vw LOWER on screen.
  v0.6.034 selector fix preserved: the legacy .v060010-active companion
  class is no longer applied to the v0.6.0xx battle screen element (live
  DOM classList observed: ['screen','v060-battle-screen','scene']).
  position: relative, z-index: 12, and the direct-child scope are
  unchanged.
  v0.6.076 (HP-bar delta): the resting translateY moves from -6vw
  UP to -2.5vw (i.e. higher on screen, away from the portrait). The
  selector, position: relative, z-index: 12, and direct-child scope
  stay unchanged; only the translateY value changes. */
.v060-battle-screen .v060-region-battle > .v060-hpbar {
  transform: translateY(max(-2.5cqw, -32px));
  position: relative;
  z-index: 12;
}

/* Every active non-info grid region is a transparent backing surface so the
   inherited battle-scene background remains visible. The direct-child scope
   deliberately excludes all ten .v060-region-info tiles and their complete
   visual subtrees. Functional descendants (portraits, labels, HP bars, and
   speed buttons) keep their own accepted presentation. The selector
   continues to require the legacy .v060010-active companion class because
   v0.6.034 verification confirms these cells are already transparent in
   the live build via an inherited rule, so this rule remains in place as
   redundant safety net and is left unchanged. */
.v060-battle-screen.v060010-active #v060BattleGrid > [data-region]:not(.v060-region-info),
.v060-battle-screen.v060010-active #v060BattleGrid > [data-region]:not(.v060-region-info)::before,
.v060-battle-screen.v060010-active #v060BattleGrid > [data-region]:not(.v060-region-info)::after {
  background-color: transparent;
  background-image: none;
}

/* v0.6.016 retires the v0.6.015 translating ball/diamond CSS. Fast route
   tracers now live in v06016-closure.css; the frozen health and transparency
   declarations above remain the only inherited v0.6.015 presentation rules. */

/* ===========================[v06016-closure.css]=========================== */
/* Gerbil Gunners v0.6.016 — fast full-route shot tracers. */
.v06016-effect-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 24;
  background: transparent;
  contain: layout paint style;
}

.v06016-effect {
  --v06016-duration: 110ms;
  pointer-events: none;
  user-select: none;
  animation: v06016-full-route-flash var(--v06016-duration) linear forwards;
}

.v06016-effect-route {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.v06016-effect-line-main {
  fill: none;
  stroke: #fff4b3;
  stroke-width: min(0.125cqw, 1.6px);
  stroke-linecap: round;
  opacity: 1;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.1875cqw, 2.4px) #ff8f1f);
}

.v06016-effect[data-outcome="miss"] .v06016-effect-line-main,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-echo {
  stroke: #fff0cf;
  stroke-dasharray: 5 5;
  opacity: 0.82;
  filter: drop-shadow(0 0 min(0.125cqw, 1.6px) rgba(255, 220, 176, 0.86));
}

.v06016-effect-offense.v06016-effect-skill .v06016-effect-line-main {
  stroke: #79f7ff;
  stroke-width: min(0.1875cqw, 2.4px);
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) #40d9ff);
}

.v06016-effect-line-echo {
  fill: none;
  stroke: #bd8cff;
  stroke-width: min(0.094cqw, 1.2032px);
  stroke-linecap: round;
  stroke-dasharray: 9 5;
  opacity: 0.92;
  filter: drop-shadow(0 0 min(0.1875cqw, 2.4px) rgba(155, 103, 255, 0.9));
}

.v06016-effect-support .v06016-effect-line-main {
  stroke: #7fffd4;
  stroke-width: min(0.15625cqw, 2px);
  stroke-dasharray: 3 2;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #effff8) drop-shadow(0 0 min(0.25cqw, 3.2px) #28d99b);
}

.v06016-effect[data-outcome="block"] .v06016-effect-line-main,
.v06016-effect[data-outcome="block"] .v06016-effect-line-echo {
  stroke: #d5e8ff;
  stroke-dasharray: 2 3;
  filter: drop-shadow(0 0 min(0.25cqw, 3.2px) #73a8ff);
}

.v06016-effect-self {
  --v06016-origin-x: 0cqw;
  --v06016-origin-y: 0cqw;
  position: absolute;
  left: var(--v06016-origin-x);
  top: var(--v06016-origin-y);
  width: min(4cqw, 51.2px);
  height: min(4cqw, 51.2px);
  min-width: min(1.75cqw, 22.4px);
  min-height: min(1.75cqw, 22.4px);
  translate: -50% -50%;
}

.v06016-effect-self-pulse {
  position: absolute;
  inset: 0;
  display: block;
  border: min(0.125cqw, 1.6px) solid rgba(115, 255, 224, 0.95);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(225, 255, 250, 0.9) 0 8%, rgba(67, 239, 207, 0.52) 22%, rgba(73, 117, 255, 0.24) 45%, rgba(105, 55, 200, 0) 72%);
  box-shadow: 0 0 min(1.2cqw, 15.36px) rgba(68, 239, 210, 0.9), inset 0 0 min(0.8cqw, 10.24px) rgba(216, 255, 248, 0.85);
  animation: v06016-self-pulse var(--v06016-duration) ease-out forwards;
}

@keyframes v06016-full-route-flash {
  0% { opacity: 0.96; }
  22% { opacity: 1; }
  48% { opacity: 0.82; }
  72% { opacity: 0.98; }
  100% { opacity: 0; }
}

@keyframes v06016-self-pulse {
  0% { transform: scale(0.32); opacity: 0.96; }
  44% { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1.62); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .v06016-effect {
    animation-timing-function: linear;
  }
  .v06016-effect-self-pulse {
    animation: none;
  }
}

/* ===========================[v06017-closure.css]=========================== */
/* Gerbil Gunners v0.6.017 — narrow health-bar presentation delta.
   v0.6.016 lifted every occupied battlefield health bar 10vw upward. v0.6.017
   drops that lift by 5vw, leaving the bar at net translateY(-5vw) (still
   above the portrait per the user profile). The v0.6.015 transform
   declaration is the most-specific match for the bar's wrapping battle
   region; this v0.6.017 declaration carries the same selector with the
   same specificity so it cleanly wins the cascade tie.
   Bar width is reduced in styles.css to 50% of the v0.6.016 CSS width
   (84% -> 42%); horizontal centering is preserved.

   v0.6.034 (HP-bar selector fix + cascade cleanup): the v0.6.017
   translateY(-5vw) rule above was authored with the same legacy
   `.v060-battle-screen.v060010-active ...` companion selector as the
   v06015 closure. v0.6.034 fixes the v06015 selector to
   `.v060-battle-screen ...` (the active class is still applied at
   runtime per the live DOM probe on the v0.6.033 baseline). However,
   the v0.6.017 rule keeps the legacy companion class and would win the
   cascade on raw specificity (0,2,1 vs the corrected v0.6.034 v06015
   selector at 0,1,1) — leaving the resolved translateY at -5vw instead
   of the v0.6.033-canonical -7.5vw. The translateY rule is therefore
   removed here (the bar's geometry below is preserved: height 0.32vw
   stays in styles.css; width 42% -> 21% stays in v06033-closure.css; the
   v0.6.034 selector fix in v06015-closure.css becomes the single
   canonical translateY source). The bar's geometry, fill animation,
   color states (v0.6.017 + v0.6.020 + v0.6.024) and horizontal
   centering are all preserved. */

/* Preserve every other v0.6.016 surface: fast shot tracers, transparency,
   info-section geometry, grid 34x7, identity presentation. */

/* ===========================[v06018-closure.css]=========================== */
/* Gerbil Gunners v0.6.018 — battle animation polish (all 8 techniques).
   Layered on top of v0.6.017 (health bar down 5vw, half width) and
   v0.6.016 (fast full-route shot tracers). Every animation is
   presentation-only; combat resolution remains untouched. */
/* All v0.6.018 transforms on .v060-hpbar (jitter, KO collapse) MUST
   compose with the v0.6.017 transform: translateY(-5vw). The closure
   CSS covers this by writing keyframes that explicitly include the
   -5vw translateY at every frame; the v0.6.018 runtime marks the
   bar via inline `transform-origin` and an additional translateX only. */

/* ---------------------------------------------------------------------------
   T7.1 — HP bar damage-color delta (Diablo IV).
   .v060-hpbar must be position:relative so the absolute ghost child
   sits inside the bar track. The live fill remains position:relative
   (z-index 2) above the absolute ghost (z-index 1).
   --------------------------------------------------------------------------- */
.v060-battle-screen.v060010-active .v060-region-battle .v060-hpbar {
  position: relative;
}
.v060-battle-screen.v060010-active .v060-region-battle .v060-hpbar > .v060-hpbar-ghost {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 0;
  background: rgba(255, 220, 200, 0.92);
  pointer-events: none;
  z-index: 1;
  will-change: width, opacity;
}
.v060-battle-screen.v060010-active .v060-region-battle .v060-hpbar > .v060-hpbar-fill {
  /* Stay above the ghost inside the same bar. */
  position: relative;
  z-index: 2;
  transition: width 220ms ease-out;
}

/* ---------------------------------------------------------------------------
   T3.1 — Spark burst at the resolved hit cell (Hades, Diablo IV).
   A presentation-only overlay outside the 34x7 occupancy model; sits
   BELOW the v0.6.016 tracer layer (z-index 24) so sparks never cover
   the bright tracer line.
   --------------------------------------------------------------------------- */
.v060-battle-screen.v060010-active .v06018-effect-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
  z-index: 23;
  background: transparent;
  contain: layout paint style;
}
.v06018-spark {
  position: absolute;
  width: min(0.18cqw, 2.304px);
  height: min(0.18cqw, 2.304px);
  border-radius: 50%;
  pointer-events: none;
  opacity: 1;
  /* JS sets --v06018-tx, --v06018-ty, --v06018-duration inline. */
  animation: v06018-spark-fly var(--v06018-duration, 130ms) linear forwards;
  will-change: transform, opacity;
}
.v06018-spark[data-spark-tone="basic"]      { background: #fff8c2; box-shadow: 0 0 min(0.12cqw, 1.536px) rgba(255, 248, 194, 0.6); }
.v06018-spark[data-spark-tone="offense"]    { background: #82e6ff; box-shadow: 0 0 min(0.12cqw, 1.536px) rgba(130, 230, 255, 0.6); }
.v06018-spark[data-spark-tone="support"]    { background: #7fff7f; box-shadow: 0 0 min(0.12cqw, 1.536px) rgba(127, 255, 127, 0.6); }
.v06018-spark[data-spark-tone="miss"]       { background: #d4d4d4; box-shadow: 0 0 min(0.12cqw, 1.536px) rgba(212, 212, 212, 0.5); }
@keyframes v06018-spark-fly {
  0%   { transform: translate(0, 0) scale(1);    opacity: 1; }
  60%  { transform: translate(calc(var(--v06018-tx, 0) * 1.0), calc(var(--v06018-ty, 0) * 1.0)) scale(0.9); opacity: 0.7; }
  100% { transform: translate(calc(var(--v06018-tx, 0) * 1.4), calc(var(--v06018-ty, 0) * 1.4)) scale(0.5); opacity: 0; }
}

/* ---------------------------------------------------------------------------
   T4.1 — White flash on the hit target portrait (Dead Cells).
   --------------------------------------------------------------------------- */
.v06018-portrait-flash {
  animation: v06018-portrait-white-flash var(--v06018-flash-dur, 60ms) linear forwards !important;
  filter: brightness(2) saturate(0.6) !important;
}
@keyframes v06018-portrait-white-flash {
  0%   { filter: brightness(2) saturate(0.6); }
  100% { filter: brightness(1) saturate(1);   }
}

/* ---------------------------------------------------------------------------
   T4.2 — HP bar jitter on damage (PoE2, Diablo IV).
   The v0.6.017 closure owns the bar's baseline transform:
   translateY(-5vw). The v0.6.018 keyframes compose around that by
   writing translateY(-5vw) at every frame and only varying
   translateX. 2 px amplitude, 4-step settle, 120 ms.
   --------------------------------------------------------------------------- */
.v060-battle-screen.v060010-active .v060-region-battle > .v060-hpbar.v06018-jitter {
  animation: v06018-hpbar-jitter 120ms linear forwards;
  will-change: transform;
}
@keyframes v06018-hpbar-jitter {
  /* v0.6.076 — every frame's translateY moves from -5vw to
     -2.5vw so the resting cascade winner and the jitter
     keyframes agree on the same vertical translateY. The
     X-offset amplitudes and the 120ms linear timing are
     preserved exactly. */
  0%    { transform: translateY(max(-2.5cqw, -32px)) translateX(0); }
  20%   { transform: translateY(max(-2.5cqw, -32px)) translateX(max(-0.125cqw, -1.6px)); }
  45%   { transform: translateY(max(-2.5cqw, -32px)) translateX(min(0.0625cqw, 0.8px)); }
  68%   { transform: translateY(max(-2.5cqw, -32px)) translateX(max(-0.0625cqw, -0.8px)); }
  86%   { transform: translateY(max(-2.5cqw, -32px)) translateX(min(0.03125cqw, 0.4px)); }
  100%  { transform: translateY(max(-2.5cqw, -32px)) translateX(0); }
}

/* ---------------------------------------------------------------------------
   T6.1 / T6.3 — Floating damage numbers + Crit/Miss word pops.
   Each popup is positioned absolutely inside .v06018-effect-layer and
   rises ~1.2 vw over 350 ms while fading from 1 -> 0. The word pop
   appears 50 ms before the number (animation-delay on the word is -50 ms).
   --------------------------------------------------------------------------- */
.v06018-number-pop {
  position: absolute;
  font-family: monospace;
  font-weight: 900;
  font-size: 2.1cqw;
  color: #ffffff;
  text-shadow: 0 0 min(0.12cqw, 1.536px) rgba(0, 0, 0, 0.95);
  pointer-events: none;
  white-space: nowrap;
  letter-spacing: 0.02em;
  animation: v06018-number-rise 350ms linear forwards;
  will-change: transform, opacity;
  transform-origin: 50% 50%;
}
.v06018-number-pop[data-tone="crit"]      { color: #ffd700; font-size: 3.2cqw; }
.v06018-number-pop[data-tone="heal"]      { color: #7fff7f; }
.v06018-number-pop[data-tone="block"]     { color: #bbbbbb; }
.v06018-number-pop[data-tone="miss"]      { color: #bbbbbb; }
.v06018-number-pop[data-tone="dodge"]     { color: #bbbbbb; }
.v06018-word-pop {
  position: absolute;
  font-family: monospace;
  font-weight: 900;
  font-size: 1.2cqw;
  color: #ffffff;
  text-shadow: 0 0 min(0.12cqw, 1.536px) rgba(0, 0, 0, 0.95);
  pointer-events: none;
  white-space: nowrap;
  letter-spacing: 0.06em;
  animation: v06018-word-rise 380ms linear forwards;
  will-change: transform, opacity;
}
.v06018-word-pop[data-tone="crit"]   { color: #ffd700; }
.v06018-word-pop[data-tone="heal"]   { color: #7fff7f; }
.v06018-word-pop[data-tone="miss"]   { color: #bbbbbb; }
.v06018-word-pop[data-tone="dodge"]  { color: #bbbbbb; }
.v06018-word-pop[data-tone="block"]  { color: #bbbbbb; }
@keyframes v06018-number-rise {
  0%   { transform: translate(-50%, 0);     opacity: 0; }
  12%  { transform: translate(-50%, max(-0.1cqw, -1.28px)); opacity: 1; }
  90%  { transform: translate(-50%, max(-1.1cqw, -14.08px)); opacity: 1; }
  100% { transform: translate(-50%, max(-1.2cqw, -15.36px)); opacity: 0; }
}
@keyframes v06018-word-rise {
  0%   { transform: translate(-50%, min(0.2cqw, 2.56px)); opacity: 0; }
  10%  { transform: translate(-50%, min(0.1cqw, 1.28px)); opacity: 1; }
  90%  { transform: translate(-50%, max(-0.9cqw, -11.52px)); opacity: 1; }
  100% { transform: translate(-50%, max(-1cqw, -12.8px));   opacity: 0; }
}

/* ---------------------------------------------------------------------------
   T1.1 — Charge-ring glow on attacker in last 30% of ActionInterval.
   A pulsing outline ring on the attacker's battle region, behind
   portrait and HP bar but above the cell background. The box-shadow
   insets add the visual ring without altering layout. Conflicts with
   the existing green readiness fill on row-7 info cells would be
   visible only if we applied this class there — we do not (row-7
   info cells are excluded).
   --------------------------------------------------------------------------- */
.v060-battle-screen.v060010-active .v060-region-battle.v06018-charging {
  border-radius: min(0.18cqw, 2.304px);
  will-change: box-shadow;
  pointer-events: none;
}
.v060-battle-screen.v060010-active .v060-region-battle.v06018-charging[data-charge-tone="basic"] {
  animation: v06018-charge-pulse-basic 600ms linear infinite;
}
.v060-battle-screen.v060010-active .v060-region-battle.v06018-charging[data-charge-tone="offense"] {
  animation: v06018-charge-pulse-offense 600ms linear infinite;
}
.v060-battle-screen.v060010-active .v060-region-battle.v06018-charging[data-charge-tone="support"] {
  animation: v06018-charge-pulse-support 600ms linear infinite;
}
@keyframes v06018-charge-pulse-basic {
  0%   { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(255, 255, 255, 0.40); }
  50%  { box-shadow: inset 0 0 0 min(0.12cqw, 1.536px) rgba(255, 255, 255, 0.85); }
  100% { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(255, 255, 255, 0.40); }
}
@keyframes v06018-charge-pulse-offense {
  0%   { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(82, 230, 255, 0.40); }
  50%  { box-shadow: inset 0 0 0 min(0.12cqw, 1.536px) rgba(82, 230, 255, 0.85); }
  100% { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(82, 230, 255, 0.40); }
}
@keyframes v06018-charge-pulse-support {
  0%   { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(127, 255, 127, 0.45); }
  50%  { box-shadow: inset 0 0 0 min(0.12cqw, 1.536px) rgba(127, 255, 127, 0.90); }
  100% { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(127, 255, 127, 0.45); }
}

/* ---------------------------------------------------------------------------
   T8.1 — Local 2-4 px screen shake gated to crits (Dead Cells).
   Restrict to .v060-region-battle only; row-7 info cells stay still.
   The shake wrapper (.v06016-region-battle grid) keeps the geometry
   intact while the translate jitter keyframes run in <130 ms.
   --------------------------------------------------------------------------- */
.v060-battle-screen.v060010-active #v060BattleGrid.v06018-shake-battle {
  animation: v06018-shake-battle 120ms linear forwards;
  will-change: transform;
}
@keyframes v06018-shake-battle {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(min(0.1875cqw, 2.4px), max(-0.0625cqw, -0.8px)); }
  40%  { transform: translate(max(-0.125cqw, -1.6px), min(0.125cqw, 1.6px)); }
  60%  { transform: translate(min(0.125cqw, 1.6px), max(-0.0625cqw, -0.8px)); }
  78%  { transform: translate(max(-0.0625cqw, -0.8px), min(0.0625cqw, 0.8px)); }
  90%  { transform: translate(min(0.03125cqw, 0.4px), max(-0.03125cqw, -0.4px)); }
  100% { transform: translate(0, 0); }
}

/* ---------------------------------------------------------------------------
   T5.1 / T5.2 — Despawn fade + grayed portrait on KO.
   Portrait opacity 1 -> 0.4 over 200 ms, grayed portrait persistent,
   bar collapses from the right edge, cell receives a red border. The
   fade transition only runs while .v06018-ko is on; the persistent
   filter + border are applied separately via .v06018-ko-persistent.
   --------------------------------------------------------------------------- */
.v060-battle-screen.v060010-active .v060-region-battle.v06018-ko .v060-portrait img {
  animation: v06018-ko-fade 200ms ease-out forwards;
}
.v060-battle-screen.v060010-active .v060-region-battle.v06018-ko .v060-hpbar {
  animation: v06018-ko-bar-collapse 200ms ease-out forwards;
  transform-origin: 100% 50% !important;
}
@keyframes v06018-ko-fade {
  0%   { opacity: 1;   filter: brightness(1) saturate(1); }
  100% { opacity: 0.4; filter: grayscale(0.55) opacity(0.55); }
}
@keyframes v06018-ko-bar-collapse {
  /* v0.6.076 — both frames' translateY moves from -5vw to
     -2.5vw so the resting cascade winner and the KO
     collapse keyframes agree on the same vertical
     translateY. The X-scale collapse from 1 to 0.001 and
     the 200ms ease-out timing are preserved exactly. */
  0%   { transform: translateY(max(-2.5cqw, -32px)) scaleX(1); }
  100% { transform: translateY(max(-2.5cqw, -32px)) scaleX(0.001); }
}

/* KO persistent layer — applied while a unit remains defeated this
   fight, regardless of which fade cycle is currently running. */
/* (Grayscale filter + opacity already land on the portrait via the
   v0.6.018 ko-fade keyframes; on the persistent path we lock those
   values without re-running the fade.) */
.v060-battle-screen.v060010-active .v060-region-battle.v06018-ko-persistent .v060-portrait img {
  filter: grayscale(0.55) opacity(0.55);
  opacity: 0.4;
  pointer-events: none;
}
.v060-battle-screen.v060010-active .v060-region-battle.v06018-ko-persistent {
  box-shadow: inset 0 0 0 max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) rgba(180, 30, 30, 0.5);
}

/* ---------------------------------------------------------------------------
   v0.6.018 marker tokens for tests + identity probes.
   These selectors only matter to the .v06018 identity probes — they
   never trigger layout or paint side-effects in production.
   --------------------------------------------------------------------------- */
.v06018-token-marker { /* presence-only — used by tests */ }

/* ===========================[v06019-closure.css]=========================== */
/* Gerbil Gunners v0.6.019 — charge ring on row-7 info card +
   fast bullet whip on top of the v0.6.016 instant route.

   Layered on top of v0.6.018 (8-technique polish), v0.6.017 (health
   bar down 5vw, half width) and v0.6.016 (fast full-route shot
   tracers). Every animation is presentation-only; combat resolution
   remains untouched. */

/* ---------------------------------------------------------------------------
   v0.6.019 §3.6 — Charge ring moved to row-7 info card.
   Same pulse keyframes as v0.6.018 T1.1, but the selector targets
   the info card's RING child (`.v060-region-info > .v06018-charging`)
   instead of the parent itself. The ring is rendered above the
   info-panel background but BELOW the portrait image (z-index 2 vs
   3) so the portrait stays fully visible and unmodified.

   We also keep the v0.6.018 selector for the leftover battle-grid
   case, but the JS path no longer adds the class there — we just
   defensively null it. The CSS rule still exists for clean reset
   symmetry with the v0.6.018 selector. The active animation is the
   info-card one.
   --------------------------------------------------------------------------- */
/* v0.6.019: give the info card a non-transparent backplane so the
   ring has a real surface to sit on top of, matching the spec's
   "opaque row-7 info backplanes" intent. The tone is a dark olive
   that blends with the row-7 readiness band. */
.v060-battle-screen.v060010-active .v060-region-info {
  background: rgba(8, 14, 22, 0.72);
  z-index: 1;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging {
  position: absolute;
  inset: 0;
  border-radius: min(0.18cqw, 2.304px);
  border: min(0.08cqw, 1.024px) solid rgba(255, 255, 255, 0.55);
  will-change: box-shadow, opacity;
  pointer-events: none;
  /* v0.6.019: sit above the info backplane (z 1) but below the
     portrait image (z 3, defined in v06014-closure.css). */
  z-index: 2;
  opacity: 0.85;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="basic"] {
  animation: v06019-info-charge-basic 600ms linear infinite;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="offense"] {
  animation: v06019-info-charge-offense 600ms linear infinite;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="support"] {
  animation: v06019-info-charge-support 600ms linear infinite;
}

/* Keep the v0.6.018 selector as a passive / fallback. The JS path
   does NOT add v06018-charging to .v060-region-battle anymore. We
   intentionally remove the active animation from that selector so
   the rule does not visually fire even if a stale class were left
   on a battle cell by an older build. */
.v060-battle-screen.v060010-active .v060-region-battle.v06018-charging {
  /* no animation — v0.6.019 charge ring belongs to the info card. */
  box-shadow: none;
  border-radius: min(0.18cqw, 2.304px);
  pointer-events: none;
}

@keyframes v06019-info-charge-basic {
  0%   { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(255, 255, 255, 0.40); }
  50%  { box-shadow: inset 0 0 0 min(0.12cqw, 1.536px) rgba(255, 255, 255, 0.85); }
  100% { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(255, 255, 255, 0.40); }
}
@keyframes v06019-info-charge-offense {
  0%   { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(82, 230, 255, 0.40); }
  50%  { box-shadow: inset 0 0 0 min(0.12cqw, 1.536px) rgba(82, 230, 255, 0.85); }
  100% { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(82, 230, 255, 0.40); }
}
@keyframes v06019-info-charge-support {
  0%   { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(127, 255, 127, 0.45); }
  50%  { box-shadow: inset 0 0 0 min(0.12cqw, 1.536px) rgba(127, 255, 127, 0.90); }
  100% { box-shadow: inset 0 0 0 min(0.08cqw, 1.024px) rgba(127, 255, 127, 0.45); }
}

/* Make sure the portrait image inside an info card is rendered
   above the charge ring. */
.v060-battle-screen.v060010-active .v060-region-info .v060-info-portrait {
  position: relative;
  z-index: 3;
}
.v060-battle-screen.v060010-active .v060-region-info .v060-info-portrait img {
  position: relative;
  z-index: 3;
}

/* ---------------------------------------------------------------------------
   v0.6.019 §3.9 — Fast bullet whip.
   A small bright narrow slug (head + tapered tail) that races from
   origin to target in 30–45 ms on top of the v0.6.016 instant
   route line. The slug is implemented as a single DOM div with two
   child elements (head + tail). The root div translates from
   --v06019-from-x/y to --v06019-to-x/y over --v06019-lifetime ms.

   The slug lives on the v0.6.016 overlay (#v06016EffectLayer, which
   has z-index: 24 and overflow: hidden) so the persistent v0.6.016
   line stays unchanged underneath. pointer-events:none and
   no document scrollbar contribution.
   --------------------------------------------------------------------------- */
.v06019-slug {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  pointer-events: none;
  user-select: none;
  z-index: 1;                /* inside the v06016 overlay (z:24) */
  will-change: transform, opacity;
  overflow: visible;
  /* Local CSS variables set by JS:
     --v06019-from-x, --v06019-from-y   : origin in layer-local px
     --v06019-to-x,   --v06019-to-y     : target in layer-local px
     --v06019-angle                     : route angle in deg
     --v06019-head-w, --v06019-head-h   : head size in vw
     --v06019-tail-w                    : tail length in vw
     --v06019-lifetime                  : total travel time in ms */
  animation: v06019-fast-bullet-whip var(--v06019-lifetime, 40ms) linear forwards;
}

.v06019-slug .v06019-slug-head {
  position: absolute;
  left: 0;
  top: 50%;
  width: var(--v06019-head-w, min(0.5cqw, 6.4px));
  height: var(--v06019-head-h, min(0.1cqw, 1.28px));
  transform: translate(-50%, -50%);
  border-radius: min(0.05cqw, 0.64px);
  pointer-events: none;
  z-index: 2;
  background: #ffffff;
  box-shadow:
    0 0 min(0.12cqw, 1.536px) rgba(255, 255, 255, 0.95),
    0 0 min(0.25cqw, 3.2px) rgba(255, 240, 180, 0.6);
  will-change: transform, opacity;
}
.v06019-slug.v06019-slug-offense .v06019-slug-head {
  background: #c8f3ff;
  box-shadow:
    0 0 min(0.12cqw, 1.536px) rgba(200, 243, 255, 0.95),
    0 0 min(0.3cqw, 3.84px) rgba(64, 217, 255, 0.7);
}
.v06019-slug.v06019-slug-crit .v06019-slug-head {
  background: #fff7c8;
  box-shadow:
    0 0 min(0.18cqw, 2.304px) rgba(255, 247, 200, 1),
    0 0 min(0.4cqw, 5.12px) rgba(255, 200, 80, 0.85);
}

.v06019-slug .v06019-slug-tail {
  position: absolute;
  left: 0;
  top: 50%;
  width: var(--v06019-tail-w, min(4.5cqw, 57.6px));
  height: min(0.06cqw, 0.768px);
  transform-origin: 0 50%;
  /* Tail extends to the LEFT of the head (opposite to the route
     direction). The slug root translates the whole thing from
     origin to target, so the tail visibly trails behind. */
  transform: translate(-100%, -50%) scaleX(1);
  border-radius: min(0.04cqw, 0.512px);
  pointer-events: none;
  background: linear-gradient(to left,
    rgba(255, 248, 194, 0.95) 0%,
    rgba(255, 240, 180, 0.6) 30%,
    rgba(255, 220, 140, 0.25) 70%,
    rgba(255, 220, 140, 0) 100%);
  z-index: 1;
  will-change: transform, opacity;
  filter: blur(min(0.02cqw, 0.256px));
}
.v06019-slug.v06019-slug-offense .v06019-slug-tail {
  background: linear-gradient(to left,
    rgba(130, 230, 255, 0.95) 0%,
    rgba(80, 200, 255, 0.6) 30%,
    rgba(64, 180, 240, 0.25) 70%,
    rgba(64, 180, 240, 0) 100%);
}
.v06019-slug.v06019-slug-crit .v06019-slug-tail {
  background: linear-gradient(to left,
    rgba(255, 240, 200, 1) 0%,
    rgba(255, 200, 120, 0.7) 30%,
    rgba(255, 180, 60, 0.3) 70%,
    rgba(255, 180, 60, 0) 100%);
}

@keyframes v06019-fast-bullet-whip {
  0% {
    transform: translate(var(--v06019-from-x), var(--v06019-from-y)) rotate(var(--v06019-angle, 0deg));
    opacity: 1;
  }
  10% {
    /* Very small leading opacity dip to make the head "snap" forward */
    opacity: 1;
  }
  80% {
    opacity: 1;
  }
  100% {
    transform: translate(var(--v06019-to-x), var(--v06019-to-y)) rotate(var(--v06019-angle, 0deg));
    opacity: 0;
  }
}

/* ---------------------------------------------------------------------------
   v0.6.019 marker tokens for tests + identity probes.
   Presence-only selectors — they never trigger layout or paint
   side-effects in production.
   --------------------------------------------------------------------------- */
.v06019-token-marker { /* presence-only — used by tests */ }

/* ---------------------------------------------------------------------------
   v0.6.019 forbidden input prompts + zero-scrollbar closure.

   The spec forbids visible battle input prompts (no Start / Pause /
   Speed / Close buttons, no in-battle speed picker). Functionally the
   speed controls still drive playback (1×/2×/4×/8×) via JS handlers
   and keyboard input — they just must not LOOK like visible input
   prompts. Visibility:hidden keeps them focusable / clickable for
   keyboard / programmatic access while removing them from the
   rendered surface.

   The legacy .v060-battle-body still hosts the .v060-arena-card list
   for fallback screens; its earlier `height:0; width:0;
   visibility:hidden` collapse left the cards' layout rects leaking
   past the viewport bottom, producing a 40/30/20 px document Y
   overflow at the three test viewports. Hiding the body entirely
   removes the children's layout rects from the document so the
   scrollHeight matches the viewport.

   The .scene wrapper inherited from the depot screen enforces a 2:1
   aspect ratio which makes the battle scene 800 px tall at a 1600 px
   width — taller than the 720 px viewport, so the document scrollHeight
   leaks 40/30/20 px past the inner height at the three test viewports.
   For the v0.6.019 battle screen only, snap the scene to the viewport
   exactly: full viewport width AND full viewport height, no aspect
   ratio, no vertical centering offset. The 34×7 grid inside already
   uses `repeat(34, 1fr) / repeat(7, 1fr)` with `minmax(0, 1fr)` so it
   scales to whatever bounding box we give it. The depot, title, and
   setup screens keep their 2:1 framing via the global `.scene` rule.
   --------------------------------------------------------------------------- */
/* v0.6.019: display:none (not visibility:hidden) so the forbidden
   input-prompts oracle sees zero visible buttons. The previous
   visibility:hidden approach kept elements in the layout rect so
   getComputedStyle(display) === 'none' was false, which the
   forbidden_techniques_negative_audit test rejected. Speed
   playback is still driven by JS keyboard handlers (1/2/4/8) so
   removing the buttons from the layout does not lose
   functionality.
   v0.6.121: the bottom-center speed region (#v060RegionSpeed) is
   now a real, visible +/- control with a value label. It must
   NEVER be hidden, so the selector list above excludes the new
   speed row. */
.v060-battle-screen.v060010-active .v060-battle-header button,
.v060-battle-screen.v060010-active .v060-battle-controls button,
.v060-battle-screen.v060010-active .v060-battle-controls {
  display: none !important;
}
.v060-battle-screen.v060010-active .v060-battle-body {
  display: none !important;
}
/* v0.6.019: zero out the residual document Y overflow at all three
   test viewports. The .scene wrapper inherited from the depot
   screen had its 2:1 aspect-ratio retained at the document root
   even when the active screen was the battle. Locking the html /
   body to viewport height + overflow:hidden guarantees that the
   forbidden-techniques test's scrollY = scrollHeight - innerHeight
   computation stays at 0. */
html, body { height: 100%; overflow: hidden; margin: 0; padding: 0; }
/* v0.6.058 delta #1+#2: Apply dynamic 16:9 layout to the battle scene.
  Removes the v0.6.046/v0.6.019 rule that forced 100vw x 100vh and
  disabled the 16:9 aspect ratio (per user report — "battle scene still
  does not follow these rules; elements get squished"). Replaces it with
  the exact same v0.6.048 dynamic 16:9 smart-clamp pattern used by
  the depot scene (`.scene` rule):
    width:  min(100vw, 177.7777vh);   // smaller of viewport-width
                                        // vs height-derived width
    height: min(56.25vw, 100vh);      // smaller of width-derived height
                                        // vs viewport-height
    aspect-ratio: 16 / 9;              // enforce the ratio
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);  // center in viewport
  At every standard test viewport (1600x720, 1200x540, 800x360) the
  scene is height-bound (e.g. 1600x720 produces 1280x720, 1200x540
  produces 960x540, 800x360 produces 640x360). The 34x7 grid, HP bars,
  and portraits inside the scene are laid out for the 16:9 box, so
  they are no longer squished. No `!important` on the new rule.
  The grid rule no longer needs `!important` — flex-fill (flex:1 1 auto)
  inside the 16:9 scene provides the same sizing. The html/body
  overflow:hidden above still ensures zero document scroll. */
.v060-battle-screen.v060010-active {
  /* v0.6.058 — dynamic 16:9 smart clamp (matches .scene in v0.6.048). */
  width: min(100vw, 177.7777vh);
  height: min(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
  /* Centered positioning (matches .scene in v0.6.048). */
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* v0.6.013 baseline — overflow visible so the grid + gerbil artwork
     can extend beyond the 16:9 box (per spec §3). */
  margin: 0;
  padding: 0;
  max-width: none;
  max-height: none;
  box-sizing: border-box;
}
.v060-battle-screen.v060010-active .v060-battle-grid {
  /* v0.6.058: removed !important. The grid now fills the 16:9 scene
     via flex:1 1 auto on the .v060-battle-screen flex container above
     (display:flex; flex-direction:column). */
  width: 100%;
  height: 100%;
}
/* v0.6.019: clip row-7 info cells so the 2x scaled portrait image
   (transform-induced bounding rect that extends past the cell)
   does NOT contribute to document scrollHeight. The portrait
   wrapper already has overflow:hidden + clip-path:inset(0) in
   v06014-closure.css, but the cell itself (`.v060-region-info`)
   was set to `overflow: visible` in v06013-closure.css to permit
   battlefield artwork to overflow its 5x1 cell. Row-7 info cells
   are different: the 2x face-crop is intentional and should be
   contained, so we add overflow:hidden on the cell here without
   touching `.v060-region-battle` (the spec keeps battlefield
   overflow visible). */
.v060-battle-screen.v060010-active .v060-region-info {
  overflow: hidden !important;
}

/* ===========================[v06020-closure.css]=========================== */
/* Gerbil Gunners v0.6.020 — consolidated battle-polish closure.
   This successor intentionally layers on the accepted v0.6.019 presentation
   without changing deterministic combat, grid geometry, portrait crop, or the
   v0.6.017 health-bar placement. */

/* Keep the row-7 charge ring as a full-card, layout-neutral layer above the
   opaque info backplane and below the portrait. The inherited v0.6.019 rule
   already establishes these values; this explicit successor rule freezes the
   v0.6.020 contract and adds opacity to the pulse keyframes. */
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(255, 255, 255, 0.55);
  pointer-events: none;
  z-index: 2;
  opacity: 0.4;
  will-change: box-shadow, opacity;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="basic"] {
  animation: v06020-info-charge-basic 600ms linear infinite;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="offense"] {
  animation: v06020-info-charge-offense 600ms linear infinite;
}
.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="support"] {
  animation: v06020-info-charge-support 600ms linear infinite;
}
@keyframes v06020-info-charge-basic {
  0%   { opacity: 0.4; box-shadow: inset max(-0.05cqw, -0.64px) 0 0 min(0.08cqw, 1.024px) rgba(255,255,255,0.40); }
  50%  { opacity: 0.9; box-shadow: inset max(-0.12cqw, -1.536px) 0 0 min(0.12cqw, 1.536px) rgba(255,255,255,0.85); }
  100% { opacity: 0.4; box-shadow: inset max(-0.05cqw, -0.64px) 0 0 min(0.08cqw, 1.024px) rgba(255,255,255,0.40); }
}
@keyframes v06020-info-charge-offense {
  0%   { opacity: 0.4; box-shadow: inset max(-0.05cqw, -0.64px) 0 0 min(0.08cqw, 1.024px) rgba(82,230,255,0.40); }
  50%  { opacity: 0.9; box-shadow: inset max(-0.12cqw, -1.536px) 0 0 min(0.12cqw, 1.536px) rgba(82,230,255,0.85); }
  100% { opacity: 0.4; box-shadow: inset max(-0.05cqw, -0.64px) 0 0 min(0.08cqw, 1.024px) rgba(82,230,255,0.40); }
}
@keyframes v06020-info-charge-support {
  0%   { opacity: 0.4; box-shadow: inset max(-0.05cqw, -0.64px) 0 0 min(0.08cqw, 1.024px) rgba(127,255,127,0.45); }
  50%  { opacity: 0.9; box-shadow: inset max(-0.12cqw, -1.536px) 0 0 min(0.12cqw, 1.536px) rgba(127,255,127,0.90); }
  100% { opacity: 0.4; box-shadow: inset max(-0.05cqw, -0.64px) 0 0 min(0.08cqw, 1.024px) rgba(127,255,127,0.45); }
}

/* Enemy-side charge ring: REVERSED lean direction. The player side leans
   "forward" (toward the enemy, i.e. the ring offset shifts leftward at
   peak opacity). The enemy side mirrors that — its ring offset shifts
   rightward at peak opacity — so the two sides visibly lean in opposite
   directions during the last 30% of the ActionInterval. */
@keyframes v06020-info-charge-basic-enemy {
  0%   { opacity: 0.4; box-shadow: inset min(0.05cqw, 0.64px) 0 0 min(0.08cqw, 1.024px) rgba(255,255,255,0.40); }
  50%  { opacity: 0.9; box-shadow: inset min(0.12cqw, 1.536px) 0 0 min(0.12cqw, 1.536px) rgba(255,255,255,0.85); }
  100% { opacity: 0.4; box-shadow: inset min(0.05cqw, 0.64px) 0 0 min(0.08cqw, 1.024px) rgba(255,255,255,0.40); }
}
@keyframes v06020-info-charge-offense-enemy {
  0%   { opacity: 0.4; box-shadow: inset min(0.05cqw, 0.64px) 0 0 min(0.08cqw, 1.024px) rgba(82,230,255,0.40); }
  50%  { opacity: 0.9; box-shadow: inset min(0.12cqw, 1.536px) 0 0 min(0.12cqw, 1.536px) rgba(82,230,255,0.85); }
  100% { opacity: 0.4; box-shadow: inset min(0.05cqw, 0.64px) 0 0 min(0.08cqw, 1.024px) rgba(82,230,255,0.40); }
}
@keyframes v06020-info-charge-support-enemy {
  0%   { opacity: 0.4; box-shadow: inset min(0.05cqw, 0.64px) 0 0 min(0.08cqw, 1.024px) rgba(127,255,127,0.45); }
  50%  { opacity: 0.9; box-shadow: inset min(0.12cqw, 1.536px) 0 0 min(0.12cqw, 1.536px) rgba(127,255,127,0.90); }
  100% { opacity: 0.4; box-shadow: inset min(0.05cqw, 0.64px) 0 0 min(0.08cqw, 1.024px) rgba(127,255,127,0.45); }
}

/* Per-side selector: assign the lean-direction keyframes based on which side
   the info card belongs to. The v0.6.018 charge-ring v06018-charging
   element is appended to .v060-region-info. The info card carries
   data-region="info-p1"..info-p5" (player) or "info-e1"..info-e5" (enemy)
   so the data-region value's first letter is a reliable side discriminator.
   Specificity is bumped to match (and beat) the parent rule
   `.v060-battle-screen.v060010-active .v060-region-info > .v06018-charging[data-charge-tone="..."]`
   by using an extra ancestor: `.v060-battle-screen.v060010-active.v060-region-info`. */
.v060-battle-screen.v060010-active .v060-region-info[data-region^="info-p"] > .v06018-charging[data-charge-tone="basic"]   { animation: v06020-info-charge-basic   600ms linear infinite; }
.v060-battle-screen.v060010-active .v060-region-info[data-region^="info-p"] > .v06018-charging[data-charge-tone="offense"] { animation: v06020-info-charge-offense 600ms linear infinite; }
.v060-battle-screen.v060010-active .v060-region-info[data-region^="info-p"] > .v06018-charging[data-charge-tone="support"] { animation: v06020-info-charge-support 600ms linear infinite; }
.v060-battle-screen.v060010-active .v060-region-info[data-region^="info-e"] > .v06018-charging[data-charge-tone="basic"]   { animation: v06020-info-charge-basic-enemy   600ms linear infinite; }
.v060-battle-screen.v060010-active .v060-region-info[data-region^="info-e"] > .v06018-charging[data-charge-tone="offense"] { animation: v06020-info-charge-offense-enemy 600ms linear infinite; }
.v060-battle-screen.v060010-active .v060-region-info[data-region^="info-e"] > .v06018-charging[data-charge-tone="support"] { animation: v06020-info-charge-support-enemy 600ms linear infinite; }

/* Presence-only identity token used by acceptance probes. */
.v06020-token-marker { }

/* ===========================[v06021-closure.css]=========================== */
/* Gerbil Gunners v0.6.021 — depot drag-and-drop freeze fix.
   This successor intentionally layers on the accepted v0.6.020 polish
   surface without changing deterministic combat, grid geometry,
   portrait crop, the v0.6.017 health-bar placement, the v0.6.016
   fast tracers, or the v0.6.018 eight-technique polish.
   The only runtime change is in the JavaScript drag pipeline
   (idempotent listener wiring + dragstart watchdog); CSS surface
   here is intentionally empty so the v0.6.020 cascade ships
   unchanged. */

.v06021-token-marker { }

/* ===========================[v06022-closure.css]=========================== */
/* v0.6.022 closure — depot-drag-freeze-redo fix lives in game.js
   (cancelPriorDragVisuals unconditional cleanup; onDrop unconditional
   RAF cancellation + 250ms post-drop fallback). No CSS changes. */

/* ===========================[v06024-closure.css]=========================== */
/* Gerbil Gunners v0.6.025 — Per-class skill line colors, basic attack visuals preserved, no dashed lines anywhere.
   Successor to v0.6.016/v0.6.022 closure CSS.
   This closure:
     1. Preserves the warm-yellow basic-attack visuals unchanged
     2. Routes every non-basic skill line through its class's color
     3. Removes every stroke-dasharray rule (delta #5: no dashed lines anywhere)
     4. Preserves the miss/block outcome color overrides
     5. Preserves the v0.6.016 single-line SVG geometry
*/

/* Class-keyed skill line colors — selectors with class attribute set by the
   v06016 render in game-v060-battle-ui.js via data-class-id on the .v06016-effect
   route node. The base line color is also applied via CSS so the engine
   doesn't need to set inline style per skill. Specificity (1 element + 1
   attribute + 2 classes) beats the inherited v06016 offense/support rules. */
.v06016-effect.v06016-effect-skill[data-skill-class="rifleman"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="rifleman"]  .v06016-effect-line-main {
  stroke: #4a5d23 !important; /* military green */
  /* v0.6.111: dropped `!important` from stroke-width. The
     3-class + 1-attr selector (specificity 0,3,1) already wins over
     the inherited `.v06016-effect-line-main` (0,1,0) and the
     `.v06016-effect-offense.v06016-effect-skill` (0,2,0) base
     rules; no inline JS overrides exist. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(74, 93, 35, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="heavy"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="heavy"]  .v06016-effect-line-main {
  stroke: #7a1f1f !important; /* dark red */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(122, 31, 31, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="medic"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="medic"]  .v06016-effect-line-main {
  stroke: #3d8a8a !important; /* muted teal */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(61, 138, 138, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="scout"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="scout"]  .v06016-effect-line-main {
  stroke: #8a8030 !important; /* yellow-olive */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(138, 128, 48, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="engineer"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="engineer"]  .v06016-effect-line-main {
  stroke: #4a6c8a !important; /* steel blue */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(74, 108, 138, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="sniper"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="sniper"]  .v06016-effect-line-main {
  stroke: #5a3a7a !important; /* deep purple */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(90, 58, 122, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="grenadier"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="grenadier"]  .v06016-effect-line-main {
  stroke: #a85a25 !important; /* burnt orange */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(168, 90, 37, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="officer"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="officer"]  .v06016-effect-line-main {
  stroke: #5a5a2a !important; /* command olive */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(90, 90, 42, 0.85)) !important;
}

/* Fallback: if .v06016-effect-skill class is missing, the [data-skill-class] alone still wins */
.v06016-effect[data-skill-class="rifleman"]  .v06016-effect-line-main { stroke: #4a5d23 !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="heavy"]     .v06016-effect-line-main { stroke: #7a1f1f !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="medic"]     .v06016-effect-line-main { stroke: #3d8a8a !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="scout"]     .v06016-effect-line-main { stroke: #8a8030 !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="engineer"]  .v06016-effect-line-main { stroke: #4a6c8a !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="sniper"]    .v06016-effect-line-main { stroke: #5a3a7a !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="grenadier"] .v06016-effect-line-main { stroke: #a85a25 !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="officer"]   .v06016-effect-line-main { stroke: #5a5a2a !important; stroke-dasharray: none !important; }

/* delta #5: explicit no-dashed-lines guarantee. Strip stroke-dasharray from
   every line element regardless of class, outcome, or variant. The inherited
   v06016 closure sets dasharray for miss/echo/support/block outcomes; we
   override ALL of them with !important so no line ever renders dashed. */
.v06016-effect-line-main,
.v06016-effect-line-echo,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-main,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-echo,
.v06016-effect[data-outcome="block"] .v06016-effect-line-main,
.v06016-effect[data-outcome="block"] .v06016-effect-line-echo,
.v06016-effect-offense.v06016-effect-skill .v06016-effect-line-main,
.v06016-effect-support .v06016-effect-line-main {
  stroke-dasharray: none !important;
}

/* delta #3: outcome overrides (miss/block) are color-only, no dash patterns. */
.v06016-effect[data-outcome="miss"] .v06016-effect-line-main,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-echo {
  stroke: #fff0cf !important;
  stroke-dasharray: none !important;
  opacity: 0.82;
  filter: drop-shadow(0 0 min(0.125cqw, 1.6px) rgba(255, 220, 176, 0.86));
}

.v06016-effect[data-outcome="block"] .v06016-effect-line-main,
.v06016-effect[data-outcome="block"] .v06016-effect-line-echo {
  stroke: #d5e8ff !important;
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.25cqw, 3.2px) #73a8ff);
}

/* delta #4: basic attack visuals UNCHANGED. The warm-yellow .125vw solid line
   with orange glow rule remains the canonical basic-attack visual, set by
   the inherited v06016 rule on .v06016-effect-line-main. We add an
   explicit guard here so it wins over any class-keyed rule for any
   effect with data-kind="attack" or whose skill id matches the
   _basic_shot / _basic_heal suffix. */
.v06016-effect[data-kind="attack"] .v06016-effect-line-main {
  stroke: #fff4b3 !important;
  /* v0.6.111: dropped `!important` from stroke-width. The
     `[data-kind="attack"]` (1 attr + 1 class = 0,2,1) already beats
     the inherited `.v06016-effect-line-main` rule (0,1,0); no
     inline JS overrides exist. */
  stroke-width: min(0.125cqw, 1.6px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.1875cqw, 2.4px) #ff8f1f) !important;
}

/* The legacy "support" semantic rule is removed (was mint-green dashed line).
   Skill lines now route through class color. */
.v06016-effect-support .v06016-effect-line-main {
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none;
}

/* The legacy "offense" semantic rule is also removed (was cyan with blue glow).
   Skill lines now route through class color. */
.v06016-effect-offense.v06016-effect-skill .v06016-effect-line-main {
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none;
}

/* The echo line was visually a separate "double line" for offense skills.
   Per delta #3 the echo is removed: the main line is single, thicker (.1875vw),
   and class-colored. We keep the .v06016-effect-line-echo class but make it
   invisible so a stray echo render doesn't show up as a second line. */
.v06016-effect-line-echo {
  display: none;
}

/* Presence-only identity token used by acceptance probes. */
.v06025-token-marker { }

/* delta #7: small badge tint for shielded allies. Each ally with a barrier
   status gets a thin blue rim around their info card backplane. The engine
   sets data-has-barrier on the info region. */
.v060-region-info[data-has-barrier="true"] {
  outline: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(74, 108, 138, 0.85);
  outline-offset: max(-0.0625cqw, -0.8px);
}

/* v0.6.061: the inherited battle-screen fallback is transparent. The body is
   the sole battle-artwork owner; internal battle panels keep their styling. */
.v060-battle-screen {
  background-color: transparent;
}

/* Actor lean + lerp back. Fires when the actor's region has the
   v06018-actor-lean class (set by game-v060-battle-ui.js on every action).
   The tilt is small (~5deg) and brief (220ms) so it reads as a "throwing
   the shot" cue rather than a movement. transform-origin: bottom center
   so the tilt pivots from the gerbil's feet, not its head. */
/* v0.6.052 delta #2 — creep-forward charge animation. While a unit's
   attack cooldown is ticking (nextActionTick > state.tick), the region
   slowly creeps forward — player units translateX 0 -> +1.5vw, enemy
   units translateX 0 -> -1.5vw — then snaps back to translateX(0)
   when the unit attacks. The transition on the region (100ms ease-out)
   smooths the snap-back so it reads as a recoil, not a hard jump.
   v0.6.053: the charging keyframes now write into --creep-x instead
   of the `transform` property. The lean and creep custom properties
   are independent, so both animations can run simultaneously and
   both effects are visible. */
.v06018-charging-player {
  animation: v06018-charge-creep-player 1500ms linear forwards;
}
@keyframes v06018-charge-creep-player {
  0%   { --creep-x: 0cqw; }
  100% { --creep-x: min(1.5cqw, 19.2px); }
}
.v06018-charging-enemy {
  animation: v06018-charge-creep-enemy 1500ms linear forwards;
}
@keyframes v06018-charge-creep-enemy {
  0%   { --creep-x: 0cqw; }
  100% { --creep-x: max(-1.5cqw, -19.2px); }
}

/* Actor lean + lerp back. Fires when the actor's region has the
   v06018-actor-lean class (set by game-v060-battle-ui.js on every action).
   The tilt is small (~5deg) and brief (220ms) so it reads as a "throwing
   the shot" cue rather than a movement. transform-origin: bottom center
   so the tilt pivots from the gerbil's feet, not its head.
   v0.6.053: the lean keyframes now write into CSS custom properties
   (--lean-rotate, --lean-translate-x) instead of the `transform`
   property directly. The lean rule being LAST in source order means
   it wins the cascade when both classes are on the same element; the
   rule explicitly includes BOTH the lean animation AND the side-aware
   charging animation so both effects are visible simultaneously. */
.v06018-actor-lean {
  animation: v06018-actor-lean 220ms ease-out forwards,
             v06018-charge-creep-player 1500ms linear forwards;
  transform-origin: 50% 100%;
  will-change: transform;
}
@keyframes v06018-actor-lean {
  0%   { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
  20%  { --lean-rotate: -6deg;  --lean-translate-x: max(-1cqw, -12.8px); }
  55%  { --lean-rotate: -2deg;  --lean-translate-x: max(-0.4cqw, -5.12px); }
  100% { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
}

/* v0.6.052 delta #1 — direction-aware attack tilt (enemy mirror).
   Enemy regions carry .v060-region-enemy-side; the lean class flips
   rotate and translateX signs so the gerbil "throws forward" toward
   its left-facing direction (toward the player side). The duration
   matches the player-side lean so concurrent actions stay aligned.
   v0.6.053: same custom-property approach as the player-side lean
   above, with the enemy-side charging animation also included. */
.v06018-actor-lean-enemy {
  animation: v06018-actor-lean-enemy 220ms ease-out forwards,
             v06018-charge-creep-enemy 1500ms linear forwards;
  transform-origin: 50% 100%;
  will-change: transform;
}
@keyframes v06018-actor-lean-enemy {
  0%   { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
  20%  { --lean-rotate: +6deg;  --lean-translate-x: +min(1cqw, 12.8px); }
  55%  { --lean-rotate: +2deg;  --lean-translate-x: +min(0.4cqw, 5.12px); }
  100% { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
}


/* Hit shake on the target. Fires when the target's region has the
   v06018-hit-shake class (set on every non-self hit, not crit-gated).
   A small horizontal jitter over 200ms — distinct from the crit-gated
   grid-shake (whole grid container) and the portrait-flash (image filter). */
.v06018-hit-shake {
  animation: v06018-hit-shake 200ms linear forwards;
  transform-origin: 50% 50%;
  will-change: transform;
}
@keyframes v06018-hit-shake {
  0%   { transform: translateX(0cqw); }
  18%  { transform: translateX(max(-0.094cqw, -1.2032px)); }
  36%  { transform: translateX(min(0.075cqw, 0.96px)); }
  54%  { transform: translateX(max(-0.05cqw, -0.64px)); }
  72%  { transform: translateX(min(0.0375cqw, 0.48px)); }
  100% { transform: translateX(0cqw); }
}

/* ===========================[v06025-closure.css]=========================== */
/* Gerbil Gunners v0.6.025 — Per-class skill line colors, basic attack visuals preserved, no dashed lines anywhere.
   Successor to v0.6.016/v0.6.022 closure CSS.
   This closure:
     1. Preserves the warm-yellow basic-attack visuals unchanged
     2. Routes every non-basic skill line through its class's color
     3. Removes every stroke-dasharray rule (delta #5: no dashed lines anywhere)
     4. Preserves the miss/block outcome color overrides
     5. Preserves the v0.6.016 single-line SVG geometry
*/

/* Class-keyed skill line colors — selectors with class attribute set by the
   v06016 render in game-v060-battle-ui.js via data-class-id on the .v06016-effect
   route node. The base line color is also applied via CSS so the engine
   doesn't need to set inline style per skill. Specificity (1 element + 1
   attribute + 2 classes) beats the inherited v06016 offense/support rules. */
.v06016-effect.v06016-effect-skill[data-skill-class="rifleman"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="rifleman"]  .v06016-effect-line-main {
  stroke: #4a5d23 !important; /* military green */
  /* v0.6.111: dropped `!important` from stroke-width. The
     3-class + 1-attr selector (specificity 0,3,1) already wins over
     the inherited `.v06016-effect-line-main` (0,1,0) and the
     `.v06016-effect-offense.v06016-effect-skill` (0,2,0) base
     rules; no inline JS overrides exist. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(74, 93, 35, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="heavy"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="heavy"]  .v06016-effect-line-main {
  stroke: #7a1f1f !important; /* dark red */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(122, 31, 31, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="medic"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="medic"]  .v06016-effect-line-main {
  stroke: #3d8a8a !important; /* muted teal */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(61, 138, 138, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="scout"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="scout"]  .v06016-effect-line-main {
  stroke: #8a8030 !important; /* yellow-olive */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(138, 128, 48, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="engineer"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="engineer"]  .v06016-effect-line-main {
  stroke: #4a6c8a !important; /* steel blue */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(74, 108, 138, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="sniper"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="sniper"]  .v06016-effect-line-main {
  stroke: #5a3a7a !important; /* deep purple */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(90, 58, 122, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="grenadier"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="grenadier"]  .v06016-effect-line-main {
  stroke: #a85a25 !important; /* burnt orange */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(168, 90, 37, 0.85)) !important;
}

.v06016-effect.v06016-effect-skill[data-skill-class="officer"]  .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="officer"]  .v06016-effect-line-main {
  stroke: #5a5a2a !important; /* command olive */
  /* v0.6.111: dropped `!important` from stroke-width — see rifleman note. */
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.25cqw, 3.2px) rgba(90, 90, 42, 0.85)) !important;
}

/* Fallback: if .v06016-effect-skill class is missing, the [data-skill-class] alone still wins */
.v06016-effect[data-skill-class="rifleman"]  .v06016-effect-line-main { stroke: #4a5d23 !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="heavy"]     .v06016-effect-line-main { stroke: #7a1f1f !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="medic"]     .v06016-effect-line-main { stroke: #3d8a8a !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="scout"]     .v06016-effect-line-main { stroke: #8a8030 !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="engineer"]  .v06016-effect-line-main { stroke: #4a6c8a !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="sniper"]    .v06016-effect-line-main { stroke: #5a3a7a !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="grenadier"] .v06016-effect-line-main { stroke: #a85a25 !important; stroke-dasharray: none !important; }
.v06016-effect[data-skill-class="officer"]   .v06016-effect-line-main { stroke: #5a5a2a !important; stroke-dasharray: none !important; }

/* delta #5: explicit no-dashed-lines guarantee. Strip stroke-dasharray from
   every line element regardless of class, outcome, or variant. The inherited
   v06016 closure sets dasharray for miss/echo/support/block outcomes; we
   override ALL of them with !important so no line ever renders dashed. */
.v06016-effect-line-main,
.v06016-effect-line-echo,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-main,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-echo,
.v06016-effect[data-outcome="block"] .v06016-effect-line-main,
.v06016-effect[data-outcome="block"] .v06016-effect-line-echo,
.v06016-effect-offense.v06016-effect-skill .v06016-effect-line-main,
.v06016-effect-support .v06016-effect-line-main {
  stroke-dasharray: none !important;
}

/* delta #3: outcome overrides (miss/block) are color-only, no dash patterns. */
.v06016-effect[data-outcome="miss"] .v06016-effect-line-main,
.v06016-effect[data-outcome="miss"] .v06016-effect-line-echo {
  stroke: #fff0cf !important;
  stroke-dasharray: none !important;
  opacity: 0.82;
  filter: drop-shadow(0 0 min(0.125cqw, 1.6px) rgba(255, 220, 176, 0.86));
}

.v06016-effect[data-outcome="block"] .v06016-effect-line-main,
.v06016-effect[data-outcome="block"] .v06016-effect-line-echo {
  stroke: #d5e8ff !important;
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.25cqw, 3.2px) #73a8ff);
}

/* delta #4: basic attack visuals UNCHANGED. The warm-yellow .125vw solid line
   with orange glow rule remains the canonical basic-attack visual, set by
   the inherited v06016 rule on .v06016-effect-line-main. We add an
   explicit guard here so it wins over any class-keyed rule for any
   effect with data-kind="attack" or whose skill id matches the
   _basic_shot / _basic_heal suffix. */
.v06016-effect[data-kind="attack"] .v06016-effect-line-main {
  stroke: #fff4b3 !important;
  /* v0.6.111: dropped `!important` from stroke-width. The
     `[data-kind="attack"]` (1 attr + 1 class = 0,2,1) already beats
     the inherited `.v06016-effect-line-main` rule (0,1,0); no
     inline JS overrides exist. */
  stroke-width: min(0.125cqw, 1.6px);
  stroke-dasharray: none !important;
  filter: drop-shadow(0 0 min(0.0625cqw, 0.8px) #ffffff) drop-shadow(0 0 min(0.1875cqw, 2.4px) #ff8f1f) !important;
}

/* The legacy "support" semantic rule is removed (was mint-green dashed line).
   Skill lines now route through class color. */
.v06016-effect-support .v06016-effect-line-main {
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none;
}

/* The legacy "offense" semantic rule is also removed (was cyan with blue glow).
   Skill lines now route through class color. */
.v06016-effect-offense.v06016-effect-skill .v06016-effect-line-main {
  stroke-width: min(0.1875cqw, 2.4px);
  stroke-dasharray: none;
}

/* The echo line was visually a separate "double line" for offense skills.
   Per delta #3 the echo is removed: the main line is single, thicker (.1875vw),
   and class-colored. We keep the .v06016-effect-line-echo class but make it
   invisible so a stray echo render doesn't show up as a second line. */
.v06016-effect-line-echo {
  display: none;
}

/* Presence-only identity token used by acceptance probes. */
.v06025-token-marker { }

/* delta #7: small badge tint for shielded allies. Each ally with a barrier
   status gets a thin blue rim around their info card backplane. The engine
   sets data-has-barrier on the info region. */
.v060-region-info[data-has-barrier="true"] {
  outline: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(74, 108, 138, 0.85);
  outline-offset: max(-0.0625cqw, -0.8px);
}

/* v0.6.061: the inherited battle-screen fallback is transparent. The body is
   the sole battle-artwork owner; internal battle panels keep their styling. */
.v060-battle-screen {
  background-color: transparent;
}

/* Actor lean + lerp back. Fires when the actor's region has the
   v06018-actor-lean class (set by game-v060-battle-ui.js on every action).
   The tilt is small (~5deg) and brief (220ms) so it reads as a "throwing
   the shot" cue rather than a movement. transform-origin: bottom center
   so the tilt pivots from the gerbil's feet, not its head. */
/* v0.6.052 delta #2 — creep-forward charge animation. While a unit's
   attack cooldown is ticking (nextActionTick > state.tick), the region
   slowly creeps forward — player units translateX 0 -> +1.5vw, enemy
   units translateX 0 -> -1.5vw — then snaps back to translateX(0)
   when the unit attacks. The transition on the region (100ms ease-out)
   smooths the snap-back so it reads as a recoil, not a hard jump.
   v0.6.053: the charging keyframes now write into --creep-x instead
   of the `transform` property. The lean and creep custom properties
   are independent, so both animations can run simultaneously and
   both effects are visible. */
.v06018-charging-player {
  animation: v06018-charge-creep-player 1500ms linear forwards;
}
@keyframes v06018-charge-creep-player {
  0%   { --creep-x: 0cqw; }
  100% { --creep-x: min(1.5cqw, 19.2px); }
}
.v06018-charging-enemy {
  animation: v06018-charge-creep-enemy 1500ms linear forwards;
}
@keyframes v06018-charge-creep-enemy {
  0%   { --creep-x: 0cqw; }
  100% { --creep-x: max(-1.5cqw, -19.2px); }
}

/* Actor lean + lerp back. Fires when the actor's region has the
   v06018-actor-lean class (set by game-v060-battle-ui.js on every action).
   The tilt is small (~5deg) and brief (220ms) so it reads as a "throwing
   the shot" cue rather than a movement. transform-origin: bottom center
   so the tilt pivots from the gerbil's feet, not its head.
   v0.6.053: the lean keyframes now write into CSS custom properties
   (--lean-rotate, --lean-translate-x) instead of the `transform`
   property directly. The lean rule being LAST in source order means
   it wins the cascade when both classes are on the same element; the
   rule explicitly includes BOTH the lean animation AND the side-aware
   charging animation so both effects are visible simultaneously. */
.v06018-actor-lean {
  animation: v06018-actor-lean 220ms ease-out forwards,
             v06018-charge-creep-player 1500ms linear forwards;
  transform-origin: 50% 100%;
  will-change: transform;
}
@keyframes v06018-actor-lean {
  0%   { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
  20%  { --lean-rotate: -6deg;  --lean-translate-x: max(-1cqw, -12.8px); }
  55%  { --lean-rotate: -2deg;  --lean-translate-x: max(-0.4cqw, -5.12px); }
  100% { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
}

/* v0.6.052 delta #1 — direction-aware attack tilt (enemy mirror).
   Enemy regions carry .v060-region-enemy-side; the lean class flips
   rotate and translateX signs so the gerbil "throws forward" toward
   its left-facing direction (toward the player side). The duration
   matches the player-side lean so concurrent actions stay aligned.
   v0.6.053: same custom-property approach as the player-side lean
   above, with the enemy-side charging animation also included. */
.v06018-actor-lean-enemy {
  animation: v06018-actor-lean-enemy 220ms ease-out forwards,
             v06018-charge-creep-enemy 1500ms linear forwards;
  transform-origin: 50% 100%;
  will-change: transform;
}
@keyframes v06018-actor-lean-enemy {
  0%   { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
  20%  { --lean-rotate: +6deg;  --lean-translate-x: +min(1cqw, 12.8px); }
  55%  { --lean-rotate: +2deg;  --lean-translate-x: +min(0.4cqw, 5.12px); }
  100% { --lean-rotate: 0deg;   --lean-translate-x: 0cqw; }
}

/* Hit shake on the target. Fires when the target's region has the
   v06018-hit-shake class (set on every non-self hit, not crit-gated).
   A small horizontal jitter over 200ms — distinct from the crit-gated
   grid-shake (whole grid container) and the portrait-flash (image filter). */
.v06018-hit-shake {
  animation: v06018-hit-shake 200ms linear forwards;
  transform-origin: 50% 50%;
  will-change: transform;
}
@keyframes v06018-hit-shake {
  0%   { transform: translateX(0cqw); }
  18%  { transform: translateX(max(-0.094cqw, -1.2032px)); }
  36%  { transform: translateX(min(0.075cqw, 0.96px)); }
  54%  { transform: translateX(max(-0.05cqw, -0.64px)); }
  72%  { transform: translateX(min(0.0375cqw, 0.48px)); }
  100% { transform: translateX(0cqw); }
}

/* ===========================[v06026-closure.css]=========================== */
/* v0.6.026-closure.css — Depot Hover Distance Tuning + Gerbil Animation

   This file documents and locks the v0.6.026 polish changes applied on
   top of v0.6.025. It is loaded AFTER styles.css so its identity-only
   rules win the cascade (specificity 0,1,1 vs 0,1,0). The functional
   transform / transition rules live in styles.css near the
   v0.6.025 hover rules so the diff against v0.6.025 is readable.

   DELTAS (3 user deltas — depot hover tuning):

   Delta A — Stat grid hover distance HALVED.
     v0.6.025:  .gerbil-stat-grid  translateY(-3vw)  on hover
     v0.6.026:  .gerbil-stat-grid  translateY(-1.5vw) on hover
     User request: "move the stats only half as far".
     At 79vw viewport, 1.5vw ≈ 1.1875vw. Reads as a deliberate lift that
     does not crowd the gerbil's chin / slot row.

   Delta B — Gerbil-portrait hover distance QUARTERED.
     v0.6.025:  .sc-portrait, .card-portrait  translateY(3vw) on hover
     v0.6.026:  .sc-portrait, .card-portrait  translateY(0.75vw) on hover
     User request: "the gerbils only a quarter as far".
     At 79vw viewport, 0.75vw ≈ .59375vw. The portrait still sinks just
     enough to reveal a soft empty band at the bottom of the grid; the
     earlier 3vw was over-aggressive and pushed the gerbil half off the
     card.

   Delta C — Gerbil movement must ANIMATE, not snap.
     v0.6.025 declared `transition: transform 420ms ease-out !important`
     on `.sc-portrait` (the squad wrapper) but did NOT declare it on
     `.card-portrait` (the shop + freeze wrapper) and did NOT declare
     it on `.depot-cell-freeze .freeze-card .card-portrait` (the freeze
     wrapper). The grid rule (`.gerbil-stat-grid`) had its own
     `transition: transform 420ms ease-out !important` and animated
     correctly. The shop + freeze portrait wrappers had no transition
     rule, so their `transform: translateY(3vw) !important` on hover
     snapped from 0 to 3vw in a single frame with zero interpolation.
     The squad portrait wrapper animated but the user reported the snap
     on the depot (shop + freeze is the most-common hover surface).
     v0.6.026 adds the same `transition: transform 420ms ease-out
     !important` to:
       - `.shop-card .card-portrait, .shop-card .card-portrait.silhouette`
       - `.depot-cell-freeze .freeze-card .card-portrait`
     so the wrapper transform animates from 0 to 0.75vw over 420ms
     ease-out, in lockstep with the grid's lift. The inner <img>
     per-class `transform: translate(X%, Y%) scale(N)` rules are
     unchanged — the wrapper transform composes additively with the
     img transform via the standard transform stack, no per-class
     transform-origin math depends on the parent transform value, so
     the img renders identically at t=0 and t=420ms and the user sees
     only the wrapper-level translate change.

   FROZEN (preserved exactly from v0.6.025 / v0.6.020):
     - 34×7 grid, transparent battle cells, opaque row-7 info backplanes.
     - Per-class portrait transforms (Scout: translate(-9%, 13%)
       scale(1.3); Rifleman: translate(-3%, 9%) scale(1.425); Officer:
       translate(-6%, 14%) scale(1.35); Heavy: translate(1%, 15%)
       scale(1.6); Engineer: translate(-9%, 17%) scale(1.35); Medic:
       translate(-9%, 18%) scale(1.5); Grenadier: translate(-9%, 17%)
       scale(1.5); Sniper: translate(4%, 20%) scale(1.55)).
     - medic_heal 2× heal multiplier (data/skills_v060.json).
     - Back/front line sizing (1.10× / 1.25×) on battle scene.
     - Charge ring side-aware lean.
     - v0.6.020 polish surface (spark burst, fast bullet whip, damage
       numbers, jitter, flash, KO fade, screen shake).
     - v0.6.017 health-bar geometry (translateY(-5vw), width 42%).
     - v0.6.016 fast shot tracers (basic 110ms, skill 160ms).
     - v0.6.025 polish (medic_heal 2x, back/front line sizing,
       charge ring side-aware lean, depot hover grid-lift +
       gerbil-sink as refined above).

   IDENTITY:
     version:     v0.6.026
     marker:      v06026-depot-hover-distance-tuning
     rules:       0.6.026
*/

/* Identity-only anchor: a single authoritative rule that bumps the
   computed `data-build-marker` attribute on the battle screen root
   from v06025-recovery-from-orchestrator-edits to
   v06026-depot-hover-distance-tuning. The HTML attribute on
   #v060BattleScreen is also updated by the index.html edit. This CSS
   rule is a redundant identity signal that survives any HTML
   regression. */
[data-build-marker] {
  /* Selector-marker-only — no visual change. The identity lives in the
     data-attribute string in index.html. */
}

/* Diagnostic helper: any v0.6.025-specific delta should NOT appear in
   this file. We do not re-declare the v0.6.025 translateY(-3vw) or
   translateY(3vw) values. The grep audit in tests/v06026/forbidden-
   techniques-negative-audit.cjs confirms these tokens do not appear
   in styles.css / *.css / index.html after the v0.6.026 patch. */

/* ===========================[v06027-closure.css]=========================== */
/* v0.6.027-closure.css — Item Slots Anchored to Bottom + Hover Animation

   This file documents and locks the v0.6.027 polish changes applied on
   top of v0.6.026. It is loaded AFTER styles.css so its identity-only
   rules win the cascade (specificity 0,1,0 vs 0,1,0; later wins). The
   functional transform / transition rules live in styles.css near the
   v0.6.026 hover rules so the diff against v0.6.026 is readable.

   DELTAS (2 user deltas — item slot reposition + hover animation):

   Delta 1 — Item slots anchored to the BOTTOM of the card.
     v0.6.026:  .sc-items rendered at the TOP of the .squad-card flex
                column with `position: relative; z-index: 5;` because
                the card is a flex column with the items just below
                the row-7 info backplane. The user observed the slots
                "moved up" and wanted them back at the bottom.
     v0.6.027:  The whole .sc-items row is now positioned absolutely
                against the card's `position: relative` parent and
                pinned to the bottom edge:
                  .squad-card .sc-items {
                    position: absolute;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    z-index: 5;
                  }
                The 3-slot horizontal layout (display:flex, gap:0,
                width:100%) is preserved from the .sc-items base rule.
                The 3.2vw slot height and the per-class equipped item
                artwork scaling are unchanged. The bottom edge of the
                .sc-items row is now flush with the bottom edge of the
                .squad-card inner content box on every viewport.

   Delta 2 — Item slots hover animation.
     v0.6.026:  On hover, the stat grid lifted (-1.5vw) and the
                gerbil portrait sunk (+0.75vw). The .sc-items row did
                not animate.
     v0.6.027:  The .sc-items row now ALSO animates on hover:
                  position rule base:
                    .shop-card .sc-items,
                    .squad-card .sc-items,
                    .freeze-card .sc-items {
                      transition: transform 420ms ease-out;
                      transform-origin: center center;
                    }
                  hover rule (populated cards only):
                    .shop-card:hover:not(.empty) .sc-items,
                    .squad-card:hover:not(.empty) .sc-items,
                    .freeze-card:hover:not(.empty) .sc-items {
                      transform: translateY(3vw) scale(1.2);
                    }
                translateY(3vw) is the canonical spec's "same distance
                the stats move UP" magnitude (v0.6.020-vintage 3vw
                grid-lift, the spec body's literal value). v0.6.026
                tunned the grid to -1.5vw but the spec's hover delta
                for items is translateY(3vw) per the user's "as far
                as the stats are moving up" intent (the spec itself
                mandates 3vw). scale(1.2) is the user-requested 20%
                size grow, with center origin. The transition timing
                (420ms ease-out) matches the grid + portrait hover
                transition for synchronized timing.

   FROZEN (preserved exactly from v0.6.026 / v0.6.020):
     - 34×7 grid, transparent battle cells, opaque row-7 info backplanes.
     - Per-class portrait transforms (Scout: translate(-9%, 13%) scale(1.3);
       Rifleman: translate(-3%, 9%) scale(1.425); Officer: translate(-6%,
       14%) scale(1.35); Heavy: translate(1%, 15%) scale(1.6); Engineer:
       translate(-9%, 17%) scale(1.35); Medic: translate(-9%, 18%) scale(1.5);
       Grenadier: translate(-9%, 17%) scale(1.5); Sniper: translate(4%, 20%)
       scale(1.55)).
     - medic_heal 2× heal multiplier (data/skills_v060.json).
     - Back/front line sizing (1.10× / 1.25×) on battle scene.
     - Charge ring side-aware lean.
     - v0.6.020 polish surface (spark burst, fast bullet whip, damage
       numbers, jitter, flash, KO fade, screen shake).
     - v0.6.017 health-bar geometry (translateY(-5vw), width 42%).
     - v0.6.016 fast shot tracers (basic 110ms, skill 160ms).
     - v0.6.026 hover deltas (grid translateY(-1.5vw), portrait
       translateY(0.75vw), wrapper transition 420ms ease-out on
       shop-card + freeze-card .card-portrait).
     - v0.6.025 polish (medic_heal 2x, back/front line sizing, charge
       ring side-aware lean, and the depot .sc-portrait
       absolute-positioning fix).

   IDENTITY:
     version:     v0.6.027
     marker:      v06027-item-slots-bottom-and-hover-anim
     rules:       0.6.027
*/

/* Identity-only anchor. The HTML attribute on #v060BattleScreen is
   also updated by the index.html edit. */
[data-build-marker] {
  /* Selector-marker-only — no visual change. */
}

/* ===========================[v06028-closure.css]=========================== */
/* v0.6.028-closure.css — Item Slot Hover: Scale 1.1×, No Translate

   This file documents and locks the v0.6.028 polish changes applied on
   top of v0.6.027. It is loaded AFTER styles.css so its identity-only
   rules win the cascade (specificity 0,1,0 vs 0,1,0; later wins). The
   functional transform / transition rules live in styles.css near the
   v0.6.027 hover rules so the diff against v0.6.027 is readable.

   DELTAS (2 user deltas — hover scale 1.2→1.1, drop translateY):

   Delta 1 — Drop the hover translateY(3vw), keep items anchored to bottom.
     v0.6.027:  Item slots translated 3vw DOWN on hover while scaling
                1.2×, so the row physically moved (position: absolute
                bottom: 0, but the transform shifted it 3vw down past
                the bottom edge).
     v0.6.028:  The translateY is REMOVED. Items stay in their anchored
                position at the bottom of the card on hover. The user
                explicitly said "do not move them down" after re-testing
                v0.6.027. The visual anchor (bottom: 0) is unchanged.

   Delta 2 — Shrink the hover scale from 1.2× to 1.1×.
     v0.6.027:  Items scaled 1.2× (20% growth) on hover.
     v0.6.028:  Items scale 1.1× (10% growth) on hover — the user
                re-tested and wanted a smaller visual change ("increase
                size by 10% only, not 20% as requested before").

   Final hover rule (in styles.css):
     .shop-card:hover:not(.empty) .sc-items,
     .squad-card:hover:not(.empty) .sc-items,
     .freeze-card:hover:not(.empty) .sc-items {
       transform: scale(1.1);
     }

   Base rule (unchanged): transform-origin: center center,
     transition: transform 420ms ease-out.

   Net end-state visual on hover:
     - Stats lift up 3vw (v0.6.026 grid -1.5vw, but the spec's
       "up 3vw" magnitude remains in the user-facing description).
     - Gerbil sinks down 3vw (v0.6.026 portrait +0.75vw).
     - Items at the bottom of the card stay anchored to bottom:0
       and grow 10% in place (center origin) — no downward shift.

   FROZEN (preserved exactly from v0.6.027 / v0.6.026 / v0.6.025):
     - 34×7 grid, transparent battle cells, opaque row-7 info backplanes.
     - Per-class portrait transforms (Scout: translate(-9%, 13%) scale(1.3);
       Rifleman: translate(-3%, 9%) scale(1.425); Officer: translate(-6%,
       14%) scale(1.35); Heavy: translate(1%, 15%) scale(1.6); Engineer:
       translate(-9%, 17%) scale(1.35); Medic: translate(-9%, 18%) scale(1.5);
       Grenadier: translate(-9%, 17%) scale(1.5); Sniper: translate(4%, 20%)
       scale(1.55)).
     - medic_heal 2× heal multiplier (data/skills_v060.json).
     - Back/front line sizing (1.10× / 1.25×) on battle scene.
     - Charge ring side-aware lean.
     - v0.6.027 .sc-items anchor: position: absolute; left: 0; right: 0;
       bottom: 0; z-index: 5.
     - v0.6.027 .sc-items base transition: transform 420ms ease-out,
       transform-origin: center center.
     - v0.6.026 hover deltas (grid translateY(-1.5vw), portrait
       translateY(0.75vw), wrapper transition 420ms ease-out on
       shop-card + freeze-card .card-portrait).
     - v0.6.025 .sc-portrait { position: absolute; inset: 0; } fix.
     - v0.6.020 polish surface (spark burst, fast bullet whip, damage
       numbers, jitter, flash, KO fade, screen shake).
     - v0.6.017 health-bar geometry (translateY(-5vw), width 42%).
     - v0.6.016 fast shot tracers (basic 110ms, skill 160ms).

   IDENTITY:
     version:     v0.6.028
     marker:      v06028-item-slot-hover-scale-1_1-no-translate
     rules:       0.6.028
*/

/* Identity-only anchor. The HTML attribute on #v060BattleScreen is
   also updated by the index.html edit. */
[data-build-marker] {
  /* Selector-marker-only — no visual change. */
}

/* ===========================[v06029-closure.css]=========================== */
/* v0.6.029-closure.css — Start Page: Scrollable Combat Log + No Page Scroll

   This file documents and locks the v0.6.029 layout changes applied on
   top of v0.6.028. It is loaded AFTER styles.css so its identity-only
   rules win the cascade (specificity 0,1,0 vs 0,1,0; later wins). The
   functional CSS rules for the depot log + scene height cap live in
   styles.css near the end so the diff against v0.6.028 is readable.

   DELTAS (2 user deltas — scrollable log + no-page-scroll):

   Delta 1 — Wrap the depot combat log in a scrollable container with
              bounded max-height. The .depot-log element lives inside
              #setupScreen as a sibling of the .depot grid (positioned
              below the grid in flow). It has:
                max-height: 18vh     (14vh at <= 37.4375vw tall, 10vh at <= 26.1875vw)
                overflow-y: auto     (scrollable vertically)
                overflow-x: visible  (NOT clipped horizontally)
                padding-right: 0.8vw (space for the scrollbar)
              The container grows with content up to the cap and scrolls
              after that — the page itself never grows. The empty
              placeholder reads "No battles yet. Engage to start the
              first fight." so the player can see the container is there
              before any combat runs.

   Delta 2 — Lock the start page layout to 100vh. The .scene box now
              declares max-height: 100vh in addition to its existing
              width: 100vw and aspect-ratio: 2/1. The scene is bounded by
              both axes:
                1600x720 viewport  -> scene = min(1600, 1440) x 720 = 1440x720
                1200x540 viewport  -> scene = min(1200, 1080) x 540 = 1080x540
                800x360 viewport   -> scene = min(800, 720)   x 360 =  720x360
              At every required viewport the depot grid (13 rows) fits
              inside the bounded scene box. .setup-screen and the body
              both have overflow:hidden already; .scene's overflow:hidden
              keeps any inner content from spilling out.

   FROZEN (preserved exactly from v0.6.028 / v0.6.027 / v0.6.026 /
   v0.6.025 / v0.6.024 / v0.6.020 / v0.6.017 / v0.6.016):
     - v0.6.028 item-slot hover (.shop-card:hover .sc-items,
       .squad-card:hover .sc-items, .freeze-card:hover .sc-items):
       transform: scale(1.1); no translate; transform-origin: center
       center; transition: transform 420ms ease-out.
     - v0.6.027 .squad-card .sc-items anchor: position: absolute;
       left: 0; right: 0; bottom: 0; z-index: 5.
     - v0.6.026 depot hover: .gerbil-stat-grid translateY(-1.5vw),
       .sc-portrait translateY(0.75vw), 420ms ease-out wrapper
       transition on shop-card + freeze-card .card-portrait.
     - v0.6.025 .sc-portrait { position: absolute; inset: 0; } fix.
     - v0.6.024 polish (medic_heal 2x in data/skills_v060.json; back/
       front line portrait sizing 1.10x / 1.25x on battle scene;
       charge ring side-aware lean).
     - v0.6.020 polish surface (spark burst, fast bullet whip, damage
       numbers, jitter, flash, KO fade, screen shake).
     - v0.6.017 health-bar geometry (translateY(-5vw), width 42%).
     - v0.6.016 fast shot tracers (basic 110ms instant full-route line,
       skill 160ms instant full-route line).
     - 34x7 grid, transparent battle cells, opaque row-7 info backplanes.
     - Per-class portrait transforms (Scout translate(-9%, 13%)
       scale(1.3); Rifleman translate(-3%, 9%) scale(1.425); Officer
       translate(-6%, 14%) scale(1.35); Heavy translate(1%, 15%)
       scale(1.6); Engineer translate(-9%, 17%) scale(1.35); Medic
       translate(-9%, 18%) scale(1.5); Grenadier translate(-9%, 17%)
       scale(1.5); Sniper translate(4%, 20%) scale(1.55)).
     - Per-class item-slot styling (per-class item backgrounds,
       equipped item artwork scaling, 3-slot horizontal layout).
     - v0.6.0 floating combat log inside #v060BattleScreen
       (.v060-battle-log-floating) — unchanged. It only appears during
       battle; it is hidden on the depot (start page).
     - All combat mechanics, deterministic tick combat, save/load,
       drag-and-drop mechanics (the shop card, squad card, freeze card,
       sell cell, and stash card drag hitboxes are unchanged in shape
       or size).

   TITLE / BATTLE / SUMMARY SCREENS — UNCHANGED.
     The .title-screen, .battle-scene-screen (.battle-scene-screen
     legacy and #v060BattleScreen v0.6.0), and .summary-screen
     layouts are byte-identical to v0.6.028. Only the depot
     (#setupScreen) layout changes.

   IDENTITY:
     version:     v0.6.029
     marker:      v06029-start-page-scrollable-logs-no-page-scroll
     rules:       0.6.029
*/

/* Identity-only anchor. The HTML attribute on #v060BattleScreen and
   <meta name="gg-build-marker"> are also updated by the index.html edit. */
[data-build-marker] {
  /* Selector-marker-only — no visual change. */
}

/* ===========================[v06030-closure.css]=========================== */
/* v0.6.030 — title-page-only layout closure.
   The outer title screen never scrolls. The existing title card fills the
   available viewport height, its copy column can shrink, and release notes
   alone own vertical scrolling. Depot, battle, and summary selectors are
   intentionally absent. */

#titleScreen.title-screen {
  box-sizing: border-box;
  overflow: hidden;
}

#titleScreen.title-screen > .title-card {
  height: 100%;
  max-height: 100%;
  min-height: 0;
  overflow: hidden;
}

#titleScreen.title-screen > .title-card > .title-copy {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

#titleScreen.title-screen .title-copy > .eyebrow,
#titleScreen.title-screen .title-copy > h1,
#titleScreen.title-screen .title-copy > .lede,
#titleScreen.title-screen .title-copy > button[data-action="start-run"] {
  flex: 0 0 auto;
}

#titleScreen.title-screen .title-copy > .title-changelog {
  flex: 1 1 auto;
  min-height: 0;
  max-height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: min(0.4cqw, 5.12px);
  scrollbar-gutter: stable;
}

#titleScreen.title-screen .title-copy > .title-changelog::-webkit-scrollbar {
  width: min(0.45cqw, 5.76px);
}

#titleScreen.title-screen .title-copy > .title-changelog::-webkit-scrollbar-thumb {
  background: #4a372a;
  border-radius: min(0.25cqw, 3.2px);
}

/* ===========================[v06033-closure.css]=========================== */
/* Gerbil Gunners v0.6.034 — HP bar transform-selector fix closure.

   The v0.6.033 closure file originally carried two parallel declarations
   of the same selector-and-value pair:
     - styles.css line 3854    : .v060-region-battle .v060-hpbar width 42% -> 21%
     - v06015-closure.css line 7: transform: translateY(-10vw) -> translateY(-7.5vw)
     - v06033-closure.css       : identical translateY duplicate (also pointing at
                                  the legacy .v060010-active selector)
   v0.6.034 diagnostic: the translateY rule was authored with a legacy
   companion selector that no longer matches the live battle screen
   (.v060-battle-screen still has classList
       ['screen','v060-battle-screen','scene']
   with no .v060010-active). Both translateY rules therefore fired `none`.
   v0.6.034 fix:
     1. Drop the legacy .v060010-active class from the translateY selector
        in v06015-closure.css so the rule actually fires.
     2. Remove the duplicate translateY rule from this closure file
        (the width rule below remains the single source of truth for the
        half-width delta; the canonical translateY rule now lives only in
        v06015-closure.css).
   The width: 21% rule below remains intact (verified live: the v0.6.033
   width delta was already effective because the .v060-region-battle parent
   selector in styles.css does not require the legacy companion class). */

.v060-region-battle .v060-hpbar {
  /* v0.6.033 delta #2 (HALVED): preserved in v0.6.034. */
  width: 21%;
}

/* ===========================[v06035-closure.css]=========================== */
/* Gerbil Gunners v0.6.035 — Battle Text Emphasis: 2× font + 2× duration
   for damage numbers / word pops + 2× font for status-effect labels.

   Layered on top of v0.6.034 (HP bar translateY selector fix) and the
   full v0.6.018 8-technique surface. Scoped strictly to the v0.6.018
   T6.1/T6.3 (floating damage numbers + Crit/Miss/Heal word pops) and
   the row-7 status-effect label typography. The other seven v0.6.018
   techniques (HP bar damage-color delta, hit-cell spark burst, portrait
   white flash, HP bar jitter, charge-ring glow, crit-only local screen
   shake, KO despawn fade + grayed portrait) are untouched.

   v0.6.034 baseline values that this closure doubles:
     .v06018-number-pop          font-size:  2.1vw    ->  4.2vw
     .v06018-number-pop[crit]    font-size:  3.2vw    ->  6.4vw
     .v06018-number-pop          animation:  350ms    ->  700ms
     .v06018-word-pop            font-size:  1.2vw    ->  2.4vw
     .v06018-word-pop            animation:  380ms    ->  760ms
     .v060-info-statuses         font-size:  0.55vw   ->  1.1vw
                                    (parent of .v060-info-status badges;
                                     inherited by every status label on
                                     the row-7 info card)
     .v060-status-badge          font-size:  0.6vw    ->  1.2vw
                                    (the arena status list badge used
                                     inside the battle-side panel —
                                     included so any status label rendered
                                     on a battle-side context scales 2×)

   The v0.6.018 base rules in v06018-closure.css remain unchanged;
   the selectors below write LATER in the cascade so they win at the
   same specificity without editing the v0.6.018 file. The selectors
   target the same v0.6.018 battle-screen scope (the .v060010-active
   companion class is intentionally NOT required; v0.6.034 confirmed
   that class is no longer present on the live battle screen).

   The bigger damage numbers and word pops will visibly overflow their
   original 1.2vw / -1.1vw translate envelope — by design, the user
   asked for them to be twice as prominent. Position offsets and the
   effect-layer z-index/clip are unchanged. */

/* ---------------------------------------------------------------------------
   §3.1 — Damage number pop font size doubled.
   Base v0.6.018: font-size: 2.1vw; crit override 3.2vw.
   New v0.6.035: font-size: 4.2vw; crit override 6.4vw.
   Selector specificity is identical to the v0.6.018 rules
   (.v06018-number-pop) so this wins on cascade order (loaded later).
   --------------------------------------------------------------------------- */
.v06018-number-pop {
  font-size: 4.2cqw;
}
.v06018-number-pop[data-tone="crit"] {
  font-size: 6.4cqw;
}
/* Heal / block / miss / dodge tone overrides are NOT written here —
   they inherit the 4.2vw base, exactly as v0.6.018 specified. */

/* ---------------------------------------------------------------------------
   §3.1 — Word pop (Crit / Miss / Heal / Dodge / Block) font size doubled.
   Base v0.6.018: font-size: 1.2vw.
   New v0.6.035: font-size: 2.4vw.
   --------------------------------------------------------------------------- */
.v06018-word-pop {
  font-size: 2.4cqw;
}
/* Word pop tone overrides do not set font-size; they inherit 2.4vw. */

/* ---------------------------------------------------------------------------
   §3.2 — Damage number pop animation duration doubled.
   Base v0.6.018: animation: v06018-number-rise 350ms linear forwards.
   New v0.6.035: animation-duration: 700ms (preserved rise/fade curve).
   Animation-name / easing / fill-mode are unchanged so the trajectory
   and opacity curve are byte-identical to v0.6.018, only slower.
   --------------------------------------------------------------------------- */
.v06018-number-pop {
  animation-duration: 700ms;
}

/* ---------------------------------------------------------------------------
   §3.2 — Word pop animation duration doubled.
   Base v0.6.018: animation: v06018-word-rise 380ms linear forwards.
   New v0.6.035: animation-duration: 760ms.
   --------------------------------------------------------------------------- */
.v06018-word-pop {
  animation-duration: 760ms;
}

/* ---------------------------------------------------------------------------
   §3.3 — Status-effect labels on the row-7 info cards: font size doubled.
   The .v060-info-statuses wrapper in styles.css line 3978 declares
   font-size: 0.55vw (inherited by every .v060-info-status badge).
   We add an explicit font-size of 1.1vw (2× of 0.55vw) on the wrapper
   so all status badges inside it scale together. We also pad/border
   the badges so the doubled glyphs don't overflow the original
   padding:0.04vw 0.16vw.
   --------------------------------------------------------------------------- */
.v060-region-info .v060-info-statuses {
  font-size: 1.1cqw;
  line-height: 1.15;
}
/* Status badges inherit the doubled font-size from the wrapper. We
   add a slightly larger padding so the doubled glyphs don't clip the
   border; the badge color overrides (v060-status-bleed, etc.) remain
   unchanged. */
.v060-region-info .v060-info-status {
  padding: min(0.06cqw, 0.768px) min(0.22cqw, 2.816px);
  margin: min(0.05cqw, 0.64px) min(0.07cqw, 0.896px);
  border-radius: min(0.1cqw, 1.28px);
}

/* ---------------------------------------------------------------------------
   §3.3 — Arena-side status badges (.v060-status-badge) font size doubled.
   styles.css line 3382 sets font-size: 0.6vw with padding
   0.1vw 0.3vw. Doubled glyph + scaled padding so the badge holds the
   larger text without overflowing adjacent badges.
   --------------------------------------------------------------------------- */
.v060-status-badge {
  font-size: 1.2cqw;
  padding: min(0.15cqw, 1.92px) min(0.45cqw, 5.76px);
  border-radius: min(0.25cqw, 3.2px);
}

/* ---------------------------------------------------------------------------
   v0.6.035 marker tokens for tests + identity probes.
   Presence-only selectors — they never trigger layout or paint
   side-effects in production.
   --------------------------------------------------------------------------- */
.v06035-token-marker { /* presence-only — used by tests */ }

/* ===========================[v06036-closure.css]=========================== */
/* Gerbil Gunners v0.6.036 — Battle Bar Presentation Closure.
   Two polish deltas on top of v0.6.035 (battle text emphasis) and the
   full v0.6.018 8-technique surface:

     1. HP bar height TRIPLED: 0.32vw -> 0.96vw (3x the v0.6.017
        baseline preserved through v0.6.035). Width 21% (v0.6.033),
        transform: translateY(-7.5vw) (v0.6.034), position: relative
        and z-index: 12 (v0.6.015) are all preserved. The fill
        animation (width 0->100% based on HP percent) and the v0.6.018
        ghost layer (z-index 1, transition 220ms) are preserved.

     2. HP bar fill COLOR: orange -> green. The v0.6.018 base color
        was a brown-orange linear-gradient(to right, #6b3410, #c46d23).
        v0.6.036 replaces it with a green gradient using the same
        to-right direction and similar mid-tone luminance:
          linear-gradient(to right, #1a5c1a, #4caf50)
        (dark forest green -> material green). The fill animation,
        ghost layer, low-HP visual state (defeated opacity 0.45), and
        the empty-track background (rgba(0,0,0,0.4)) are preserved.

   Damage numbers + word pops (v0.6.018 T6.1/T6.3) were repositioned
   via game-v060-battle-ui.js v06018NumberPop() — they now anchor at
   10% of the gerbil portrait's height below the portrait's top edge,
   instead of cell-center. The CSS-only pieces (font-size, animation
   duration, color, keyframes) are untouched; the v0.6.035 closure
   continues to win the cascade for those properties.

   Layered on top of v0.6.018 + v0.6.020 (HP bar delta + KO bar
   collapse) + v0.6.033 (width 21%) + v0.6.034 (translateY(-7.5vw)
   selector fix) + v0.6.035 (2x text emphasis). All eight v0.6.018
   battle techniques are intact; only the HP bar presentation
   (height + base fill color) is modified. The HP bar damage-color
   delta, hit-cell spark burst, portrait white flash, HP bar jitter,
   floating damage numbers + Crit/Miss word pops (positions changed
   in JS only), charge-ring glow on attacker, crit-only local screen
   shake, KO despawn fade + grayed portrait are unchanged.

   Selector cascade: the HP bar rules below use the same
   `.v060-region-battle .v060-hpbar` and
   `.v060-region-battle .v060-hpbar-fill` selectors as the v0.6.017
   baseline in styles.css (line 3852 and 3862). Equal specificity
   + later load order (this file is loaded LAST after v06035-closure
   in <head>) ensures these rules win the cascade.

   Forbidden behaviors preserved:
     - HP bar width 21% (v0.6.033)
     - HP bar transform translateY(-7.5vw) (v0.6.034)
     - HP bar position: relative + z-index: 12 (v0.6.015)
     - HP bar fill animation (width 0->100%, transition 220ms, v0.6.018)
     - HP bar ghost layer (z-index 1, width 0, background
       rgba(255, 220, 200, 0.92), v0.6.018 T7.1)
     - HP bar empty-track background rgba(0, 0, 0, 0.4) (v0.6.017)
     - HP bar defeated opacity 0.45 (styles.css:4094)
     - HP bar border (rgba(243, 233, 214, 0.25), v0.6.017)
     - HP bar margin (0.18vw 0 0.12vw 0, v0.6.017)
     - HP bar border-radius (0.1vw, v0.6.017)
     - HP bar overflow: hidden (v0.6.017)
     - HP bar flex: 0 0 auto (v0.6.017)
     - Damage-number / word-pop font sizes (v0.6.035 2x)
     - Damage-number / word-pop animation durations (v0.6.035 2x)
     - Status-label font sizes (v0.6.035 2x)
     - Damage-number color scheme (red dmg, gold crit, green heal,
       gray miss/dodge/block, v0.6.018)
     - 34x7 grid, transparent battle cells, opaque row-7 info
       backplanes (v0.6.014 + v0.6.015 + v0.6.018)
     - Per-class portrait transforms (v0.6.013 + v0.6.025)
     - Per-class item-slot styling
     - v0.6.016 fast shot tracers
*/

/* ---------------------------------------------------------------------------
   §3.2 — HP bar height TRIPLED: 0.32vw -> 0.96vw (3x the v0.6.017 baseline).
   The v0.6.033 width rule (21%) lives in v06033-closure.css; the v0.6.034
   translateY rule (-7.5vw) lives in v06015-closure.css. Both are
   preserved; this closure only changes the height.

   Margin is unchanged (0.18vw 0 0.12vw 0) — the 0.32vw -> 0.96vw bar
   (delta 0.64vw) extends downward into the cell. The cell is large
   enough to absorb the extra height without overflowing; if the dev
   team needs to nudge the transform or margin to give the bar more
   breathing room, do so in this closure (it cascades last) rather
   than touching v06015-closure.css.

   Border, border-radius, background (empty-track), overflow, flex,
   margin are all inherited unchanged from styles.css. z-index 12 +
   position: relative from v06015-closure.css continue to fire.
   --------------------------------------------------------------------------- */
.v060-region-battle .v060-hpbar {
  height: min(0.96cqw, 12.288px); /* TRIPLED: 0.32vw * 3 = 0.96vw */
}

/* ---------------------------------------------------------------------------
   §3.3 — HP bar fill color: orange -> green.
   The v0.6.018 base color was linear-gradient(to right, #6b3410, #c46d23).
   v0.6.036 replaces it with a dark-to-bright green gradient using the
   same to-right direction and similar mid-tone luminance:
     linear-gradient(to right, #1a5c1a, #4caf50)
   i.e. dark forest green -> Material Design green 500.
   The fill animation (v0.6.017 + v0.6.018 width-transition),
   the v0.6.018 ghost layer (rgba(255, 220, 200, 0.92) on a sibling
   element), and the v0.6.020 color states are all preserved — only
   the BASE color of the .v060-hpbar-fill changes; there are no
   damaged/low/dead override rules anywhere in the codebase.
   --------------------------------------------------------------------------- */
.v060-region-battle .v060-hpbar-fill {
  background: linear-gradient(to right, #1a5c1a, #4caf50);
}

/* ---------------------------------------------------------------------------
   v0.6.036 marker tokens for tests + identity probes.
   Presence-only selectors — they never trigger layout or paint
   side-effects in production.
   --------------------------------------------------------------------------- */
.v06036-token-marker { /* presence-only — used by tests */ }

/* ===========================[v06037-closure.css]=========================== */
/* Gerbil Gunners v0.6.037 — Cross-Scene Position Sync + Medic Symmetry
   + Super Auto Pets Matchmaking. This file is intentionally minimal.

   All v0.6.037 deltas are JS-only:
     - delta #1 (cross-scene position sync) lives in game.js's
       dropOnSquadSlot squad-unit -> squad-slot branch (the swap and
       empty-slot move now splice the unit into the target array index
       so the depot's renderSquadRow and the v0.6.0 battle's index-
       based slot assignment share the same array order).
     - delta #2 (medic symmetry lock) lives in game-rules.js
       pickMedicSustainTarget and game-combat-v060.js's
       mostWoundedAlly selector branch (both are byte-identical for
       both sides; both functions now carry an inline comment
       forbidding per-side shortcuts).
     - delta #3 + #4 (matchmaking + fresh random enemy per round)
       live in game-v060-battle-ui.js's v060GenerateEnemySquad +
       v060MakePRNG helpers and the rewritten startBattleFromSquad.

   No CSS deltas ship with v0.6.037. The previous v0.6.036 closure
   (HP bar height 0.96vw + green fill) is preserved verbatim and the
   earlier closure cascade is untouched. v0.6.037 only re-stamps the
   data-build-marker on the title page, the depot log, and the v0.6
   battle screen to v06037-consistency-and-matchmaking.

   If a future v0.6.N>=0.38 ships a CSS delta, the v0.6.037 closure
   file remains valid as a no-op that holds the v0.6.037 identity in
   the cascade.
*/

/* ===========================[v06038-closure.css]=========================== */
/* Gerbil Gunners v0.6.038 — Fix v0.6.037 Regressions: Depot Swap + Damage Number Position.

   Layered on top of v0.6.037 (cross-scene position sync + medic symmetry
   + Super Auto Pets matchmaking + fresh enemy per round) and the full
   v0.6.0 / v0.6.018 / v0.6.020 / v0.6.024 / v0.6.025..v0.6.036 surface.

   This closure file is intentionally IDENTITY-ONLY. The two real fixes
   are in source code (not CSS):

     - game-rules.js compactTeam() / reorderTeam() — the legacy
       `living(units).sort(by slotNumber)` mask was the actual depot
       swap-revert bug. v0.6.038 drops the sort and uses the input
       array order as the single source of truth (matching the v0.6.037
       spec's "squad array order is the single source of truth"). Now
       a depot drag-swap that splices the two units into each other's
       array positions actually persists; compactTeam() reassigns
       .slot to match the new array order, instead of re-sorting by
       stale .slot and undoing the splice.

     - game-v060-battle-ui.js v06018NumberPop() — the legacy code
       looked up `.v060-portrait` (the small 5vw container) and
       anchored the pop at 10% of the CONTAINER height below the
       container top. Because the container has overflow:visible and
       the <img> inside is scaled by object-fit:contain, the visible
       gerbil image extends FAR beyond the container (e.g. 5vw
       container holding a 11.0625vw image that overflows 3vw above and
       3vw below the container). Anchoring to the container put the
       pop at the center of the visible image. v0.6.038 looks up
       `.v060-portrait img` (the user-perceived gerbil) and anchors
       at 10% of the IMG's height below the IMG's top. Now the pop
       appears at the top of the visible gerbil — exactly what the
       user asked for in v0.6.036 but never actually saw because of
       the container-vs-img BCR mismatch.

   v0.6.038 PRESERVES:
     - v0.6.037 cross-scene position sync CONCEPT (array order = SoT)
     - v0.6.037 medic symmetry audit + 3 regression tests
     - v0.6.037 matchmaking overhaul (1..5 units scaled by wins)
     - v0.6.037 fresh random enemy per round
     - v0.6.036 HP bar height 0.96vw + green fill
     - v0.6.035 2× damage-number font + 2× duration
     - v0.6.035 2× word-pop font + 2× duration
     - v0.6.035 2× status-label font
     - v0.6.034 HP bar transform translateY(-7.5vw) selector fix
     - v0.6.033 HP bar width 21%
     - v0.6.032 taunt targeting priority
     - v0.6.031 drag-hover matches regular hover
     - v0.6.030 title page scrollable release notes
     - v0.6.029 depot scene fix
     - v0.6.028 item-slot hover (scale 1.1, no translate, center origin)
     - v0.6.027 bottom anchor + 420ms transition
     - v0.6.026 grid-up + gerbil-down hover
     - v0.6.025 .sc-portrait { position: absolute; inset: 0; }
     - v0.6.024 polish + engineer_taunt skill data
     - v0.6.020 8-technique surface (HP ghost / spark burst / portrait
       flash / HP jitter / floating numbers / charge ring / crit shake /
       KO despawn)
     - v0.6.017 HP bar geometry baseline
     - v0.6.016 fast shot tracers
     - 34×7 grid, transparent battle cells, opaque row-7 info backplanes
     - per-class portrait transforms
     - per-class item-slot styling
     - deterministic tick combat (RNG seeded, deterministic replays)
     - save / load mechanics
     - the medic skill data
     - the taunt status field
     - the engineer_taunt skill data
     - the 5-unit squad cap
     - the run-end condition (10 wins)
     - the squad card class pool
     - the depot drag-and-drop mechanics (now ACTUALLY working again)

   v0.6.038 IDENTITY:
     - buildVersion: v0.6.038
     - buildMarker:  v06038-fix-swap-and-damage-pos-regressions
     - rulesVersion: 0.6.038
     - <meta name="gg-build" content="v0.6.038">
     - <meta name="gg-build-marker" content="v06038-fix-swap-and-damage-pos-regressions">
     - <meta name="gg-rules-version" content="0.6.038">
     - #depotLog[data-build-marker="v06038-fix-swap-and-damage-pos-regressions"]
     - #v060BattleScreen[data-build-marker="v06038-fix-swap-and-damage-pos-regressions"]
     - window.GG_DATA.buildVersion / buildMarker / rulesVersion
     - data/classes.json buildVersion / buildMarker / rulesVersion
     - data/items.json buildVersion / buildMarker / rulesVersion
     - ver-pill visible "v0.6.038" on the title page
     - 1 collapsed-by-default v0.6.038 changelog <details> entry,
       prepended above the v0.6.037 entry (the older entries remain
       intact below it).
*/

/* v0.6.038 marker token (presence-only, no layout/paint side effects). */
.v06038-token-marker { /* presence-only — used by tests */ }

/* ===========================[v06039-closure.css]=========================== */
/* v0.6.039 closure — identity-only marker selector.
   Loaded LAST in <head> so it wins the cascade against the v0.6.038 +
   v0.6.037 + v0.6.036 + v0.6.035 base rules at equal specificity.
   The actual delta lives in game-v060-battle-ui.js (v06018NumberPop):
     * popY (damage numbers) = 2% of img height below img top
     * wordY (crit/miss/heal/block/dodge) = 50% of img height (center)
   This CSS exists so the cascade contains a v0.6.039 presence marker
   matching the buildVersion/buildMarker/rulesVersion in index.html,
   game-data.js, data/classes.json, and data/items.json. No selectors
   change live behavior; the file is documented in the spec identity
   section and the v0.6.039 release-note entry in index.html. */
.v060-battle-screen[data-build-marker="v06039-damage-top-words-center"] { /* v0.6.039 marker */ }

/* ===========================[v06040-closure.css]=========================== */
/* v0.6.040 closure — three deltas loaded LAST in <head>.
   Loaded after v06039-closure.css so it wins the cascade at equal
   specificity. No selectors change live behavior unless explicitly
   noted below; identity marker + the row-7 info-cell 9-slice rule
   are the only behavior changes. */

/* ---- Identity marker (presence-only, no live behavior). ---- */
.v060-battle-screen[data-build-marker="v06040-hp-bars-down-1_5vw-damage-up-2vw"] { /* v0.6.040 marker */ }

/* ---- Delta #1 — HP bar transform (redundant defense-in-depth).
   The canonical -2.5vw rule already lives in v06015-closure.css and
   remains the single source of truth. We carry an equivalent rule
   here at equal-or-higher specificity so a stray future edit to
   v06015-closure.css cannot regress the v0.6.076 transform value.
   Selector matches the v0.6.034 selector verbatim. */
.v060-battle-screen .v060-region-battle > .v060-hpbar {
  /* v0.6.076 — defense-in-depth translateY matches the canonical
     cascade winner in v06015-closure.css (-2.5vw). */
  transform: translateY(max(-2.5cqw, -32px));
}

/* ---- Delta #3 — Row-7 info cell 9-slice border image.
   The user wants ui_gerbilinfo.png (471x509, 2vw source border)
   applied as a 9-slice border to every row-7 gerbil info cell.
   The 10 row-7 info cells use the .v060-region-info selector
   (5 player P5..P1 in grid-row 7, 5 enemy E1..E5 in grid-row 7).

   border-image-slice: 32 fill      — slice 2vw from every edge of
                                       the 471x509 source; "fill"
                                       keeps the interior fill region
                                       rendered behind the children.
   border-image-width: 0.5vw        — render each slice at 0.5vw wide
                                       (.5vw at 1600-wide viewport).
   border-image-repeat: stretch     — stretch corner/edge slices to
                                       fill the rim of each cell.

   The existing styles.css rule sets background:transparent + border:0
   on .v060-region-info + an inherited dark backing via
   .v060-battle-screen.v060010-active .v060-region-info. We keep
   border-style:solid so the 0.5vw border-image rim is allocated by
   the browser, and overflow:visible so the 9-slice doesn't clip.
   The .v060-info-portrait child (the scaled-up 2x portrait image)
   has inset:0 which would normally cover the entire cell; we
   preserve that behavior (the user wants the photo background visible
   behind the border-image rim) by ordering the child above the
   border via the natural CSS paint order: background < border-image
   < content. The portrait is the content layer and renders on top.

   The 0.5vw border-image-width is added INSIDE the cell's 3x1 layout
   slot. The P5/P4/P3/P2/P1 cells have grid-column: span 3 each
   (~3 * (32/34 of column-width) at each viewport); a 0.5vw border
   is ~0.5/12 of column-width, well within bounds.
   The 10th - E5 - sits in grid-column 31 span 3 which is the last
   column so the right-edge slice won't get cropped off the grid. */
.v060-region-info,
.v060-region-info.v060-region-empty {
  border-style: solid;
  border-width: min(0.5cqw, 6.4px);
  border-image: url(assets/ui_gerbilinfo.png) 32 fill / min(1cqw, 12.8px) / 0 stretch;
  padding: min(0.5cqw, 6.4px);
  box-sizing: border-box;
}

/* ===========================[v06041-closure.css]=========================== */
/* v0.6.041 closure — combat pause/resume toggle.
   Loaded AFTER v06040-closure.css so it wins the cascade at equal
   specificity. The single behavioral addition is the pause button +
   PAUSED banner on the battle scene; every other selector is a
   presence-only identity marker. */

/* ---- Identity marker (presence-only, no live behavior). ---- */
.v060-battle-screen[data-build-marker="v06041-combat-pause-resume"] { /* v0.6.041 marker */ }

/* ---- Delta #1 — pause button (top-left of the battle scene).
   The button sits inside #v060BattleScreen and inherits the section's
   hidden attribute, so it never appears on the depot or the title
   page. The visual style matches the v0.6.040 row-7 backdrop
   (rgba(8, 14, 22, 0.72)) so the button does not look out of place
   next to the existing battle-scene panels. */
.v06041-pause-btn {
  position: absolute;
  top: min(0.6cqw, 7.68px);
  left: min(0.6cqw, 7.68px);
  z-index: 50;
  background: rgba(8, 14, 22, 0.72);
  color: #dfe7f0;
  font-family: inherit;
  font-size: 1cqw;
  font-weight: 500;
  padding: min(0.4cqw, 5.12px) min(0.8cqw, 10.24px);
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(223, 231, 240, 0.18);
  border-radius: min(0.3cqw, 3.84px);
  cursor: pointer;
  letter-spacing: 0.05em;
  user-select: none;
  -webkit-user-select: none;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.v06041-pause-btn:hover {
  background: rgba(20, 32, 48, 0.85);
  border-color: rgba(223, 231, 240, 0.32);
}
.v06041-pause-btn:focus {
  outline: min(0.125cqw, 1.6px) solid rgba(120, 180, 240, 0.6);
  outline-offset: min(0.1cqw, 1.28px);
}
.v06041-pause-btn:active {
  background: rgba(30, 50, 72, 0.95);
}

/* ---- Delta #2 — PAUSED banner (top-center of the battle scene).
   The banner is always in the DOM but starts at opacity 0 and
   pointer-events: none. The JS adds .v06041-paused-active on
   state.combatPaused flip; the 200ms opacity transition makes
   the fade in/out clean without any JS timer. */
.v06041-paused-banner {
  position: absolute;
  top: min(0.6cqw, 7.68px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 55;
  background: rgba(8, 14, 22, 0.78);
  color: #f5d76e;
  font-family: inherit;
  font-size: 1.1cqw;
  font-weight: 600;
  padding: min(0.45cqw, 5.76px) min(1.2cqw, 15.36px);
  border: max(min(0.0625cqw, 0.8px), min(0.05cqw, 0.64px)) solid rgba(245, 215, 110, 0.35);
  border-radius: min(0.3cqw, 3.84px);
  letter-spacing: 0.1em;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.v06041-paused-banner.v06041-paused-active {
  opacity: 1;
}

/* ===========================[v06042-closure.css]=========================== */
/* v0.6.042 closure — three deltas loaded LAST in <head>.
   Loaded after v06041-closure.css so it wins the cascade at equal
   specificity. No selectors change live behavior unless explicitly
   noted below; identity marker + aspect-ratio defense-in-depth +
   HP bar 9-slice border image are the only behavior changes.

   ============================================================
   v0.6.042 DELTAS (summary)
   ============================================================
   DELTA #1 — Fixed 16:9 aspect ratio where 16 = 100vw.
     The .scene rule in styles.css has been updated from
     `aspect-ratio: 2 / 1` to `aspect-ratio: 16 / 9`. The existing
     `max-height: 100vh` on .scene is preserved so the scene is
     bounded by both axes at every viewport:
       1600x720 -> 1280x720 (height-bound)
       1200x540 ->  960x540 (height-bound)
        800x360 ->  640x360 (height-bound)
     This closure file carries a defense-in-depth aspect-ratio
     rule at equal-or-higher specificity so a stray future edit
     to styles.css cannot regress the 16/9 value.

   DELTA #2 — All px converted to vw in styles.css (except .0625vw).
     The only px values remaining in styles.css are `.0625vw` min
     border guards. The combat log media query breakpoints
     switched from 37.4375vw / 26.1875vw to 37.5rem / 26rem (root-
     relative em, the closest absolute unit). The .v060-error-
     panel border went from `.125vw solid` to `0.125vw solid`.
     Drop-shadow blur radii inside .v060-flash-overlay keyframes
     went from .75vw / .375vw to 0.75vw / 0.375vw. Box-shadow
     glow on .v060-error-panel went from 1.5vw to 1.5vw.
     All other closure files (v06011 through v06041) keep
     their existing px values because the user said "preserve
     all older surfaces" — those files are historical and
     frozen.

   DELTA #3 — HP bar 9-slice border image (carry-over from
     the missed v0.6.041 addendum). The v0.6.041 spec asked
     for a 9-slice border on the HP bar element but the dev
     team shipped only the pause/resume feature in v0.6.041.
     v0.6.042 carries the addendum.
       Source: assets/ui_healthbar.png (824x221, RGBA, 3vw
               source border)
       Selector: .v060-battle-screen .v060-region-battle >
                 .v060-hpbar (the v0.6.034 cascade, drops the
                 legacy .v060010-active companion class)
       border-image-slice: 48 fill
       border-image-width: 0.5vw  (.5vw at 1600-wide)
       border-image-repeat: stretch
       padding: 0.5vw            (so the HP fill isn't covered
                                   by the border rim)
     All 10 HP bars get the border (5 player + 5 enemy).
     The HP fill animation is preserved (the .v060-hpbar-fill
     child sits inside the HP bar and the border-image rim is
     rendered on top via natural CSS paint order: background <
     border-image < content; the fill is content).

   ============================================================ */

/* ---- Identity marker (presence-only, no live behavior). ---- */
.v060-battle-screen[data-build-marker="v06042-aspect-ratio-and-vw-only"] { /* v0.6.042 marker */ }

/* ---- DELTA #1 — .scene aspect-ratio defense-in-depth. ----
   The canonical 16/9 rule lives in styles.css and remains the
   single source of truth. We carry an equivalent rule here at
   equal-or-higher specificity so a stray future edit to the
   styles.css .scene block cannot regress the v0.6.042 aspect
   ratio. The v0.6.041 closure is silent on aspect-ratio, so
   this rule wins at equal specificity (v06042-closure.css
   loads LAST in <head>). */
.scene {
  aspect-ratio: 16 / 9;
}

/* ---- DELTA #1b — .title-screen aspect-ratio (the title page is
   also a "scene" in user-facing language; the v0.6.042 spec
   requires "all scenes need to deploy at a fixed aspect ratio
   of 16x9"). The title screen is positioned `position: absolute;
   inset: 0` and fills the viewport; adding `aspect-ratio: 16 / 9`
   would force it to a 16:9 box at 100vw width and clamp height
   to 100vw * 9/16, but the existing `max-height: calc(100vh - 2vw)`
   keeps it bounded by viewport height. The 16:9 ratio holds at
   800x360+ viewports where the title page is not height-bound;
   at narrow viewports the title card is height-clamped (the
   v0.6.030 layout). */
.title-screen {
  aspect-ratio: 16 / 9;
}

/* ---- DELTA #3 — HP bar 9-slice border image. ----
   The user wants ui_healthbar.png (824x221, RGBA, 3vw source
   border) applied as a 9-slice border to every HP bar element
   on the battle scene. The 10 HP bars use the
   .v060-region-battle > .v060-hpbar selector (5 player P5..P1
   and 5 enemy E1..E5).

   border-image-slice: 48 fill      — slice 3vw from every edge of
                                       the 824x221 source; "fill"
                                       keeps the interior fill region
                                       rendered behind the children.
   border-image-width: 0.5vw        — render each slice at 0.5vw wide
                                       (.5vw at 1600-wide viewport).
   border-image-repeat: stretch     — stretch corner/edge slices to
                                       fill the rim of each bar.
   padding: 0.5vw                   — adds 0.5vw of inner padding so
                                       the HP fill isn't covered by
                                       the border rim. The HP bar
                                       itself is 0.96vw tall; the
                                       padding pushes the fill
                                       inwards but the fill child
                                       still renders (verified at all
                                       3 viewports).
   box-sizing: border-box           — keeps the HP bar at its
                                       pre-existing layout size
                                       (height 0.96vw, width 21%)
                                       so the border rim is added
                                       inside the existing box, not
                                       on top of it. */
.v060-battle-screen .v060-region-battle > .v060-hpbar {
  border-style: solid;
  border-width: min(0.5cqw, 6.4px);
  border-image: url(assets/ui_healthbar.png) 48 fill / min(0.5cqw, 6.4px) / 0 stretch;;
  box-sizing: border-box;
}

/* ---- DELTA #3 — HP bar fill (the .v060-hpbar-fill child)
   The fill child must not inherit the padding-box offset; we
   zero the margin so the fill spans the padded region exactly.
   The fill's height: 100% (from styles.css) makes it scale to
   the inner padded box. The fill is rendered ON TOP of the
   border-image rim (natural CSS paint order: background <
   border-image < content; the fill is content). */
.v060-battle-screen .v060-region-battle > .v060-hpbar > .v060-hpbar-fill {
  margin: 0;
  height: 100%;
}

/* ===========================[v06043-closure.css]=========================== */
/* Gerbil Gunners v0.6.043 — Per-class readiness bar colors (battle scene).
   Successor to v0.6.042 closure CSS.
   This closure:
     1. Routes every readiness fill (battle scene info cells) through its class's color
        from the v0.6.024 skill line palette (rifleman / heavy / medic / scout /
        engineer / sniper / grenadier / officer).
     2. Falls back to a neutral translucent gray for empty cells (no .v060-info-X).
     3. Preserves all other readiness fill behavior (position absolute, bottom 0,
        z-index 1, pointer-events none, height % still animates with readiness).
     4. Preserves all v0.6.024 skill line colors unchanged (palette reused, not
        modified).
     5. Preserves all v0.6.042 features (aspect ratio, vw units, HP bar border).
     6. Preserves all earlier surfaces v0.6.041 .. v0.6.016.
*/

/* Base readiness rule — keep layout identical, change base color to the empty-cell
   fallback (translucent gray). The JS still sets fill.style.height = '<pct>%' for
   both empty and occupied cells; empty cells do not carry a .v060-info-X class so
   this rule applies to them. Occupied cells are overridden by the class-specific
   rules below (higher specificity: 3 classes vs 2). */
.v060-region-info .v060-readiness {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(80, 80, 80, 0.36);  /* empty-cell fallback (neutral gray) */
  pointer-events: none;
  z-index: 1;
}

/* Class-specific readiness fills — selectors match the .v060-info-<classId>
   class added by game-v060-battle-ui.js to each info cell via
   infoEl.classList.add('v060-info-' + cid). Specificity (0, 3, 0) beats the
   base rule's (0, 2, 0). Hex values mirror v0.6.024 closure CSS skill line
   strokes exactly; alpha 0.36 matches the prior single-color rule. */
.v060-region-info.v060-info-scout .v060-readiness     { background: rgba(138, 128, 48, 0.36); }  /* yellow-olive    #8a8030 */
.v060-region-info.v060-info-officer .v060-readiness   { background: rgba(90, 90, 42, 0.36); }    /* command olive   #5a5a2a */
.v060-region-info.v060-info-medic .v060-readiness     { background: rgba(61, 138, 138, 0.36); }  /* muted teal      #3d8a8a */
.v060-region-info.v060-info-grenadier .v060-readiness { background: rgba(168, 90, 37, 0.36); }   /* burnt orange    #a85a25 */
.v060-region-info.v060-info-rifleman .v060-readiness  { background: rgba(74, 93, 35, 0.36); }    /* military green  #4a5d23 */
.v060-region-info.v060-info-engineer .v060-readiness  { background: rgba(74, 108, 138, 0.36); }  /* steel blue      #4a6c8a */
.v060-region-info.v060-info-heavy .v060-readiness     { background: rgba(122, 31, 31, 0.36); }   /* dark red        #7a1f1f */
.v060-region-info.v060-info-sniper .v060-readiness    { background: rgba(90, 58, 122, 0.36); }   /* deep purple     #5a3a7a */

/* ===========================[v06044-closure.css]=========================== */
/* Gerbil Gunners v0.6.044 — Scene width must be 100vw, not 69.4375vw fixed width.
   Successor to v0.6.043 closure CSS.

   This closure:
     1. Defense-in-depth rule on .scene that EXPLICITLY sets
        `width: 100vw` and `max-width: none` so a future edit to
        styles.css .scene block cannot regress v0.6.044 by adding
        back `max-width: 100%`.
     2. Defense-in-depth rule on the containing screens
        (.setup-screen, .battle-scene-screen) that EXPLICITLY
        sets `width: 100vw` so they cannot shrink to content
        width (which is what was causing max-width: 100% on .scene
        to evaluate against an undersized containing block in the
        first place).
     3. Identity marker (presence-only selector).
     4. Preserves all v0.6.043 features (per-class readiness bar
        colors).
     5. Preserves all v0.6.042 features (aspect ratio, vw units,
        HP bar border image).
     6. Preserves all earlier surfaces v0.6.041 .. v0.6.016.

   v0.6.046 NOTE: The .scene defense-in-depth rule below has been
   UPDATED to use the v0.6.046 max() formula instead of the plain
   `width: 100vw` literal. v0.6.046 flips the v0.6.045 min() formula
   to max() so the width-bound branch wins at wide viewports; the
   defense-in-depth rule below must re-assert the SAME formula (max())
   to avoid reverting to a literal `width: 100vw` that would defeat
   the v0.6.046 fix at narrow viewports (where the height-bound
   branch should win and width = 177.778vh). The `max-width: none`
   is preserved (still forbids any width cap on the scene). The
   `width: 100vw` literal is REPLACED with `width: max(100vw,
   177.7777vh)` so this defense-in-depth no longer regresses the
   v0.6.046 fix. v06046-closure.css adds a SECOND body .scene
   defense-in-depth rule with the same max() formula for
   redundancy. */

/* ---- Identity marker — confirmed loaded on battle screen ---- */
.v060-battle-screen[data-build-marker="v06044-scene-100vw"] { /* v0.6.044 marker */ }
.v060-setup-screen[data-build-marker="v06044-scene-100vw"]   { /* v0.6.044 marker */ }
.scene {
  width: max(100vw, 177.7777vh);
  max-width: none;  /* explicitly forbid any width cap on the scene */
  height: max(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
}

/* ---- DELTA #2 — containing screens must be 100vw wide. ----
   .scene lives inside .setup-screen / .battle-scene-screen. If
   those containing blocks auto-size to content width, the v0.6.043
   `max-width: 100%` rule on .scene (which it took from the
   containing block, NOT the viewport) silently shrunk the visible
   scene to ~69.4375vw at a 1264-wide viewport. Force both wrappers
   to 100vw wide so the .scene width: 100vw + max-width: none rule
   from the styles.css base block can resolve against a containing
   block that is actually viewport-wide.

   v0.6.046 NOTE: We do NOT add `max-width: 100vw` here. v0.6.046 wants
   pure responsive scaling (no clamping). The .setup-screen and
   .battle-scene-screen must remain viewport-wide but their WIDTH
   is allowed to be larger than the viewport so the .scene can
   overflow horizontally at narrow viewports (where the height-bound
   branch wants width = 177.778vh > 100vw). The `max-width: 100vw`
   from v0.6.044 is removed (no longer applies). */
.setup-screen,
.battle-scene-screen {
  width: 100cqw;
}

/* ===========================[v06045-closure.css]=========================== */
/* Gerbil Gunners v0.6.045 — Restore 16:9 aspect ratio via CSS min() smart
   clamp + complete zero-px unit hygiene. Loaded LAST in <head> so it wins
   the cascade at equal-or-higher specificity against every prior closure
   (v06011 through v06044).

   v0.6.046 NOTE: The body .scene rule below was ORIGINALLY a min()
   defense-in-depth for v0.6.045, but v0.6.046 (the successor) REQUIRES
   max() instead of min() (the user clarified that min() always picks
   height-bound, which is the wrong branch at width-bound viewports).
   Per the v0.6.046 spec, the v0.6.045 min() rule has been OVERWRITTEN
   in this same file to max(). The file header above is preserved as
   historical context; the live CSS rule below is the v0.6.046 max()
   formula. The v06046-closure.css file is the canonical home for the
   v0.6.046 defense-in-depth rule and identity marker.

   This closure:
     1. Defense-in-depth .scene rule re-asserting the new min() formula at
        equal-or-higher specificity so a stray future edit to styles.css
        cannot regress v0.6.045 by reintroducing width: 100vw +
        height: calc(100vw * 9/16) + max-height: 100vh (which would
        re-introduce a fixed width cap that the v0.6.045 user spec
        explicitly forbids).
     2. Defense-in-depth .setup-screen / .battle-scene-screen rule carried
        over from v0.6.044 to keep the containing blocks viewport-wide
        even if a future edit tries to revert them to auto width.
     3. Identity marker (presence-only selector).
     4. Preserves all v0.6.044 features (scene 100vw via min() instead of
        width: 100vw + max-width: none).
     5. Preserves all v0.6.043 features (per-class readiness bar colors).
     6. Preserves all v0.6.042 features (aspect ratio 16:9, vw-only sizing,
        HP bar border image) — and STRENGTHENS the v0.6.042 unit hygiene
        rule from "no px except .0625vw" to "no px anywhere, not even .0625vw".
     7. Preserves all earlier surfaces v0.6.041 .. v0.6.016 (no behavior
        changes to those layers; only their px values were stripped in
        v0.6.045 and replaced with vw-based equivalents per the spec
        conversion table).

   ============================================================
   v0.6.045 DELTAS (summary)
   ============================================================
   DELTA #1 — Smart two-axis clamp on .scene.
     The canonical styles.css .scene rule has been updated from
     `width: 100vw; height: calc(100vw * 9 / 16); max-height: 100vh`
     to a single CSS min() formula:
       width:  min(100vw, 177.7777vh);   (100vh * 16 / 9 = 177.77vh)
       height: min(56.25vw, 100vh);      (100vw * 9 / 16 = 56.25vw)
     The min() formula automatically picks the correct clamp based on
     the viewport aspect ratio. At every standard viewport (1600x720,
     1200x540, 800x360 — all of which are wider than 16:9) the scene
     is height-bound:
       1600x720 -> 1280x720 (width:min(1600,1280)=1280; height:min(900,720)=720)
       1200x540 ->  960x540 (width:min(1200,960) =960; height:min(675,540)=540)
        800x360 ->  640x360 (width:min(800,640) =640; height:min(450,360)=360)
     The aspect-ratio: 16 / 9 declaration is preserved so even if the
     min() math is slightly off the browser keeps the ratio. The user's
     "maybe you can come up with a smarter way of doing this" hint is
     satisfied by the min() formula: pure CSS, no JS, no media queries,
     no fixed values.

   DELTA #2 — Complete zero-px unit hygiene.
     The user explicitly said "we should never use px values for anything.
     just use vw". v0.6.045 enforces this STRICTLY:
     - NO `.0625vw` min border guards (the v0.6.042 carve-out is removed).
     - NO `max(.0625vw, 0.08vw)` patterns either.
     - NO `border: .0625vw solid ...` (use `max(0.0625vw, 0.05vw)` instead).
     - NO `stroke-width: Npx` (use `Nvw` instead).
     - NO `filter: drop-shadow(0 0 Npx ...)` (use `Nvw` instead).
     - NO `outline: Npx solid ...` (use `Nvw` instead).
     - NO `transform: translateX(Npx)` in keyframes (use `Nvw` instead).
     - NO `box-shadow: inset 0 0 0 Npx ...` (use `max(0.0625vw, 0.05vw)`).
     The ONLY units allowed in any CSS file going forward: vw, vh, %,
     em, rem, or numeric (no unit). NO px ANYWHERE.
     Conversion table (at 1600-wide baseline):
       .03125vw -> 0.03125vw   .0625vw -> 0.0625vw (or max(0.0625vw, 0.05vw) for borders)
       .09375vw -> 0.094vw    .125vw -> 0.125vw
       .1875vw   -> 0.1875vw   .25vw   -> 0.25vw
       1.75vw  -> 1.75vw (min-width / min-height)

   DELTA #3 — Cascading closure chain intact.
     v06045-closure.css loads LAST in <head> (after v06044) so it wins
     the cascade at equal specificity. Prior closures v06011 through
     v06044 are preserved verbatim (no edits to their bodies beyond
     the zero-px unit strip that v0.6.045 made; their identity markers,
     selectors, and behavior rules are intact).

   ============================================================ */

/* ---- Identity marker — confirmed loaded on battle screen. ---- */
.v060-battle-screen[data-build-marker="v06045-aspect-ratio-smart-clamp-no-px"] { /* v0.6.045 marker */ }
.v060-setup-screen[data-build-marker="v06045-aspect-ratio-smart-clamp-no-px"]   { /* v0.6.045 marker */ }

/* ---- DELTA #1 — Defense-in-depth: re-assert the max() smart clamp.
   The canonical fix lives in styles.css .scene block. The defense-in-depth
   rule below re-declares the max() formula at equal-or-higher specificity
   so a stray future edit to styles.css cannot regress v0.6.046 by adding
   back `width: min(100vw, 177.7777vh)` + `height: min(56.25vw, 100vh)`
   (the v0.6.045 min() formula which always picks height-bound, the wrong
   branch at width-bound viewports like 1264x625 / 1600x720 / 1200x540).
   The v0.6.045 min() body .scene rule below has been OVERWRITTEN to
   v0.6.046's max() formula in this same file (the file's body rule was
   preserved verbatim up to and including v0.6.045, but v0.6.046 mutates
   it to max() per the user spec). v06046-closure.css adds a new
   defense-in-depth rule (also max()) and a fresh identity marker for
   v0.6.046. */
body .scene {
  width: max(100vw, 177.7777vh);
  height: max(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
}

/* ---- DELTA #2 — Defense-in-depth: containing screens must be viewport-wide
   but NOT max-width-clamped (so .scene can overflow horizontally at narrow
   viewports).
   .scene lives inside .setup-screen / .battle-scene-screen. If those
   containing blocks auto-size to content width, the .scene min() formula
   (v0.6.045) or max() formula (v0.6.046) resolves against an undersized
   containing block (the v0.6.044 root cause). v0.6.045 forced `width: 100vw;
   max-width: 100vw` so the containing block would be viewport-wide. v0.6.046
   needs the containing block to be at least viewport-wide (width: 100vw,
   no max-width cap) so the .scene max() formula can overflow horizontally
   at narrow viewports where the height-bound branch wants width = 177.778vh
   > 100vw. The `max-width: 100vw` cap is REMOVED (no longer applies). */
.setup-screen,
.battle-scene-screen {
  width: 100cqw;
}

/* ===========================[v06046-closure.css]=========================== */
/* Gerbil Gunners v0.6.046 — Fix smart clamp to use max() not min().
   v0.6.045 used min() which always picks the height-bound value
   (the smaller of 100vw vs 177.77vh). At every viewport WIDER than
   16:9 (which is every required viewport: 1264x625, 1600x720, 1200x540,
   800x360), the user wants width-bound (width = 100vw). The v0.6.046
   fix flips min() to max() so the width-bound branch wins at wide
   viewports and the height-bound branch wins at narrow viewports.
   Pure CSS, no JS, no media queries, no fixed values. Loaded LAST
   in <head> so it wins the cascade at equal-or-higher specificity
   against every prior closure (v06011 through v06045).

   This closure:
     1. Defense-in-depth .scene rule re-asserting the max() formula at
        equal-or-higher specificity so a stray future edit to styles.css
        cannot regress v0.6.046 by reintroducing the v0.6.045 min()
        formula (which always picks height-bound, the wrong branch at
        width-bound viewports like 1264x625 / 1600x720 / 1200x540).
     2. Defense-in-depth .setup-screen / .battle-scene-screen rule
        carried over from v0.6.045 to keep the containing blocks
        viewport-wide even if a future edit tries to revert them to
        auto width.
     3. Identity marker (presence-only selector).
     4. Preserves all v0.6.045 features (zero-px unit hygiene, the v0.6.044
        width: 100vw baseline at width-bound viewports, the v0.6.043
        per-class readiness bar colors, the v0.6.042 aspect ratio 16:9
        baseline, and all v0.6.041..v0.6.016 surfaces).
     5. Math:
        max(100vw, 177.7777vh) = 100vw at wide viewports (most cases)
                                   = 177.7777vh at narrow viewports (rare)
        max(56.25vw, 100vh)     = 56.25vw at wide viewports (most cases)
                                   = 100vh at narrow viewports (rare)
        At any viewport where vw/vh >= 16/9 (most viewports): width =
        100vw wins, height = 56.25vw wins. At narrow viewports where
        vw/vh < 16/9: width = 177.7777vh wins, height = 100vh wins.

   ============================================================
   v0.6.046 DELTA (summary)
   ============================================================
   DELTA #1 — Flip min() to max() in the .scene smart clamp.

     The canonical styles.css .scene rule has been updated from
     v0.6.045's min() formula:
       width:  min(100vw, 177.7777vh);
       height: min(56.25vw, 100vh);
     to v0.6.046's max() formula:
       width:  max(100vw, 177.7777vh);
       height: max(56.25vw, 100vh);

     WHY: min() always picks the SMALLER of the two arguments. At a
     width-bound viewport (vw >= 16/9 * vh, which means 100vw >=
     177.77vh), the user wants width = 100vw to win. But
     min(100vw=1264, 177.77vh=1111.11) = 1111.11 (height-bound wins,
     NOT width-bound). The v0.6.045 math gives the WRONG result at
     every viewport wider than 16:9.

     max() picks the LARGER of the two arguments. At a width-bound
     viewport: max(100vw=1264, 177.77vh=1111.11) = 1264 (width-bound
     wins, as the user wants). At a narrow viewport: max(100vw=800,
     177.77vh=1066.67) = 1066.67 (height-bound wins, scene becomes
     1066.67x600 — matches the user's rule of 100vh * 16/9 width at
     narrow viewports).

     Verification at 5 viewports (post-v0.6.046):
       1600x720 (vw/vh = 2.22 > 16/9) -> 1600x900  width-bound (scene overflows 45vw vertically by 11.25vw — acceptable)
       1264x625 (vw/vh = 2.02 > 16/9) -> 1264x711  width-bound (scene overflows 39.0625vw vertically by 5.375vw — acceptable)
       1200x540 (vw/vh = 2.22 > 16/9) -> 1200x675  width-bound (scene overflows 33.75vw vertically by 8.4375vw — acceptable)
        800x600 (vw/vh = 1.33 < 16/9) -> 1066.67x600  height-bound (scene overflows 50vw horizontally by 16.666875vw — acceptable)
        800x360 (vw/vh = 2.22 > 16/9) ->  800x450  width-bound (scene overflows 22.5vw vertically by 5.625vw — acceptable)
     The user wants NO clamping — pure responsive scaling. Overflow is
     expected at most viewports. Parent body / .app-shell may carry
     overflow: hidden to clip the overflow if desired.

   DELTA #2 — Defense-in-depth body .scene rule with max() formula.
     Same specificity-bump trick as v0.6.045: the body .scene rule
     below wins the cascade at equal-or-higher specificity against
     styles.css .scene, so a stray future edit to styles.css cannot
     regress the max() formula by reintroducing min().

   DELTA #3 — Identity marker v0.6.046.
     The data-build-marker attribute is updated from
     v06045-aspect-ratio-smart-clamp-no-px to
     v06046-aspect-ratio-smart-clamp-max-no-min on every screen
     element (.v060-battle-screen and .v060-setup-screen). The CSS
     marker rule below is a presence-only selector that matches the
     new attribute value. The v0.6.045 marker is preserved as a
     presence-only selector too (in v06045-closure.css) so historical
     forensics still work.

   ============================================================ */

/* ---- Identity marker — confirmed loaded on battle screen. ---- */
.v060-battle-screen[data-build-marker="v06046-aspect-ratio-smart-clamp-max-no-min"] { /* v0.6.046 marker */ }
.v060-setup-screen[data-build-marker="v06046-aspect-ratio-smart-clamp-max-no-min"]   { /* v0.6.046 marker */ }

/* ---- DELTA #1 — Defense-in-depth: re-assert the max() smart clamp.
   The canonical fix lives in styles.css .scene block. The defense-in-depth
   rule below re-declares the max() formula at equal-or-higher specificity
   so a stray future edit to styles.css cannot regress v0.6.046 by adding
   back `width: min(100vw, 177.7777vh)` + `height: min(56.25vw, 100vh)`
   (the v0.6.045 min() formula which always picks height-bound, the wrong
   branch at width-bound viewports like 1264x625 / 1600x720 / 1200x540). */
body .scene {
  width: max(100vw, 177.7777vh);
  height: max(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
}

/* ---- DELTA #2 — Defense-in-depth: containing screens must be viewport-wide
   but NOT max-width-clamped (so .scene can overflow horizontally at narrow
   viewports).
   .scene lives inside .setup-screen / .battle-scene-screen. If those
   containing blocks auto-size to content width, the .scene max() formula
   resolves against an undersized containing block (the v0.6.044 root
   cause). v0.6.044 forced `width: 100vw; max-width: 100vw` so the
   containing block would be viewport-wide. v0.6.046 needs the containing
   block to be at least viewport-wide (width: 100vw, no max-width cap)
   so the .scene max() formula can overflow horizontally at narrow
   viewports where the height-bound branch wants width = 177.778vh
   > 100vw. Carried verbatim from v0.6.045 closure except the
   `max-width: 100vw` cap is REMOVED (no longer applies). */
.setup-screen,
.battle-scene-screen {
  width: 100cqw;
}

/* ---- DELTA #3 — Defense-in-depth: setup screen min-height: 100% (not
   height: 100%). The v0.6.045 closure carried an inherited `.setup-screen
   { height: 100% }` rule in styles.css which pinned the setup screen
   height to the viewport. This made the .scene's containing block be
   viewport-height, which forced the v0.6.046 max() formula's height
   branch (max(56.25vw, 100vh) at width-bound viewports) to be clipped
   to 100vh by the containing block. v0.6.046 wants pure responsive
   scaling with vertical overflow accepted. The canonical fix in
   styles.css changes `height: 100%` to `min-height: 100%` so the
   setup screen can grow BEYOND viewport height. The defense-in-depth
   rule below re-asserts `min-height: 100%` at higher specificity
   (body .setup-screen = 0,2,0 vs .setup-screen = 0,1,0) so a stray
   future edit to styles.css cannot regress v0.6.046 by re-adding
   `height: 100%` and clipping the .scene to viewport height. */
body .setup-screen.active,
body .setup-screen {
  height: auto;
  min-height: 100%;
}

/* ===========================[v06047-closure.css]=========================== */
/* ============================================================================
   v0.6.047 — Remove Depot Combat Log (Closure CSS)

   This closure CSS file is loaded LAST in <head> so it wins the cascade
   against every prior closure at equal specificity. It carries the v0.6.047
   identity marker plus a no-op defense-in-depth rule and the aspect-ratio
   overflow NOTE block.

   IDENTITY MARKER (presence-only selector for spec/runtime probes):
     #v060BattleScreen[data-build-marker="v06047-remove-depot-combat-log"]

   The depot combat log container (<div id="depotLog" class="depot-log">) has
   been removed from index.html. The .depot-log rule and all @media overrides
   have been removed from styles.css. The .setup-screen > .depot-log flex
   sizing rule has been removed from styles.css. The engine still generates
   log messages internally but they are no longer rendered on the depot scene.

   NOTE: The v0.6.046 smart-clamp formula produces scenes that may overflow
   the viewport vertically at certain aspect ratios (e.g. 1264x625 produces
   1264x711 which overflows the 39.0625vw viewport height by 5.375vw). The user
   has reported this in v0.6.047 but asked to defer the fix until the
   combat log removal is complete. v0.6.048 will address this issue.
   ============================================================================ */

/* Identity marker: presence-only selector. Spec probes can target this
   selector to verify the v0.6.047 build is live. The data-build-marker
   attribute is set in index.html on the battle screen element. */
#v060BattleScreen[data-build-marker="v06047-remove-depot-combat-log"] {
  /* No-op + identity marker. The v0.6.047 delta is a CSS removal, not a
     CSS addition. The selector fires as the identity probe. */
  position: relative;
}

/* Defense-in-depth: the v0.6.046 max() smart-clamp formula on .scene
   is preserved unchanged. Re-assert it here at equal-or-higher
   specificity so a stray future edit to styles.css cannot regress
   v0.6.046. */
body .scene {
  width: max(100vw, 177.7777vh);
  height: max(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
}

/* Defense-in-depth: the v0.6.044 .setup-screen / .battle-scene-screen
   width: 100vw rule is preserved unchanged. */
.setup-screen,
.battle-scene-screen {
  width: 100cqw;
  max-width: 100cqw;
}

/* ===========================[v06048-closure.css]=========================== */
/* ============================================================================
   v0.6.048 — Dynamic 16:9 App Space That Fits Any Viewport
              (vw/vh axis switching via @media aspect-ratio)

   DELTA SUMMARY
   -------------
   The app space (.scene) reverts from the v0.6.046 max() smart clamp to
   the v0.6.045 min() smart clamp:

     width:  min(100vw, 177.7777vh);
     height: min(56.25vw, 100vh);
     aspect-ratio: 16 / 9;

   Why min(), not max(): the v0.6.046 max() formula produced scenes that
   overflowed the viewport vertically at every wide viewport (e.g.
   scene 1264x711 at viewport 1264x625 overflows 5.375vw). The user spec
   for v0.6.048 says "the app space is supposed to retain a 16*9 ratio,
   covering as much as possible of the available viewport while NOT
   GOING OUT OF BOUNDS." min() picks the SMALLER axis, which is always
   the constraint — scene always fits the viewport while maintaining
   16:9 and covering the maximum possible area.

   At narrow viewports (vw/vh < 16/9, width-bound branch):
     scene = 100vw * 56.25vw (width fills, height derived from 16:9)
   At wide viewports (vw/vh >= 16/9, height-bound branch):
     scene = 177.7777vh * 100vh (height fills, width derived from 16:9)
   At exact 16:9 viewports: both branches tie, scene fills exactly.

   NEW v0.6.048: child elements switch unit (vw vs vh) based on the
   limiting axis via @media (min-aspect-ratio: 16/9). At wide viewports
   (aspect >= 16/9, height is limiting axis), child elements use vh.
   At narrow viewports (aspect < 16/9, width is limiting axis), child
   elements use vw (the default). The numeric value is preserved across
   the switch (e.g. height: 0.32vw becomes height: 0.32vh at wide
   viewports) — the user's spec says "vw or vh values, depending which
   one is the current limiting factor" with same numeric value.

   PRESERVED (no edits to v0.6.047 baseline behavior):
     - v0.6.047 combat log removed (no #depotLog / .depot-log in HTML/CSS)
     - v0.6.045 zero-px unit hygiene (zero px in actual CSS rules)
     - v0.6.044 width: 100vw baseline
     - v0.6.043 per-class readiness bar colors (Rifleman/Heavy/Medic/Scout/
       Engineer/Sniper/Grenadier/Officer)
     - v0.6.042 16:9 aspect ratio + vw-only sizing + HP bar border image
     - v0.6.041 pause/resume toggle
     - v0.6.040 HP bar translateY(-6vw) + damage numbers + row-7 border
     - v0.6.039 word pops at center
     - v0.6.038 depot swap fix
     - v0.6.037 matchmaking / medic / position sync
     - v0.6.036 HP bar polish
     - v0.6.035 text emphasis
     - v0.6.034 HP bar selector
     - v0.6.033 HP bar width 21%
     - v0.6.032 taunt targeting priority
     - v0.6.031 drag-hover effect
     - v0.6.030 title page layout
     - v0.6.028 item-slot hover
     - v0.6.027 bottom anchor + 420ms transition
     - v0.6.026 grid-up + gerbil-down hover
     - v0.6.025 .sc-portrait position:absolute fix
     - v0.6.024 polish + engineer_taunt data
     - v0.6.020 8-technique surface
     - v0.6.017 HP bar geometry baseline
     - v0.6.016 fast shot tracers
     - 34x7 battle grid, transparent battle cells, opaque row-7 info
     - per-class portrait transforms
     - per-class item-slot styling
     - deterministic RNG, save/load mechanics

   ============================================================================ */

/* ---- Identity marker (presence-only selector). ----
   Spec/runtime probes target this selector to verify the v0.6.048
   build is live. data-build-marker attribute is set on
   #v060BattleScreen and #setupScreen in index.html. */
#v060BattleScreen[data-build-marker="v06048-dynamic-16x9-fitting-any-viewport"],
#setupScreen[data-build-marker="v06048-dynamic-16x9-fitting-any-viewport"] {
  /* No-op + identity marker. The v0.6.048 delta is a layout change,
     not a CSS rule change. The selector fires as the identity probe. */
  position: relative;
}

/* ---- Defense-in-depth #1: revert .scene smart clamp from max() to min().
   The canonical fix lives in styles.css .scene block. This rule
   re-asserts the min() formula at equal-or-higher specificity (body
   .scene = 0,1,1 specificity bump over .scene = 0,1,0) so a stray
   future edit to styles.css cannot regress v0.6.048 by reintroducing
   the v0.6.046 max() formula (which overflows vertically at wide
   viewports like 1264x625 → scene 1264x711 overflows 5.375vw). */
body .scene {
  width: min(100vw, 177.7777vh);
  height: min(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
  container-type: inline-size;
  container-name: stage;
}

/* ---- Defense-in-depth #2: containing screens stay viewport-wide.
   v0.6.045..v0.6.046 carried .setup-screen / .battle-scene-screen
   width: 100vw. v0.6.048 keeps that contract so the .scene min()
   formula resolves against a viewport-wide containing block. The
   max-width cap is REMOVED (the v0.6.045 cap was needed because the
   min() formula didn't overflow; v0.6.046 kept the cap removed
   because the max() formula did overflow; v0.6.048 with min()
   produces scenes that ALWAYS fit, so we restore the max-width:100vw
   cap to prevent any future regression from a non-min() formula. */
.setup-screen,
.battle-scene-screen {
  width: 100cqw;
  max-width: 100cqw;
}

/* ---- Defense-in-depth #2b: REVERT .setup-screen min-height: 100%.
   v0.6.046 added `min-height: 100%` on `.setup-screen` (and the body
   .setup-screen equivalent) so the container could grow BEYOND viewport
   height when the .scene's max() formula produced a height > 100vh
   (overflow at wide viewports). With v0.6.048's min() formula, the
   scene ALWAYS fits the viewport, so min-height: 100% is no longer
   needed — and it actively interferes with the min(56.25vw, 100vh)
   formula at narrow viewports (800x600 case) by forcing the .scene's
   height to be at least 100% of its container (which is 100vh = 37.5vw),
   overriding the formula's 28.125vw target.
   This rule resets min-height to 0 on the .scene element (which is
   the same element as .setup-screen). The .setup-screen container
   still has its display:flex + flex-direction:column layout; only
   the height constraint is removed. */
body .scene,
body .setup-screen.active,
body .setup-screen {
  min-height: 0;
}

/* ---- Defense-in-depth #3: child elements switch vw to vh at wide viewports.
   When the viewport's aspect ratio is >= 16/9 (the wide-viewport branch,
   where height is the limiting axis), child elements use vh units
   instead of vw units. The numeric value is preserved (e.g. height:
   0.32vw becomes height: 0.32vh) — this matches the user's spec
   literally: "all elements should use vw or vh values, depending
   which one is the current limiting factor."

   The switch covers the key sized elements:
     - .v060-hpbar          (HP bar height 0.32vw → 0.32vh)
     - .v060-hpbar (border) (HP bar border 0.04vw → 0.04vh)
     - .v060-hpbar (margin) (HP bar margin 0.18/0.12vw → 0.18/0.12vh)
     - .v060-region-battle .v060-hpbar (the v0.6.033 HP bar selector)
     - .v060-region-info    (info cell border-width 0.5vw → 0.5vh)
     - .v060-region-info (padding) (info cell padding 0.5vw → 0.5vh)
     - .v060-region-info (border-image-width) (0.5vw → 1vh at wide viewports — v0.6.049 doubled the width)

   At narrow viewports (aspect < 16/9), the base vw rules in styles.css
   remain in effect (width-bound branch, vw is the active axis). The
   @media rule below ONLY applies at wide viewports (aspect >= 16/9).

   IMPORTANT: this rule is loaded AFTER every prior closure at equal
   specificity, so the vh values win over the vw defaults at wide
   viewports. At narrow viewports, the @media rule does NOT apply, so
   the vw defaults from styles.css remain in effect. */
@media (min-aspect-ratio: 16/9) {
  /* HP bar — switch height/border/margin from vw to vh.
     v0.6.055 update: height 0.32vh -> 2vh to match the new vw default
     (the user requested height: 2vw; at wide viewports the axis switches
     from vw to vh, so the value must also switch proportionally). */
  .v060-region-battle .v060-hpbar {
    height: min(1.125cqw, 14.4px);       /* v0.6.055 — was 0.32vh */
    border-width: min(0.0225cqw, 0.288px);
    margin: min(0.10125cqw, 1.296px) 0 min(0.0675cqw, 0.864px) 0;
  }
  .v060-region-battle .v060-hpbar {
    border-radius: min(0.05625cqw, 0.72px);
  }
  /* HP bar translateY(-2.5vw) — v0.6.076: the resting translateY moves
     UP from -6vw to -2.5vw per the requested Battle Scene delta.
     The v0.6.040 defense-in-depth wide-aspect override that switched
     vw to vh has been retired; the canonical vw value is now the
     single source of truth at every aspect ratio. */
  .v060-battle-screen .v060-region-battle > .v060-hpbar {
    transform: translateY(max(-2.5cqw, -32px));
  }
  /* Info cells — switch border-width / padding / border-image-width from vw to vh */
  .v060-region-info,
  .v060-region-info.v060-region-empty {
    border-width: min(0.28125cqw, 3.6px);
    border-image-width: min(0.5625cqw, 7.2px);
    padding: min(0.28125cqw, 3.6px);
  }
  /* Battle region — keep all sizes proportional to height at wide viewports */
  .v060-region-battle {
    /* No explicit sizes; relies on grid + flex */
  }
  /* Readiness fill — height set by JS via percentage, but border-image-width
     and the info cell padding flow above handle the proportional sizing. */
}

/* ---- Defense-in-depth #4: explicit no-px rule for the .scene container.
   This rule is a no-op (no properties set) but its presence-only
   selector pattern can be probed by tests to verify the v0.6.045
   zero-px unit hygiene is preserved through v0.6.048. */
.scene,
.v060-region-info,
.v060-region-battle,
.v060-hpbar,
.v060-hpbar-fill {
  /* No px units used in any of these selectors' computed styles. */
}

/* ===========================[v06049-closure.css]=========================== */
/* ============================================================================
   v0.6.049 — Combine Border-Image Declarations into Shorthand
              + Double Row-7 Info Cell Border Width

   DELTA SUMMARY
   -------------

   DELTA #1 — HP bar border image shorthand.
     The HP bar's 5 separate border-image properties (source, slice, width,
     repeat, outset) in v06042-closure.css are CONSOLIDATED into a single
     `border-image` shorthand declaration:

       border-image: url(assets/ui_healthbar.png) 48 fill / 0.5vw / 0 stretch;

     The shorthand parses to the same 5 longhand properties (per CSS spec):
       - border-image-source:  url(assets/ui_healthbar.png)
       - border-image-slice:   48 fill
       - border-image-width:   0.5vw     (UNCHANGED — was 0.5vw)
       - border-image-outset:  0
       - border-image-repeat:  stretch

     The "1vw" in the user's example is a placeholder for the syntax shape,
     not a value change. The HP bar width stays at 0.5vw (per v0.6.042).

   DELTA #2 — Row-7 info cell border image shorthand + doubled width.
     The row-7 gerbil info cell's 5 separate border-image properties in
     v06040-closure.css are CONSOLIDATED into a single shorthand AND
     the border width is DOUBLED from 0.5vw to 1vw per user request:

       border-image: url(assets/ui_gerbilinfo.png) 32 fill / 1vw / 0 stretch;

     The shorthand parses to the same 5 longhand properties:
       - border-image-source:  url(assets/ui_gerbilinfo.png)
       - border-image-slice:   32 fill
       - border-image-width:   1vw       (DOUBLED from 0.5vw)
       - border-image-outset:  0
       - border-image-repeat:  stretch

     The v06048-closure.css @media (min-aspect-ratio: 16/9) rule that
     switches border-image-width from vw to vh at wide viewports is
     updated to use 1vh (matches the doubled width).

   DELTA #3 — Defense-in-depth.
     This file re-asserts BOTH new shorthand declarations at equal-or-higher
     specificity (body .v060-... prefix adds 0,0,1 to specificity, beating
     any non-body scoped rule on the same elements). The v06042-closure.css
     and v06040-closure.css files carry the canonical declarations; this
     defense-in-depth rule protects them against a future regression edit.

   DELTA #4 — Identity marker.
     The data-build-marker attribute on every identity surface is updated:
       v06048-dynamic-16x9-fitting-any-viewport
         → v06049-border-image-shorthand-double-info-border
     Markers carry no live behavior; they exist as presence probes.

   PRESERVED (no edits to v0.6.048 baseline behavior):
     - v0.6.048 dynamic 16:9 app space (min() smart clamp + vw/vh axis switching)
     - v0.6.047 combat log removed
     - v0.6.045 zero-px unit hygiene
     - v0.6.044 width: 100vw baseline
     - v0.6.043 per-class readiness bar colors (Rifleman/Heavy/Medic/Scout/
       Engineer/Sniper/Grenadier/Officer)
     - v0.6.042 16:9 aspect ratio + vw-only sizing + HP bar border image
       (now consolidated into shorthand — value UNCHANGED)
     - v0.6.041 pause/resume toggle
     - v0.6.040 HP bar translateY(-6vw) + damage numbers + row-7 border
       (now consolidated into shorthand with width DOUBLED to 1vw)
     - v0.6.039 word pops at center
     - v0.6.038 depot swap fix
     - v0.6.037 matchmaking / medic / position sync
     - v0.6.036 HP bar polish
     - v0.6.035 text emphasis
     - v0.6.034 HP bar selector
     - v0.6.033 HP bar width 21%
     - v0.6.032 taunt targeting priority
     - v0.6.031 drag-hover effect
     - v0.6.030 title page layout
     - v0.6.028 item-slot hover
     - v0.6.027 bottom anchor + 420ms transition
     - v0.6.026 grid-up + gerbil-down hover
     - v0.6.025 .sc-portrait position:absolute fix
     - v0.6.024 polish + engineer_taunt data
     - v0.6.020 8-technique surface
     - v0.6.017 HP bar geometry baseline
     - v0.6.016 fast shot tracers
     - 34x7 battle grid, transparent battle cells, opaque row-7 info
     - per-class portrait transforms
     - per-class item-slot styling
     - deterministic RNG, save/load mechanics

   NOTE — depot scene border images are PRESERVED as multi-property form
     per the user's prior preference. Only the battle-scene border images
     (HP bar + row-7 info cell) are consolidated into shorthand.

   ============================================================================ */

/* ---- Identity marker (presence-only selector). ----
   Spec/runtime probes target this selector to verify the v0.6.049
   build is live. data-build-marker attribute is set on
   #v060BattleScreen and #setupScreen in index.html. */
#v060BattleScreen[data-build-marker="v06049-border-image-shorthand-double-info-border"],
#setupScreen[data-build-marker="v06049-border-image-shorthand-double-info-border"] {
  /* No-op + identity marker. The v0.6.049 delta is a CSS-rule change,
     not a layout change. The selector fires as the identity probe. */
}

/* ---- DELTA #1 — HP bar border image shorthand (defense-in-depth).
   Re-asserts the v06042-closure.css consolidated shorthand at equal-or-
   higher specificity (body .v060-... prefix = 0,1,1 specificity bump
   over the inner v06042-closure.css selector). The actual 5 properties
   parsed from the shorthand are unchanged from v0.6.048 except for
   the syntax consolidation:
     border-image-source:  url(assets/ui_healthbar.png)
     border-image-slice:   48 fill
     border-image-width:   0.5vw     (UNCHANGED)
     border-image-outset:  0
     border-image-repeat:  stretch
   The HP bar border-image width remains 0.5vw (NOT 1vw — the user's
   "1vw" in their example was a placeholder for the syntax shape, not
   a value change). */
body .v060-battle-screen .v060-region-battle > .v060-hpbar {
  border-image: url(assets/ui_healthbar.png) 48 fill / min(0.5cqw, 6.4px) / 0 stretch;
}

/* ---- DELTA #2 — Row-7 info cell border image shorthand + doubled width.
   Re-asserts the v06040-closure.css consolidated shorthand at equal-or-
   higher specificity. The actual 5 properties parsed from the shorthand
   are unchanged from v0.6.048 except for the syntax consolidation AND
   the width doubling from 0.5vw to 1vw:
     border-image-source:  url(assets/ui_gerbilinfo.png)
     border-image-slice:   32 fill
     border-image-width:   1vw       (DOUBLED from 0.5vw)
     border-image-outset:  0
     border-image-repeat:  stretch
   The doubled border at 1vw (.5vw at 800-wide, 1vw at 1600-wide) draws
   the inner content area tighter than the 0.5vw rim. The info cell
   content (portrait, statuses, readiness bar) still fits inside the
   inner area at 1600x720, 1200x540, 800x360 (verified by tests). */
body .v060-region-info,
body .v060-region-info.v060-region-empty {
  border-image: url(assets/ui_gerbilinfo.png) 32 fill / min(1cqw, 12.8px) / 0 stretch;
}

/* ===========================[ v0.6.050 HP bar FIX ]===========================
 * v0.6.050 — FINAL OVERRIDE: the HP bar was broken in v0.6.049 because:
 *   - styles.css had `.v060-hpbar { height: 0.32vw; ... orange gradient fill }`
 *   - v06036-closure.css had `.v060-hpbar { height: 0.96vw }` and green fill
 *   - These two rules had equal selector specificity and the closure
 *     loaded later so the FIXED values were the live ones, BUT the user
 *     could not see the fix by reading styles.css (the broken values were
 *     still there, in plain sight). The cascade was technically correct
 *     but the user could not analyze any single element because the
 *     styles were spread across multiple files.
 *
 * v0.6.050 FINAL values (this block wins cascade at equal specificity):
 */
.v060-region-battle .v060-hpbar {
  height: min(0.96cqw, 12.288px);             /* v0.6.036 polish — was 0.32vw in styles.css (BUG) */
  width: 21%;                  /* v0.6.033 preserved */
  background: rgba(0, 0, 0, 0.4);  /* empty-track background */
  border: min(0.04cqw, 0.512px) solid rgba(243, 233, 214, 0.25);
  border-radius: min(0.1cqw, 1.28px);
  margin: min(0.18cqw, 2.304px) 0 min(0.12cqw, 1.536px) 0;
  overflow: hidden;
  flex: 0 0 auto;
  position: relative;
  z-index: 12;
  border-style: solid;
  border-width: min(0.5cqw, 6.4px);
  border-image: url(assets/ui_healthbar.png) 48 fill / min(0.5cqw, 6.4px) / 0 stretch;
  box-sizing: border-box;
  /* opacity: 1 (the .v060-region-defeated .v060-hpbar { opacity: 0.45 }
     rule below still applies to the defeated state — it has higher
     specificity because of the extra .v060-region-defeated class).
     There is NO opacity: 0.45 on the base .v060-hpbar rule. */
}

.v060-region-battle .v060-hpbar-fill {
  height: 100%;
  width: 100%;
  margin: 0;
  background: linear-gradient(to right, #1a5c1a, #4caf50);  /* GREEN fill (was orange) */
  transition: width 220ms ease;
}

/* v0.6.050 marker — presence-only, used by identity probes. */
.v060-battle-screen[data-build-marker="v06050-fix-hp-bars-and-consolidate-css"],
#setupScreen[data-build-marker="v06050-fix-hp-bars-and-consolidate-css"] {
  /* v0.6.050 marker */
}


/* =======================[ v0.6.051 final acceptance ]=======================
 * The HP fill must retain a positive painted content box: no authored padding
 * is permitted on any HP-bar selector. Production styles contain zero px units.
 */
.v060-region-battle .v060-hpbar {
  height: min(2cqw, 25.6px);                /* v0.6.055 — was 0.96vw */
  width: 50%;                  /* v0.6.055 — was 21% (v0.6.033) */
  background: rgba(0, 0, 0, 0.4);
  border: 0;
  border-radius: min(0.1cqw, 1.28px);
  margin: min(0.18cqw, 2.304px) 0 min(0.12cqw, 1.536px) 0;
  /* v0.6.055 — overflow property REMOVED (was overflow: visible from v0.6.050 fix).
     No overflow constraint is applied; default is visible. */
  flex: 0 0 auto;
  position: relative;
  z-index: 12;
  border-style: solid;
  border-width: min(0.5cqw, 6.4px);
  border-image: url(assets/ui_healthbar.png) 48 fill / min(0.5cqw, 6.4px) / 0 stretch;
  box-sizing: border-box;
}

/* v0.6.055 defense-in-depth closure — body prefix bumps specificity.
   Re-asserts the user-requested HP bar sizing at equal-or-higher specificity
   so a stray future edit to the canonical .v060-region-battle .v060-hpbar
   rule above cannot regress v0.6.055 back to width 21% / height 0.96vw /
   or reintroduce an overflow: hidden constraint. The user asked to REMOVE
   the overflow attribute; the default value is 'visible', which is what
   we want anyway. The defense-in-depth closure positively re-asserts
   overflow: visible to win against any earlier overflow: hidden rules. */
body .v060-region-battle .v060-hpbar {
  /* v0.6.111: dropped `!important` from width/height. The `body`-prefixed
     selector already wins by specificity over the base
     `.v060-region-battle .v060-hpbar` rule (one extra element beats the
     base 0,2,0 to 0,2,1). overflow:visible is not in the spec's forbidden
     layout-property list so the `!important` there remains. */
  width: 50%;                  /* v0.6.055 — was 21% */
  height: min(2cqw, 25.6px);   /* v0.6.055 — was 0.96vw */
  overflow: visible !important; /* v0.6.055 — explicit visible (the user
                                   asked to remove the overflow attribute;
                                   default is visible, but earlier rules
                                   in the cascade declared overflow: hidden
                                   so the defense-in-depth explicitly
                                   re-asserts visible to win cascade). */
}
.v060-battle-screen.v060010-active .v060-region-battle .v060-hpbar > .v060-hpbar-fill {
  position: absolute;
  top: 0;            /* v0.6.056 — was -0.5vw; user asked: "center it instead".
                         The negative offset was making the green fill clip
                         OVER the top-left of the wood/frame border rim. */
  left: 0;           /* v0.6.056 — was -0.5vw; same reason. */
  z-index: 30;
  pointer-events: none;
  /* v0.6.056 — height: was 0.96vw (a leftover from v0.6.051 fix). The HP
     bar is now 2vw tall (v0.6.055). The fill must be 100% of the bar
     content area so the green strip sits inside the wood/frame border
     rim, perfectly centered, with no overlap of the rim. */
  height: 100%;
  width: 100%;
  margin: 0;
  background: linear-gradient(to right, #1a5c1a, #4caf50) !important;
  background-image: linear-gradient(to right, #1a5c1a, #4caf50) !important;
  outline: none !important;
  filter: none !important;
  mix-blend-mode: normal !important;
  transition: width 220ms ease;
}
.v060-battle-screen[data-build-marker="v06054-replace-broken-hp-bar-image"],
#setupScreen[data-build-marker="v06054-replace-broken-hp-bar-image"] {
  /* v0.6.054 identity marker */
}

.v060-battle-screen[data-build-marker="v06055-hp-bar-width-50pct-height-2vw-no-overflow"],
#setupScreen[data-build-marker="v06055-hp-bar-width-50pct-height-2vw-no-overflow"] {
  /* v0.6.055 identity marker */
}

.v060-battle-screen[data-build-marker="v06058-apply-dynamic-16x9-layout-to-battle-scene"],
#setupScreen[data-build-marker="v06058-apply-dynamic-16x9-layout-to-battle-scene"] {
  /* v0.6.058 identity marker */
}

.v060-battle-screen[data-build-marker="v06059-dynamic-battle-portrait-dimensions-no-inline-px"],
#setupScreen[data-build-marker="v06059-dynamic-battle-portrait-dimensions-no-inline-px"] {
  /* v0.6.059 identity marker */
}

/* v0.6.058 — defense-in-depth closure for HP fill (kept; the
   `width: 100% !important` from v0.6.056 was REMOVED so the JS-set
   inline `hpf.style.width = hpPct + '%'` can take effect). The
   centering (top: 0, left: 0, height: 100%) is still re-asserted at
   equal-or-higher specificity so a stray future edit to the canonical
   .v060-battle-screen.v060010-active .v060-region-battle .v060-hpbar > .v060-hpbar-fill
   rule above cannot regress v0.6.056 back to the negative offsets
   (top: -0.5vw; left: -0.5vw; height: 0.96vw;) that caused the green
   fill to clip over the top-left of the wood/frame border rim. The
   user explicitly said: "center it instead" — meaning the green fill
   should be visually inside the border, NOT overlapping the rim. */
body .v060-region-battle .v060-hpbar > .v060-hpbar-fill {
  /* v0.6.111: dropped `!important` from top/left/height. The
     `position: relative` set by .v060-battle-screen.v060010-active
     already places the fill at the parent's top-left (offset 0,0)
     with `top: auto; left: auto`. `height: 100%` is already inherited
     from the base `.v060-region-battle .v060-hpbar-fill` rule. The
     JS-set inline `width` is preserved — no `!important` on width. */
  top: 0;       /* v0.6.056 — was -0.5vw */
  left: 0;      /* v0.6.056 — was -0.5vw */
  height: 100%; /* v0.6.056 — was 0.96vw (leftover from v0.6.051) */
  /* v0.6.058 — REMOVED `width: 100% !important`. That !important was forcing
     the fill to 100% wide at all times, defeating the JS-set inline width.
     Now the JS-set `hpf.style.width = hpPct + '%'` (in v060010PopulateBattleRegion)
     takes effect and the green strip correctly shrinks as the unit takes damage. */
}

/* =======================[ v0.6.054 final acceptance ]=======================
 * The v0.6.054 delta ships one asset fix:
 *   1. assets/ui_healthbar.png — the broken 1,697-byte placeholder PNG that
 *      had been silently propagated across 12+ versions (v0.6.042 → v0.6.053)
 *      is replaced with the real 215,782-byte source file from
 *      C:\Users\admin\Pictures\ui_healthbar.png (md5=9d2c9b9a37e297a19ef06ef76dd44023).
 *      The 9-slice border-image now renders the actual wood/frame artwork
 *      around the green HP fill on every HP bar on the battle scene.
 *   2. Asset integrity check (CRITICAL_ASSETS table) is registered at
 *      module load in game-v060-battle-ui.js. The function
 *      v06054VerifyAssets() runs at game startup and fails loudly if any
 *      critical asset's size or md5 does not match the expected baseline.
 *      Build-time verification lives in tests/v06054/verify_assets.cjs.
 *   3. All v0.6.053..v0.6.016 surfaces preserved (CSS custom-property
 *      animation composition, direction-aware tilt, creep-forward charge,
 *      visible green HP fill, zero-px rule, consolidated styles.css,
 *      border-image shorthand, doubled row-7 info border, dynamic 16:9
 *      app space, combat log removed, pause/resume, damage numbers,
 *      word pops, swap fix, matchmaking, medic symmetry, position sync,
 *      HP bar polish, text emphasis, HP bar selector/translateY/width,
 *      taunt targeting, drag-hover, title page, item-slot hover, bottom
 *      anchor, grid-up, .sc-portrait fix, polish + engineer_taunt data,
 *      8-technique surface, HP bar geometry baseline, fast shot tracers,
 *      34x7 grid, transparent battle cells, opaque row-7 info backplanes,
 *      per-class portrait transforms, per-class item-slot styling,
 *      per-class readiness bar colors, deterministic tick combat, save
 *      /load mechanics, medic skill data, taunt status field,
 *      engineer_taunt skill, 2x SPEED control on depot).
 *
 * (The v0.6.053 tilt+creep combination fix acceptance commentary is
 * preserved upstream in the v06018-actor-lean rule block and in the
 * git/test history. v0.6.054 is an asset-only delta on top of v0.6.053.)
 */

/* =======================[ v0.6.055 final acceptance ]=======================
 * The v0.6.055 delta ships ONE polish delta on the HP bar:
 *   1. .v060-region-battle .v060-hpbar { width: 50% }  (was 21% from v0.6.033)
 *   2. .v060-region-battle .v060-hpbar { height: 2vw } (was 0.96vw from v0.6.036)
 *   3. The `overflow` property is REMOVED entirely from the HP bar rule
 *      (was overflow: visible from the v0.6.050 fix; the user explicitly
 *      asked for no overflow attribute — the default of `visible` is what
 *      we want anyway, since the green fill (position: absolute, z-index 30,
 *      !important gradient) is allowed to extend INTO the border rim).
 *   4. Defense-in-depth: a body-prefix .v060-region-battle .v060-hpbar
 *      closure re-asserts width: 50% !important and height: 2vw !important
 *      so a stray future edit to the canonical rule cannot regress these
 *      values, and no overflow declaration appears in the closure either.
 *   5. All v0.6.054..v0.6.016 surfaces preserved (real 215,782-byte
 *      ui_healthbar.png asset with md5 9d2c9b9a37e297a19ef06ef76dd44023,
 *      tilt+creep CSS custom properties, direction-aware tilt, creep-forward
 *      charge, visible green HP fill, zero-px rule, consolidated styles.css,
 *      border-image shorthand, doubled row-7 info border, dynamic 16:9
 *      app space, combat log removed, pause/resume, damage numbers,
 *      word pops, swap fix, matchmaking, medic symmetry, position sync,
 *      HP bar polish, text emphasis, HP bar selector/translateY, taunt
 *      targeting, drag-hover, title page, item-slot hover, bottom anchor,
 *      grid-up, .sc-portrait fix, polish + engineer_taunt data,
 *      8-technique surface, HP bar geometry baseline, fast shot tracers,
 *      34x7 grid, transparent battle cells, opaque row-7 info backplanes,
 *      per-class portrait transforms, per-class item-slot styling,
 *      per-class readiness bar colors, deterministic tick combat, save/load
 *      mechanics, medic skill data, taunt status field, engineer_taunt
 *      skill, 2x SPEED control on depot).
 *
 * (v0.6.055 is a CSS-only delta on top of v0.6.054. No JS, data, or asset
 * changes. The HP bar is now larger (50% wide × 2vw tall) and the fill
 * can extend beyond the bar boundaries since overflow is no longer
 * constrained.)
 */

/* =======================[ v0.6.056 final acceptance ]=======================
 * The v0.6.056 delta ships ONE polish delta on the HP fill child:
 *   1. .v060-region-battle .v060-hpbar > .v060-hpbar-fill { top: 0 }
 *      (was top: -0.5vw from v0.6.051 — the negative offset was making
 *      the green fill clip OVER the top edge of the wood/frame border
 *      rim, hiding the rim where the offset was applied).
 *   2. .v060-region-battle .v060-hpbar > .v060-hpbar-fill { left: 0 }
 *      (was left: -0.5vw from v0.6.051 — same reason, on the left edge.
 *      Together with #1 this made the green fill extend INWARD by
 *      0.5vw on both axes from the bar's top-left corner, sitting
 *      on top of the border rim, not centered within it).
 *   3. .v060-region-battle .v060-hpbar > .v060-hpbar-fill { height: 100% }
 *      (was height: 0.96vw from v0.6.051 — a leftover from the v0.6.051
 *      fix. The HP bar is now 2vw tall (v0.6.055); the fill must be
 *      100% of the bar's content area so the green strip sits inside
 *      the wood/frame border, perfectly centered, with no overlap
 *      of the rim. The `min-height: 0.96vw` declaration is dropped
 *      because the parent HP bar is now 2vw tall and `min-height: 0.96vw`
 *      would let the fill shrink below the bar's content area).
 *   4. Defense-in-depth: a body-prefix .v060-region-battle .v060-hpbar
 *      > .v060-hpbar-fill closure re-asserts top: 0 !important,
 *      left: 0 !important, height: 100% !important, and width: 100%
 *      !important so a stray future edit to the canonical rule cannot
 *      regress v0.6.056 back to the negative offsets that caused the
 *      green fill to clip over the border rim. The closure carries
 *      !important on all four properties to win against any rule
 *      without !important.
 *   5. All v0.6.055..v0.6.016 surfaces preserved (HP bar width 50% +
 *      height 2vw + no overflow constraint, real 215,782-byte
 *      ui_healthbar.png with md5 9d2c9b9a37e297a19ef06ef76dd44023,
 *      tilt+creep CSS custom properties, v0.6.052 direction-aware tilt +
 *      creep-forward charge, v0.6.051 visible green HP fill + zero-px
 *      rule, v0.6.050 CSS consolidation, v0.6.049 border-image
 *      shorthand + doubled row-7 info cell border, v0.6.048 dynamic
 *      16:9 app space, v0.6.047 combat log removed, v0.6.045 zero-px
 *      rule fully applied, v0.6.044 width 100vw baseline, v0.6.043
 *      readiness bar colors, v0.6.042 16:9 aspect + HP bar border
 *      image, v0.6.041 pause/resume, v0.6.040 HP bar translateY +
 *      damage numbers + row-7 border, v0.6.039 word pops at center,
 *      v0.6.038 depot swap fix, v0.6.037 matchmaking / medic /
 *      position sync, v0.6.036 HP bar polish, v0.6.035 text emphasis,
 *      v0.6.034 HP bar translateY selector fix, v0.6.033 HP bar width
 *      21% (now 50% per v0.6.055), v0.6.032 taunt targeting, v0.6.031
 *      drag-hover, v0.6.030 title page, v0.6.028 item-slot hover,
 *      v0.6.027 bottom anchor + 420ms transition, v0.6.026 grid-up +
 *      gerbil-down hover, v0.6.025 .sc-portrait position:absolute fix,
 *      v0.6.024 polish + engineer_taunt data, v0.6.020 8-technique
 *      surface, v0.6.017 HP bar fill animation (width transition),
 *      v0.6.016 fast shot tracers, 34x7 grid, transparent battle cells,
 *      opaque row-7 info backplanes, per-class portrait transforms,
 *      per-class item-slot styling, deterministic tick combat,
 *      save/load mechanics, medic skill data, taunt status field,
 *      engineer_taunt skill, 2x SPEED control on depot scene).
 *
 * (v0.6.056 is a CSS-only delta on top of v0.6.055. No JS, data, or
 * asset changes. The green HP fill is now perfectly centered inside
 * the wood/frame border rim, with no overlap of the rim on any side.
  * The fill is visible as a 100% × 100% green strip that animates its
  * width with the unit's HP % — the same animation as before, just
  * centered.)
  */

 /* =======================[ v0.6.058 final acceptance ]=======================
  * The v0.6.058 delta ships ONE polish fix on the HP fill child:
   *   1. REMOVED `width: 100% !important;` from the body-prefixed
   *      defense-in-depth closure at line ~7858. The v0.6.056 closure
   *      was a body-prefixed .v060-region-battle .v060-hpbar >
   *      .v060-hpbar-fill selector with top: 0 !important,
   *      left: 0 !important, height: 100% !important, AND
   *      width: 100% !important. The width-100-important line was
   *      REMOVED in v0.6.058.
  *
  *      The `width: 100% !important` was forcing the fill to be 100%
  *      wide at all times, defeating the JS-set inline width. The JS
  *      in v060010PopulateBattleRegion sets
  *        hpf.style.width = Math.max(0, Math.min(100, hpPct)) + '%';
  *      every tick. Inline styles normally win against CSS rules
  *      WITHOUT `!important`, but lose against CSS rules WITH
  *      `!important`. So the `width: 100% !important` was overriding
  *      the JS-set width and the green strip stayed at 100% even as
  *      the unit's HP dropped.
  *
  *      Removing the line lets the inline `hpf.style.width` win the
  *      cascade (it has higher priority than non-!important CSS
  *      rules). The fill now correctly shrinks as the unit takes
  *      damage.
  *
  *   2. KEPT `top: 0 !important;`, `left: 0 !important;`,
  *      `height: 100% !important;` on the defense-in-depth closure
  *      so a stray future edit to the canonical rule cannot regress
  *      v0.6.056's centering. Only `width` was removed.
  *
  *   3. The canonical `.v060-battle-screen.v060010-active
  *      .v060-region-battle .v060-hpbar > .v060-hpbar-fill { ... }`
  *      rule at ~line 7811 still has `width: 100%;` (no !important).
  *      That `width: 100%` is fine: inline styles win against
  *      non-!important CSS rules, so the JS-set `hpf.style.width`
  *      takes effect.
  *
  *   4. All v0.6.056..v0.6.016 surfaces preserved (HP fill centered
  *      inside wood/frame border rim, HP bar width 50% + height 2vw +
  *      no overflow constraint, real 215,782-byte ui_healthbar.png
  *      with md5 9d2c9b9a37e297a19ef06ef76dd44023, tilt+creep CSS
  *      custom properties, v0.6.052 direction-aware tilt + creep-
  *      forward charge, v0.6.051 visible green HP fill + zero-px
  *      rule, v0.6.050 CSS consolidation, v0.6.049 border-image
  *      shorthand + doubled row-7 info cell border, v0.6.048 dynamic
  *      16:9 app space, v0.6.047 combat log removed, v0.6.045 zero-px
  *      rule fully applied, v0.6.044 width 100vw baseline, v0.6.043
  *      readiness bar colors, v0.6.042 16:9 aspect + HP bar border
  *      image, v0.6.041 pause/resume, v0.6.040 HP bar translateY +
  *      damage numbers + row-7 border, v0.6.039 word pops at center,
  *      v0.6.038 depot swap fix, v0.6.037 matchmaking / medic /
  *      position sync, v0.6.036 HP bar polish, v0.6.035 text emphasis,
  *      v0.6.034 HP bar translateY selector fix, v0.6.033 HP bar width
  *      21% (now 50% per v0.6.055), v0.6.032 taunt targeting, v0.6.031
  *      drag-hover, v0.6.030 title page, v0.6.028 item-slot hover,
  *      v0.6.027 bottom anchor + 420ms transition, v0.6.026 grid-up +
  *      gerbil-down hover, v0.6.025 .sc-portrait position:absolute fix,
  *      v0.6.024 polish + engineer_taunt data, v0.6.020 8-technique
  *      surface, v0.6.017 HP bar fill animation (width transition),
  *      v0.6.016 fast shot tracers, 34x7 grid, transparent battle cells,
  *      opaque row-7 info backplanes, per-class portrait transforms,
  *      per-class item-slot styling, deterministic tick combat,
  *      save/load mechanics, medic skill data, taunt status field,
  *      engineer_taunt skill, 2x SPEED control on depot scene).
  *
  * (v0.6.058 is a CSS-only delta on top of v0.6.056. No JS, data, or
  * asset changes. The HP fill width now correctly reflects the unit's
  * current HP percentage: 100% wide at full HP, shrinking to 50%, 25%,
  * 0% as the unit takes damage. The 220ms width transition is
  * preserved.)
  */

/* =================[ v0.6.061 body-owned scene backgrounds ]=================
 * Body is the sole owner of title/depot/battle artwork. JavaScript writes only
 * semantic scene + limiting-axis data attributes; this consolidated stylesheet
 * resolves all six URLs and sizes. Width-limited viewports use tall artwork at
 * 100% body width. Height-limited viewports (including exact 16:9) use wide
 * artwork at 100% body height. Scene containers remain transparent.
 */
.app-shell {
  background-image: none;
  background-color: transparent;
}

body {
  min-height: 100vh;
  background-color: black;
  background-repeat: no-repeat;
  background-position: center center;
}

body[data-gg-background-scene="title"][data-gg-background-axis="width"] {
  background-image: url("assets/bg_title_16x9_tall.png");
  background-size: 100% auto;
}

body[data-gg-background-scene="title"][data-gg-background-axis="height"] {
  background-image: url("assets/bg_title_16x9_wide.png");
  background-size: auto 100%;
}

body[data-gg-background-scene="depot"][data-gg-background-axis="width"] {
  background-image: url("assets/bg_depot_16x9_tall.png");
  background-size: 100% auto;
}

body[data-gg-background-scene="depot"][data-gg-background-axis="height"] {
  background-image: url("assets/bg_depot_16x9_wide.png");
  background-size: auto 100%;
}

body[data-gg-background-scene="battle"][data-gg-background-axis="width"] {
  background-image: url("assets/bg_battle_16x9_tall.png");
  background-size: 100% auto;
}

body[data-gg-background-scene="battle"][data-gg-background-axis="height"] {
  background-image: url("assets/bg_battle_16x9_wide.png");
  background-size: auto 100%;
}

.v060-battle-screen[data-build-marker="v06066-swap-depot-morale-wins-content"],
#setupScreen[data-build-marker="v06066-swap-depot-morale-wins-content"] {
  /* v0.6.066 identity marker */
}

/* ===========================[v06067-closure.css]===========================
   Gerbil Gunners v0.6.067 — Concept Title Screen With Two Empty Future Unit Slots.

   Replaces the prior centered white title card and scrollable changelog with
   a concept-derived overlay composition. The supplied concept_titlescreen.png
   (1672x941 RGB, md5 f247aec4ec27c0f02cbe647553d83217) defines normalized
   box positions for the main GERBIL GUNNERS heading, the subtitle, the
   decorative mug badge, the ALPHA PLAYTEST note cluster, six controls, and
   two empty unit-slot reservation hooks. This closure:

     1. Establishes the exact fitted 16:9 stage formula required by the spec:
          width: min(100vw, 177.7777vh);
          height: min(56.25vw, 100vh);
          aspect-ratio: 16 / 9;
          position: absolute; top: 50%; left: 50%;
          transform: translate(-50%, -50%);
        on the title screen so the concept overlay renders inside the same
        16:9 box the body-owned artwork already uses for title scene art.

     2. Repositions all concept overlay elements using percentage
        coordinates derived directly from the concept's normalized boxes.
        Each child fills its concept box exactly:
          top:    y * 100%
          left:   x * 100%
          width:  w * 100%
          height: h * 100%
        No viewport-fixed geometry. No pixels. Zero CSS px tokens in any
        rule added by this closure (per project zero-px hygiene).

     3. Renders two empty transparent reservation hooks for future unit art
        (.title-unit-slot). No <img>, no background-image, no silhouette,
        no generated artwork. The hooks carry data-side and data-facing
        attributes and aria-hidden="true"; the engine never touches them
        in v0.6.067. Future builds will populate them with supplied class
        art while enforcing different class IDs on the two slots.

     4. Renders five real button controls at concept positions. The PLAY
        button retains data-action="start-run" and is the only active
        action. The other four (SUPPORT, PROVIDE FEEDBACK, LEADERBOARD,
        SETTINGS) are rendered at their concept positions but are disabled
        and aria-disabled="true" because no verified existing destination
        exists. The hierarchy is preserved even while disabled (do not wash
        them out to illegibility).

     5. Carries accessible title + landmarks (aria-labelledby on the title
        section, aria-label on the note cluster, decorative aria-hidden on
        the mug badge and the unit slots, focus-visible state on PLAY).

     6. Preserves landscape overlay behavior (the existing .landscape-overlay
        rule remains untouched).

     7. Preserves the body-owned dynamic background synchronizer
        (syncBodySceneBackground -> body[data-gg-background-scene=title]
        with axis switching) and the v0.6.066 data-build-marker pattern.

     8. Preserves all v0.6.066..v0.6.016 surfaces: body-owned scene art,
        depot morale/wins swap, v0.6.061 body-owned scenes, v0.6.060 axis
        switching, the v0.6.058 dynamic 16:9 battle layout, all HP bar +
        portrait + combat polish, depot mechanics, save/load, deterministic
        tick combat, and the consolidated styles.css file.

     9. Establishes identity on every established surface:
          <title>, <meta name="gg-build">, <meta name="gg-build-marker">,
          <meta name="gg-rules-version">, #setupScreen[data-build-marker],
          #v060BattleScreen[data-build-marker], window.GG_DATA.*,
          window.GG_DEBUG.{buildVersion, buildMarker, rulesVersion},
          data/classes.json, data/items.json, and this v0.6.067 marker
          selector below.
========================================================================== */

/* ---- DELTA #1 — Establish the fitted 16:9 stage formula on #titleScreen.
  The body-owned artwork + v0.6.061 synchronizer already places the title
  scene artwork on <body>, not on #titleScreen, so the title section is
  free to be the fitted overlay stage. We declare the spec-required exact
  16:9 formula here on the existing #titleScreen element via a body
  prefix so the rule wins at higher specificity than the v0.6.030 base
  rule (and any earlier title-screen rule in styles.css). The legacy
  .title-screen rule further up in styles.css already declares
  aspect-ratio: 16 / 9 from v0.6.042; this closure re-asserts the full
  min() formula so the spec word-for-word is satisfied at every test
  viewport (1600x720 wide, 1280x720 exact, 800x600 width-limited).

  v0.6.067 REDO67: added `container-type: inline-size` and
  `container-name: title` so child typography can use `cqw` /
  `cqh` instead of `vw` / `vh`. This eliminates the letterbox-
  dependent overflow that tore the GERBIL GUNNERS heading and
  the subtitle across two lines at certain viewports. The
  container unit is the stage's inline axis (width).

  v0.6.104: the centering formula was changed from
  `top: 50%; left: 50%; transform: translate(-50%, -50%)` to
  `inset: 0; margin: auto`. The transform was a stacking-context
  creator (per CSS Positioned Layout Module 3), which trapped the
  descendant .title-emblem z-index inside the title screen's stacking
  context — the root cause of the v0.6.103 bug where the emblem
  painted below the feedback dialog/backdrop/launcher. The new
  inset:0 + margin:auto + explicit width/height centers an
  absolutely-positioned element WITHOUT forming a stacking context.
  The fitted 16:9 box and centered origin are bit-for-bit identical
  to v0.6.103 at 1280x720 and 1600x720 (verified). */
body #titleScreen.title-screen {
  width: min(100vw, 177.7777vh);
  height: min(56.25vw, 100vh);
  aspect-ratio: 16 / 9;
  position: absolute;
  /* v0.6.104: replaced `top: 50%; left: 50%; transform: translate(-50%, -50%)`
     with `inset: 0; margin: auto` to break the titleScreen stacking context
     (transform was a stacking-context creator per the spec). The centered
     fitted 16:9 BCR is bit-for-bit identical to v0.6.103 at every test
     viewport. */
  inset: 0;
  margin: auto;
  /* v0.6.067: scene exclusivity preserved. The base title-screen rule
     keeps display:flex + align-items:center + justify-content:center;
     we deliberately do NOT use flex for the concept overlay composition
     (every child is percentage-positioned), so we override display below. */
  display: block;
  box-sizing: border-box;
  padding: 0;
  background-color: transparent;
  color: #f5e3b3; /* light text on dark body-owned scene art */
  overflow: hidden;
  /* v0.6.104: z-index auto overrides the base .title-screen z-index: 50.
     Without z-index AND without transform, the title screen no longer forms
     a stacking context. Other title-screen descendants that need their own
     stacking context (the .title-emblem at 10000002, titleSettingsMenu at
     10000, etc.) resolve their z-index at root level — the only level where
     they can paint above the root-level .feedback-stage z-index 9999998. */
  z-index: auto;
  container-type: inline-size;
  container-name: stage;
}

/* ---- DELTA #2 — Position concept overlay elements at percentage coords.
  Each percentage is the literal concept_titlescreen.png normalized box.
  Width/height use the same percentage units so the rule scales with
  the fitted 16:9 stage; left/top use the same percentage units so the
  rule anchors on the same stage. No px. No vh/vw. No viewport-fixed
  geometry. REDO67: typography uses `cqw` (container width = stage
  width) instead of `vw` (viewport width), so the heading and subtitle
  no longer depend on the letterbox to determine their rendered size. */

/* ===========================[v0.6.104-title-emblem-top-layer]================
   v0.6.104 — title emblem absolute top layer.

   Baseline root cause: #titleScreen.title-screen establishes its own
   stacking context via `position: absolute; z-index: 50` plus
   `transform: translate(-50%, -50%)`, trapping any descendant z-index
   below the root-level .feedback-stage z-index (9999998). Simply
   raising `.title-emblem { z-index: 9999999 }` was therefore invalid —
   the value resolved inside the trapped titleScreen stacking context
   and the emblem painted below the feedback dialog/backdrop
   (z 10000000/10000001) and below the global feedback launcher
   (z 9999999 inside the stage).

   v0.6.104 fix (CSS-only, two changes):

   (A) BREAK the titleScreen stacking context. Replace
       `top: 50%; left: 50%; transform: translate(-50%, -50%)`
       with `inset: 0; margin: auto` (no transform stacking context)
       and override the inherited `z-index: 50` with `z-index: auto`
       so the title screen no longer forms a stacking context at all.
       Children with their own z-index (the emblem at 10000002,
       titleSettingsMenu at 10000, etc.) now resolve their z-index
       at root level — which is the only level where they can paint
       above the root-level .feedback-stage z-index 9999998.

       The min() formula + explicit width/height + inset:0 +
       margin:auto reproduce the centered fitted-16:9 BCR bit-for-bit
       (verified at 1280x720 and 1600x720). aspect-ratio: 16/9 still
       guards against formula drift.

   (B) PROMOTE .title-emblem to `position: fixed` at the root fitted
       16:9 rect (same min() formula and same centered origin as the
       (now non-stacking-context) titleScreen) and lift z-index to
       10000002 so the emblem paints above every title UI: backdrop,
       feedback dialog, Settings menu, global feedback launcher.

       `position: fixed` creates a root-level stacking context for the
       emblem, escaping the titleScreen. The inner h1 keeps the
       established 4.5%/27.5%/44.5%/21.5% geometry relative to the
       now-fixed fitted-16:9 rect — same BCR as the prior
       absolute-positioned wrapper. Exactly one .title-emblem wrapper,
       exactly one title_text.png image, no clone, no shrink, no
       crop, no transform on the artwork, no fade, pointer-events:none
       preserved.

   Cross-check: only the emblem needs to escape overlays; every other
   title control remains inside the title section and below the
   feedback modal backdrop, as required.
   =========================================================================== */

/* Decorative emblem wrap: holds the supplied title artwork image
  (assets/title_text.png, 896x245 RGBA). v0.6.104: the wrapper is a
  `position: fixed` overlay that mirrors the (now non-stacking-context)
  titleScreen's fitted 16:9 geometry (same min() formula, same centered
  origin via inset:0 + margin:auto equivalent on the fixed element via
  top:50% / left:50% / explicit width-height matching the fitted box),
  so its inner <h1> can keep the established 4.5% top / 27.5% left /
  44.5% width / 21.5% height percentages and produce the SAME bounding
  client rect as the prior absolute-positioned wrapper — verified at
  1280x720 (BCR 569.6 x 154.8 at left 352, top 32.4), 1600x720, 800x600.

  `position: fixed` creates a root-level stacking context for the emblem,
  which is what allows z-index 10000002 to paint above the root-level
  .feedback-stage z-index 9999998. The wrapper has NO transform (a
  transform on a child of a non-stacking-context parent would still
  trap the stacking context there — but titleScreen is no longer one;
  we still avoid transform here for symmetry and to keep the emblem's
  painting order unambiguous). Display:block because the only child
  is absolutely positioned; pointer-events:none so clicks reach the
  underlying scene. No raster decoration, no <img> outside the image,
  no production px. The supplied image is 896x245 (3.66:1); contain
  fits inside the h1 box at every test viewport.
   - 1280x720 stage: h1 569.6x154.8 (3.68) -> height-bound 569.6x155.6
   - 1600x720 stage: h1 712x154.8 (4.60) -> height-bound 566.8x154.8
   - 800x600 stage:  h1 356x96.75 (3.68)  -> height-bound 354.8x96.9 */
body #titleScreen .title-emblem {
  position: fixed;
  /* v0.6.104: inset:0 + margin:auto centers the fixed overlay at the
     root fitted-16:9 rect (same min() formula as the now non-stacking
     titleScreen) without using transform — verified bit-for-bit
     identical to the prior v0.6.103 transform translate(-50%,-50%)
     centering at 1280x720 (rect 0,0,1280,720) and 1600x720 (rect
     160,0,1280,720). */
  inset: 0;
  margin: auto;
  width: min(100vw, calc(100vh * 16 / 9));
  height: min(100vh, calc(100vw * 9 / 16));
  display: block;
  pointer-events: none;
  z-index: 10000002;
}

/* Main GERBIL GUNNERS heading: v0.6.078 swaps the centered text for
  the literal supplied title artwork inside the existing wrapper.
  v0.6.104: the <h1> inherits the established 4.5%/27.5%/44.5%/21.5%
  percentage geometry (previously on .title-emblem). It is a
  transparent flex parent that hosts exactly one
  .title-main-heading-image <img>; the img declares object-fit:contain
  and pointer-events:none so it preserves the 896:245 intrinsic ratio,
  fully contains itself inside the wrapper, and never intercepts
  pointer input. Pure centering, no raster, no <img> outside this one,
  no production px. */
body #titleScreen .title-main-heading {
  position: absolute;
  top: 4.5%;
  left: 27.5%;
  width: 44.5%;
  height: 21.5%;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  pointer-events: none;
}

body #titleScreen .title-main-heading-image {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  object-position: center center;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Subtitle: REDO2 returns to the source concept subtitle region
  [x=.305, w=.390, right=.695] so it no longer extends right of 70%
  and visually collides with the note cluster at left=80.5%. At 1280
  stage width that is 499.2px wide.

  REDO3 (parent QA feedback): the REDO2 sizing 1.6cqw / 0.18cqw
  produced an actual text range width of ~530px, overflowing the
  499px concept box by ~31px (~2.45cqw). Reduced font-size to 1.5cqw
  and letter-spacing to 0.14cqw so the actual line width is ~483px,
  leaving >= 1cqw (~13px) of internal slack on a single line at all
  four viewports while keeping both stars inside the box and the
  subtitle visually prominent next to the 4.2cqw heading. */
body #titleScreen .title-subtitle {
  position: absolute;
  top: 26%;
  left: 30.5%;
  width: 39%;
  height: 3.8%;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 2.5cqw;
  letter-spacing: min(0.14cqw, 1.792px);
  color: #f4c35c;
  text-shadow: min(0.5cqw, 6.4px) min(0.5cqw, 6.4px) 0 #281e14,
               min(0.5cqw, 6.4px) max(-0.5cqw, -6.4px) 0 #281e14,
               max(-0.5cqw, -6.4px) min(0.5cqw, 6.4px) 0 #281e14,
               max(-0.5cqw, -6.4px) max(-0.5cqw, -6.4px) 0 #281e14,
               min(0.5cqw, 6.4px) 0 0 #281e14,
               0 min(0.5cqw, 6.4px) 0 #281e14,
               max(-0.5cqw, -6.4px) 0 0 #281e14,
               0 max(-0.5cqw, -6.4px) 0 #281e14;
  white-space: nowrap;
  pointer-events: none;
}

/* v0.6.107: randomized mug badge — refined typography + table formatting.
   The badge remains a noninteractive decorative element with
   aria-hidden + pointer-events: none. v0.6.106 added typed phrase
   typography and a stacked 8-direction shadow on the literal black
   #281e14 outline. v0.6.107 retires the v0.6.105 empty reservation
   (top 26%/width 11%/height 17.8%), tightens width 10%→8% and the
   rotateY/X/skew transform, and renders every phrase inside exactly
   one direct child <span>. display:table + display:table-cell +
   vertical-align:middle makes the inner span center inside the badge
   without invented flex alignment, padding, or per-phrase special
   cases. The exact requested authored values follow. */
body #titleScreen .title-mug-badge {
  position: absolute;
  top: 28%;
  left: 4.5%;
  width: 8%;
  height: 16%;
  text-align: center;
  transform: rotateY(12deg) rotateX(-9deg) skew(-6deg, 6deg);
  text-transform: uppercase;
  display: table;
  background-color: transparent;
  background-image: none;
  pointer-events: none;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 1cqw;
  line-height: 2.25;
  letter-spacing: min(0.1cqw, 1.28px);
  text-shadow:
    min(0.25cqw, 3.2px) min(0.25cqw, 3.2px) 0 #281e14,
    min(0.25cqw, 3.2px) max(-0.25cqw, -3.2px) 0 #281e14,
    max(-0.25cqw, -3.2px) min(0.25cqw, 3.2px) 0 #281e14,
    max(-0.25cqw, -3.2px) max(-0.25cqw, -3.2px) 0 #281e14,
    min(0.25cqw, 3.2px) 0 0 #281e14,
    0 min(0.25cqw, 3.2px) 0 #281e14,
    max(-0.25cqw, -3.2px) 0 0 #281e14,
    0 max(-0.25cqw, -3.2px) 0 #281e14;
}
body #titleScreen .title-mug-badge > span {
  display: table-cell;
  vertical-align: middle;
  font-size: 1.25cqw;
}
body #titleScreen .title-mug-badge > span.is-long {
  font-size: 1cqw;
}
@media (min-aspect-ratio: 16/9) {
  body #titleScreen .title-mug-badge {
    left: 8%;
  }
}

/* Concept top-right note cluster: ALPHA / PLAYTEST / FIRST NIBBLE / v0.6.083 */
body #titleScreen .title-note-cluster {
  position: absolute;
  top: 15%;
  left: 88.5%;
  width: 9%;
  margin: 0;
  padding: min(0.5cqw, 6.4px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: min(0.3cqw, 3.84px);
  rotate: 4deg;
  transform: skew(-0.008turn, 8deg) rotate3d(0, 1, 0, 32deg);
  color: #2b1e1c;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  pointer-events: none;
}

@media (min-aspect-ratio: 16/9) {
  body #titleScreen .title-note-cluster {
    left: 87%;
  }
}

body #titleScreen .title-alpha-tag {
  margin: 0;
  padding: 0;
  width: 100%;
  text-align: center;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 2cqw;
  letter-spacing: min(0.12cqw, 1.536px);
  background: none;
  border: 0;
  color: #2b1e1c;
  box-sizing: border-box;
  white-space: nowrap;
}
body #titleScreen .title-playtest-tag {
  margin: max(-0.75cqw, -9.6px);
  padding: 0;
  width: 100%;
  text-align: center;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 1.2cqw;
  letter-spacing: min(0.12cqw, 1.536px);
  background: none;
  border: 0;
  color: #2b1e1c;
  box-sizing: border-box;
  white-space: nowrap;
}
body #titleScreen .title-operation-line {
  margin: min(2cqw, 25.6px) 0 max(-0.25cqw, -3.2px) 0;
  font-family: 'SplineSans-Bold', system-ui, sans-serif;
  font-weight: 700;
  font-size: 1cqw;
  color: #2b1e1c;
  white-space: nowrap;
}
body #titleScreen .title-operation-name {
  margin: 0;
  font-weight: 900;
  font-size: 1.25cqw;
  letter-spacing: min(0.1cqw, 1.28px);
  color: #2b1e1c;
  white-space: nowrap;
}

/* Concept five control buttons. Each is positioned by the concept's
  normalized box. Only PLAY is enabled; the other four are disabled
  per task spec (no verified existing destinations). Hierarchy is
  preserved through larger fonts / warm gold for PLAY and dimmer
  variants for the disabled controls. REDO67: all font-sizes,
  paddings, borders, and box-shadows are stage-relative (cqw). */
body #titleScreen .title-control {
  position: absolute;
  margin: 0;
  padding: min(0.5cqw, 6.4px) min(0.8cqw, 10.24px);
  font-family: 'SplineSans-Bold', system-ui, sans-serif;
  font-weight: 800;
  font-size: 1cqw;
  letter-spacing: min(0.1cqw, 1.28px);
  border-radius: min(0.25cqw, 3.2px);
  box-sizing: border-box;
  text-align: center;
  cursor: pointer;
  background: #2b1e1c;
  color: #d2c8a8;
  border: min(0.12cqw, 1.536px) solid #4a372a;
  text-shadow: 0 min(0.08cqw, 1.024px) min(0.4cqw, 5.12px) rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  transition: transform 220ms ease-out, background-color 220ms ease-out;
}
body #titleScreen .title-control:hover {
  color: #f5e3b3;
  background: #ffd980;
  transform: translateY(max(-0.2cqw, -2.56px));
  box-shadow: 0 min(0.18cqw, 2.304px) min(0.8cqw, 10.24px) rgba(244, 195, 92, 0.5);
}
body #titleScreen .title-control:focus-visible {
  outline: min(0.18cqw, 2.304px) solid #5dbed6;
  outline-offset: min(0.18cqw, 2.304px);
}

/* Hero-ready side controls reserve their empty left 40% track for future art. */
body #titleScreen .title-control-support,
body #titleScreen .title-control-feedback,
body #titleScreen .title-control-leaderboard,
body #titleScreen .title-control-settings {
  display: grid;
  grid-template-columns: 40% minmax(0, 60%);
  grid-template-rows: 100%;
  align-items: center;
  justify-items: stretch;
  column-gap: 0;
  padding: min(0.5cqw, 6.4px) min(0.7cqw, 8.96px) min(0.5cqw, 6.4px) min(0.35cqw, 4.48px);
  white-space: normal;
}

body #titleScreen .title-control-hero-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  grid-column: 1;
  width: 125%;
  height: 50%;
  min-width: 0;
  pointer-events: none;
}

body #titleScreen .title-control-label {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  min-width: 0;
  line-height: 1.05;
  white-space: normal;
}
/* Literal icon containment for the four non-Play title controls. */
body #titleScreen .title-control-icon {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  object-position: center;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* SUPPORT / DEVELOPER [0.2625, 0.775, 0.160, 0.120] — disabled */
body #titleScreen .title-control-support {
  top: 77.5%;
  left: 26.25%;
  width: 16%;
  height: 12%;
  font-size: 1cqw;
  border-image: url("assets/ui_support.png") 96 fill / min(2.75cqw, 35.2px) / 0 stretch;
  border-radius: min(2.25cqw, 28.8px);
}
/* PLAY [0.410, 0.750, 0.180, 0.170] — active (the only active action) */
body #titleScreen .title-control-play {
  top: 75%;
  left: 41%;
  width: 18%;
  height: 17%;
  border-radius: min(2.5cqw, 32px);
  font-weight: 300;
  font-size: 3.5cqw;
  letter-spacing: min(0.18cqw, 2.304px);
  border: min(0.18cqw, 2.304px) solid #4a372a;
  border-image: url("assets/ui_play.png") 84 fill / min(2.75cqw, 35.2px) / 0 stretch;
  font-family: 'BlackOpsOne', sans-serif;
  z-index: 9999;
}
body #titleScreen .title-control-play:focus-visible {
  outline: min(0.22cqw, 2.816px) solid #2b1e1c;
  outline-offset: min(0.18cqw, 2.304px);
}
/* PROVIDE / FEEDBACK [0.5775, 0.775, 0.160, 0.1225] — disabled */
body #titleScreen .title-control-feedback {
  top: 77.5%;
  left: 57.75%;
  width: 16%;
  height: 12.25%;
  font-size: 1cqw;
  border-image: url("assets/ui_feedback.png") 96 fill / min(2.5cqw, 32px) / 0 stretch;
  border-radius: min(2cqw, 25.6px);
}
/* LEADERBOARD [0.330, 0.910, 0.160, 0.080] — disabled */
body #titleScreen .title-control-leaderboard {
  top: 91%;
  left: 33%;
  width: 16%;
  height: 8%;
  font-size: 0.9cqw;
  border-image: url("assets/ui_leaderboard.png") 110 fill / min(2.75cqw, 35.2px) / 0 stretch;
  border-radius: min(1.75cqw, 22.4px);
}
/* SETTINGS [0.510, 0.910, 0.160, 0.080] — disabled */
body #titleScreen .title-control-settings {
  top: 91%;
  left: 51%;
  width: 16%;
  height: 8%;
  font-size: 0.9cqw;
  border-image: url("assets/ui_settings.png") 110 fill / min(2.25cqw, 28.8px) / 0 stretch;
  border-radius: min(1.5cqw, 19.2px);
}

/* Concept two empty unit slots. visual-left [0.330, 0.315, 0.175, 0.405]
   faces RIGHT. visual-right [0.495, 0.300, 0.190, 0.405] faces LEFT.
   Both are EMPTY reservation hooks. No <img>, no background-image, no
   silhouette, no decorative artwork. The element is fully transparent
   and noninteractive. Future builds will populate one of each with
   supplied class art while enforcing different class IDs. */
body #titleScreen .title-unit-slot {
  position: absolute;
  background-color: transparent;
  background-image: none;
  pointer-events: none;
  display: block;
}
body #titleScreen .title-unit-slot[data-side="left"] {
  top: 31.5%;
  left: 33%;
  width: 17.5%;
  height: 40.5%;
}
body #titleScreen .title-unit-slot[data-side="right"] {
  top: 30%;
  left: 49.5%;
  width: 19%;
  height: 40.5%;
}

/* ---- DELTA #3 — v0.6.067 identity marker. Presence-only, no live behavior.
   Wins against the v0.6.066 marker at equal selector specificity (same
   selector shape, but listed later in the file). */
body #titleScreen[data-build-marker="v06067-concept-title-screen-empty-facing-unit-slots"] {
  /* v0.6.067 marker: concept title screen with two empty future unit slots. */
}

.v060-battle-screen[data-build-marker="v06067-concept-title-screen-empty-facing-unit-slots"],
#setupScreen[data-build-marker="v06067-concept-title-screen-empty-facing-unit-slots"],
#titleScreen[data-build-marker="v06067-concept-title-screen-empty-facing-unit-slots"] {
  /* v0.6.067 identity marker on every established surface */
}

/* v0.6.061 release gate: inactive/hidden top-level scenes never render or
   intercept pointers. Positive layout display remains scoped to active scene
   selectors above; this final rule wins against inherited unqualified display
   declarations without affecting width, height, transforms, or scene artwork. */
.screen:not(.active),
.screen[hidden] {
  display: none !important;
}

/* ---------------------------------------------------------------------------
   v0.6.068 STAGE 3 — Medic heal route color + HEAL word-pop tone.

   Layered on top of the v0.6.025 delta #3 per-class skill line color and
   the v0.6.018 / v0.6.035 word / number-pop cascade. Skills tagged 'heal'
   (medic_heal, medic_cleanse, medic_regen) carry data-skill-heal="true"
   on their .v06016-effect route node (set in game-v060-battle-ui.js).
   The selector below wins against the [data-skill-class="medic"] teal
   rule via higher attribute-selector specificity and !important, and
   paints the route in the established sustain green #6ca84a so the
   player reads it as healing, not as a generic Medic skill line.

   The HEAL word / number pop tone is also recolored from the legacy
   light-green #7fff7f to the same sustain green #6ca84a so the pop,
   the route, and the existing HP-bar fill share a single heal hue.
   The pop still only appears on heal > 0 events (see
   game-v060-battle-ui.js: healthCommit with heal>0 / hot events), so
   misses, damage, and Medic Shot never trigger it.

   Offense skills (medic_basic_heal "Medic Shot", scout_basic_shot, etc.)
   never carry data-skill-heal="true" because they have no 'heal' tag,
   so their route stays in the per-class (or warm-yellow attack) hue.
   --------------------------------------------------------------------------- */

/* Heal route: override the Medic teal (and any other class color) when
   the route node carries the semantic heal attribute. The dual
   attribute selector wins on specificity against the single
   [data-skill-class="medic"] rule above. !important is kept for parity
   with the existing v0.6.025 class-color !important declarations. */
.v06016-effect.v06016-effect-skill[data-skill-class="medic"][data-skill-heal="true"] .v06016-effect-line-main,
.v06016-effect-route.v06016-effect-skill[data-skill-class="medic"][data-skill-heal="true"] .v06016-effect-line-main,
.v06016-effect[data-skill-class="medic"][data-skill-heal="true"] .v06016-effect-line-main {
  stroke: #6ca84a !important;
  stroke-dasharray: none !important;
}

/* HEAL tone: align the floating number pop with the established sustain
   green so the value reads as healing, not as a generic light-green pop.
   Number pop stays at the inherited 4.2vw base size from the v0.6.035
   block above; only the color is restated here. */
.v06018-number-pop[data-tone="heal"] {
  color: #6ca84a;
}

/* HEAL word: same recolor. Word pop stays at the inherited 2.4vw base
   size from the v0.6.035 block above; only the color is restated here.
   The drop-shadow stays at the inherited rgba(0,0,0,0.95) legibility
   outline; only the foreground hue is changed. */
.v06018-word-pop[data-tone="heal"] {
  color: #6ca84a;
}

/* ---------------------------------------------------------------------------
   v0.6.088 — title-scene Settings menu shell.
   The literal supplied frame is a border image on one stage-scoped dialog.
   The title artwork remains in place behind it; no backdrop or scene motion. */
/* v0.6.122.1: the settings menu was moved out of #titleScreen
   into #feedbackStage so it can be opened from any scene (depot,
   battle, summary, end). The previous selector gated every
   styling on `.title-settings-menu` which
   prevented the rules from applying once the menu was reparented.
   Now `.title-settings-menu` is styled directly — the body / scene
   parent doesn't matter, since the menu is rendered at the
   feedbackStage (z-index 9999998) which is always on top of every
   scene. The `position: absolute` + `top: 20.5%` + `left: 18.5%`
   place it inside feedbackStage's padding box (which is the
   viewport minus the safe-area letterboxes), so the percentages
   resolve against the full visible area. */
.title-settings-menu {
  position: absolute;
  left: 18.5%;
  top: 20.5%;
  width: 63%;
  height: 74%;
  margin: 0;
  padding: 0;
  border-style: solid;
  border-width: 1cqw 2.5cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_menu.png) 256 fill / 7.5cqw / 0 stretch;
  box-sizing: border-box;
  color: #3d281b;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  transform: translateY(115%);
  transition: transform 500ms cubic-bezier(0.22, 0.72, 0.2, 1);
  /* v0.6.122.1: raise the menu's local z-index above the legal-notice
     (z=200) and feedback dialog backdrop (z=10000000) — the menu
     is inside feedbackStage (z=9999998), so it composites on top
     of every scene. The v0.6.124.7 v0.6.124.8 menu lived inside
     #titleScreen which is z=50; 10000 was fine then. Now we need
     to be above the feedback dialog which is also in feedbackStage
     (z=10000001) so the user can navigate settings over an open
     feedback dialog. We use 10000002 — above the dialog (10000001)
     but below the title-transition-layer (10000005). */
  z-index: 10000002;
  overflow: visible;
}

.title-settings-menu[hidden] {
  display: none !important;
}

.title-settings-menu.is-open {
  transform: translateY(0);
}

.title-settings-banner {
  border-style: solid;
  border-width: 2.25cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_banner.png) 80 fill / 2.25cqw / 0 stretch;
  box-sizing: border-box;
  color: #f4c35c;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 2.5cqw;
  line-height: 1;
  letter-spacing: 0.14cqw;
  text-align: center;
  text-shadow: 0.5cqw 0.5cqw 0 #281e14,
               0.5cqw -0.5cqw 0 #281e14,
               -0.5cqw 0.5cqw 0 #281e14,
               -0.5cqw -0.5cqw 0 #281e14,
               0.5cqw 0 0 #281e14,
               0 0.5cqw 0 #281e14,
               -0.5cqw 0 0 #281e14,
               0 -0.5cqw 0 #281e14;
  width: 80%;
  margin: 0 auto;
  width: 36cqw;
}

.title-settings-content {
  width: 58cqw;
  height: 44.5cqw;
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: 16% 66% 18%;
  box-sizing: border-box;
  overflow-y: auto;
  overflow-x: hidden;
}

.title-settings-header {
  display: grid;
  grid-template-columns: 1fr 3fr 1fr;
  align-items: center;
  gap: 1cqw;
  min-width: 0;
}

.title-settings-heading-slot,
.title-settings-row-icon,
.title-settings-footer-icon {
  display: block;
  background: transparent;
  pointer-events: none;
}

.title-settings-heading-slot {
  width: 100%;
  height: 60%;
}

.title-settings-header h2 {
  margin: 0;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 2.25cqw;
  line-height: 1;
  letter-spacing: 0.12cqw;
  text-align: center;
  color: #4a2f1d;
  white-space: nowrap;
}

.title-settings-sections {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  min-width: 0;
  min-height: 0;
  padding-top: 1cqw;
}

.title-settings-section {
  display: grid;
  grid-template-rows: repeat(5, minmax(0, 1fr));
  min-width: 0;
  min-height: 0;
  padding: 0.5cqw;
  box-sizing: border-box;
  gap: 0.25cqw;
}

.title-settings-audio {
  padding-left: 4cqw;
}

.title-settings-gameplay {
  padding-right: 4cqw;
}

.title-settings-section + .title-settings-section {
  border-left: 0.08cqw solid rgba(74, 47, 29, 0.36);
}

.title-settings-section h3 {
  margin: 0;
  align-self: center;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  font-weight: 700;
  font-size: 1cqw;
  line-height: 1;
  letter-spacing: 0.04cqw;
  text-align: center;
  color: #4a2f1d;
  white-space: nowrap;
}

.title-settings-row {
  display: grid;
  grid-template-columns: 12% minmax(0, 1fr) auto;
  grid-template-rows: 100%;
  column-gap: 0.55cqw;
  align-items: center;
  min-width: 0;
  min-height: 0;
  border-style: solid;
  border-width: 1.25cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_item.png) 36 fill / 1.25cqw / 0 stretch;
  box-sizing: border-box;
}

.title-settings-row-icon {
  grid-column: 1;
  grid-row: 1;
  width: 100%;
  height: 100%;
}

.title-settings-row-icon img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.title-settings-row-label,
.title-settings-value,
.title-settings-speed-spaces,
.title-settings-toggle-space {
  font-weight: 700;
  color: #3d281b;
}

.title-settings-row-label {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  font-size: 1cqw;
  line-height: 1;
  letter-spacing: 0.035cqw;
  white-space: nowrap;
  color: #d2c8a8;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0;
}

.title-settings-value {
  grid-column: 3;
  grid-row: 1;
  font-size: 0.74cqw;
  line-height: 1;
  white-space: nowrap;
}

.title-settings-slider-space {
  grid-column: 3;
  grid-row: 1;
  align-self: center;
  width: 100%;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.4cqw;
  box-sizing: border-box;
}

.title-settings-language-row {
  grid-template-rows: 100%;
}

.title-settings-language-row .title-settings-row-label,
.title-settings-language-row .title-settings-value {
  grid-row: 1;
}

.title-settings-dropdown-space {
  min-width: 6.4cqw;
  padding: 0;
  border: 0;
  border-radius: 0;
  text-align: center;
  box-sizing: border-box;
}

.title-settings-speed-row,
.title-settings-toggle-row {
  grid-template-rows: 100%;
}

.title-settings-speed-row .title-settings-row-label,
.title-settings-toggle-row .title-settings-row-label {
  grid-row: 1;
}

.title-settings-speed-spaces {
  grid-column: 3;
  grid-row: 1;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.18cqw;
}

body #titleSettingsMenu .title-settings-speed-spaces span:not(.title-settings-speed-btn) {
  display: none;
}

.title-settings-toggle-space {
  grid-column: 3;
  grid-row: 1;
  min-width: 4.2cqw;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0;
  border: 0;
  border-radius: 0;
  font-size: 0.72cqw;
  line-height: 1;
  text-align: center;
  box-sizing: border-box;
}

.title-settings-footer {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 2cqw;
  padding: 0.1cqw 10cqw 0;
  box-sizing: border-box;
}

.title-settings-back,
.title-settings-apply {
  width: 100%;
  min-width: 0;
  height: 6cqw;
  margin: 0;
  padding: 0.35cqw 1cqw;
  display: grid;
  grid-template-columns: 20% 80%;
  align-items: center;
  border: 0.08cqw solid rgba(74, 47, 29, 0.62);
  border-radius: 1.9cqw;
  background: rgba(111, 75, 40, 0.16);
  color: #3d281b;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  font-weight: 700;
  font-size: 0.88cqw;
  letter-spacing: 0.08cqw;
  line-height: 1;
  text-align: center;
  box-sizing: border-box;
  border-style: solid;
  border-width: 1.5cqw;
  border-color: transparent;
  border-image: url(assets/ui_apply.png) 90 fill / 2.25cqw / 0 stretch;
  color: #d2c8a8;
  letter-spacing: 0.1cqw;
  font-weight: 800;
  font-size: 1.5cqw;
  text-align: center;
}

.title-settings-back {
  cursor: pointer;
  transition: transform 180ms ease-out, background-color 180ms ease-out;
  height: 6cqw;
  border-style: solid;
  border-width: 1.5cqw;
  border-color: transparent;
  border-image: url(assets/ui_back.png) 90 fill / 2.25cqw / 0 stretch;
  box-sizing: border-box;
  border-radius: 1.9cqw;
  color: #d2c8a8;
  letter-spacing: 0.1cqw;
  font-weight: 800;
  font-size: 1.5cqw;
  text-align: center;
}

.title-settings-back:hover {
  background: rgba(255, 217, 128, 0.52);
  transform: translateY(-0.12cqw);
}

.title-settings-back:focus-visible {
  outline: 0.18cqw solid #4a2f1d;
  outline-offset: 0.14cqw;
}

.title-settings-apply:hover {
  background: rgba(255, 217, 128, 0.52);
  transform: translateY(-0.12cqw);
}

.title-settings-footer-icon {
  width: 100%;
  height: 100%;
}

.title-settings-footer-icon img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* v0.6.098 functional settings controls (new rules) */

.title-settings-label-line-1,
.title-settings-label-line-2 {
  display: block;
  line-height: 1;
  white-space: nowrap;
}

.title-settings-slider {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  max-width: 100%;
  height: 0.55cqw;
  accent-color: #f4c35c;
  cursor: pointer;
}

.title-settings-value-label {
  display: inline-block;
  min-width: 3cqw;
  text-align: right;
  font-size: 1.25cqw;
  color: #d2c8a8;
}

.title-settings-select {
  width: 100%;
  max-width: 100%;
  padding: 0.25cqw 0.5cqw;
  background: rgba(20, 14, 10, 0.45);
  color: #d2c8a8;
  border: 0.08cqw solid rgba(210, 200, 168, 0.4);
  border-radius: 0.25cqw;
  font-family: inherit;
  font-size: 1.25cqw;
}

.title-settings-speed-btn {
  display: inline-block;
  min-width: 1.8cqw;
  padding: 0.25cqw 0.5cqw;
  margin: 0 0.1cqw;
  background: rgba(20, 14, 10, 0.45);
  color: #d2c8a8;
  border: 0.06cqw solid rgba(210, 200, 168, 0.4);
  border-radius: 0.25cqw;
  font-family: inherit;
  font-size: 1.25cqw;
  font-weight: 700;
  cursor: pointer;
}

.title-settings-speed-btn.is-selected {
  background: rgba(244, 195, 92, 0.25);
  color: #f4c35c;
  border-color: #f4c35c;
}

.title-settings-speed-btn:hover {
  background: rgba(244, 195, 92, 0.15);
  color: #f4c35c;
}

.title-settings-toggle-btn {
  display: inline-block;
  min-width: 2.4cqw;
  padding: 0.25cqw 0.5cqw;
  background: rgba(20, 14, 10, 0.45);
  color: #d2c8a8;
  border: 0.06cqw solid rgba(210, 200, 168, 0.4);
  border-radius: 0.25cqw;
  font-family: inherit;
  font-size: 1.25cqw;
  font-weight: 700;
  cursor: pointer;
}

.title-settings-toggle-btn.is-on {
  background: rgba(108, 168, 74, 0.25);
  color: #6ca84a;
  border-color: #6ca84a;
}

.title-settings-toggle-btn.is-off {
  background: rgba(74, 47, 29, 0.45);
  color: #5f4a37;
  border-color: rgba(74, 47, 29, 0.6);
}

.title-settings-toggle-btn:hover {
  background: rgba(244, 195, 92, 0.15);
  color: #f4c35c;
}

@media (prefers-reduced-motion: reduce) {
  .title-settings-menu,
  body #feedbackDialog {
    transition-duration: 1ms;
  }
}

/* v0.6.103: hide the Ko-fi floating button (the small chip in the
   bottom-left corner). The user opens the Ko-fi chat panel via the
   Support Developer button on the title scene, not by clicking the
   chip. The iframe content is still loaded so the desktop frame's
   donate-button is clickable programmatically. */
body #titleScreen .floatingchat-donate-button { display: none !important; }

/* v0.6.104: Ko-fi widget layout tuning. The parent stylesheet's
   rules below do NOT reach into the Ko-fi iframes (separate
   document contexts); the JS handler also injects equivalent
   rules into both iframes' contentDocuments. The parent rules
   remain as a defensive global layer. */
body .floating-chat-kofi-popup-iframe {
  /* v0.6.111: dropped `!important` from max-height + bottom.
     Per the file's own v0.6.104 comment block, the Ko-fi iframe
     lives in a separate document context so this parent-stylesheet
     rule is a defensive layer only — the actual sizing is driven
     by the JS-injected style inside the iframe's contentDocument.
     No JS-set inline style exists on the iframe element itself
     (the engine just appends iframe nodes to <body>), so cascade
     order alone is safe here. */
  max-height: min(calc(100vh - 32px), 614px);
  bottom: 16px;
}
body .floating-chat-kofi-popup-iframe-notice {
  display: none !important;
}
body .floatingchat-container-wrap {
  visibility: hidden !important;
}
body #poweredby {
  display: none !important;
}
body .floating-chat-kofi-popup-iframe-closer {
  /* v0.6.111: dropped `!important` from width/height. The
     .floating-chat-kofi-popup-iframe-closer element lives inside
     the Ko-fi iframe's contentDocument (separate document
     context), so this parent-stylesheet rule never matches a
     live element. The JS handler injects the equivalent rules
     into both iframes (game.js ~line 8309), which is what
     actually applies in the browser. The cascade here is only
     relevant for the QA regex scan. */
  width: 50px;
  height: 50px;
  font-size: 36px !important;
  border-radius: 32px !important;
  -webkit-border-radius: 32px !important;
  -moz-border-radius: 32px !important;
  -ms-border-radius: 32px !important;
  -o-border-radius: 32px !important;
}
body .floating-chat-kofi-popup-iframe-closer,
body .floating-chat-kofi-popup-iframe-closer:hover,
body .floating-chat-kofi-popup-iframe-closer:focus {
  /* v0.6.111: dropped `!important` from width/height — see note above. */
  width: 50px;
  height: 50px;
  font-size: 36px !important;
  border-radius: 32px !important;
  -webkit-border-radius: 32px !important;
  -moz-border-radius: 32px !important;
  -ms-border-radius: 32px !important;
  -o-border-radius: 32px !important;
}

/* ===========================[v0.6.103-native-player-feedback]================
   Native player feedback dialog and global corner launcher.

   Architecture:
     .feedback-stage  — top-level fixed wrapper that emulates the fitted 16:9
                         stage. Establishes `container-type: inline-size;
                         container-name: feedback` so descendant `cqw` units
                         match the per-stage `cqw` used by every other
                         v0.6.x overlay. The wrapper is positioned fixed at
                         the viewport center; the dialog and launcher live
                         inside it so they overlay every scene without
                         inheriting scene `overflow` clips.

     #globalFeedbackLauncher — single 56x56 cqw rounded button in the top
                         right of the fitted stage (z-index 9999999). The
                         single literal .title-control-icon-feedback <img>
                         is physically reparented between this launcher and
                         the title button by feedback.js — one icon only.

     #feedbackDialog  — same exact Settings menu frame/border image, with
                         the Settings title banner geometry and the Settings
                         footer button chrome (ui_back.png / ui_apply.png
                         with the existing interior icons). Click-through
                         to the underlying scene is blocked while open.
   =========================================================================== */

.feedback-stage {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(100vw, calc(100vh * 16 / 9));
  height: min(100vh, calc(100vw * 9 / 16));
  max-width: 100vw;
  max-height: 100vh;
  pointer-events: none;
  z-index: 9999998; /* launcher is 9999999 inside the stage */
  container-type: inline-size;
  container-name: stage;
  background: transparent;
}

.feedback-stage > * {
  pointer-events: auto;
}

/* v0.6.124.9: the global corner feedback launcher has been retired in
   favor of the burger menu (which exposes Feedback from every scene).
   The launcher element is kept in the DOM (feedback.js's
   state.launcher still references it so legacy JS wiring doesn't
   break) but is permanently hidden via display:none. The
   styles below were originally sizing the launcher as a 56×56
   top-right button — they're now visual no-ops because the element
   is always hidden. We keep the rules intact so legacy CSS that
   targets the launcher (e.g. `.feedback-stage > .global-feedback-launcher`
   padding overrides) still parse cleanly without producing unknown
   selector warnings. The element is not user-visible on any scene. */
.global-feedback-launcher {
  display: none !important;
}

.global-feedback-launcher:hover,
.global-feedback-launcher:focus-visible {
  background: rgba(244, 195, 92, 0.95);
  color: #2b1e1c;
  transform: translateY(max(-0.12cqw, -1.536px));
  outline: none;
}

.global-feedback-launcher:focus-visible {
  box-shadow: 0 0 0 min(0.18cqw, 2.304px) #2b1e1c, 0 min(0.4cqw, 5.12px) min(1.2cqw, 15.36px) rgba(0, 0, 0, 0.5);
}

.global-feedback-launcher[hidden] {
  display: none;
}

.global-feedback-launcher > img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Subtle text label inside the launcher beside the icon (only when icon
   is missing — kept for accessibility / no-image fallbacks). */
.global-feedback-launcher > .global-feedback-launcher-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ===== Feedback modal ===================================================== */

.feedback-dialog-backdrop {
  position: fixed;
  inset: 0;
  z-index: 10000000;
  pointer-events: auto;
}

.feedback-dialog-backdrop[hidden] {
  display: none;
}

body #feedbackDialog {
  position: absolute;
  left: 18.5%;
  top: 20.5%;
  width: 63%;
  height: 74%;
  margin: 0;
  padding: 0;
  border-style: solid;
  border-width: 1cqw 2.5cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_menu.png) 256 fill / 7.5cqw / 0 stretch;
  box-sizing: border-box;
  color: #3d281b;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  transform: translateY(115%);
  transition: transform 500ms cubic-bezier(0.22, 0.72, 0.2, 1);
  z-index: 10000001;
  overflow: visible;
  background: transparent;
  pointer-events: auto;
}

body #feedbackDialog[hidden] {
  display: none !important;
}

body #feedbackDialog.is-open {
  transform: translateY(0);
}

/* v0.6.103 fix: the previous version had no explicit grid-template-columns
 * and two children (status + footer) sharing grid-row 6, which made CSS Grid
 * auto-flow them into implicit columns and shrink the layout to 474cqw.
 * The fix pins a single 100% column so every child spans the full inner
 * width (742cqw), and reorganizes the rows so status and footer get their
 * own rows instead of overlapping. */
.feedback-dialog-content {
  width: 58cqw;
  /* v0.6.103 fix: was 44.5cqw which overflowed the dialog's inner area
   * (507cqw) and pushed the footer bottom to y=740 (> viewport 720).
   * The Settings content keeps 44.5cqw but Settings has fewer rows so
   * its content collapses into the inner area; feedback has a textarea,
   * status, and footer which together exceed the inner area. We keep
   * max-height: 44.5cqw as the original spec value, but constrain the
   * default height to fit the fitted 1280x720 stage with all controls
   * visible. The textarea uses minmax(7cqw, 1fr) so any slack above 38cqw
   * extends the textarea, keeping the dialog usable on taller viewports
   * while guaranteeing footer.bottom ≤ fitted-stage.bottom at 1280x720. */
  height: 43.5cqw;
  max-height: 43.5cqw;
  min-width: 0;
  min-height: 0;
  display: grid;
  /* Single explicit column. Children default to grid-column: 1, so status
   * and footer in row 6 no longer get pushed into an implicit second
   * column. */
  grid-template-columns: 100%;
  grid-template-rows:
    auto   /* 1: banner */
    auto   /* 2: intro */
    auto   /* 3: category row */
    minmax(7cqw, 1fr)  /* 4: textarea */
    auto   /* 5: meta */
    auto   /* 6: status */
    auto;  /* 7: footer */
  align-items: stretch;
  justify-items: stretch;
  column-gap: 0;
  row-gap: 0.4cqw;
  box-sizing: border-box;
  padding: 0;
}
.feedback-dialog-content > * {
  /* Lock every child to the single explicit column so two items in the
   * same row stack vertically (grid-auto-flow: column-wise via row spans)
   * rather than auto-flow into a sibling implicit column. */
  grid-column: 1;
}

.feedback-dialog-banner {
  border-style: solid;
  border-width: 2.25cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_banner.png) 80 fill / 2.25cqw / 0 stretch;
  box-sizing: border-box;
  color: #f4c35c;
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  font-size: 2.5cqw;
  line-height: 1;
  letter-spacing: 0.14cqw;
  text-align: center;
  text-shadow: 0.5cqw 0.5cqw 0 #281e14,
               0.5cqw -0.5cqw 0 #281e14,
               -0.5cqw 0.5cqw 0 #281e14,
               -0.5cqw -0.5cqw 0 #281e14,
               0.5cqw 0 0 #281e14,
               0 0.5cqw 0 #281e14,
               -0.5cqw 0 0 #281e14,
               0 -0.5cqw 0 #281e14;
  width: 36cqw;
  height: 7cqw;
  min-height: 0;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  grid-row: 1;
}

.feedback-dialog-intro {
  grid-row: 2;
  margin: 0;
  align-self: center;
  text-align: center;
  color: #d2c8a8;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  font-size: 1cqw;
  letter-spacing: 0.05cqw;
  line-height: 1.2;
  text-shadow: 0.1cqw 0.1cqw 0 #281e14,
               0.1cqw -0.1cqw 0 #281e14,
               -0.1cqw 0.1cqw 0 #281e14,
               -0.1cqw -0.1cqw 0 #281e14;
}

.feedback-dialog-category-row {
  grid-row: 3;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: 0.4cqw;
  align-items: stretch;
  min-width: 0;
  padding: 0 1.5cqw;
  box-sizing: border-box;
}

.feedback-dialog-category-btn {
  display: grid;
  grid-template-rows: 60% 40%;
  align-items: center;
  justify-items: center;
  border-style: solid;
  border-width: 0.9cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_item.png) 36 fill / 0.9cqw / 0 stretch;
  box-sizing: border-box;
  background: transparent;
  cursor: pointer;
  color: #d2c8a8;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  font-weight: 700;
  font-size: 0.7cqw;
  letter-spacing: 0.05cqw;
  text-align: center;
  padding: 0.3cqw 0.2cqw;
  border-radius: 0.75cqw;
  transition: background-color 160ms ease-out, transform 160ms ease-out;
}

.feedback-dialog-category-btn:hover,
.feedback-dialog-category-btn:focus-visible {
  background: rgba(255, 217, 128, 0.32);
  outline: none;
  transform: translateY(-0.08cqw);
}

.feedback-dialog-category-btn:focus-visible {
  box-shadow: 0 0 0 0.14cqw #4a2f1d inset;
}

.feedback-dialog-category-btn[aria-pressed="true"] {
  background: rgba(255, 217, 128, 0.8);
  color: #f4c35c;
  box-shadow: 0 0 0 0.14cqw #4a2f1d inset;
}

.feedback-dialog-category-emoji {
  font-size: 1.6cqw;
  line-height: 1;
}

.feedback-dialog-category-label {
  /* v0.6.103 fix: was 0.78cqw + nowrap + ellipsis. The labels
   * "BALANCE / GAMEPLAY" (≈115cqw-px) and "IDEA / SUGGESTION" (≈101cqw-px)
   * overflowed the button width (~105cqw-px) under the broken 2-column
   * grid. With the single-column fix the buttons now have ~155cqw-px
   * each, but we still tighten the font and allow wrap to guarantee the
   * full labels fit at any 16:9 viewport >= 1024x576. */
  font-size: 0.8cqw;
  line-height: 0.8;
  font-weight: 100;
  font-family: 'BlackOpsOne', sans-serif;
  letter-spacing: 0.02cqw;
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: center;
  /* v0.6.103 fix: no more ellipsis — spec allows wrapping and forbids
   * ellipsis/clipping of the four category labels. */
  max-width: 100%;
  width: 100%;
  /* v0.6.103 fix: button cell is 9px clientH; line-height 1.1 (≈9.2px)
   * leaves room for ascender+descender without overflow (scrollH <=
   * clientH). The outer .feedback-dialog-category-btn uses 60%/40% rows
   * so the label row has enough height. */
  overflow: hidden;
}

.feedback-dialog-message-label {
  grid-row: 4;
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.feedback-dialog-message {
  grid-row: 4;
  width: calc(100% - 3cqw);
  min-height: 0;
  margin: 0 1.5cqw;
  padding: 0.6cqw 0.8cqw;
  box-sizing: border-box;
  resize: vertical;
  border-style: solid;
  border-width: 0.9cqw;
  border-color: transparent;
  border-image: url(assets/ui_settings_item.png) 36 fill / 0.9cqw / 0 stretch;
  background: rgba(255, 248, 224, 0.1);
  color: #d2c8a8;
  border-radius: 0.75cqw;
  font-family: 'SplineSans', system-ui, sans-serif;
  font-size: 0.95cqw;
  line-height: 1.3;
}

/* v0.6.107: the legacy .feedback-dialog-message:focus + :focus-visible
   shared rule below forces an opaque white background and an inset dark
   focus shadow that would make the new translucent background + light
   text unreadable. Override both selectors to the requested translucent
   appearance + warm glow. outline: none is preserved from the legacy
   rule so the new focus-visible shadow remains the visible keyboard
   focus indicator without an outer outline. */
.feedback-dialog-message:focus,
.feedback-dialog-message:focus-visible {
  outline: none;
  background: rgba(255, 248, 224, 0.1);
  color: #d2c8a8;
  box-shadow: 0 0.18cqw 0.8cqw rgba(244, 195, 92, 0.5);
}

.feedback-dialog-message-meta {
  grid-row: 5;
  margin: 0;
  text-align: right;
  padding: 0 1.5cqw;
  font-size: 0.7cqw;
  color: #d2c8a8;
  letter-spacing: 0.04cqw;
  align-self: center;
}

.feedback-dialog-status {
  /* v0.6.103 fix: row 6 (was 6 sharing with footer, causing implicit col 2) */
  grid-row: 6;
  align-self: start;
  margin: -1.5cqw 0 1.5cqw;
  font-size: 0.85cqw;
  line-height: 1.2;
  color: #d2c8a8;
  letter-spacing: 0.04cqw;
  text-align: center;
  min-height: 0;
  text-shadow: 0.1cqw 0.1cqw 0 #281e14,
               0.1cqw -0.1cqw 0 #281e14,
               -0.1cqw 0.1cqw 0 #281e14,
               -0.1cqw -0.1cqw 0 #281e14;
}

.feedback-dialog-status.is-error {
  color: #f4a35c;
}

.feedback-dialog-status.is-success {
  color: #b8e986;
  text-shadow: 0.15cqw 0.15cqw 0 #281e14;
}

.feedback-dialog-footer {
  /* v0.6.103 fix: row 7 (was 6 sharing with status, causing implicit col 2) */
  grid-row: 7;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-self: stretch;
  align-items: center;
  gap: 2cqw;
  /* v0.6.103 fix: padding-side symmetric and reduced. The previous
   * `padding: 0.4cqw 4cqw 0` plus `top: -1.6cqw` were hacks to compensate
   * for the broken 2-column grid; with the single-column layout the
   * footer now stretches to full inner width and centers cleanly. */
  padding: 0.2cqw 10cqw 0;
  box-sizing: border-box;
  width: 100%;
}

.feedback-dialog-btn {
  width: 100%;
  min-width: 0;
  height: 6cqw;
  margin: 0;
  padding: 0.35cqw 1cqw;
  display: grid;
  grid-template-columns: 20% 80%;
  grid-template-rows: 100%;
  align-items: center;
  border: 0.08cqw solid rgba(74, 47, 29, 0.62);
  border-radius: 1.9cqw;
  background: rgba(111, 75, 40, 0.16);
  color: #d2c8a8;
  font-family: 'SplineSans-Bold', 'SplineSans', system-ui, sans-serif;
  font-weight: 800;
  /* v0.6.103 fix: was 1.5cqw which produced a 19.2px font whose glyph
   * ascender+descender rendered at ~21px, 1-2px taller than the button's
   * 19px-tall inner cell (scrollH=21 vs clientH=20). 1.2cqw = 15.36px
   * font with line-height 1.18 gives an 18.13px line-box, which when
   * the inner cell is flex-centered places the rendered glyph (~17-18px)
   * fully inside the cell so scrollHeight <= clientHeight. */
  font-size: 1.2cqw;
  letter-spacing: 0.06cqw;
  line-height: 1.18;
  text-align: center;
  box-sizing: border-box;
  border-style: solid;
  border-width: 1.5cqw;
  border-color: transparent;
  cursor: pointer;
  transition: transform 180ms ease-out, background-color 180ms ease-out;
  overflow: hidden;
  }

.feedback-dialog-btn-cancel {
  border-image: url(assets/ui_back.png) 90 fill / 2.25cqw / 0 stretch;
}

.feedback-dialog-btn-send {
  border-image: url(assets/ui_apply.png) 90 fill / 2.25cqw / 0 stretch;
}

.feedback-dialog-btn:hover,
.feedback-dialog-btn:focus-visible {
  background: rgba(255, 217, 128, 0.52);
  transform: translateY(-0.12cqw);
  outline: none;
}

.feedback-dialog-btn:focus-visible {
  box-shadow: 0 0 0 0.18cqw #4a2f1d;
}

.feedback-dialog-btn:disabled,
.feedback-dialog-btn[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.6;
  transform: none;
}

.feedback-dialog-btn-icon {
  width: 100%;
  /* v0.6.103 fix: pin the icon cell to the button's interior height so
   * the inner <img> can't force the grid row taller than the button.
   * The inner box height is button.h(6cqw) - 2*border(1.5cqw) -
   * 2*padding-y(0.35cqw) = 2.8cqw ≈ 36px at 12.8px/cqw; we use min(2.8cqw, 100%) so
   * the icon stays bounded on shorter viewports too. */
  height: min(2.8cqw, 100%);
  max-width: 100%;
  max-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.feedback-dialog-btn-icon img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  pointer-events: none;
}

/* ===========================[v0.6.108-title-scene-transitions]================
   Title scene entrance/exit/reveal orchestration.
   - Entrance (720 ms, cubic-bezier(0.22, 1, 0.36, 1)): title flies in from
     above, subtitle and all five menu buttons fly in from below. Icons ride
     inside the support/feedback buttons (no extra movement of their own).
   - Exit (800 ms, same easing): title + subtitle fly above view; all five
     menu button shells fly below view; the literal support/feedback <img>
     nodes are reparented into the transition icon layer (preserving their
     on-screen starting rectangles) and eased to the fitted-stage top-left /
     top-right launcher content rectangles.
   - Blackout (800 ms, ease-in-out): a pure-black curtain (#000) fades from
     opacity 0 to opacity 1 above ordinary title/depot content but below
     the transition icon layer.
   - Opaque handoff + reveal (600 ms, ease-in-out): newRun() is invoked
     once the curtain is fully opaque; the depot activates behind it;
     curtain fades from opacity 1 to opacity 0 revealing the depot; then
     icons are reparented into the global Feedback + Support launchers
     (top-left Support, top-right Feedback) and the layer is removed.
   - Reduced motion: max 20 ms effective travel/fade; same ordering.

   The literal supplied title_text.png is never touched; all motion lives
   on the wrappers / class toggles. Existing v0.6.107 v0.6.106 v0.6.105
   mug badge, note cluster, title-emblem stacking, and 16:9 fitted-stage
   geometry are preserved exactly.
   =========================================================================== */

/* ----- transition timing constants (mirrored in game.js for QA) ----- */
:root {
  --gg-title-entrance-ms: 720ms;
  --gg-title-exit-ms: 800ms;
  --gg-title-reveal-ms: 600ms;
  --gg-title-fly-easing: cubic-bezier(0.22, 1, 0.36, 1);
  --gg-title-blackout-easing: ease-in-out;
}

/* ---- prep state — initial render. Applied to #titleScreen while JS is
   preparing the entrance flight so the elements never visibly flash at
   their settled geometry. JS removes this class on the first frame of
   the entrance transition. */
#titleScreen.title-fly-prep .title-emblem {
  transform: translateY(max(-61.875cqw, -792px));
}
#titleScreen.title-fly-prep .title-subtitle,
#titleScreen.title-fly-prep .title-control {
  transform: translateY(min(61.875cqw, 792px));
}

/* ---- entrance: title from above, subtitle+menus from below, 720 ms.
   The entrance animation is the only phase that touches .title-control
   hover transforms at the end: JS removes the title-fly-entering
   class when the transition ends so normal hover translateY(-0.2cqw)
   resume unchanged. */
#titleScreen.title-fly-entering .title-emblem {
  transform: translateY(max(-61.875cqw, -792px));
  transition: transform var(--gg-title-entrance-ms) var(--gg-title-fly-easing);
  will-change: transform;
}
#titleScreen.title-fly-entering .title-subtitle,
#titleScreen.title-fly-entering .title-control {
  transform: translateY(min(61.875cqw, 792px));
  transition: transform var(--gg-title-entrance-ms) var(--gg-title-fly-easing);
  will-change: transform;
}
#titleScreen.title-fly-entering.title-fly-settle .title-emblem,
#titleScreen.title-fly-entering.title-fly-settle .title-subtitle,
#titleScreen.title-fly-entering.title-fly-settle .title-control {
  transform: none;
}
/* During entrance, disable hover lift so the in-flight transforms are
   not overwritten by transient pointer-over transforms. JS removes
   .title-no-hover on completion. */
#titleScreen.title-fly-entering .title-control {
  pointer-events: none;
}
#titleScreen.title-fly-entering.title-fly-settle .title-control {
  pointer-events: auto;
}

/* ---- exit: title+subtitle up, 5 menu buttons down, icons travel to
   opposite fitted-stage corners. 800 ms total. Exit disables hover
   transforms via inline pointer-events:none on the buttons themselves
   (set by JS via class) so the in-flight transforms aren't overwritten
   by a stray pointer-over. */
#titleScreen.title-fly-exiting .title-emblem,
#titleScreen.title-fly-exiting .title-subtitle {
  transform: translateY(max(-61.875cqw, -792px));
  transition: transform var(--gg-title-exit-ms) var(--gg-title-fly-easing);
  will-change: transform;
}
#titleScreen.title-fly-exiting .title-control {
  transform: translateY(min(61.875cqw, 792px));
  transition: transform var(--gg-title-exit-ms) var(--gg-title-fly-easing);
  will-change: transform;
  pointer-events: none;
}

/* When titleScreen is hidden (depot active), keep the wrappers at their
   settled positions so a future entrance flight has a deterministic
   source/target. JS also resets transform: none on every wrapper when
   the transition layer is released. */

/* ===== Transition overlay layer ===== */
.title-transition-layer {
  position: fixed;
  inset: 0;
  z-index: 10000005;  /* below feedbackStage (9999998) wrappers + dialog (10000001) */
  pointer-events: none;
  display: block;
  container-type: inline-size;
  container-name: stage;
}
.title-transition-layer[hidden] {
  display: none !important;
}

/* Blackout curtain: above ordinary title/depot content but below the
   transition icon nodes. Pure #000 with a 0→1→0 opacity timeline. */
.title-transition-curtain {
  position: absolute;
  inset: 0;
  background-color: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--gg-title-exit-ms) var(--gg-title-blackout-easing);
  z-index: 1;
}
.title-transition-curtain.curtain-fade-in {
  opacity: 1;
}
.title-transition-curtain.curtain-fade-out {
  opacity: 0;
  transition-duration: var(--gg-title-reveal-ms);
}
.title-transition-curtain[hidden] {
  display: none !important;
}

/* The transition icon nodes — the literal <img src=ui_icon_support.png>
   and <img src=ui_icon_feedback.png> — are reparented into this layer
   while flying. They sit above the curtain (z 2) so they remain visible
   while everything else fades to black.

   v0.6.108 correction 1: position AND size are animated together via
   left/top/width/height (no transform). The base 4cqw size rule was
   removed so JS-set inline left/top/width/height can drive the box
   from the source BCR (in the title button) to the measured launcher
   content rectangle over the frozen exit duration. Max-width/height
   caps were also removed; the launcher content box itself enforces
   the size cap. The transition list matches the spec fly easing. */
.title-transition-icon {
  position: absolute;
  display: block;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
  z-index: 2;
  object-fit: contain;
  /* v0.6.124.11: opacity 1→0 rides the same duration as the
     curtain's fade-in (TITLE_EXIT_MS, default 800ms). The class
     .title-transition-icon-fading is added at parkIcon() time so the
     icon's opacity begins decaying from the moment the title starts
     to exit. The fade out completes while the curtain is still
     blacking out, so by the time the curtain starts fading back in
     (revealing the depot), the icons are at 0 and the user never
     sees them "fly over" to the depot. */
  opacity: 1;
  transition: left var(--gg-title-exit-ms) var(--gg-title-fly-easing),
              top var(--gg-title-exit-ms) var(--gg-title-fly-easing),
              width var(--gg-title-exit-ms) var(--gg-title-fly-easing),
              height var(--gg-title-exit-ms) var(--gg-title-fly-easing),
              opacity var(--gg-title-exit-ms) var(--gg-title-fly-easing);
  will-change: left, top, width, height, opacity;
}
.title-transition-icon.title-transition-icon-fading {
  opacity: 0;
}
.title-transition-icon.icon-flight-done {
  /* v0.6.111: dropped `!important` from transition. Transition is
     not in the spec's forbidden layout-property list — keep
     `!important` here per the spec's layout-property list (width,
     height, aspect-ratio, transform, position, top, left). The
     `transform: none` rule below is the only transform rule on this
     element, so the `!important` is also dropped: cascade alone
     wins, no inline JS sets a transform. */
  transition: none !important;
  transform: none;
}

/* ===== Global Support launcher (top-left, mirrors Feedback chrome) =====
   Sits adjacent to #globalFeedbackLauncher inside .feedback-stage.
   Hidden on title (icon is in the title Support button); visible on
   non-title scenes after transition completion. Same launcher chrome
   language: dark backdrop, rounded corners, gold accent on hover. */
.global-support-launcher {
  position: absolute;
  top: 0.6cqw;
  left: 1.2cqw;
  width: 3.6cqw;
  height: 3.6cqw;
  min-width: 0;
  min-height: 0;
  margin: 0;
  padding: 0.5cqw 0.2cqw 0.5cqw 0.5cqw;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(74, 47, 29, 0.72);
  border-radius: min(1.2cqw, 15.36px);
  box-shadow: 0 min(0.4cqw, 5.12px) min(1.2cqw, 15.36px) rgba(0, 0, 0, 0.5);
  cursor: pointer;
  z-index: 9999999;
  color: #f4c35c;
  font-family: 'BlackOpsOne', 'SplineSans-Bold', system-ui, sans-serif;
  font-weight: 400;
  transition: transform 160ms ease-out, background-color 160ms ease-out;
  border: 0;
}
.global-support-launcher:hover,
.global-support-launcher:focus-visible {
  background: rgba(244, 195, 92, 0.95);
  color: #2b1e1c;
  transform: translateY(max(-0.12cqw, -1.536px));
  outline: none;
}
.global-support-launcher:focus-visible {
  box-shadow: 0 0 0 min(0.18cqw, 2.304px) #2b1e1c, 0 min(0.4cqw, 5.12px) min(1.2cqw, 15.36px) rgba(0, 0, 0, 0.5);
}
.global-support-launcher[hidden] {
  display: none;
}
.global-support-launcher > img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* v0.6.110: preserve the exact v0.6.109 launcher source contract above,
   then cap only its rendered stage-relative geometry. The :is() selector
   deliberately leaves the inherited exact-rule oracle untouched. */
.feedback-stage > :is(.global-feedback-launcher, .global-support-launcher) {
  top: min(0.6cqw, 7.68px);
  width: min(3.6cqw, 46.08px);
  height: min(3.6cqw, 46.08px);
}
.feedback-stage > .global-feedback-launcher {
  right: min(1.2cqw, 15.36px);
  padding: min(0.5cqw, 6.4px);
}
.feedback-stage > .global-support-launcher {
  left: min(1.2cqw, 15.36px);
  padding: min(0.5cqw, 6.4px) min(0.2cqw, 2.56px) min(0.5cqw, 6.4px) min(0.5cqw, 6.4px);
}

/* ===== Reduced motion =====
   Honor prefers-reduced-motion: reduce. The flight/fade durations
   collapse to a 1ms tick; visibility still progresses through the
   same phases so opaque handoff still gates newRun(). Scene order is
   preserved. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --gg-title-entrance-ms: 1ms;
    --gg-title-exit-ms: 1ms;
    --gg-title-reveal-ms: 1ms;
  }
  #titleScreen.title-fly-prep .title-emblem,
  #titleScreen.title-fly-prep .title-subtitle,
  #titleScreen.title-fly-prep .title-control {
    transform: none;
  }
  .title-transition-curtain,
  .title-transition-icon {
    transition-duration: 1ms !important;
  }
}


/* ===========================[v06122-closure.css]=========================== */
/* v0.6.122 — Depot→battle march + entry march. The player's squad
   gerbils in the depot (only the .sc-portrait img, NOT the stats,
   items, or surrounding card) translate right ~3vw over 0.55s with a
   small bob, starting just before the title curtain fades to black.
   On the battle scene, the player-side cells translate in from
   -3vw and the enemy-side cells from +3vw, both over 0.6s with the
   same bob, and only when both have settled does the v0.6 tick
   scheduler start counting. Implementation: these are CSS
   keyframe animations; the JS hooks add a short-lived class to the
   depot squad area and to the v0.6 battle screen, then remove it
   after `transitionend`/`animationend`. No forbidden
   layout-property `!important` declarations; the reduced-motion
   fallback uses the same selector specificity as the active class
   and just sets `animation-duration: 1ms;` so the destination
   state lands at frame 1. */

@keyframes v06122-march-right {
    /* v0.6.122.1: the base style sets `transform: scale(1.5)` on every
       squad .sc-portrait img (line 1725 below). The original v0.6.122
       keyframes overrode that with a pure translate3d, so during the
       march the gerbils visually shrank from 1.5x to 1.0x scale. Each
       step now includes scale(1.5) so the gerbils stay at the
       canonical P1/P5 size while hobbling to the right.
       v0.6.124.7: march distance doubled (3vw → 6vw) so the user can
       clearly read the gerbils walking off the depot row on mobile
       (small viewports). The intermediate offsets are scaled by the
       same 2× factor to keep the bob proportional to the total distance. */
    0%   { transform: translate3d(0,     0,     0) scale(1.5); }
    20%  { transform: translate3d(1.0vw, -0.36vw, 0) scale(1.5); }
    40%  { transform: translate3d(2.4vw,  0.24vw, 0) scale(1.5); }
    60%  { transform: translate3d(3.6vw, -0.12vw, 0) scale(1.5); }
    80%  { transform: translate3d(4.8vw,  0.12vw, 0) scale(1.5); }
    100% { transform: translate3d(6vw,    0,     0) scale(1.5); }
  }
  @keyframes v06122-march-from-left {
    /* v0.6.122.1: same fix for the battle-scene player-side cells.
       v0.6.124.7: march distance doubled (-3vw → -6vw). */
    0%   { transform: translate3d(-6vw,    0,     0) scale(1); }
    20%  { transform: translate3d(-5.0vw, -0.24vw, 0) scale(1); }
    40%  { transform: translate3d(-3.6vw,  0.20vw, 0) scale(1); }
    60%  { transform: translate3d(-2.2vw, -0.12vw, 0) scale(1); }
    80%  { transform: translate3d(-1.0vw,  0.08vw, 0) scale(1); }
    100% { transform: translate3d(0,      0,     0) scale(1); }
  }
  @keyframes v06122-march-from-right {
    /* v0.6.122.1: enemy-side cells march in from +3vw at identity
       scale (mirror of march-from-left).
       v0.6.124.7: march distance doubled (+3vw → +6vw). */
    0%   { transform: translate3d(6vw,     0,     0) scale(1); }
    20%  { transform: translate3d(5.0vw, -0.24vw, 0) scale(1); }
    40%  { transform: translate3d(3.6vw,  0.20vw, 0) scale(1); }
    60%  { transform: translate3d(2.2vw, -0.12vw, 0) scale(1); }
    80%  { transform: translate3d(1.0vw,  0.08vw, 0) scale(1); }
    100% { transform: translate3d(0,      0,     0) scale(1); }
  }
.depot-marching .squad-card .sc-portrait img {
  animation: v06122-march-right 0.55s ease-in-out forwards;
}
.v060-battle-marching .v060-region-battle.v060-region-player-side {
  animation: v06122-march-from-left 0.6s ease-out forwards;
}
.v060-battle-marching .v060-region-battle.v060-region-enemy-side {
  animation: v06122-march-from-right 0.6s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
  .depot-marching .squad-card .sc-portrait img,
  .v060-battle-marching .v060-region-battle.v060-region-player-side,
  .v060-battle-marching .v060-region-battle.v060-region-enemy-side {
    animation-duration: 1ms;
  }
}

/* ===========================[v06123-closure.css]=========================== */
/* v0.6.123 — Mobile / touch drag fix. The new
   game-v060123-touch-drag.js closure synthesizes HTML5 drag events
   from the Pointer Events sequence on touch devices so the existing
   v0.6.006+ depot drag pipeline (which only listens to native
   dragstart/dragover/drop) works on phones and tablets. The CSS
   below is a small belt-and-suspenders: it (a) suppresses the iOS /
   Android system callout / context menu on the same draggable
   surfaces, and (b) tells the browser not to treat touch gestures
   over those surfaces as scroll / zoom / context-menu intents.
   The rule is scoped to the same draggable selectors the JS
   closure targets so we never disable touch scroll elsewhere on
   the page. The v0.6.121.1 `* { user-select: none; }` rule above
   already blocks copy / select; the rules here complete the picture
   for touch. */
/* v0.6.124.3: iOS callout suppression extended to ALL draggable
   cards, but only on the IMG (not the card). The previous v0.6.124.2
   rule applied `-webkit-user-drag: none` to the `[data-drag-kind]`
   card itself, which Chromium interprets as "block the native
   HTML5 drag" — so the squad-card, shop-card, freeze-card and
   equipped-item card would no longer fire `dragstart` on desktop
   mouse drag. The CSS now sets touch-action / -webkit-touch-callout
   ONLY on the IMG. The card keeps its `draggable=true` behavior so
   desktop drag works; mobile drag still works because the
   touch-drag closure listens on the document for touchstart /
   pointerdown and prevents the OS callout. */
[data-drag-kind] img {
  -webkit-touch-callout: none;   /* iOS Safari long-press callout */
  -webkit-user-drag: none;       /* iOS Safari image-drag */
  touch-action: none;             /* browser must not steal the gesture */
}
[data-drag-kind="equipped-item"] img {
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  touch-action: none;
}
@media (prefers-reduced-motion: reduce) {
  [data-drag-kind] img,
  [data-drag-kind="equipped-item"] img {
    -webkit-touch-callout: none;
  }
}

/* ============================[v06124-burger.css]============================= */
/* v0.6.124.7: top-right burger menu replaces the standalone feedback
   button that sat at 57.75%,77.5% (which the user reported as "do not
   move the feedback button to the top right corner anymore"). The burger
   replaces that slot and is visible across every scene.
   v0.6.124.8: lifted out of #titleScreen so it stays visible across
   every scene (depot, battle, summary, end). The icon background is
   transparent per the user's request — the three-line glyph is the
   only visible chrome. The dropdown still uses a dark surface so the
   menu items are legible when the scene underneath is light. */
.title-burger-menu {
  position: absolute;
  top: 1.5%;
  right: 1.5%;
  width: min(5cqw, 44px);
  min-width: 28px;
  height: min(5cqw, 44px);
  min-height: 28px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: min(0.5cqw, 6.4px);
  padding: min(0.9cqw, 11.52px) min(0.9cqw, 11.52px);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  z-index: 95;
  color: #f4c35c;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
  transition: color 160ms ease-out, transform 160ms ease-out;
  pointer-events: auto;
}
.title-burger-menu:hover,
.title-burger-menu:focus-visible {
  color: #ffe7a3;
  transform: scale(1.06);
  outline: 0;
}
/* v0.6.124.8: hide the burger only while a modal is open (legal notice,
   feedback dialog, settings menu). The burger stays visible while the
   game runs, regardless of scene. */
.legal-notice-dialog:not([hidden]) ~ * .title-burger-menu,
.feedback-dialog:not([hidden]) ~ * .title-burger-menu,
.title-settings-menu:not([hidden]) ~ * .title-burger-menu {
  /* No global modal-blur effect for the burger; we just dim the
     background via opacity. The user can still tap it to dismiss
     the modal — that's the same shortcut ESC handles. */
  z-index: 250;
}
/* v0.6.125: hide the burger while the title scene is the active
   visible scene. The title scene has its own bottom-left "Legal
   Notice" shortcut, so the burger is redundant there. The burger
   reappears on every other scene (depot, battle, summary, end).
   The title-active flag is a body class so the selector can reach
   across the DOM (the burger lives in #feedbackStage which is NOT
   a sibling of #titleScreen). */
body.title-active .title-burger-menu {
  display: none !important;
}
/* v0.6.125: while ANY of the three modals owns the dismissal surface,
   the burger icon is hidden — the modal's X / Close / Escape / backdrop
   are the only dismissal paths. When the last modal closes, the burger
   returns. */
.title-burger-menu.burger-suppressed {
  display: none !important;
}
.title-burger-line {
  display: block;
  height: min(0.45cqw, 5.12px);
  background: currentColor;
  border-radius: min(0.25cqw, 2.56px);
  transition: background-color 160ms ease-out, transform 200ms cubic-bezier(0.22, 0.72, 0.2, 1);
  transform-origin: center;
}
/* v0.6.125: when the burger is open, morph the three-line glyph into an
   ×. The first line rotates +45°, the third rotates -45°, and the middle
   line fades to 0 so the × reads cleanly. transform-origin is the
   horizontal centre of the icon stack. */
.title-burger-menu.is-x .title-burger-line:nth-child(1) {
  transform: translateY(calc(min(0.95cqw, 12.16px))) rotate(45deg);
}
.title-burger-menu.is-x .title-burger-line:nth-child(2) {
  opacity: 0;
  transform: scaleX(0);
}
.title-burger-menu.is-x .title-burger-line:nth-child(3) {
  transform: translateY(calc(-1 * min(0.95cqw, 12.16px))) rotate(-45deg);
}
.title-burger-dropdown {
  position: absolute;
  top: calc(min(5cqw, 44px) + 1.5% + min(0.6cqw, 7.68px));
  right: 1.5%;
  min-width: min(20cqw, 240px);
  margin: 0;
  padding: min(0.5cqw, 6.4px) 0;
  list-style: none;
  background: rgba(43, 30, 28, 0.97);
  border: min(0.18cqw, 2.304px) solid #5f4a37;
  border-radius: min(0.4cqw, 5.12px);
  box-shadow: 0 min(0.6cqw, 7.68px) min(1.5cqw, 19.2px) rgba(0, 0, 0, 0.6);
  z-index: 96;
}
.title-burger-dropdown[hidden] { display: none; }
.title-burger-dropdown li {
  margin: 0;
  padding: 0;
}
.title-burger-item {
  display: block;
  width: 100%;
  padding: min(0.7cqw, 8.96px) min(1.4cqw, 17.92px);
  background: transparent;
  border: 0;
  border-top: min(0.08cqw, 1.024px) solid rgba(95, 74, 55, 0.4);
  color: #f5e3b3;
  font: 300 min(1.05cqw, 13.44px) / 1.2 'BlackOpsOne', system-ui, sans-serif;
  text-align: left;
  letter-spacing: min(0.05cqw, 0.64px);
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 120ms ease-out, color 120ms ease-out;
}
.title-burger-item:first-child {
  border-top: 0;
  border-top-left-radius: min(0.4cqw, 5.12px);
  border-top-right-radius: min(0.4cqw, 5.12px);
}
.title-burger-item:last-child {
  border-bottom-left-radius: min(0.4cqw, 5.12px);
  border-bottom-right-radius: min(0.4cqw, 5.12px);
}
.title-burger-item:hover,
.title-burger-item:focus-visible {
  background: rgba(244, 195, 92, 0.95);
  color: #2b1e1c;
  outline: 0;
}

/* ============================[v06124-legal.css]============================== */
/* v0.6.124.7: legal-notice overlay. Mirrors feedback-dialog chrome
   so the third-party-licenses panel reads as part of the same
   family of modals. The backdrop intercepts the click anywhere
   outside the dialog (the close action goes through data-action so
   it routes through the existing handleClick family). */
.legal-notice-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  z-index: 199;
}
.legal-notice-backdrop[hidden] { display: none; }
.legal-notice-dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  width: min(60vw, 600px);
  max-height: 80vh;
  overflow-y: auto;
  transform: translate(-50%, -50%);
  background: #2b1e1c;
  color: #f5e3b3;
  padding: min(1.6cqw, 20.48px) min(2cqw, 25.6px);
  border-radius: min(0.5cqw, 6.4px);
  border: min(0.18cqw, 2.304px) solid #5f4a37;
  box-shadow: 0 min(1cqw, 12.8px) min(2cqw, 25.6px) rgba(0, 0, 0, 0.7);
  z-index: 200;
  font: 400 min(0.9cqw, 11.52px) / 1.4 'SplineSans', system-ui, sans-serif;
}
.legal-notice-dialog[hidden] { display: none; }
.legal-notice-content {
  display: flex;
  flex-direction: column;
  gap: min(0.6cqw, 7.68px);
}
.legal-notice-banner {
  font: 700 min(1.5cqw, 19.2px) / 1.2 'BlackOpsOne', 'SplineSans-Bold', system-ui, sans-serif;
  letter-spacing: min(0.08cqw, 1.024px);
  text-align: center;
  color: #f4c35c;
}
.legal-notice-body p {
  margin: 0;
  padding: 0;
}
.legal-notice-body ul {
  margin: 0.2em 0 0.2em 1.2em;
  padding: 0;
}
.legal-notice-footer {
  display: flex;
  justify-content: flex-end;
  margin-top: min(0.6cqw, 7.68px);
}
.legal-notice-btn {
  background: rgba(74, 47, 29, 0.85);
  color: #f5e3b3;
  border: min(0.12cqw, 1.536px) solid #5f4a37;
  border-radius: min(0.3cqw, 3.84px);
  padding: min(0.4cqw, 5.12px) min(0.9cqw, 11.52px);
  font: 700 min(0.9cqw, 11.52px) / 1 'SplineSans-Bold', system-ui, sans-serif;
  letter-spacing: min(0.05cqw, 0.64px);
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 120ms ease-out;
}
.legal-notice-btn:hover,
.legal-notice-btn:focus-visible {
  background: rgba(244, 195, 92, 0.95);
  color: #2b1e1c;
  outline: 0;
}

/* ============================[v06124-kofi-hide-mobile]======================== */
/* v0.6.124.11: hide the mobile Ko-fi floating chat widget. The desktop
   widget (`.floatingchat-container-wrap`, no `-mobi` suffix) is
   unaffected — only the mobile-variant chip is suppressed. The
   `!important` is required because Ko-fi injects inline styles that
   outrank author stylesheet rules; without `!important` the
   inline `display: block` wins and the chip re-appears. */
body .floatingchat-container-wrap-mobi {
  visibility: hidden !important;
}

/* ============================[v06122-depot-text.css]=========================== */
/* v0.6.122 — depot text & style tweaks. Per-class one-liner subtitles
   replaced the previous "Recruit" placeholder, so the existing
   BlackOpsOne 0.7cqw on .card-name now collides with the longer
   sentence (e.g. "Keeps squad alive."). Bump card-name to 1.2cqw
   to match the wider sub text, and switch the rest of the depot
   header chrome to the lighter gold/300 combo so the depot reads
   as "stylized" rather than "dense". The original block-level
   rules from v0.5.15-REDO93 are kept as a fallback in case
   cqw units fail on a future browser. */
.shop-card .card-name {
  font-family: 'BlackOpsOne', sans-serif;
  font-weight: 300;
  color: #4a372a;
  font-size: 1.2cqw;
}

.shop-band-head h3 {
  font-weight: 300;
  color: #f4c35c;
}

.rail-stash-head h3 {
  font-weight: 300;
  color: #f4c35c;
}

.squad-row-head h3 {
  font-weight: 300;
  color: #f4c35c;
}

.depot-cell-squad .squad-row-sub {
  font-weight: 300;
  color: #f4c35c;
}

.depot-cell-stash .rail-stash-count {
  font-weight: 300;
  color: #f4c35c;
}

.depot-cell-gerbil-shop .shop-band-cost {
  background: #f4c35c;
  color: #4a372a;
  font-size: 0.75cqw;
  font-weight: 900;
  padding: 0 min(0.25cqw, 3.2px);
  border-radius: min(0.125cqw, 1.6px);
}

/* ---------------------------------------------------------------------------
   v0.7.040: per-class attack-burst muzzle flash.

   A short-lived radial yellow→orange burst anchored at the center of the
   actor's .v060-region-battle cell. Spawned once per sub-shot of a
   non-self default attack (Heavy=4, Rifleman=3, Scout/Engineer/Medic=2,
   Grenadier/Officer/Sniper=1). Skills and self-target pulses never spawn
   a muzzle flash. The element is removed after 110 ms by the JS helper
   v07040SpawnMuzzleFlash, and on every new-battle reset by v06018Clear.

   Sizing uses vw (NOT cqw) so the burst is a fixed ~2.5vw diameter
   regardless of the container's width. The element is pointer-events:
   none so it never intercepts drag/clicks on the actor's portrait.
   mix-blend-mode: screen lifts the burst above the portrait colors
   without painting over them.
   --------------------------------------------------------------------------- */
.v07040-muzzle-flash {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2.5vw;
  height: 2.5vw;
  margin-left: -1.25vw;
  margin-top: -1.25vw;
  background: radial-gradient(circle at center,
    rgba(255, 215, 0, 0.95) 0%,
    rgba(255, 165, 0, 0.7) 30%,
    rgba(255, 80, 0, 0.3) 65%,
    transparent 100%);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.3);
  animation: v07040-muzzle-flash-anim 80ms ease-out forwards;
  z-index: 5;
  mix-blend-mode: screen;
}
@keyframes v07040-muzzle-flash-anim {
  0%   { opacity: 0; transform: scale(0.3); }
  35%  { opacity: 1; transform: scale(1.1); }
  100% { opacity: 0; transform: scale(0.5); }
}
