/* ============================================================
   JJK: Peak Sorcery — HGSS-inspired stylesheet
   Aesthetic: cream/off-white panels, deep navy text, rounded
   double-bordered panels, soft drop shadows, pixel font.
   ============================================================ */

:root {
  /* Dark textured BLACK palette
     Variable names kept for compatibility — colors are inverted from the original cream theme.
     --cream is now near-black bg; --navy is now light text. Purple stays as accent only. */
  --cream: #0a0a0d;         /* main bg — near-black */
  --cream-2: #16161c;       /* panel bg (subtle gray) */
  --cream-3: #1f1f28;       /* inset / secondary panel */
  --navy: #ece6dd;          /* primary text + borders (warm off-white) */
  --navy-2: #9a96a8;        /* secondary text (cool muted gray) */
  --accent: #d94a3d;        /* JJK red — kept */
  --accent-2: #a855f7;      /* JJK purple — accent only now (not in bg) */
  --ce-blue: #a855f7;       /* CE bar still violet */
  --hp-green: #5fdc5f;
  --hp-yellow: #f0c84a;
  --hp-red: #ff5544;
  --buff-green: #5fdc5f;
  --debuff-red: #ff5544;
  --shadow: 0 2px 0 rgba(0, 0, 0, 0.55), 0 8px 24px rgba(0, 0, 0, 0.7);
  --glow: 0 0 12px rgba(168, 85, 247, 0.3);
  --border: 2px solid var(--navy);
  --radius: 10px;

  --font-pixel: 'Press Start 2P', monospace;
  --font-text: 'VT323', monospace;
  --font-scale: 1;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  /* BUG-339 (the other half of BUG-328). `height:100%` resolves against the
     INITIAL CONTAINING BLOCK, which on mobile is the LARGE viewport — the area
     including whatever the collapsing URL bar currently overlays. BUG-328 gave
     `body.layout-portrait #app` a `100dvh` override, but left this rule alone, so
     the flex container that CENTERS #app stayed large-viewport-tall while #app
     itself correctly shrank: `align-items:center` then pushed #app's top down by
     half the difference, straight under the URL bar, with `overflow:hidden`
     (below) leaving no scroll path to recover it. Measured on a 375x812 stage:
     body 812 / #app 730 / #app top offset 41px.
     Sizing the container dynamically too makes the offset 0. This ALSO closes the
     gap for the DEFAULT (non-portrait) layout, which never had any dvh override at
     all — #app there is `height:100%`, chained to this same rule. Desktop is
     unaffected: with no dynamic toolbar, dvh == vh == 100%. */
  height: 100%;
  height: 100dvh;
  background-color: var(--cream);
  /* Textured black — neutral gray noise with very faint accent flecks */
  background-image:
    radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.04) 1px, transparent 0),
    radial-gradient(circle at 8px 8px, rgba(255, 255, 255, 0.018) 1px, transparent 0),
    radial-gradient(circle at 3px 9px, rgba(168, 85, 247, 0.025) 1px, transparent 0);
  background-size: 14px 14px, 28px 28px, 36px 36px;
  color: var(--navy);
  font-family: var(--font-text);
  font-size: 18px;
  line-height: 1.4;
  overflow: hidden;
}

#app {
  width: 100%;
  height: 100%;
  position: relative;
  /* BUG-257 (Joe's ruling: game-wide scope, 2026-07-19) — past ~2:1 width:height
     the whole shell stretches into an unplayable ultra-wide strip. Cap it so
     EVERY screen, not just battles, gets a consistent classic-console letterbox
     frame; the body texture above shows through the gutters. Deliberately
     generous (200vh, i.e. only engages past 2:1) — the battle arena's own
     tighter native-ratio cap lives on .battle-arena itself (it has a narrower
     sub-allocation of the app's height, so it needs a tighter cap to avoid
     cropping its 16:9 art even at ordinary window ratios). Mobile is untouched:
     body.layout-portrait #app below fully overrides this rule. */
  max-width: min(100%, 200vh);
  margin: 0 auto;
}

/* Screens stack on top of each other; .active reveals one. Crossfade (GJ-6):
   .screen-entering starts the incoming screen at opacity 0 (removed next
   frame by JS to transition to 1); .screen-leaving keeps the outgoing screen
   painted at display:flex while it fades to 0, instead of an instant cut. */
.screen {
  position: absolute;
  inset: 0;
  display: none;
  opacity: 1;
  transition: opacity 150ms ease;
  background: transparent; /* let body texture show through */
}
.screen.active { display: flex; }
.screen.screen-entering { opacity: 0; }
.screen.screen-leaving { display: flex; opacity: 0; }

/* ============================================================
   ART PLACEHOLDER STYLE
   Distinct dashed outline + checker pattern so it's obvious
   what to replace later. Includes a label readout.
   ============================================================ */
.art-placeholder {
  background-color: #0f0f14;
  background-image:
    linear-gradient(45deg, #1c1c24 25%, transparent 25%),
    linear-gradient(-45deg, #1c1c24 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #1c1c24 75%),
    linear-gradient(-45deg, transparent 75%, #1c1c24 75%);
  background-size: 14px 14px;
  background-position: 0 0, 0 7px, 7px -7px, -7px 0;
  border: 2px dashed var(--navy-2);
  color: var(--navy-2);
  font-family: var(--font-pixel);
  font-size: 9px;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1.4;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  position: relative;
}
.art-placeholder::after {
  content: attr(data-asset);
  position: absolute;
  bottom: 4px; right: 6px;
  font-size: 7px;
  background: var(--accent-2);
  color: var(--cream);
  padding: 2px 4px;
  letter-spacing: 0;
}
/* Suppress the data-asset debug chip on the arena background — once a real
   arena bg is loaded the chip is just visual noise behind the combatant HUD. */
.arena-bg::after { display: none; }
/* Same for the title screen: the real title art is in, so the "TITLE_BACKGROUND"
   dev chip is just clutter. */
.title-bg-art::after { display: none; }

/* ============================================================
   TITLE SCREEN
   ============================================================ */
#screen-title { flex-direction: column; align-items: center; justify-content: center; padding: 0; }

.title-bg-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  font-size: 0;
  border: none;
  /* New single-image title art: Gojo / Sukuna title shot. `contain` shows the
     whole image (no crop) — letterboxes if aspect ratios mismatch. Black
     background fills any letterbox bars. */
  background-color: #000;
  background-image: url('assets/menu_bg/title.jpg?v=3');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

/* Ambient energy glow (2026-07-12, Joe's ask): two slow, low-opacity color-
   breathing overlays anchored to the SAME 941×1672 image coordinate space as
   .title-art-frame below (a direct sibling of .title-bg-art at z-index 1, so
   its mix-blend-mode composites against the art painted at z-index 0 — both
   simple z-indexed children of #screen-title, the standard supported case for
   sibling blend-mode compositing). Positioned in the top/bottom CORNERS only
   (not over either character's face) because that's where the source art's
   own lightning/cursed-energy crackle is already concentrated — the glow
   reads as the art breathing, not a patch slapped over it. Present across
   every titleStyle tier (harmless under Tier A/B/C's own transient overlays
   or Tier C's persistent mascot-grade filter, which only targets
   .title-bg-art itself, a separate element). */
.title-ambient-frame {
  position: absolute;
  inset: 0;
  margin: auto;
  aspect-ratio: 941 / 1672;
  max-width: 100%;
  max-height: 100%;
  z-index: 1;
  pointer-events: none;
}
.title-ambient {
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
}
/* Sky glow — cool blue-white, echoes Gojo's lightning-crackle backdrop (top
   corners, y 0-42% of the frame ≈ above his shoulders). Uses `color` blend
   (NOT the shared `screen` from .title-ambient) — 2026-07-12 fix: `screen`
   only ADDS light, so against an already-bright sky/cloud backdrop there was
   almost no headroom left and the pulse read as invisible (it only worked on
   Sukuna's dark backdrop, where adding light has huge contrast). `color`
   blend instead imposes the overlay's hue/saturation while KEEPING the
   backdrop's existing brightness — visible on bright pixels too. */
.title-ambient-sky {
  mix-blend-mode: color;
  background:
    radial-gradient(ellipse 42% 50% at 5% 6%,  rgba(150, 205, 255, 0.85) 0%, rgba(150, 205, 255, 0) 72%),
    radial-gradient(ellipse 42% 50% at 95% 6%, rgba(150, 205, 255, 0.85) 0%, rgba(150, 205, 255, 0) 72%);
  animation: title-sky-glow 10s ease-in-out infinite;
}
/* Cursed-energy glow — warm pink-purple, echoes Sukuna's crackle around his
   clawed hands (bottom corners, y 60-96% of the frame). 2026-07-12: widened
   opacity swing + slowed the cycle per Joe's ask for a clearer, more gradual
   alternation (was 0.6-0.9/7.5s, now 0.3-1.0/10s). */
.title-ambient-ce {
  background:
    radial-gradient(ellipse 38% 30% at 3% 80%,  rgba(232, 90, 200, 0.55) 0%, rgba(232, 90, 200, 0) 72%),
    radial-gradient(ellipse 38% 30% at 97% 80%, rgba(232, 90, 200, 0.55) 0%, rgba(232, 90, 200, 0) 72%);
  animation: title-ce-glow 10s ease-in-out infinite;
}
/* Sun edge-glow (2026-07-12, Joe's ask): a slight bright-gold gradient hue on
   the EDGES of the sun + its rays in the art's top-left (Gojo's half) — sun
   center ≈ (5.4%, 7%) of the frame (pixel-measured densest flare-core cluster,
   2026-07-13). Annular gradient: transparent core
   (the sun's white center stays white), gold rim over the flare edge/rays,
   fading out. `screen` blend adds the gold as light, which reads on the
   mid-brightness ray/halo pixels while the already-white core clips unchanged.
   2026-07-15 (Joe's ask — supersedes the 07-12 "static by design" note): the
   glare now PULSES slightly — a gentle opacity/brightness breathe on a 7s
   cycle, deliberately OFF-PERIOD from the 10s sky/CE pulses so the three
   ambient glows drift organically instead of beating in sync. */
.title-ambient-sun {
  background:
    radial-gradient(ellipse 15% 10% at 5.4% 7%,
      rgba(255, 214, 110, 0) 0%,
      rgba(255, 214, 110, 0) 30%,
      rgba(255, 206, 92, 0.55) 52%,
      rgba(255, 190, 70, 0.28) 68%,
      rgba(255, 190, 70, 0) 82%);
  animation: title-sun-pulse 7s ease-in-out infinite;
}

@keyframes title-sky-glow {
  0%, 100% { filter: hue-rotate(-24deg) saturate(1.2); opacity: 0.55; }
  50%      { filter: hue-rotate(30deg)  saturate(1.7); opacity: 0.95; }
}
@keyframes title-ce-glow {
  0%, 100% { filter: hue-rotate(0deg);    opacity: 0.3; }
  50%      { filter: hue-rotate(-18deg);  opacity: 1;   }
}
@keyframes title-sun-pulse {
  0%, 100% { opacity: 0.68; filter: brightness(1); }
  50%      { opacity: 1;    filter: brightness(1.18); }
}
@media (prefers-reduced-motion: reduce) {
  .title-ambient-sky, .title-ambient-ce, .title-ambient-sun { animation: none; opacity: 0.65; filter: none; }
}

.title-foreground {
  /* Span the full screen so the PRESS START button's vh-based positioning
     resolves relative to the viewport (not the foreground's content box). */
  position: absolute;
  inset: 0;
  z-index: 2;
  text-align: center;
  pointer-events: none; /* let clicks pass through to children that re-enable */
}
.title-foreground > * { pointer-events: auto; }

.title-foreground .title-logo,
.title-foreground .season {
  /* Strong text shadow so labels stay legible over any background art */
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.7), 0 0 14px rgba(168, 85, 247, 0.4);
}
/* PRESS START gets its own bespoke text treatment (2026-07-15 cursed-energy-
   seal polish) instead of the shared logo/season glow above — see
   .press-start-btn / press-start-pulse below. */

/* Frame that exactly matches the rendered title-image rect: same aspect as the
   941×1672 art, contain-fitted and centered (margin:auto + max 100%). Because it
   tracks the image on every viewport aspect, the PRESS START button can be sized
   and placed as a PERCENTAGE of the art — overlaying the baked-in button box in
   both landscape and any portrait phone. `container-type: size` lets the button
   scale its type via cqh (= vh in landscape where the frame fills height, so the
   landscape look is unchanged from the old vh values). */
.title-art-frame {
  position: absolute;
  inset: 0;
  margin: auto;
  aspect-ratio: 941 / 1672;
  max-width: 100%;
  max-height: 100%;
  container-type: size;
  pointer-events: none;   /* clicks pass through; the button re-enables them */
}

/* PRESS START — the title art has its own baked-in button: a gold pixel-art
   octagonal frame with a black inlay and painted "PRESS START" text (current
   art file 675×1200; inlay measured by pixel analysis 2026-07-15: left 27.4%,
   top 91.35%, width 45.2%, height 4.15% of the art, chamfered corners, inlay
   black ≈ #050103). Joe's design (2026-07-15): the ART's gold frame IS the
   button chrome — this element is just a black octagon that exactly covers
   the inlay (hiding the baked-in static text) and renders our own live,
   hard-blinking white label inside the art's frame. All % values are of
   .title-art-frame == the rendered art rect, so the fit holds at every
   viewport aspect.
   BUG-251 (DO NOT REGRESS): font-size 2.0cqh + letter-spacing 0.05cqh +
   white-space nowrap keep the label to ONE line everywhere. */
.press-start-btn {
  position: absolute;
  left: 27.4%;
  top: 91.35%;
  width: 45.2%;
  height: 4.15%;
  transform: none;
  margin: 0;
  padding: 0;
  font-family: var(--font-pixel);
  font-size: 2.0cqh;
  letter-spacing: 0.05cqh;
  white-space: nowrap;
  color: #f2e6d0;
  background: #050103;               /* sampled from the art's inlay black */
  border: none;
  border-radius: 0;
  /* chamfered octagon matching the inlay's cut corners */
  clip-path: polygon(2.6% 0, 97.4% 0, 100% 30%, 100% 70%, 97.4% 100%, 2.6% 100%, 0 70%, 0 30%);
  cursor: pointer;
  pointer-events: auto;
  /* the hard retro on/off text blink IS the base behavior, independent of the
     .blink class in the markup */
  animation: blink-text 1s steps(2) infinite;
  z-index: 2;   /* orbs (title-orbs.js) pass in FRONT (z4) or BEHIND (z1) */
}
/* Hit-area extender: the inlay is only ~28px tall on a 375px-wide portrait
   phone — well under the 44px tap guideline — so this extends the CLICKABLE
   box. Geometry matches the flame strip's authored layout (assets/ui/
   pressstart_fire_strip.png, 6 frames of 384×96). */
.press-start-btn::after {
  content: '';
  position: absolute;
  top: -175%;
  bottom: -25%;
  left: -8.2%;
  right: -8.2%;
  background: none;
  clip-path: none;
  pointer-events: auto;
  image-rendering: pixelated;
}
.press-start-btn::before { content: none; }
/* Hover / keyboard focus: brighten the label + a soft gold inner glow that
   stays inside the art's frame. background/border-color/color declared
   explicitly — the generic `button:hover` rule (~line 4128) otherwise leaks
   a flat purple background over this (verified during the seal polish). */
.press-start-btn:hover,
.press-start-btn:focus-visible {
  animation: none;                  /* blink parks VISIBLE while targeted */
  color: #fffdf5;
  background: #050103;
  border-color: transparent;
  box-shadow: inset 0 0 0 0.22cqh rgba(232, 180, 74, 0.65);
  text-shadow: 0 0 1.2cqh rgba(255, 220, 150, 0.8);
}
/* Cursed-energy fire — PRESS ONLY (Joe, 2026-07-17: the pixel flames should
   only appear when the button is actually pressed, not on hover — the
   previous hover-trigger rule + the click-time "explosion" embellishment
   (embers/fill squares tried across several iterations, see BUGLOG BUG-269
   for that history) are both removed). Pixel-art flames — a 6-frame sprite
   strip cycled with steps(6, jump-none) for an authentic chunky flicker (v2
   recipe, gen_pressstart_fire.py) — wrap the button for the 650ms
   .pressed-start beat only. LIVES ON A SIBLING DIV, not the button's ::after:
   the button's octagon clip-path clips its own pseudo-elements. Geometry
   derived from the button's inlay box (left 27.4% / top 91.35% / w 45.2% /
   h 4.15% of the frame) expanded to the strip's authored layout (button
   band = x 6.25–93.75%, y 41.67–75% of each frame): fire box = w 51.66% @
   left 24.17%, h 12.45% @ top 86.16%. No scaling of the sprite itself —
   transform: scale() on small pixel art just blows up the existing pixels
   into bigger blocks, it can't add detail. */
.press-start-fire {
  position: absolute;
  left: 24.17%;
  top: 86.16%;
  width: 51.66%;
  height: 12.45%;
  pointer-events: none;
  display: none;
  z-index: 3;   /* orbs (title-orbs.js) pass in FRONT (z4) or BEHIND (z1) */
  background-image: url('assets/ui/pressstart_fire_strip.png?v=4');
  background-size: 100% 600%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}
#screen-title.pressed-start .press-start-fire {
  display: block;
  animation: pressstart-fire 0.6s steps(6, jump-none) infinite;
}
@keyframes pressstart-fire {
  0%   { background-position: 0 0%; }
  100% { background-position: 0 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .press-start-fire { animation: none; background-position: 0 0%; }
  #screen-title.pressed-start .press-start-fire { animation: none; }
}
/* ── Large purple pixel-art flame ring around the VS-splash glyph ──────────
   (Joe's ask 2026-08-02: "Copy the 2D pixel-art flame animation from title
   menu and make a large purple one around the VS in the VS Splash.") Same
   sprite-strip + steps() technique as .press-start-fire above, generated by
   tools/gen_vs_fire_strip.py into assets/ui/vs_fire_strip.png (6 frames,
   192x160 each, purple ramp, open transparent center so the "VS" glyph
   overlaying it stays legible). Width/height are set inline per-instance in
   buildVsFlame() (game.js) since the box scales with the sizeTier param;
   this class only owns the sprite lookup + centering + animation cycle.
   Replaces the old canvas polar-fire-automaton simulation. */
.vs-pixel-flame {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 0;
  background-image: url('assets/ui/vs_fire_strip.png?v=1');   /* bump with every generator re-run — same convention as pressstart_fire_strip (cold-review 2026-08-02) */
  background-size: 100% 600%;
  background-repeat: no-repeat;
  background-position: 0 0%;
  image-rendering: pixelated;
  animation: vs-pixel-flame-cycle 0.6s steps(6, jump-none) infinite;
}
@keyframes vs-pixel-flame-cycle {
  0%   { background-position: 0 0%; }
  100% { background-position: 0 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .vs-pixel-flame { animation: none; background-position: 0 0%; }
}
/* ── Lapse Blue / Reversal Red orbit -> helix -> purple black hole ──────────
   (Joe's ask 2026-07-16 pm, choreography v2 same evening.) Two cursed-energy
   orbs — each 25% of a PRESS START letter — circle the button on an
   oscillating pseudo-3D ellipse, then spiral UPWARD around each other to the
   title and collapse into a purple black hole (dark core + accretion disk +
   lensing halo), which implodes and leaves glitter twinkling around "PEAK
   SORCERY". 10s cycle. All motion math + z-order swaps + triggers live in
   js/title-orbs.js (rAF, transforms only); this block is the orbs' look and
   the black-hole/glitter choreography. Layer order inside .title-art-frame:
   1 orb-behind · 2 button · 3 fire · 4 orb-in-front · 5 .bh/.glitter. */
.ce-orb {
  position: absolute;
  left: 0; top: 0;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}
.ce-orb-blue {
  background: radial-gradient(circle at 38% 35%, #eaf6ff 0%, #7cc4ff 40%, #2b6fe0 75%, rgba(30,86,200,0) 100%);
  box-shadow: 0 0 0.9cqh 0.15cqh rgba(74,168,255,0.85), 0 0 2cqh 0.6cqh rgba(43,111,224,0.4);
}
.ce-orb-red {
  background: radial-gradient(circle at 38% 35%, #fff0ee 0%, #ff6a55 40%, #d92318 75%, rgba(160,20,16,0) 100%);
  box-shadow: 0 0 0.9cqh 0.15cqh rgba(255,106,85,0.85), 0 0 2cqh 0.6cqh rgba(217,35,24,0.4);
}
/* ── purple black hole over the title (a la Gargantua) ──────────────────────
   The helix apex detonation, v2 (Joe: "highly-detailed... trembles...
   bursts to cover the entire screen"). The hole itself is a pre-rendered
   512px sprite (assets/ui/blackhole.png, tools/gen_blackhole.py: void core,
   photon ring, Doppler-bright turbulent accretion disk, lensed halo) at
   50%/48.5% of the art frame — over the PEAK SORCERY lettering. Timeline
   (one 2.35s one-shot, class-triggered from js/title-orbs.js): form with a
   slight overshoot -> hold ~1.5s while TREMBLING (infinite jitter on an
   inner wrapper) -> violent implosion -> full-screen burst (.bh-burst, a
   sibling layer on #screen-title so it covers the whole viewport, not just
   the art frame). */
.bh {
  position: absolute;
  left: 50%; top: 48.5%;
  width: 0; height: 0;
  pointer-events: none;
  z-index: 5;
  display: none;
}
.bh.on { display: block; }
.bh-scale {
  position: absolute; left: 0; top: 0;
  width: 46cqw; height: 46cqw;
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
}
.on .bh-scale { animation: bh-play 2.85s ease-out forwards; }
/* form -> ERRATIC, VIOLENT PULSATING GROWTH to 4x (+300%) over exactly 2s
   (12%..82% of the 2.85s timeline; ragged surges and hard crashes around a
   rising baseline — no two beats alike — tremble jittering on top) ->
   violent implosion */
@keyframes bh-play {
  0%   { transform: translate(-50%,-50%) scale(0);    opacity: 0; }
  9%   { transform: translate(-50%,-50%) scale(1.12); opacity: 1; }
  12%  { transform: translate(-50%,-50%) scale(1);    opacity: 1; }
  16%  { transform: translate(-50%,-50%) scale(1.85); opacity: 1; }
  20%  { transform: translate(-50%,-50%) scale(1.15); opacity: 1; }
  27%  { transform: translate(-50%,-50%) scale(2.3);  opacity: 1; }
  31%  { transform: translate(-50%,-50%) scale(1.45); opacity: 1; }
  36%  { transform: translate(-50%,-50%) scale(2.1);  opacity: 1; }
  44%  { transform: translate(-50%,-50%) scale(2.9);  opacity: 1; }
  48%  { transform: translate(-50%,-50%) scale(1.8);  opacity: 1; }
  55%  { transform: translate(-50%,-50%) scale(3.3);  opacity: 1; }
  60%  { transform: translate(-50%,-50%) scale(2.4);  opacity: 1; }
  66%  { transform: translate(-50%,-50%) scale(3.7);  opacity: 1; }
  71%  { transform: translate(-50%,-50%) scale(2.75); opacity: 1; }
  77%  { transform: translate(-50%,-50%) scale(3.55); opacity: 1; }
  82%  { transform: translate(-50%,-50%) scale(4);    opacity: 1; }
  88%  { transform: translate(-50%,-50%) scale(0.15); opacity: 1; }
  91%  { transform: translate(-50%,-50%) scale(0.03); opacity: 1; }
  100% { transform: translate(-50%,-50%) scale(0);    opacity: 0; }
}
/* the tremble: an unstable mass barely holding itself together (violent) */
.bh-shake { position: absolute; inset: 0; }
.on .bh-shake { animation: bh-tremble 0.12s linear infinite; }
@keyframes bh-tremble {
  0%   { transform: translate(0, 0)            rotate(0deg); }
  24%  { transform: translate(1.4%, -1%)       rotate(0.9deg); }
  47%  { transform: translate(-1.2%, 1.1%)     rotate(-0.7deg); }
  71%  { transform: translate(1%, 0.8%)        rotate(0.8deg); }
  100% { transform: translate(-0.9%, -1.1%)    rotate(-0.9deg); }
}
/* glow bed under the sun: baked photon-ring rim + lensed halo (the orbital
   accretion ring was REMOVED 2026-07-17, Joe's call — the sun stands alone) */
.bh-core-sprite {
  position: absolute; inset: 0;
  z-index: 2;
  background: url('assets/ui/blackhole_core.png?v=1') center / contain no-repeat;
}
/* ── the SWIRLING PURPLE SUN (Joe, 2026-07-17) ──────────────────────────────
   The orb is no longer a void: a blazing violet star (blackhole_sun.png —
   swirl-armed plasma, sunspots, baked corona) sits where the event horizon
   was, spinning FASTER AND FASTER as the hole pulsates (one heavily
   end-weighted rotation over the 2.85s life = accelerating spin), over the
   core sprite's glow bed. Replaced the deep-field/galaxy-swarm interior —
   an opaque sun covers it. */
.bh-sun-spin { position: absolute; inset: 0; z-index: 2; }
.on .bh-sun-spin { animation: bh-sun-accel 2.85s cubic-bezier(0.55, 0.05, 0.9, 0.4) forwards; }
@keyframes bh-sun-accel {
  from { transform: rotate(0deg); }
  to   { transform: rotate(2520deg); }   /* 7 turns, nearly all in the final second */
}
.bh-sun {
  position: absolute; inset: 0;
  background: url('assets/ui/blackhole_sun.png?v=1') center / contain no-repeat;
}
/* Blue/Red FUSION marbling (Joe, 2026-07-17): the sun forms already showing
   its parent colours marbled together, then settles to pure violet — "Blue
   and Red collide" resolving into Hollow Purple, not spawning violet outright.
   Same swirl silhouette as .bh-sun (drawn from the identical field in
   gen_blackhole.py) layered ON TOP, spinning together with .bh-sun-spin
   (it's a child of the same rotating wrapper) and fading out over the
   sun's first ~0.6s of the 2.85s life. */
.bh-fusion {
  position: absolute; inset: 0;
  background: url('assets/ui/blackhole_fusion.png?v=1') center / contain no-repeat;
  opacity: 0;
}
.on .bh-fusion { animation: bh-fusion-fade 2.85s ease-out forwards; }
@keyframes bh-fusion-fade {
  0%   { opacity: 1; }
  9%   { opacity: 1; }    /* holds through the sun's "form" beat (bh-play 0-12%) */
  21%  { opacity: 0; }    /* marbling resolves to pure violet as growth begins */
  100% { opacity: 0; }
}
/* full-viewport burst when the hole collapses — lives on #screen-title so it
   swallows the ENTIRE screen (letterbox included). Same 2.35s timeline as
   bh-play; its action lands in the final ~16%. */
.bh-burst {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 6;
  display: none;
}
.bh-burst.on { display: block; }
.bh-burst-wash {
  position: absolute; inset: 0;
  background: radial-gradient(circle at var(--bx, 50%) var(--by, 48%),
              #ffffff 0%, #e9d5ff 14%, #c084fc 32%, rgba(168,85,247,0.85) 52%,
              rgba(109,40,217,0.65) 74%, rgba(76,29,149,0.5) 100%);
  opacity: 0;
}
.on > .bh-burst-wash { animation: bh-burst-wash 2.85s linear forwards; }
@keyframes bh-burst-wash {
  0%, 86%  { opacity: 0; }
  90%, 93% { opacity: 1; }
  100%     { opacity: 0; }
}
.bh-burst-ring {
  position: absolute; left: var(--bx, 50%); top: var(--by, 48%);
  width: 20vmax; height: 20vmax;
  border-radius: 50%;
  border: 0.6vmax solid rgba(255,255,255,0.95);
  box-shadow: 0 0 3vmax rgba(233,213,255,1), 0 0 7vmax rgba(168,85,247,0.85);
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
}
.on > .bh-burst-ring { animation: bh-burst-ring 2.85s linear forwards; }
@keyframes bh-burst-ring {
  0%, 87% { transform: translate(-50%,-50%) scale(0);  opacity: 0; }
  89%     { transform: translate(-50%,-50%) scale(0.4); opacity: 1; }
  100%    { transform: translate(-50%,-50%) scale(14); opacity: 0; }
}
/* the explosion's SHUDDER: the whole title screen slams and settles, class-
   triggered by js/title-orbs.js the instant the burst detonates */
#screen-title.bh-quake { animation: bh-quake 0.7s linear; }
@keyframes bh-quake {
  0%   { transform: translate(0, 0); }
  8%   { transform: translate(-2.6%, 2%)    rotate(-0.3deg); }
  18%  { transform: translate(2.3%, -1.7%)  rotate(0.25deg); }
  30%  { transform: translate(-1.8%, 1.3%)  rotate(-0.2deg); }
  42%  { transform: translate(1.4%, -1%); }
  55%  { transform: translate(-1%, 0.7%); }
  68%  { transform: translate(0.6%, -0.45%); }
  82%  { transform: translate(-0.3%, 0.2%); }
  100% { transform: translate(0, 0); }
}
/* ── glitter left over from the burst, congregating around "PEAK SORCERY" ──
   16 pixel-diamond sparkles. Each is anchored at its final title-band spot
   (left/top inline) and starts displaced by --dx/--dy (burst debris strewn
   across the whole frame, set inline by js/title-orbs.js) — the animation
   flies them inward to congregate around the lettering, then they twinkle
   in place and fade. ~3s total, staggered per-sparkle. */
.glitter {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  display: none;
}
.glitter.on { display: block; }
.glit {
  position: absolute;
  width: calc(1cqh * var(--gs, 1));
  height: calc(1cqh * var(--gs, 1));
  background: #f3e8ff;
  box-shadow: 0 0 1cqh rgba(216,180,254,0.95), 0 0 2.2cqh rgba(168,85,247,0.5);
  transform: rotate(45deg) scale(0);
  opacity: 0;
}
.glit:nth-child(4n) {
  background: #ffe9b0;   /* a few gold flecks in the violet dust */
  box-shadow: 0 0 1cqh rgba(255,220,140,0.95), 0 0 2.2cqh rgba(255,180,60,0.5);
}
.on > .glit { animation: glit-con 3s ease-in-out forwards; }
@keyframes glit-con {
  /* born in the burst, strewn across the frame... */
  0%   { transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(45deg)  scale(0.3);  opacity: 0; }
  8%   { transform: translate(var(--dx, 0px), var(--dy, 0px)) rotate(45deg)  scale(1.3);  opacity: 1; }
  /* ...flying inward to congregate around the title... */
  50%  { transform: translate(0, 0) rotate(405deg) scale(1.15); opacity: 1; }
  /* ...then twinkling in place and fading out */
  62%  { transform: translate(0, 0) rotate(405deg) scale(0.45); opacity: 0.35; }
  74%  { transform: translate(0, 0) rotate(405deg) scale(1.05); opacity: 1; }
  86%  { transform: translate(0, 0) rotate(405deg) scale(0.4);  opacity: 0.3; }
  100% { transform: translate(0, 0) rotate(405deg) scale(0);    opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ce-orb, .bh, .bh-burst, .glitter { display: none !important; }
}
.press-start-btn:focus-visible {
  box-shadow: inset 0 0 0 0.3cqh rgba(232, 180, 74, 0.95);
}
.press-start-btn:active {
  color: #cdbd9d;
  text-shadow: none;
  background: #050103;
  border-color: transparent;
}
/* Hard on/off retro blink — text only; the black octagon stays solid so the
   art's inlay never appears to flicker. */
.blink { animation: blink-text 1s steps(2) infinite; }
@keyframes blink-text {
  50% { color: transparent; }
}
/* .blink on the button is redundant with the base animation now (kept because
   the static markup still carries the class — same values, no conflict). */
.press-start-btn.blink { animation: blink-text 1s steps(2) infinite; }
/* prefers-reduced-motion: no blink — static visible label. */
@media (prefers-reduced-motion: reduce) {
  .press-start-btn,
  .press-start-btn.blink,
  .blink {
    animation: none;
  }
}


.corner-btn {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 10;
  font-family: var(--font-pixel);
  font-size: 9px;
  padding: 8px 14px;
  background: var(--cream);
  border: 2px solid var(--navy);
  border-radius: 6px;
  cursor: pointer;
  letter-spacing: 1px;
  box-shadow: var(--shadow);
}
.corner-btn:hover { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); }
/* Quick-win juice batch (2026-07-14, item 2): pressed feedback for the corner
   utility buttons (Tutorial/Glossary/Options/Back). */
.corner-btn:active { background: var(--accent); color: #fff5e8; border-color: var(--accent); filter: brightness(0.92); transform: scale(0.96); }

#screen-mode .corner-btn {
  position: static;
  box-shadow: none;
}
.mode-utility-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  flex-shrink: 0;
}

.version {
  position: absolute;
  bottom: 12px; left: 18px;
  font-size: 14px;
  color: var(--navy-2);
  z-index: 3;
}

/* ============================================================
   MODE SELECT SCREEN
   ============================================================ */
/* Two-row layout: mode art on top, SELECT MODE list + selected mode details on bottom. */
#screen-mode { flex-direction: column; padding: 8px 24px 16px; gap: 12px; }
.mode-pane {
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
}
.mode-art-pane {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.mode-controls-pane {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  gap: 20px;
}
.mode-list-area    { display: flex; flex-direction: column; min-height: 0; }
.mode-details-area { display: flex; flex-direction: column; gap: 14px; min-height: 0; }

h2 {
  font-family: var(--font-pixel);
  font-size: 14px;
  margin-bottom: 18px;
  letter-spacing: 1px;
  color: var(--navy);
  border-bottom: 2px solid var(--navy);
  padding-bottom: 8px;
}

.mode-list { list-style: none; flex: 1; }
.mode-list > li {
  padding: 10px 12px;
  margin: 4px 0;
  font-family: var(--font-pixel);
  font-size: 11px;
  cursor: pointer;
  border-radius: 6px;
  transition: background 0.15s;
}
.mode-list > li.selected, .mode-list > li:hover:not(.disabled) {
  background: var(--accent-2);
  color: #fff5e8;
  box-shadow: var(--glow);
}
/* Quick-win juice batch (2026-07-14, item 2): pressed feedback for the
   Practice/Tower/Multiplayer category rows — dims + insets slightly on tap. */
.mode-list > li:active:not(.disabled) {
  background: var(--accent);
  filter: brightness(0.9);
}
.mode-list > li.disabled { color: #888; cursor: not-allowed; }
.mode-list > li ul { margin-top: 6px; padding-left: 20px; }
.mode-list > li ul li {
  font-size: 9px;
  padding: 4px 8px;
  opacity: 0.8;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, opacity 0.12s;
}
.mode-list > li ul li[data-mode]:hover {
  background: rgba(168, 85, 247, 0.4);
  opacity: 1;
}
/* Quick-win juice batch (2026-07-14, item 2): pressed feedback for the
   Practice/Casual/Ranked/etc sub-mode tiles (the ones players actually tap
   to select a mode). Mirrors the .mode-list > li:active treatment above. */
.mode-list > li ul li[data-mode]:active:not(.disabled) {
  background: rgba(168, 85, 247, 0.65);
  opacity: 1;
  filter: brightness(0.92);
}
.mode-list > li ul li[data-mode].selected {
  background: var(--accent);
  color: #fff5e8;
  opacity: 1;
  box-shadow: 0 0 6px rgba(217, 74, 61, 0.6);
}
/* Two-tap confirm hint (armOrConfirmMode) — lives INSIDE the selected pill
   itself (Joe: not off in the details panel below), hidden until that specific
   tile is armed by a first tap. */
.mode-list > li ul li[data-mode] .li-tap-hint { display: none; }
.mode-list > li ul li[data-mode].armed .li-tap-hint {
  display: block;
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 1px;
  margin-top: 3px;
  color: #fff5e8;
  animation: blink-text 1s steps(2) infinite;
}
/* Sub-items are disabled INDIVIDUALLY (their own .disabled class), not by
   inheriting a locked parent's state — a parent category tile (e.g.
   "Multiplayer") can be a non-actionable header while some of its children
   are fully playable (Casual/Ranked) and others aren't (Local). */
.mode-list > li ul li[data-mode].disabled { color: #888; cursor: not-allowed; opacity: 0.5; }
.mode-list > li ul li[data-mode].disabled:hover { background: none; }
/* W3a item 2: QUICK MATCH row — same base row styling as every other sub-mode
   (inherits .mode-list > li ul li above), just a thin accent-colored left edge
   so it visually reads as "the fast one" next to plain Practice. */
.mode-list > li ul li.quick-match-row { border-left: 2px solid var(--accent); padding-left: 6px; }
.lock { font-size: 10px; }

.mode-footer {
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid var(--navy);
  font-size: 16px;
}

.mode-art { flex: 1; min-height: 200px; font-size: 12px; }
/* When a real image is loaded into the mode-art slot (selectMode adds the
   `has-art` class), override the .art-placeholder checker pattern + dashed
   border so the image shows through cleanly. */
.mode-art.has-art {
  background-color: #000 !important;
  background-image: none;                    /* inline backgroundImage takes precedence */
  border: 2px solid var(--navy);
}
.mode-art.has-art::after { display: none; }  /* hide the placeholder asset-id chip */
/* Practice-mode cross-fade: the base .mode-art shows image A; this overlay
   shows image B. A pure-CSS keyframe loop fades the overlay in and out so the
   two images alternate every ~5s — no JS timer (immune to background-tab
   throttling / re-render resets, and starts the moment the overlay mounts). */
.mode-art { position: relative; }
.mode-art-fade {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-repeat: no-repeat;
  background-color: #000;
  opacity: 0;
  pointer-events: none;
  animation: modeArtCrossfade 10s ease-in-out infinite;
}
@keyframes modeArtCrossfade {
  0%,  40% { opacity: 0; }   /* base art shows (~0–4s) */
  50%, 90% { opacity: 1; }   /* overlay art shows (~5–9s), 1s fade each way */
  100%     { opacity: 0; }   /* fade back to base */
}
#mode-title { font-family: var(--font-pixel); font-size: 14px; }
#mode-desc { font-size: 18px; }
.mode-options label { font-size: 16px; }
.mode-options select {
  font-family: var(--font-text); font-size: 16px;
  padding: 4px 8px; background: var(--cream); border: 2px solid var(--navy);
  border-radius: 4px;
}
/* ============================================================
   ROSTER SCREEN — full-width grid + popup modal
   ============================================================ */
#screen-roster { padding: 24px; }
.roster-full {
  flex: 1;
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  width: 100%;
}
.roster-header {
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto;
  align-items: center;
  gap: 16px;
  margin-bottom: 12px;
}
/* BUG-252 (resolved, W3a item 5): at desktop window widths in the ~700-820px
   gap between the layout-portrait breakpoint (w<700) and a roomy desktop
   width, the "auto" grid column let this h2 wrap mid-phrase, orphaning the
   team-count tail ("...TEAM 1 / OF 2") — same failure class as the
   flex-min-width cluster, here on a grid "auto" track instead of a flex item.
   The original fix put a blanket nowrap on the whole h2; that's now relaxed —
   js/screens.js's renderRosterHeader wraps just the "TEAM N OF M" token in its
   own .roster-header-count span (below), so "ROSTER — PICK" is free to wrap
   on narrow widths while the count itself can never be split. */
.roster-header h2 { margin: 0; border: none; padding: 0; grid-column: 1; }
.roster-header-count { white-space: nowrap; }
.roster-header .match-type-selector { grid-column: 3; justify-self: center; }
.roster-header .corner-btn { position: static; box-shadow: none; grid-column: 5; }

.match-type-selector {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 8px;
  background: var(--cream-2);
  border: 2px solid var(--navy);
  border-radius: 6px;
}
.match-type-arrow {
  font-family: var(--font-pixel);
  font-size: 10px;
  padding: 4px 8px;
  background: var(--accent);
  color: #fff5e8;
  border: 2px solid var(--accent-2);
  border-radius: 4px;
  cursor: pointer;
  line-height: 1;
}
.match-type-arrow:hover { background: var(--accent-2); transform: translateY(-1px); }
.match-type-arrow:active { transform: translateY(0); }
.match-type-label {
  font-family: var(--font-pixel);
  font-size: 10px;
  letter-spacing: 1px;
  color: var(--navy);
  min-width: 90px;
  text-align: center;
  line-height: 1.4;
}

/* AI difficulty carousel — top of roster screen. Same visual language as the
   match-type selector but with a leading "AI:" label for context. */
.ai-difficulty-carousel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 6px 10px;
  margin: 12px auto 0;
  background: var(--cream-2);
  border: 2px solid var(--navy);
  border-radius: 6px;
  width: fit-content;
}
.ai-difficulty-prelabel {
  font-family: var(--font-pixel);
  font-size: 10px;
  letter-spacing: 1px;
  color: var(--navy-2);
}
.ai-difficulty-arrow {
  font-family: var(--font-pixel);
  font-size: 10px;
  padding: 4px 8px;
  background: var(--accent);
  color: #fff5e8;
  border: 2px solid var(--accent-2);
  border-radius: 4px;
  cursor: pointer;
  line-height: 1;
}
.ai-difficulty-arrow:hover { background: var(--accent-2); transform: translateY(-1px); }
.ai-difficulty-arrow:active { transform: translateY(0); }
.ai-difficulty-label {
  font-family: var(--font-pixel);
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--navy);
  min-width: 120px;
  text-align: center;
}

/* Spacing between season toggle and the character grid inside each season-section */
.season-section .season-toggle { margin-bottom: 14px; }
.season-section .season-toggle + .roster-grid {
  border-top: 1px dashed var(--navy-2);
  padding-top: 14px;
}

.roster-pane {
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
}
.roster-grid-pane { flex: 1; }
.roster-detail-pane { flex: 1.2; }
/* Practice opponent-pick (#screen-opponent) is the one screen never wrapped in a
   scroll container — on a short viewport its tall detail stack pushed #opp-back /
   #opp-pick off the bottom, unreachable without overscroll (which embedded webviews
   block). Scoped so the confirmed-safe roster pick screen is untouched. */
#screen-opponent .roster-detail-pane { min-height: 0; overflow-y: auto; }

/* Wide grid — bigger tiles, more columns */
.roster-grid-wide {
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 12px;
}
.roster-grid-wide .roster-tile {
  aspect-ratio: 1;
  font-size: 11px;
}
.roster-grid-wide .roster-tile .tile-label { font-size: 10px; }

/* ---------- Modal ---------- */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
.modal-backdrop.open { display: flex; }
.modal-card {
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  padding: 24px;
  box-shadow: var(--shadow), var(--glow);
  max-width: 640px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
}
/* BUG-282 (U1): the scroll on a tall modal (e.g. Kechizu's Blood Link kit)
   was invisible — no scrollbar thumb, so it read as "no scroll, buttons
   just off-screen". Same visible-thumb convention as .glossary-list/.glossary-detail. */
.modal-card::-webkit-scrollbar { width: 10px; }
.modal-card::-webkit-scrollbar-thumb { background: rgba(236, 230, 221, 0.3); border-radius: 6px; }
.modal-card::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.3); }
/* GJ-7: entrance pop, driven purely by the existing .open toggle — no JS change. */
.modal-backdrop.open .modal-card { animation: modalCardIn 160ms ease-out; }
@keyframes modalCardIn {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: scale(1); }
}
.modal-close {
  position: absolute;
  top: 10px; right: 10px;
  width: 32px; height: 32px;
  background: var(--cream-3);
  border: 2px solid var(--navy);
  color: var(--navy);
  border-radius: 6px;
  font-family: var(--font-pixel);
  font-size: 12px;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-close:hover { background: var(--accent); color: #fff5e8; border-color: var(--accent); }
.modal-actions {
  display: flex;
  gap: 10px;
  margin-top: 16px;
  justify-content: center;
  /* BUG-282 (U1): Pick/Cancel could scroll off the bottom of a tall modal
     card (Kechizu's Blood Link kit crosses the overflow threshold). Sticking
     the actions row to the scrollport's bottom + bleeding it to the card's
     full width/edge (negative margin cancels the card's own 24px padding)
     keeps both buttons reachable at every scroll position, not just after
     scrolling all the way down. */
  position: sticky;
  bottom: -24px;
  margin: 16px -24px -24px;
  padding: 12px 24px 24px;
  background: var(--cream-2);
  border-top: 1px solid rgba(236, 230, 221, 0.18);
  border-radius: 0 0 var(--radius) var(--radius);
}
/* W3a item 6: "Pick This Character" measured ~30px tall (generic `button` rule's
   8px 14px padding) — below the ~44px comfortable touch target. Scoped to the
   char-detail modal's action row (both buttons, so Pick/Cancel stay the same
   height) rather than a global `button` bump, so nothing else on the page shifts.
   Applies in both desktop and portrait — portrait's `.modal-actions button { width:
   100% }` override (below) only touches width, not height. */
#char-modal-backdrop .modal-actions button { min-height: 44px; padding: 12px 14px; }

.roster-section-label {
  font-family: var(--font-pixel);
  font-size: 10px;
  letter-spacing: 1px;
  margin: 4px 0 10px 0;
  padding: 4px 10px;
  background: var(--accent-2);
  color: #fff5e8;
  display: inline-block;
  border-radius: 4px;
  box-shadow: var(--glow);
}

.roster-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  margin-bottom: 16px;
}

.char-bio {
  font-size: 15px;
  line-height: 1.5;
  margin: 8px 0 12px 0;
  padding: 10px 12px;
  background: var(--cream);
  border: 2px solid var(--navy);
  border-radius: 6px;
  max-height: 140px;
  overflow-y: auto;
}

.ability-tabs {
  display: flex;
  gap: 4px;
  margin-top: 12px;
  border-bottom: 2px solid var(--navy);
}
.tab-btn {
  font-family: var(--font-pixel);
  font-size: 9px;
  /* BUG-253: the flex row stretches every tab to match the tallest one, but
     the 2-line full labels ("ATTACK ABILITIES") needed ~21.6px of text at the
     default line-height inside a 32px box (6px+6px padding = 20px of room) —
     the wrapped second line touched the tab's own bottom border/the panel
     edge below it. line-height 1.3 + a touch more vertical padding gives the
     wrapped label real clearance; single-word tabs (PASSIVES/TOOLS/OBJECTS)
     just get a little extra breathing room via the shared stretch height. */
  line-height: 1.3;
  padding: 7px 8px 9px;
  background: var(--cream);
  border: 2px solid var(--navy);
  border-bottom: none;
  border-radius: 4px 4px 0 0;
  cursor: pointer;
  letter-spacing: 1px;
}
.tab-btn.active { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); }
/* Ability-tab labels: DESKTOP shows the full descriptive label ("ATTACK
   ABILITIES"). The short one-word variant ("ATTACKS") is hidden here and only
   shown on the mobile-width modal (see @media max-width:540px), so desktop is
   unchanged and only the narrow modal swaps in the compact labels. */
.tab-lbl-mini { display: none; }

.tab-content {
  background: var(--cream);
  border: 2px solid var(--navy);
  border-top: none;
  border-radius: 0 0 6px 6px;
  padding: 10px 12px;
  font-size: 15px;
  min-height: 100px;
  max-height: 200px;
  overflow-y: auto;
}
.tab-content .ability-row {
  padding: 6px 0;
  border-bottom: 1px dashed rgba(42, 53, 80, 0.2);
}
.tab-content .ability-row:last-child { border-bottom: none; }
.tab-content .ability-name { font-family: var(--font-pixel); font-size: 10px; display: block; margin-bottom: 4px; }
.tab-content .ability-cost { font-size: 13px; opacity: 0.8; display: block; }
.tab-content .ability-story { display: block; margin-top: 4px; font-size: 13px; font-style: italic; opacity: 0.85; line-height: 1.4; }
.tab-content .ability-cost + .ability-desc,
.tab-content .ability-story + .ability-desc { display: block; margin-top: 4px; }
.tab-content .ability-desc { font-size: 14px; line-height: 1.4; }

/* Inline button used inside an ability-row (e.g. Yuji's Sukuna Vessel passive) */
.passive-btn {
  font-family: var(--font-pixel);
  font-size: 9px;
  padding: 6px 10px;
  background: var(--accent);
  color: #fff5e8;
  border: 2px solid var(--accent-2);
  border-radius: 5px;
  cursor: pointer;
  letter-spacing: 1px;
  margin-bottom: 6px;
  display: inline-block;
  box-shadow: var(--glow);
}
.passive-btn:hover { background: var(--accent-2); border-color: var(--accent); transform: translateY(-1px); }
.passive-btn + .ability-desc { display: block; margin-top: 6px; }

/* Sukuna sub-modal sits ABOVE the char modal */
#sukuna-modal-backdrop { z-index: 2000; }
.sukuna-modal { border-color: var(--accent); box-shadow: var(--shadow), 0 0 24px rgba(217, 74, 61, 0.5); }

/* Arena screen */
.arena-pane {
  flex: 1;
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  margin: 24px;
  padding: 20px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
}
/* 2026-07-12 (Joe): the bottom [A] Start Battle / [B] Back row was clipped on
   short phone viewports — Back now lives in the header row, and confirming an
   arena is a tap-the-tile → yes/no modal (#arena-confirm, reusing the tower
   pause-confirm's tw-* styling). */
.arena-header {
  display: flex;
  align-items: center;
  justify-content: flex-start;   /* back button left, title beside it — the fixed
                                    corner toggles own the top-right */
  gap: 14px;
  margin-bottom: 14px;   /* spacing the h2's default margin used to provide */
}
.arena-header h2 { margin: 0; }
.arena-top-back {
  font-family: var(--font-pixel);
  font-size: 9px;
  padding: 8px 12px;
  border: 2px solid var(--navy-2);
  border-radius: 6px;
  background: var(--cream);
  color: var(--navy);
  cursor: pointer;
  white-space: nowrap;
}
.arena-top-back:hover { background: var(--navy-2); color: var(--cream); }
.arena-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-bottom: 16px;
}
.arena-tile {
  aspect-ratio: 16/10;
  border: 2px dashed var(--navy-2);
  border-radius: 6px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: var(--font-pixel);
  font-size: 9px;
  padding: 6px;
  transition: all 0.15s;
  position: relative;
  background-color: #0f0f14;
  background-image:
    linear-gradient(45deg, #1c1c24 25%, transparent 25%),
    linear-gradient(-45deg, #1c1c24 25%, transparent 25%);
  background-size: 12px 12px;
}
.arena-tile:hover { transform: translateY(-2px); }
.arena-tile.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent), var(--glow); }
/* BUG-171: keyboard/gamepad nav cursor (matches .roster-tile.kbd-cursor's purple glow). */
.arena-tile.kbd-cursor {
  border-color: var(--accent);
  box-shadow: 0 0 4px 1px rgba(170, 110, 255, 0.6), 0 0 12px 4px rgba(170, 110, 255, 0.45), 0 0 22px 10px rgba(170, 110, 255, 0.25);
  transform: translateY(-2px);
}
.arena-tile .arena-label {
  background: rgba(15, 15, 20, 0.88);
  color: var(--navy);
  padding: 4px 6px;
  border-radius: 3px;
}
/* Tiles that loaded real background art: show it as a cropped cover, drop the
   dashed placeholder + dev chip, and pin a legible label to the bottom edge. */
.arena-tile.has-art {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-style: solid;
  border-color: var(--navy);
}
.arena-tile.has-art::after { display: none; }
.arena-tile.has-art .arena-label {
  background: rgba(0, 0, 0, 0.66);
  color: #fff5e8;
  align-self: flex-end;
}
.arena-detail {
  background: var(--cream);
  border: 2px solid var(--navy);
  border-radius: 6px;
  padding: 12px 14px;
  margin-bottom: 14px;
  flex: 1;
  font-size: 16px;
  /* 2026-07-12 (Joe): hidden in SOLO — the tap-to-confirm modal shows the same
     name+desc, so this panel was redundant (and could scroll off short phones).
     MP map-voting still uses #arena-name/#arena-desc as its vote instructions,
     so body.mp-select re-shows it below. */
  display: none;
}
body.mp-select #screen-arena .arena-detail { display: block; }
.arena-detail h3 { font-family: var(--font-pixel); font-size: 12px; margin-bottom: 6px; }
.roster-tile {
  aspect-ratio: 1;
  background: var(--cream);
  border: 2px solid var(--navy);
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-pixel);
  font-size: 9px;
  cursor: pointer;
  text-align: center;
  padding: 4px;
  transition: all 0.15s;
  position: relative;
  overflow: hidden;
}
.roster-tile.locked { background: #888; color: #ccc; cursor: not-allowed; }
/* .tile-art is position:absolute;inset:0 and fully covers the tile's own background,
   so .locked's dimming above is otherwise invisible — grayscale+darken the portrait
   itself and add an explicit lock glyph on top. */
.roster-tile.locked .tile-art { filter: grayscale(1) brightness(0.35); }
.roster-tile .tile-lock { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center; font-size: 26px; text-shadow: 0 2px 5px rgba(0,0,0,0.85); pointer-events: none; }
/* W3a item 1: Practice's full-roster bypass. A lighter treatment than .locked —
   art stays full-color (no grayscale filter, no .locked class at all), just a
   small corner badge marking "not yet earned, but trial it here." */
.roster-tile .tile-trial-badge {
  position: absolute; top: 3px; right: 3px; z-index: 2;
  background: rgba(217, 74, 61, 0.92); color: #fff5e8;
  font-family: var(--font-pixel); font-size: 6px; letter-spacing: 0.5px;
  padding: 2px 4px; border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.5);
  pointer-events: none;
}
/* Gojo summon-only roster tile (docs/GOJO-SUMMON-DESIGN.md §13 — Practice's roster
   and both online-MP rosters). Visible and NOT .locked-greyed, but dimmed enough to
   read as "not a normal pick," plus its own corner badge — same corner-badge pattern
   as .tile-trial-badge above, recolored to Gojo's established Limitless-blue accent
   (matches #gojo-summon-overlay's border/title color) so the two badges never look
   interchangeable at a glance. */
.roster-tile.summon-only .tile-art { filter: brightness(0.75) saturate(0.85); }
.roster-tile .tile-summon-badge {
  position: absolute; top: 3px; right: 3px; z-index: 2;
  background: rgba(74, 163, 255, 0.92); color: #06243f;
  font-family: var(--font-pixel); font-size: 6px; letter-spacing: 0.5px;
  padding: 2px 4px; border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.5);
  pointer-events: none;
}
.roster-tile.selected { border-color: var(--accent); background: var(--cream-2); box-shadow: 0 0 0 2px var(--accent); }
.roster-tile:not(.locked):hover,
.roster-tile.kbd-cursor {
  border-color: var(--accent);
  box-shadow:
    0 0 4px 1px rgba(170, 110, 255, 0.6),
    0 0 12px 4px rgba(170, 110, 255, 0.45),
    0 0 22px 10px rgba(170, 110, 255, 0.25);
  transform: translateY(-2px);
}
.roster-tile.placeholder { opacity: 0.5; }
.roster-tile.placeholder .placeholder-art {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-pixel);
  font-size: 28px;
  color: var(--navy-2);
  background: var(--cream-2);
  width: 100%;
  height: 100%;
}
.roster-tile:hover:not(.locked) { transform: translateY(-2px); }
.roster-tile .tile-art {
  position: absolute; inset: 0;
  background-color: #0f0f14;
  /* Default checker pattern shows when no portrait image is set */
  background-image:
    linear-gradient(45deg, #1c1c24 25%, transparent 25%),
    linear-gradient(-45deg, #1c1c24 25%, transparent 25%);
  background-size: 8px 8px;
  z-index: 0;
}
/* When a portrait image url is supplied via inline style, override checker to show the image */
.roster-tile .tile-art[style*="url"] {
  background-color: #0f0f14;
  background-size: cover !important;
  background-position: center top !important;
  background-repeat: no-repeat !important;
}
.roster-tile .tile-label {
  position: absolute;
  bottom: 4px;
  left: 4px;
  right: 4px;
  z-index: 2;
  background: rgba(15, 15, 20, 0.88);
  color: var(--navy);
  padding: 3px 4px;
  border-radius: 3px;
  text-align: center;
}

.team-meta { font-size: 16px; margin-top: auto; }
.team-meta .hint { font-size: 14px; color: var(--navy-2); opacity: 0.7; }

#char-name { font-family: var(--font-pixel); font-size: 16px; margin-bottom: 4px; }
.char-meta { font-size: 16px; margin-bottom: 12px; color: var(--navy-2); }
.char-sprite {
  height: 200px;
  margin-bottom: 6px;
  font-size: 11px;
  overflow: visible;
  aspect-ratio: 1 / 1;
  background-color: transparent;
  background-image: none;
  border: none;
  filter:
    drop-shadow(0.5px 0 0 white) drop-shadow(-0.5px 0 0 white)
    drop-shadow(0 0.5px 0 white) drop-shadow(0 -0.5px 0 white)
    drop-shadow(0.5px 0.5px 0 white) drop-shadow(-0.5px -0.5px 0 white)
    drop-shadow(0.5px -0.5px 0 white) drop-shadow(-0.5px 0.5px 0 white);
}
.char-sprite.art-placeholder { background-color: transparent; background-image: none; border: none; }
.char-sprite::after { display: none; }

/* 2-column layout: stats left, sprite right (used in char modal + Sukuna sub-modal) */
.stats-sprite-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  margin: 10px 0 12px 0;
  align-items: stretch;
}
.stats-col, .sprite-col { min-width: 0; }
.sprite-col .char-sprite-wrap { height: 100%; }
.sprite-col .char-sprite { height: 100%; min-height: 220px; margin-bottom: 0; }

/* Compact stat block — used inside the modal stats column */
.stat-block.compact { display: flex; flex-direction: column; gap: 5px; margin-bottom: 0; }
.stat-block.compact .stat-row { grid-template-columns: 32px 1fr 38px; gap: 6px; font-size: 13px; }
.stat-block.compact .stat-label { font-size: 8px; }
.stat-block.compact .stat-bar { height: 9px; border-width: 1px; }
.stat-block.compact .stat-val { font-size: 13px; }

/* Season collapsibles */
.season-section { margin-bottom: 14px; }
.seasons-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* Allow the season list to scroll when content overflows the roster panel
     (long season grids, multi-team layout pushing the team-slots row down).
     Flex hierarchy needs min-height:0 so this doesn't blow out the parent. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 6px;
}
.seasons-container .season-section { margin-bottom: 0; }

/* Three states for a season: expanded (full grid), mini (75% smaller preview), collapsed (tab only) */
.season-section.state-expanded { order: 0; }
.season-section.state-mini { order: 1; }
.season-section.state-collapsed { order: 2; }

/* Mini state: shrink the grid using compact tile dimensions (no transform, so layout reflows naturally) */
.season-section.state-mini .roster-grid {
  display: grid;
  gap: 3px;
  cursor: pointer;
  max-width: 25%;
}
.season-section.state-mini .roster-tile {
  height: auto;
  aspect-ratio: 1 / 1;
  padding: 0;
  border-width: 1px;
}
.season-section.state-mini .roster-tile .tile-label { display: none; }
.season-section.state-mini .roster-tile .tile-art { font-size: 0; }

/* Collapsed-tab: hide the grid, only the toggle button is visible */
.season-section.state-collapsed .roster-grid { display: none; }

/* Override roster grid columns to 4 for S2/S3/S4 placeholder grids */
.roster-grid.roster-grid-s234 { grid-template-columns: repeat(4, 1fr); }
.season-tabs-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 14px;
  margin-bottom: 14px;
}
.season-tabs-row .season-section { flex: 1 1 0; min-width: 140px; margin-bottom: 0; }
.season-tabs-row .season-toggle { display: block; width: 100%; text-align: left; }
.season-toggle {
  font-family: var(--font-pixel);
  font-size: 10px;
  letter-spacing: 1px;
  padding: 6px 12px;
  margin-bottom: 8px;
  background: var(--cream-2);
  color: var(--navy);
  border: 2px solid var(--navy);
  border-radius: 6px;
  cursor: pointer;
  display: inline-block;
}
.season-toggle.expanded { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); box-shadow: var(--glow); }
.season-toggle:hover { transform: translateY(-1px); }
.roster-grid.collapsed { display: none; }
.grid-empty {
  grid-column: 1 / -1;
  padding: 30px;
  text-align: center;
  font-family: var(--font-pixel);
  font-size: 10px;
  color: var(--navy-2);
  font-style: italic;
}

.char-sprite-wrap { position: relative; margin-bottom: 14px; }
.sprite-cycle-btn {
  display: none; /* hidden until JS detects multiple variants */
  position: absolute;
  bottom: 6px;
  left: 6px;
  font-family: var(--font-pixel);
  font-size: 9px;
  padding: 5px 10px;
  background: rgba(15, 15, 20, 0.85);
  color: var(--navy);
  border: 2px solid var(--accent-2);
  border-radius: 5px;
  cursor: pointer;
  letter-spacing: 1px;
  z-index: 3;
  box-shadow: var(--glow);
}
.sprite-cycle-btn:hover { background: var(--accent-2); color: #fff5e8; }
.sprite-cycle-btn.visible { display: inline-block; }

.stat-block { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.stat-row { display: grid; grid-template-columns: 40px 1fr 50px; align-items: center; gap: 10px; font-size: 16px; }
.stat-label { font-family: var(--font-pixel); font-size: 9px; }
.stat-bar { height: 12px; background: #0a0612; border: 2px solid var(--navy); border-radius: 4px; overflow: hidden; position: relative; }
.stat-fill { height: 100%; background: var(--navy); transition: width 0.3s; }
/* W3a item 3: roster-median tick — a thin marker at the roster-median value for
   this stat (js/screens.js statMedian()), so a raw number reads as above/below
   par at a glance. Only injected in the roster char-detail modal (#screen-roster). */
.stat-median-tick {
  position: absolute; top: 0; bottom: 0; width: 2px;
  background: rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
  z-index: 2;
  pointer-events: none;
}
/* Per-stat colors — distinct hues for quick visual scanning */
.stat-fill[data-stat="HP"],  .stat-fill[data-opp-stat="HP"],  .stat-fill[data-sukuna-stat="HP"]  { background: linear-gradient(90deg, #5fdc5f, #2f9e44); }
.stat-fill[data-stat="CE"],  .stat-fill[data-opp-stat="CE"],  .stat-fill[data-sukuna-stat="CE"]  { background: linear-gradient(90deg, #c084fc, #7c3aed); }
.stat-fill[data-stat="ACC"], .stat-fill[data-opp-stat="ACC"], .stat-fill[data-sukuna-stat="ACC"] { background: linear-gradient(90deg, #ffb347, #e8801f); }
.stat-fill[data-stat="EVA"], .stat-fill[data-opp-stat="EVA"], .stat-fill[data-sukuna-stat="EVA"] { background: linear-gradient(90deg, #6fdcf0, #2596be); }
.stat-fill[data-stat="UM"],  .stat-fill[data-opp-stat="UM"],  .stat-fill[data-sukuna-stat="UM"]  { background: linear-gradient(90deg, #ff7070, #d94a3d); }
.stat-fill[data-stat="CT"],  .stat-fill[data-opp-stat="CT"],  .stat-fill[data-sukuna-stat="CT"]  { background: linear-gradient(90deg, #ff6fd0, #c92a8a); }
.stat-val { text-align: right; font-size: 16px; }

.roster-actions { display: flex; gap: 10px; margin-top: auto; }

/* ============================================================
   BATTLE SCREEN
   ============================================================ */
#screen-battle { flex-direction: column; padding: 0; gap: 0; }

/* BUG-342 — battle-type banner + sacrificial top buffer (Joe, 2026-08-01).
   Two jobs: it names the battle ("PEAK SORCERY — QUICK MATCH"), and it is the
   strip that gets eaten first if mobile browser chrome ever overlaps the top of
   the stage, so the enemy name/HP row underneath stays readable. `flex: 0 0 auto`
   so it takes its own height out of the column and never squeezes the arena
   ambiguously. Kept deliberately short — every pixel here is one the arena loses. */
.battle-mode-banner {
  flex: 0 0 auto;
  background: #05070b;
  color: #ece6dd;
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 2px;
  text-align: center;
  padding: 7px 10px;
  border-bottom: 2px solid rgba(236, 230, 221, 0.18);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.battle-arena {
  /* C2 revised: back to the original 2-row grid (top = enemy / bottom = player).
     The legacy 1v1 layout is restored — multi-team battles still happen, but
     only the current attacker/defender pair shows here; everyone else lives in
     the turn-queue strip below. */
  /* BUG (2026-07-07 playtest, fit-in-viewport pass): was flex: 0 1 auto — the
     arena never grew past its own content height, so on tall desktop viewports
     (e.g. 1600x1300) the turn-queue + action menu below sat at a fixed height
     and the remaining ~40% of the screen was dead black space. flex-grow:1
     lets the arena absorb any extra vertical room (still capped by max-height
     so it never overwhelms the fixed HUD/action-menu rows below), while
     flex-shrink:1 keeps the existing shrink-to-fit behavior for short viewports. */
  flex: 1 1 auto;
  max-height: 64vh;
  /* BUG-257 fix: the arena only ever gets 64vh of height but was allowed full
     column width, so even an ordinary 16:9 window inflated its effective ratio
     past 2.5:1 and cropped the 16:9 arena art's horizon band (background-size:
     cover). Cap width to what a 16:9 box would need at this max-height (64 *
     16/9 ≈ 113.8vh) so it can't outgrow its own art's native ratio, and center
     it — letterboxed on the sides instead of stretched. */
  /* Explicit width:100% is required here, not just max-width — this is a flex
     item, and auto margins on a flex item's cross axis suppress the default
     align-items:stretch fill (they absorb the extra space instead of the item
     growing into it), so without an explicit width it collapses to content
     size instead of filling-then-clamping. */
  width: 100%;
  max-width: min(100%, 113.8vh);
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-rows: 0.8fr 1fr;
  padding: 14px;
  gap: 14px;
}

.arena-bg {
  position: absolute; inset: 0;
  z-index: 0;
  font-size: 14px;
}
/* POV crossfade — runs when the active character's POV switches between
   inside-a-closed-DE and outside (escapee). The class lives on .battle-arena
   so BOTH the arena background AND the combatant sprites/cards fade
   together — otherwise sprites snap-swap before the bg has a chance to
   change and the transition looks jarring. */
.battle-arena.pov-swap > .arena-bg,
.battle-arena.pov-swap > .combatant {
  animation: arena-pov-fade 250ms ease-in-out;
}
@keyframes arena-pov-fade {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0; }
}

/* Dual-DE simultaneous activation — two layered backgrounds split diagonally
   (top-left → bottom-right) with a fading blend, each behind its respective caster. */
.arena-bg .dual-layer {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
  pointer-events: none;
}
/* Player (bottom-left half) — visible toward bottom-left, fades out toward top-right */
.arena-bg .dual-layer.dual-player {
  -webkit-mask-image: linear-gradient(45deg, #000 35%, transparent 65%);
          mask-image: linear-gradient(45deg, #000 35%, transparent 65%);
}
/* Enemy (top-right half) — visible toward top-right, fades out toward bottom-left */
.arena-bg .dual-layer.dual-enemy {
  -webkit-mask-image: linear-gradient(45deg, transparent 35%, #000 65%);
          mask-image: linear-gradient(45deg, transparent 35%, #000 65%);
}

/* Original 1v1 combatant layout — two rows of (sprite + card). Player at bottom,
   enemy at top. Contents swap dynamically (see renderBattle) so multi-team
   battles still appear here as the current "exchange pair". */
.combatant {
  position: relative;
  display: grid;
  z-index: 2;
}
body:not(.layout-portrait) .combatant.enemy  { grid-template-columns: 410px 1fr; gap: 14px; align-items: start; }
body:not(.layout-portrait) .combatant.player { grid-template-columns: 1fr 410px; gap: 14px; align-items: end; }
body:not(.layout-portrait) .combatant.enemy .battle-sprite  { justify-self: end;   margin-right: 40px; }
body:not(.layout-portrait) .combatant.player .battle-sprite { justify-self: start; margin-left: 40px; }
/* Back (player) sprite slot — 15% larger than front */
body:not(.layout-portrait) .combatant.player .battle-sprite { height: 242px; max-width: 242px; }
/* Drop the bottom-left (player, back-view) sprite so the character's feet sit at
   the bottom of the HUD band. Back-sprite footroom (measured across all chars) ranges
   from 10–39 px rendered; 24 px is the largest offset that keeps every character's
   feet above the arena clip line (card_bottom + 14 px padding = arena overflow:hidden).
   `top` (not transform) so it composes with the lunge/turn-hop animations. */
body:not(.layout-portrait) .combatant.player .battle-sprite { top: 24px; }

.battle-sprite {
  font-size: 11px;
  overflow: visible;
  /* Sprites are pre-cutout transparent PNGs; we paint a thin white outline via drop-shadow */
  background-color: transparent;
  background-image: none;
  border: none;
  filter:
    drop-shadow(0.5px 0 0 white) drop-shadow(-0.5px 0 0 white)
    drop-shadow(0 0.5px 0 white) drop-shadow(0 -0.5px 0 white)
    drop-shadow(0.5px 0.5px 0 white) drop-shadow(-0.5px -0.5px 0 white)
    drop-shadow(0.5px -0.5px 0 white) drop-shadow(-0.5px 0.5px 0 white);
}
body:not(.layout-portrait) .battle-sprite {
  height: 210px;
  width: 100%;
  max-width: 210px;
}
/* Override .art-placeholder defaults so the checker bg + dashed border don't show through */
.battle-sprite.art-placeholder {
  background-color: transparent;
  background-image: none;
  border: none;
}
/* When a real sprite is loaded, setSpriteBackground sets background-image inline — re-allow it */
.battle-sprite.has-sprite { background-image: var(--bg, none); }
/* No data-asset label chip on battle sprites */
.battle-sprite::after { display: none; }

/* Active-turn emphasis: the sprite of whoever is currently acting hops forward
   and sits slightly enlarged with a warm halo, so it's obvious whose turn it is
   — including a lone player character that can't rotate out of the slot. Only
   the acting side lights up; the opposite slot (the target/next-up) stays calm.
   Static-sprite model: this is transform + a glow drop-shadow, never new frames.
   The filter restates the 8 white-outline shadows so the outline survives. */
.battle-sprite.turn-active {
  animation: spriteTurnHop 0.5s ease-out;
  transform: scale(1.06);
  transform-origin: center bottom;
  z-index: 5;
  filter:
    drop-shadow(0.5px 0 0 white) drop-shadow(-0.5px 0 0 white)
    drop-shadow(0 0.5px 0 white) drop-shadow(0 -0.5px 0 white)
    drop-shadow(0.5px 0.5px 0 white) drop-shadow(-0.5px -0.5px 0 white)
    drop-shadow(0.5px -0.5px 0 white) drop-shadow(-0.5px 0.5px 0 white)
    drop-shadow(0 0 8px rgba(255, 216, 77, 0.9));
}
@keyframes spriteTurnHop {
  0%   { transform: translateY(0)     scale(1.0);  }
  35%  { transform: translateY(-12px) scale(1.09); }
  100% { transform: translateY(0)     scale(1.06); }
}

/* Task 36: AI target-cycle scanning pulse — applied to whichever sprite is
   currently being previewed while the AI cycles through candidate targets.
   Toggled via body.ai-targeting-cycle (set by cycleAiTargetVisual). */
body.ai-targeting-cycle .enemy-sprite,
body.ai-targeting-cycle .player-sprite {
  animation: ai-cycle-pulse 0.22s ease-in-out infinite alternate;
}
/* GJ-1: DoT tick pulse — deliberately lighter than flinchFlash's full strobe
   (no shake/lunge/impact-burst layering; a DoT tick should read as "draining",
   not "impact"). One color per polarity: orange=burn, dark-red=bleed/leech,
   sickly-green=decay/poison. */
.dot-pulse-burn   { animation: dot-tick-burn 320ms ease-out; }
.dot-pulse-bleed  { animation: dot-tick-bleed 320ms ease-out; }
.dot-pulse-toxic  { animation: dot-tick-toxic 320ms ease-out; }
@keyframes dot-tick-burn {
  0%   { filter: drop-shadow(0 0 0 rgba(255, 140, 20, 0)); }
  35%  { filter: drop-shadow(0 0 10px rgba(255, 140, 20, 0.85)); }
  100% { filter: drop-shadow(0 0 0 rgba(255, 140, 20, 0)); }
}
@keyframes dot-tick-bleed {
  0%   { filter: drop-shadow(0 0 0 rgba(180, 20, 20, 0)); }
  35%  { filter: drop-shadow(0 0 10px rgba(180, 20, 20, 0.85)); }
  100% { filter: drop-shadow(0 0 0 rgba(180, 20, 20, 0)); }
}
@keyframes dot-tick-toxic {
  0%   { filter: drop-shadow(0 0 0 rgba(110, 200, 90, 0)); }
  35%  { filter: drop-shadow(0 0 10px rgba(110, 200, 90, 0.85)); }
  100% { filter: drop-shadow(0 0 0 rgba(110, 200, 90, 0)); }
}

/* GJ-4: status-land ring pulse — a distinct visual language (ring outline,
   not a tint glow) from the DoT tick pulses above, so "a new status just
   landed" reads differently from "an existing status is ticking". */
.status-land-hostile { animation: status-land-hostile-ring 380ms ease-out; }
.status-land-buff    { animation: status-land-buff-ring 380ms ease-out; }
@keyframes status-land-hostile-ring {
  0%   { filter: drop-shadow(0 0 0 rgba(120, 70, 200, 0)); }
  30%  { filter: drop-shadow(0 0 12px rgba(120, 70, 200, 0.9)); }
  100% { filter: drop-shadow(0 0 0 rgba(120, 70, 200, 0)); }
}
@keyframes status-land-buff-ring {
  0%   { filter: drop-shadow(0 0 0 rgba(90, 200, 220, 0)); }
  30%  { filter: drop-shadow(0 0 12px rgba(90, 200, 220, 0.9)); }
  100% { filter: drop-shadow(0 0 0 rgba(90, 200, 220, 0)); }
}

/* GJ-3: block/guard intercept — a sharper, brighter flash than a raw hit so
   "the guard held" reads distinctly from taking the hit unguarded. */
.block-intercept { animation: block-intercept-flash 260ms ease-out; }
@keyframes block-intercept-flash {
  0%   { filter: drop-shadow(0 0 0 rgba(240, 250, 255, 0)) brightness(1); }
  20%  { filter: drop-shadow(0 0 14px rgba(240, 250, 255, 1)) brightness(1.5); }
  100% { filter: drop-shadow(0 0 0 rgba(240, 250, 255, 0)) brightness(1); }
}

@keyframes ai-cycle-pulse {
  from { filter: drop-shadow(0 0 6px rgba(255, 215, 0, 0.0)); }
  to   { filter: drop-shadow(0 0 12px rgba(255, 215, 0, 0.85)); }
}
/* C2 legacy rules removed — .combatant.player / .combatant.enemy no longer
   exist (replaced by .team-zone). Per-slot sprite sizing now flows from
   .team-slots-strip[data-count="N"] rules above so it scales with team size. */

/* Character Card (battle) */
.char-card {
  background: var(--cream);
  border: var(--border);
  border-radius: var(--radius);
  padding: 6px 14px;
  box-shadow: var(--shadow);
  font-size: 16px;
}
/* Online-match player handle, shown ABOVE the character name in each HUD. */
.mp-player-name {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 0.5px;
  color: var(--accent, #e8b84b);
  background: var(--navy);
  border-radius: 4px;
  padding: 3px 6px;
  margin: -2px -8px 6px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
body.layout-portrait .mp-player-name { font-size: 7px; padding: 2px 5px; margin: -1px -4px 4px; }

/* ── Online SELECTION phase — reuse the roster/arena screens, hide SP chrome ── */
/* NOTE: .team-done-row is deliberately NOT hidden here — it carries the "LOCK IN
   TEAM →" confirm button (renderTeamDoneRow sets that label for _mpSelectMode==='roster',
   onTeamDone → confirmMpRoster), letting a player confirm their pick immediately instead
   of waiting out the select timer. Mirrors practice mode's Done button. */
body.mp-select #screen-roster .roster-header,
body.mp-select #screen-roster .ai-difficulty-carousel,
body.mp-select #screen-roster .completed-teams-banner,
body.mp-select #screen-roster .team-meta,
body.mp-select #screen-arena .roster-actions,
body.mp-select #screen-arena #arena-back { display: none !important; }
/* Show all seasons in MP with toggles intact — matches practice mode look */
body.mp-select #screen-roster .season-section { display: block !important; }
body.mp-select #screen-roster .season-section .roster-grid { display: grid !important; }
body.mp-select #screen-roster, body.mp-select #screen-arena { padding-top: 130px; padding-bottom: 20px; }

@keyframes mpsb-timer-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.55; transform: scale(1.12); }
}
#mp-select-bar {
  position: fixed; left: 50%; top: 12px; transform: translateX(-50%);
  z-index: 9000; min-width: 320px; max-width: 92vw;
  background: #c0001a; border: 3px solid #000;
  border-radius: 10px; box-shadow: 0 6px 26px rgba(0,0,0,0.65);
  padding: 11px 18px; display: flex; flex-direction: column; gap: 8px;
  font-family: var(--font-pixel);
}
#mp-select-bar .mpsb-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
#mp-select-bar .mpsb-title { font-size: 11px; letter-spacing: 1px; color: #000; }
#mp-select-bar .mpsb-timer { font-size: 15px; color: #fff; min-width: 30px; text-align: right; animation: mpsb-timer-pulse 1s ease-in-out infinite; }
#mp-select-bar .mpsb-timer.low { color: #fff; animation-duration: 0.4s; }
#mp-select-bar .mpsb-status { font-size: 8px; color: #000; opacity: 0.9; line-height: 1.55; text-align: center; }
#mp-select-bar .mpsb-status .mp-handle { color: #fff; }
#mp-select-bar .mpsb-confirm {
  font-family: var(--font-pixel); font-size: 10px; letter-spacing: 1px;
  padding: 9px 12px; border-radius: 6px; border: 2px solid #000;
  background: #000; color: #fff; cursor: pointer;
}
#mp-select-bar .mpsb-confirm:disabled { opacity: 0.5; cursor: default; background: transparent; color: #000; }
/* BUG-203: voluntary leave control — reuses .mp-btn (defined in the #mp-connect
   screen's injected <style id="mp-styles">) for visual parity with #mp-cancel;
   only overrides the bits needed to sit correctly inside the red select bar. */
#mp-select-bar #mpsb-leave {
  align-self: center; width: 100%; margin-top: 2px;
  border-color: #000; color: #000; background: rgba(0,0,0,0.08);
}
#mp-select-bar #mpsb-leave:hover { background: rgba(0,0,0,0.18); }

.arena-tile { position: relative; }
.mp-vote-rank {
  position: absolute; top: 5px; right: 5px; z-index: 3;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--hp-yellow); color: var(--navy);
  font-family: var(--font-pixel); font-size: 11px; line-height: 22px; text-align: center;
  box-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-pixel);
  font-size: 11px;
  border-bottom: 1px solid var(--navy);
  padding-bottom: 4px;
  margin-bottom: 5px;
  letter-spacing: 1px;
}
.card-header .char-name { flex-shrink: 0; }
.card-header .char-grade {
  font-size: 8px;
  opacity: 0.85;
  letter-spacing: 0.5px;
  text-align: right;
  line-height: 1.3;
}
.card-row {
  display: flex;
  align-items: center;
  gap: 22px;
  margin: 2px 0;
}
/* Bar has a fixed width so it never shifts between rows. Number sits immediately right of it. */
.bar-wrap { display: flex; align-items: center; gap: 8px; }
.bar-label {
  font-family: var(--font-pixel);
  font-size: 9px;
  color: #ffffff;
  min-width: 16px;
  text-align: right;
  flex: 0 0 auto;
  pointer-events: none;
}
.bar {
  flex: 0 0 80px;
  width: 80px;
  height: 12px;
  background: #4a4a4a;
  /* Design triage item 14: the old 2px var(--navy) (#ece6dd, warm off-white)
     border read weak against busy/bright arena art. Pure-white border for max
     contrast against dark art, plus a 1px near-black outer rim (box-shadow, so
     it doesn't affect layout) so the bar still reads a clean double-ring edge
     against LIGHT arena backgrounds too — a single white border alone can wash
     out on a pale sky/sand arena. */
  border: 2px solid #ffffff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.85);
  border-radius: 4px;
  overflow: hidden;
  position: relative;
}
.bar-fill {
  height: 100%;
  width: 100%;
  transition: width 0.5s ease, background-color 0.3s;
  position: relative;
  z-index: 1;
}
.bar-ghost {
  position: absolute;
  top: 0; left: 0;
  height: 100%;
  width: 0%;
  background: #c81010;
  z-index: 0;
  pointer-events: none;
}
/* GJ-8: CE's ghost trail — pale blue/white, distinct from HP's red "damage" tone. */
.bar-ghost-ce {
  position: absolute;
  top: 0; left: 0;
  height: 100%;
  width: 0%;
  background: #cfe8ff;
  z-index: 0;
  pointer-events: none;
}
.hp-bar .bar-fill { background: var(--hp-green); }
.hp-bar .bar-fill.med { background: var(--hp-yellow); }
.hp-bar .bar-fill.low { background: var(--hp-red); }
.ce-bar .bar-fill { background: var(--ce-blue); }

.bar-text {
  position: static;
  font-size: 13px;
  color: #fff5e8;
  white-space: nowrap;
  text-align: left;
  min-width: 90px;
  pointer-events: none;
}

.stat-inline {
  font-family: var(--font-pixel);
  font-size: 9px;
  white-space: nowrap;
  min-width: 100px;
  text-align: left;
}
.stat-inline.buffed { color: var(--buff-green); }
.stat-inline.debuffed { color: var(--debuff-red); }

.card-status-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}
.ap-dots {
  font-size: 18px;
  letter-spacing: 2px;
  color: var(--navy);
}
.status-chips { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 4px; max-width: 100%; overflow: hidden; }
.status-chip {
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 0.3px;
  padding: 2px 4px;
  border: 1px solid var(--navy);
  border-radius: 3px;
  background: var(--cream-2);
  cursor: help;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  white-space: nowrap;
  max-width: 100%;
  /* min-width:0 lets the ellipsis below actually engage: a flex item defaults to
     min-width:auto, which forbids shrinking below its content and makes a long
     chip (e.g. "DE: MALEVOLENT SHRINE") overflow the card and get clipped instead
     of ellipsized. With min-width:0 the chip shrinks to the card and ellipsizes. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* GJ-9: pop-in for a genuinely new chip (or one whose text just changed —
   e.g. a stack count ticking up), instead of snapping into the rebuilt row.
   .chip-escalate (INCINERATED) gets a stronger flash on top of the same pop. */
.status-chip.chip-enter { animation: chipPopIn 220ms ease-out; }
@keyframes chipPopIn {
  0%   { transform: scale(0.6); opacity: 0; box-shadow: 0 0 0 rgba(255,255,255,0); }
  55%  { transform: scale(1.12); opacity: 1; box-shadow: 0 0 8px rgba(255,255,255,0.7); }
  100% { transform: scale(1); opacity: 1; box-shadow: 0 0 0 rgba(255,255,255,0); }
}
.status-chip.chip-escalate { animation: chipEscalateFlash 420ms ease-out; }
@keyframes chipEscalateFlash {
  0%   { transform: scale(0.6); opacity: 0; box-shadow: 0 0 0 rgba(255, 60, 40, 0); }
  40%  { transform: scale(1.3); opacity: 1; box-shadow: 0 0 14px rgba(255, 60, 40, 1); }
  100% { transform: scale(1); opacity: 1; box-shadow: 0 0 0 rgba(255, 60, 40, 0); }
}
.status-chip.buff,
.status-chip.s-buff { background: var(--buff-green); color: #0a0612; border-color: var(--buff-green); font-weight: bold; }
.status-chip.debuff,
.status-chip.s-debuff { background: var(--debuff-red); color: #fff5e8; border-color: var(--debuff-red); font-weight: bold; }
.status-chip.bf,
.status-chip.s-bf { background: #0a0a13; color: #fff5e8; border-color: #ffd84d; font-weight: bold; }
/* Bashful Fortress (Panda Triceratops Mode): red cartoon triceratops-face icon
   leads the chip — assets/ui/triceratops_face.svg (CC BY 3.0, Delapouite /
   game-icons.net, recolored). Joe's ruling 2026-07-13: icon, not an emoji. */
.status-chip.s-bashful { background: #1a0705; color: #ffd9d2; border-color: #ff4b3b; font-weight: bold; display: inline-flex; align-items: center; gap: 4px; }
.status-chip.s-bashful::before { content: ''; width: 13px; height: 13px; flex: 0 0 auto; background: url('assets/ui/triceratops_face.svg?v=1') center/contain no-repeat; }
/* BF cluster: ATK boost (golden), CE Cost discount (sky-blue), CE Regen boost
   (mint). All share the deep BF window aesthetic but each color codes one
   distinct stat so the player can read the cluster at a glance. */
.status-chip.s-bf-atk   { background: #2a1f00; color: #fff5e8; border-color: #ffd24a; }
.status-chip.s-bf-cost  { background: #001a2a; color: #fff5e8; border-color: #6ec6ff; }
.status-chip.s-bf-regen { background: #001a14; color: #fff5e8; border-color: #5ee69a; }
/* Chain chip: surfaces the current die threshold so the player knows what
   roll to chase for the NEXT BF stack. Magenta for distinction from the
   three buff chips above. Hidden when stack is already at the ×5 cap. */
.status-chip.s-bf-chain { background: #1a002a; color: #fff5e8; border-color: #d27aff; font-weight: bold; }
.status-chip.s-burn,
.status-chip.s-scorch { background: #ff7a00; color: #1a0f0a; border-color: #ff7a00; font-weight: bold; }
.status-chip.s-scorch { box-shadow: 0 0 6px rgba(255, 122, 0, 0.7); }
/* INCINERATED — terminal phase of the Burn cascade. Final dramatic chip
   that overrides Burn + Scorched display when total stacks exceed 40.
   Next DoT tick after it appears KOs the target. */
.status-chip.s-incinerated {
  background: #1a0000;
  color: #ffe066;
  border-color: #ffae42;
  font-weight: bold;
  text-shadow: 0 0 4px rgba(255, 174, 66, 0.9);
  box-shadow: 0 0 10px rgba(255, 60, 0, 0.9), inset 0 0 4px rgba(255, 200, 80, 0.6);
}
.status-chip.s-bleed { background: #c0392b; color: #fff5e8; border-color: #c0392b; font-weight: bold; }
.status-chip.s-leech { background: #d4a417; color: #1a0f0a; border-color: #d4a417; font-weight: bold; }
.status-chip.s-stun { background: #2e7dc2; color: #fff5e8; border-color: #2e7dc2; font-weight: bold; }
.status-chip.s-infinity { background: #6a45c4; color: #fff5e8; border-color: #6a45c4; font-weight: bold; }
.status-chip.s-sixeyes { background: #6fbce8; color: #06243f; border-color: #bfe4fb; font-weight: bold; box-shadow: 0 0 6px rgba(135, 206, 235, 0.75); }   /* Six Eyes — sky blue, always-on Gojo passive */
.status-chip.s-cer { background: #2c8a4a; color: #fff5e8; border-color: #2c8a4a; font-weight: bold; }
.status-chip.s-trapped { background: #5a1a4a; color: #fff5e8; border-color: #5a1a4a; font-weight: bold; }
/* BUG-296 (T7): stands alongside TRAPPED — only rendered when casterCanSureHit
   is actually true for this combatant (game.js renderCombatantCard), so a
   crimson/danger accent reads as "guaranteed hit" without duplicating TRAPPED's
   purple. */
.status-chip.s-surehit { background: #7a0000; color: #fff5e8; border-color: #ff3b3b; font-weight: bold; box-shadow: 0 0 6px rgba(255, 59, 59, 0.6); }
/* SURE-HIT USER (Joe's 2026-07-31 ruling): caster-side twin of s-surehit — same
   crimson family so both sides of the auto-hit read as one mechanic, but with
   the caster chip's gold text so USER vs TARGET is distinguishable at a glance. */
.status-chip.s-surehit-user { background: #7a0000; color: #ffd76a; border-color: #ff3b3b; font-weight: bold; box-shadow: 0 0 6px rgba(255, 59, 59, 0.6); }
.status-chip.s-escaped { background: #5a5a6a; color: #fff5e8; border-color: #5a5a6a; font-weight: bold; }
.status-chip.s-domain-caster { background: #1a3a5c; color: #fff5e8; border-color: #4a8cd9; font-weight: bold; box-shadow: 0 0 4px rgba(74, 140, 217, 0.5); }
.status-chip.s-sukuna-emergence { background: #b8001f; color: #fff5e8; border-color: #ff3a55; font-weight: bold; box-shadow: 0 0 4px rgba(255, 58, 85, 0.6); }
.status-chip.s-finger { background: #4a0812; color: #ffd9de; border-color: #c81e2c; font-weight: bold; }
/* Nobara nail stacks — iron/steel look so the planted-nails count reads at a
   glance on the target's card (Hairpin/Resonance detonate these). */
.status-chip.s-nails { background: #3c3f4a; color: #f0eadf; border-color: #b9bcc8; font-weight: bold; box-shadow: 0 0 4px rgba(185, 188, 200, 0.45); }

/* Shikigami statuses — 10 distinct colors, mnemonic where natural */
.status-chip.s-tracked     { background: #6e6e7a; color: #f0f0f5; border-color: #9a9ab0; font-weight: bold; }   /* scent — gray */
.status-chip.s-guarded     { background: #6a45c4; color: #fff5e8; border-color: #9472d8; font-weight: bold; box-shadow: 0 0 4px rgba(106, 69, 196, 0.5); }  /* Totality — deep purple */
.status-chip.s-shielded    { background: #2080a8; color: #e0f4ff; border-color: #58b4d4; font-weight: bold; box-shadow: 0 0 4px rgba(32, 128, 168, 0.5); }   /* Nue wings — teal */
.status-chip.s-grounded    { background: #6b4321; color: #f5dbb5; border-color: #8a5a30; font-weight: bold; }   /* earthbound — brown */
.status-chip.s-airborne    { background: #7cb4e8; color: #0a1a2e; border-color: #b0d4f5; font-weight: bold; }   /* sky — light blue */
.status-chip.s-bind        { background: #58234a; color: #f5d5e8; border-color: #8a3a72; font-weight: bold; box-shadow: 0 0 4px rgba(138, 58, 114, 0.6); }  /* restraint — magenta */
.status-chip.s-repositioned{ background: #d4b020; color: #1a0f0a; border-color: #f0d052; font-weight: bold; }   /* dodge — gold */
.status-chip.s-wet         { background: #1a4d7e; color: #d0e8f8; border-color: #4080b8; font-weight: bold; box-shadow: 0 0 4px rgba(64, 128, 184, 0.6); }  /* water — deep blue */
.status-chip.s-off-balance { background: #c4651a; color: #fff5e8; border-color: #e08838; font-weight: bold; }   /* unsteady — orange */
.status-chip.s-summoner    { background: #4a3a7c; color: #fff5e8; border-color: #6a5aa0; font-weight: bold; }   /* shikigami's owner relation — indigo */
/* VOW chips — each vow its own distinct color, with a glow so the binding-contract
   trio reads as a family: Gamble = neon pink, Restrict = cyan, Reveal = amber. */
.status-chip.s-gamble   { background: #e0218a; color: #fff5e8; border-color: #ff5cb0; font-weight: bold; box-shadow: 0 0 5px rgba(224, 33, 138, 0.65); }
.status-chip.s-restrict { background: #0baecf; color: #0a0612; border-color: #4fd6ee; font-weight: bold; box-shadow: 0 0 5px rgba(11, 174, 207, 0.6); }
.status-chip.s-reveal   { background: #f59e0b; color: #0a0612; border-color: #ffc24d; font-weight: bold; box-shadow: 0 0 5px rgba(245, 158, 11, 0.6); }

/* ============================================================
   DOMAIN EXPANSION BANNER OVERLAY
   Plays a 3-step black banner sequence when a DE activates uncontested:
     1. "DOMAIN EXPANSION" (2s)   2. ability name (2s)   3. character flash art (2s)
   Scoped to the .battle-arena container (does NOT cover the action menu below).
   ============================================================ */
#de-banner-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  z-index: 50;
  display: none;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
#de-banner-overlay.active { display: flex; }
.de-banner {
  width: 100%;
  background: #000;
  border-top: 3px solid #fff;
  border-bottom: 3px solid #fff;
  color: #fff5e8;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-pixel);
  font-size: 18px;
  letter-spacing: 2px;
  text-transform: uppercase;
  text-align: center;
  padding: 14px 20px;
  box-sizing: border-box;
  animation: de-banner-slide 0.35s cubic-bezier(.22,1,.36,1);
  overflow: hidden;
}
.de-banner.de-banner-art {
  font-size: 0;
  padding: 0;
  background: #000;
  height: 50%;          /* 50% of original arena height — image inside fills this; banner remains full-width */
  max-height: 50%;
  width: 100%;
}
.de-banner.de-banner-art img {
  width: auto;
  height: 100%;         /* fills the now-smaller banner card vertically */
  max-width: 100%;
  object-fit: contain;
  display: block;
  margin: 0 auto;       /* horizontal centering reinforcement */
}
@keyframes de-banner-slide {
  from { transform: translateX(-100%); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

/* ----- Maximum Technique flash banner — reuses #de-banner-overlay but with a
   crimson accent so it reads distinct from the white Domain Expansion banner.
   Sequence: "MAXIMUM TECHNIQUE" -> technique name -> pulsating character art,
   then the ability VFX/SFX + damage fire (see playMaxTechBanners). ----- */
.maxtech-banner {
  border-top-color: #d94a3d;
  border-bottom-color: #d94a3d;
  color: #ffe1da;
  text-shadow: 0 0 10px rgba(217, 74, 61, 0.75), 0 0 22px rgba(217, 74, 61, 0.4), 0 2px 0 #000;
}
.maxtech-banner.maxtech-banner-art { border-color: #d94a3d; }

/* ----- Clash cinematic — side-by-side cards, alternating pulse, roll reveal ----- */
/* Clash title banner — step 1 of the cinematic, before the side-by-side
   cards appear. Bigger, glowy, pulses to grab attention so the player sees
   it announce the clash even with the timer running in the background. */
.de-banner-clash-title {
  font-size: 28px !important;
  letter-spacing: 5px !important;
  color: #ffd84d !important;
  text-shadow:
    0 0 12px rgba(255, 216, 77, 0.85),
    0 0 24px rgba(217, 74, 61, 0.6),
    0 2px 0 #000;
  animation: de-banner-slide 0.35s cubic-bezier(.22,1,.36,1),
             clash-title-pulse 1.2s ease-in-out 0.35s infinite alternate;
}
@keyframes clash-title-pulse {
  from { transform: scale(1);     filter: brightness(1);    }
  to   { transform: scale(1.06);  filter: brightness(1.18); }
}

/* Task 37: trap-roll banner (4th step after DE flash banner). Larger turn-queue
   strip with each non-caster's d20 trap roll. Red = trapped, green = escaped. */
.de-trap-roll-banner {
  background: rgba(0, 0, 0, 0.92) !important;
  border: 2px solid rgba(255, 216, 77, 0.75) !important;
  padding: 18px 28px !important;
  display: flex !important;
  flex-direction: column !important;
  align-items: center !important;
  gap: 16px !important;
  min-width: 70vw;
  max-width: 92vw;
  transition: opacity 0.6s ease, transform 0.6s ease;
  /* BUG-285 (U4): .de-banner's overflow:hidden silently clipped a tall
     wrapped .trap-roll-strip (many combatants → 2+ rows) instead of scrolling
     — the banner just cut results off with no way to see them. Override with
     a real scroll budget + visible thumb (same convention as .modal-card). */
  max-height: min(70vh, 560px) !important;
  overflow-y: auto !important;
  overflow-x: hidden !important;
}
.de-trap-roll-banner::-webkit-scrollbar { width: 10px; }
.de-trap-roll-banner::-webkit-scrollbar-thumb { background: rgba(255, 216, 77, 0.4); border-radius: 6px; }
.de-trap-roll-banner::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.3); }
.de-trap-roll-banner.trap-roll-exit {
  opacity: 0;
  transform: translateY(-12px);
}
.trap-roll-title {
  font-family: var(--font-pixel, monospace);
  font-size: 22px;
  letter-spacing: 4px;
  color: #ffd84d;
  text-shadow: 0 0 10px rgba(255, 216, 77, 0.6), 0 2px 0 #000;
}
.trap-roll-strip {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
  align-items: flex-start;
}
.trap-roll-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  background: rgba(20, 20, 20, 0.85);
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-radius: 8px;
  min-width: 92px;
  animation: trap-roll-reveal 0.35s cubic-bezier(.22,1,.36,1) both;
  transition: opacity 0.6s ease, transform 0.5s ease;
}
@keyframes trap-roll-reveal {
  from { opacity: 0; transform: translateY(10px) scale(0.92); }
  to   { opacity: 1; transform: translateY(0)    scale(1);    }
}
.trap-roll-pfp {
  width: 64px;
  height: 64px;
  border-radius: 6px;
  background-size: cover;
  background-position: center top;
  background-color: #222;
  border: 1.5px solid rgba(255, 255, 255, 0.4);
}
.trap-roll-name {
  font-family: var(--font-pixel, monospace);
  font-size: 11px;
  letter-spacing: 1.5px;
  color: #eee;
}
.trap-roll-num {
  font-family: var(--font-pixel, monospace);
  font-size: 28px;
  font-weight: 800;
  line-height: 1;
}
.trap-roll-num.is-trapped { color: #ff5252; text-shadow: 0 0 8px rgba(255, 82, 82, 0.7), 0 2px 0 #000; }
.trap-roll-num.is-escaped { color: #4ade80; text-shadow: 0 0 8px rgba(74, 222, 128, 0.7), 0 2px 0 #000; }
/* Slot-machine spin: neutral color + slight jitter while the number cycles. */
.trap-roll-num.is-spinning {
  color: #cfe3ff;
  text-shadow: 0 0 6px rgba(160, 200, 255, 0.6), 0 2px 0 #000;
  animation: trap-roll-spin-jitter 0.14s steps(1) infinite alternate;
}
@keyframes trap-roll-spin-jitter {
  from { transform: translateY(-1px); opacity: 0.8; }
  to   { transform: translateY(1px);  opacity: 1; }
}
.trap-roll-vs {
  font-family: var(--font-pixel, monospace);
  font-size: 10px;
  color: #aaa;
  letter-spacing: 1px;
}

/* Escapee chip: fade out after rolls resolve. */
.trap-roll-chip.chip-fade {
  opacity: 0;
  transform: scale(0.85);
}
/* Trapped chip: blink purple — mirrors the turn queue's tq-domain-pulse. */
.trap-roll-chip.chip-blink {
  animation: trap-roll-blink 0.85s ease-in-out infinite alternate;
  border-color: rgba(160, 90, 255, 0.9);
}
@keyframes trap-roll-blink {
  from { background: rgba(20, 15, 30, 0.85); box-shadow: 0 0 0 2px #8a3dff, 0 0 4px 1px rgba(160, 90, 255, 0.55); }
  to   { background: rgba(50, 20, 80, 0.95); box-shadow: 0 0 0 2px #c79bff, 0 0 18px 4px rgba(184, 120, 255, 0.95); }
}

/* Sequential reveal: per-character result label ("Escaped!" / "Trapped?!")
   above the PFP, plus pending/rolling/resolved chip states. */
.trap-roll-result-label {
  font-family: var(--font-pixel, monospace);
  font-size: 13px; font-weight: 800; letter-spacing: 1px;
  min-height: 15px; white-space: nowrap;
  opacity: 0; transform: translateY(8px) scale(0.7);
  transition: opacity 0.25s ease, transform 0.28s cubic-bezier(.22, 1.5, .36, 1);
}
.trap-roll-result-label.show { opacity: 1; transform: translateY(0) scale(1); }
.trap-roll-result-label.is-trapped { color: #ff5252; text-shadow: 0 0 9px rgba(255, 82, 82, 0.85), 0 2px 0 #000; }
.trap-roll-result-label.is-escaped { color: #4ade80; text-shadow: 0 0 9px rgba(74, 222, 128, 0.85), 0 2px 0 #000; }
.trap-roll-chip.is-pending { animation: none; opacity: 0.32; }
.trap-roll-chip.is-rolling {
  animation: none; opacity: 1; transform: scale(1.07);
  border-color: #ffd84d; box-shadow: 0 0 18px rgba(255, 216, 77, 0.55);
}
.trap-roll-chip.chip-resolved { animation: none; opacity: 1; border-color: rgba(74, 222, 128, 0.85); }

/* DOMAIN-ART REVEAL — a circle of the dome interior grows from the centre to
   fill the arena while it shakes, over 3s. The reveal sits above the arena bg
   but below the combatant sprites (z-index 1) so the domain manifests behind
   the fighters. clip-path circle 0 → 75% covers the corners. */
.de-circle-reveal {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background-size: cover; background-position: center; background-repeat: no-repeat;
  clip-path: circle(0% at 50% 50%);
  animation: deCircleGrow 5s ease-in forwards;
}
@keyframes deCircleGrow {
  from { clip-path: circle(0% at 50% 50%); }
  to   { clip-path: circle(75% at 50% 50%); }
}
.battle-arena.de-grow-shake { animation: arenaGrowShake 0.3s ease-in-out infinite; }
@keyframes arenaGrowShake {
  0%,100% { transform: translate(0, 0); }
  25% { transform: translate(-4px, 3px); }
  50% { transform: translate(4px, -3px); }
  75% { transform: translate(-3px, 2px); }
}

.de-banner-clash {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 10px;
  height: 60%;
  background: #000;
  position: relative;
}
.clash-card {
  flex: 0 0 32%;
  height: 100%;
  border: 2px solid rgba(255,255,255,0.92);
  border-radius: 4px;
  position: relative;
  animation: clash-pulse 1.4s ease-in-out infinite;
  transform-origin: center center;
  container-type: inline-size;   /* BUG-349: lets the WINNER/LOSER label size itself to the CARD (cqw) — layout mode is a manual toggle, so a viewport/class-scoped rule can't cover a desktop-layout user in a narrow window */
}
/* BUG-349: the flash art + its soft-edge mask live on a ::before layer now,
   NOT on the card itself — a mask-image on the card alpha-faded EVERYTHING
   inside it, including the WINNER/LOSER labels pinned to the card's top edge
   (the mask's own fade band): the label text rendered at ~25-60% alpha and
   read as cut off. The art keeps its blend; the labels paint unmasked above.
   The card's background-image is delivered via --clash-art (inline style set
   in playDomainClashCinematic) so this layer split needs no extra JS. */
.clash-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--clash-art);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: inherit;
  /* Soft edges so overlapping cards blend during pulse */
  -webkit-mask-image: radial-gradient(ellipse at center, #000 70%, rgba(0,0,0,0.25) 100%);
          mask-image: radial-gradient(ellipse at center, #000 70%, rgba(0,0,0,0.25) 100%);
}
.clash-card.card-right { animation-delay: 0.7s; } /* alternating phase */
.clash-card.card-tie {
  animation: clash-pulse-tie 0.55s ease-in-out infinite;
  animation-delay: 0s !important; /* sync both cards */
  box-shadow: 0 0 14px 4px rgba(170, 110, 255, 0.85), 0 0 28px 8px rgba(170, 110, 255, 0.4);
  border-color: rgba(220, 180, 255, 0.95);
}
@keyframes clash-pulse {
  0%, 100% { transform: scale(1.0); }
  50%      { transform: scale(1.08); }
}
@keyframes clash-pulse-tie {
  0%, 100% { transform: scale(1.0); }
  50%      { transform: scale(1.15); }
}

/* Red splotchy spray-paint X on losing card */
.clash-card-x {
  position: absolute; inset: 0;
  pointer-events: none;
  animation: clash-x-reveal 0.35s ease-out forwards;
}
.clash-card-x::before,
.clash-card-x::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 4%;
  right: 4%;
  height: 22%;
  background-color: transparent;
  background-image:
    radial-gradient(ellipse at 50% 50%, rgba(220,0,15,0.96) 0%, rgba(170,0,10,0.85) 40%, rgba(140,0,5,0.35) 75%, transparent 100%),
    radial-gradient(circle at 18% 38%, rgba(255,30,30,0.92) 0%, rgba(190,5,5,0.65) 30%, transparent 55%),
    radial-gradient(circle at 38% 62%, rgba(245,15,15,0.9) 0%, rgba(170,0,0,0.55) 30%, transparent 55%),
    radial-gradient(circle at 62% 36%, rgba(255,40,40,0.88) 0%, rgba(180,0,0,0.6) 30%, transparent 55%),
    radial-gradient(circle at 82% 60%, rgba(230,10,10,0.92) 0%, rgba(150,0,0,0.55) 30%, transparent 55%),
    radial-gradient(circle at 8% 18%, rgba(200,0,0,0.85) 0%, transparent 8%),
    radial-gradient(circle at 14% 78%, rgba(220,10,10,0.7) 0%, transparent 6%),
    radial-gradient(circle at 28% 14%, rgba(190,5,5,0.6) 0%, transparent 7%),
    radial-gradient(circle at 30% 88%, rgba(255,30,30,0.5) 0%, transparent 5%),
    radial-gradient(circle at 48% 6%, rgba(180,0,0,0.55) 0%, transparent 7%),
    radial-gradient(circle at 56% 94%, rgba(220,15,15,0.6) 0%, transparent 6%),
    radial-gradient(circle at 72% 14%, rgba(200,0,0,0.7) 0%, transparent 8%),
    radial-gradient(circle at 80% 86%, rgba(255,25,25,0.5) 0%, transparent 5%),
    radial-gradient(circle at 92% 24%, rgba(180,0,0,0.6) 0%, transparent 7%),
    radial-gradient(circle at 95% 72%, rgba(220,10,10,0.55) 0%, transparent 6%);
  transform-origin: center;
  filter: blur(0.4px) contrast(1.05);
}
.clash-card-x::before { transform: translateY(-50%) rotate(28deg) skewX(-4deg); }
.clash-card-x::after  { transform: translateY(-50%) rotate(-32deg) skewX(3deg); }
@keyframes clash-x-reveal {
  0%   { opacity: 0; transform: scale(0.3) rotate(-12deg); }
  60%  { opacity: 1; transform: scale(1.2) rotate(4deg); }
  100% { opacity: 1; transform: scale(1.0) rotate(0deg); }
}

/* WINNER / LOSER labels — replace the old red-X marker. Pinned to the
   TOP of each clash card (rolls live at the bottom), full width with a punchy
   text-shadow so they read against the flash art behind. Slide-down reveal. */
.clash-card-label {
  position: absolute;
  left: 0; right: 0; top: 0;
  z-index: 1;   /* above the masked ::before art layer (BUG-349) */
  padding: 10px 0;
  font-family: var(--font-pixel);
  /* BUG-349 second face: at a fixed 22px + 4px letter-spacing, "WINNER" needs
     more width than a narrow clash card has (measured: 156px of glyphs vs a
     121px card at a 375px viewport) — the word overflowed both sides and read
     as chopped. Card-relative sizing (cqw, via the card's container-type):
     ~13.5cqw keeps 6 glyphs + spacing inside ANY card width, capped at the
     original 22px so wide/desktop cards render exactly as before. */
  font-size: clamp(9px, 13.5cqw, 22px);
  letter-spacing: min(4px, 1.5cqw);
  text-align: center;
  text-shadow:
    0 2px 0 #000,
    0 0 10px rgba(0,0,0,0.85);
  pointer-events: none;
  animation: clash-label-reveal 0.45s cubic-bezier(.22,1,.36,1) forwards;
}
.clash-card-label-winner {
  color: #ffd84d;
  background: linear-gradient(to bottom, rgba(40,30,0,0.85) 0%, rgba(40,30,0,0) 100%);
  text-shadow:
    0 2px 0 #000,
    0 0 14px rgba(255, 216, 77, 0.85);
}
.clash-card-label-loser {
  color: #ff5c5c;
  background: linear-gradient(to bottom, rgba(40,0,0,0.85) 0%, rgba(40,0,0,0) 100%);
  text-shadow:
    0 2px 0 #000,
    0 0 14px rgba(217, 74, 61, 0.75);
}
@keyframes clash-label-reveal {
  0%   { opacity: 0; transform: translateY(-20px) scale(0.85); }
  60%  { opacity: 1; transform: translateY(4px) scale(1.08); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

/* Roll reveal — accumulating row-by-row */
.clash-rolls {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 6px 12px;
  background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 100%);
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 1px;
  color: #fff5e8;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: center;
}
.clash-roll-row {
  opacity: 0;
  animation: clash-roll-fade 0.35s ease-out forwards;
}
.clash-roll-row .roll-num { color: #ffd84d; font-weight: bold; }
.clash-roll-row.tie-row {
  /* TIE rows pulse + scale to grab attention since they're the dramatic
     simultaneous-activation trigger. Replaces fade-in with a punchier reveal. */
  animation: clash-tie-flash 0.6s ease-out forwards;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-shadow: 0 0 8px rgba(199, 154, 255, 0.8), 0 0 16px rgba(199, 154, 255, 0.5);
}
.clash-roll-row.tie-row    .roll-num { color: #c79aff; font-size: 13px; }
.clash-roll-row.winner-row .roll-num { color: #5ed479; }
@keyframes clash-roll-fade {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes clash-tie-flash {
  0%   { opacity: 0; transform: scale(0.6); }
  40%  { opacity: 1; transform: scale(1.25); }
  70%  { transform: scale(1.0); }
  100% { opacity: 1; transform: scale(1.05); }
}

/* Domain-clash slot reels — each round SPINS, then each reel LOCKS and colours by
   win/lose, mirroring the trap-roll slot machine: green for the higher roll (with a
   ding), red for the lower (with a buzzer); a tie greens BOTH with a rapid pulse +
   five dings; at the finale the winner's reels flash green before the domain opens. */
.clash-round-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  opacity: 0;
  animation: clash-roll-fade 0.3s ease-out forwards;
}
.clash-round-label { font-size: 8px; color: #9a8fb0; letter-spacing: 1px; min-width: 22px; text-align: right; }
.clash-side { font-size: 9px; color: #d6cfa8; letter-spacing: 1px; min-width: 54px; text-align: center; }
.clash-roll-vs { font-size: 8px; color: #888; letter-spacing: 1px; }
.clash-roll-cell {
  font-family: var(--font-pixel, monospace);
  font-size: 18px; font-weight: 800; line-height: 1;
  min-width: 28px; text-align: center;
  color: #fff5e8; text-shadow: 0 2px 0 #000;
  transition: color 0.15s ease;
}
.clash-roll-cell.spinning {
  color: #cfe3ff; text-shadow: 0 0 6px rgba(160, 200, 255, 0.6), 0 2px 0 #000;
  animation: trap-roll-spin-jitter 0.14s steps(1) infinite alternate;
}
.clash-roll-cell.win  { color: #4ade80; text-shadow: 0 0 10px rgba(74, 222, 128, 0.85), 0 2px 0 #000; animation: clash-cell-win 0.5s ease-out 3; }
.clash-roll-cell.lose { color: #ff5252; text-shadow: 0 0 10px rgba(255, 82, 82, 0.85), 0 2px 0 #000;  animation: clash-cell-lose 0.5s ease-out 3; }
.clash-roll-cell.tie  { color: #4ade80; text-shadow: 0 0 12px rgba(74, 222, 128, 0.95), 0 2px 0 #000;  animation: clash-cell-tie 0.22s ease-in-out 8; }
.clash-roll-cell.finale-flash {
  color: #5ed479; text-shadow: 0 0 16px rgba(94, 212, 121, 1), 0 0 28px rgba(94, 212, 121, 0.7), 0 2px 0 #000;
  animation: clash-cell-finale 0.45s ease-in-out infinite;
}
@keyframes clash-cell-win    { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.35); } }
@keyframes clash-cell-lose   { 0%, 100% { transform: scale(1); } 40% { transform: scale(0.82); } }
@keyframes clash-cell-tie    { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.3); } }
@keyframes clash-cell-finale { 0%, 100% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.25); filter: brightness(1.5); } }

/* Post-clash winner's flash art pulsates */
.de-banner.de-banner-art.pulsate img {
  animation: clash-winner-pulse 1.2s ease-in-out infinite;
  transform-origin: center;
}
@keyframes clash-winner-pulse {
  0%, 100% { transform: scale(1.0);  filter: brightness(1.0); }
  50%      { transform: scale(1.05); filter: brightness(1.15); }
}

/* ============================================================
   DOMAIN CLASH PROMPT
   Pops over the arena when a DE is declared and the opponent has a counter ready.
   Scoped to the .battle-arena container.
   ============================================================ */
#clash-prompt-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
}
#clash-prompt-overlay.active { display: flex; }
.clash-prompt-card {
  background: var(--cream);
  border: 4px solid var(--accent);
  border-radius: 10px;
  padding: 18px 26px;
  box-shadow: 0 0 20px rgba(170, 110, 255, 0.6);
  font-family: var(--font-pixel);
  text-align: center;
  max-width: 90%;
  pointer-events: auto;
}
.clash-prompt-title {
  font-size: 14px;
  color: var(--accent-2);
  letter-spacing: 2px;
  margin-bottom: 10px;
}
.clash-prompt-body {
  font-family: 'Press Start 2P', monospace;
  font-size: 10px;
  margin-bottom: 16px;
  line-height: 1.6;
  color: var(--navy);
}
.clash-prompt-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
}
.clash-prompt-actions button {
  font-family: var(--font-pixel);
  font-size: 9px;
  padding: 10px 14px;
  background: var(--accent);
  color: #fff5e8;
  border: 2px solid var(--accent-2);
  border-radius: 5px;
  cursor: pointer;
  letter-spacing: 1px;
}
.clash-prompt-actions button:hover { background: var(--accent-2); transform: translateY(-1px); }
.clash-prompt-actions button:disabled { opacity: 0.5; cursor: wait; }
.clash-prompt-timer { display: inline-block; margin-top: 10px; font-size: 12px; color: var(--accent-2); font-weight: bold; letter-spacing: 1px; }

#gojo-nullify-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
}
#gojo-nullify-overlay.active { display: flex; }
.gojo-nullify-card { border-color: #6fbce8; box-shadow: 0 0 20px rgba(111, 188, 232, 0.55); }
.gojo-nullify-title { color: #6fbce8; }
#gojo-nullify-yes { background: #6fbce8; border-color: #bfe4fb; color: #06243f; }
#gojo-nullify-yes:hover { background: #bfe4fb; }
#gojo-nullify-no { background: var(--accent); border-color: var(--accent-2); color: #fff5e8; }
#gojo-nullify-no:hover { background: var(--accent-2); }

/* Sukuna Emergence prompt (Joe 2026-07-18) — same structure as the domain-clash
   prompt (reuses .clash-prompt-* for card/title/body/actions/timer) with a crimson
   King-of-Curses accent, mirroring the gojo-nullify override pattern above. */
#emergence-prompt-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
}
#emergence-prompt-overlay.active { display: flex; }
.emergence-prompt-card { border-color: #c0392b; box-shadow: 0 0 22px rgba(192, 57, 43, 0.65); }
.emergence-prompt-title { color: #c0392b; }

/* Gojo summon confirm — same card geometry as the clash/emergence prompts (Joe's
   spec), recoloured to Gojo's Limitless blue so the three prompts stay visually
   distinct at a glance while sharing one structure. */
#gojo-summon-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
}
#gojo-summon-overlay.active { display: flex; }
#gojo-summon-overlay .clash-prompt-card { border-color: #4aa3ff; box-shadow: 0 0 26px rgba(74, 163, 255, 0.7); }
#gojo-summon-overlay .clash-prompt-title { color: #4aa3ff; }

/* Gojo roster-info popup (docs/GOJO-SUMMON-DESIGN.md §13) — same card geometry as
   the battle-scoped clash/emergence/gojo-summon prompts above (reuses
   .clash-prompt-card/.emergence-prompt-card as-is), but it fires from the ROSTER
   screen, outside .battle-arena, so it needs its own fixed full-viewport wrapper
   instead of position:absolute;inset:0 relative to the arena. */
#gojo-roster-info-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  z-index: 1000;
  display: none;
  align-items: center;
  justify-content: center;
}
#gojo-roster-info-overlay.active { display: flex; }

#emergence-prompt-yes { background: #c0392b; border-color: #e8674f; color: #fff5e8; }
#emergence-prompt-yes:hover { background: #e8674f; }
#emergence-prompt-no { background: var(--accent); border-color: var(--accent-2); color: #fff5e8; }
#emergence-prompt-no:hover { background: var(--accent-2); }

/* VFX layer — sits ABOVE arena bg but BELOW character cards/sprites so it never bleeds over UI */
.vfx-layer { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.vfx-flash {
  position: absolute; inset: 0;
  background: white;
  animation: flash 0.25s ease-out forwards;
}
.vfx-bf {
  position: absolute; inset: 0;
  background: radial-gradient(circle, rgba(255, 216, 77, 0.7) 0%, rgba(26, 34, 56, 0.7) 60%, transparent 100%);
  animation: bfBurst 0.6s ease-out forwards;
}
@keyframes flash { from { opacity: 1; } to { opacity: 0; } }
/* Self-target picker hover: pulse the actor's own duel-slot sprite with a
   glowing yellow outline so the player sees "you're targeting yourself"
   without the duel slot swap that ally/enemy hover would trigger. */
.self-target-blink {
  animation: self-target-pulse 0.7s ease-in-out infinite alternate;
  filter: drop-shadow(0 0 14px rgba(255, 215, 64, 0.95))
          drop-shadow(0 0 26px rgba(255, 215, 64, 0.65));
}
@keyframes self-target-pulse {
  from { filter: drop-shadow(0 0 6px rgba(255, 215, 64, 0.55))
                 drop-shadow(0 0 14px rgba(255, 215, 64, 0.35)); }
  to   { filter: drop-shadow(0 0 18px rgba(255, 215, 64, 1.0))
                 drop-shadow(0 0 32px rgba(255, 215, 64, 0.85)); }
}
@keyframes bfBurst {
  0% { opacity: 0; transform: scale(0.3); }
  40% { opacity: 1; transform: scale(1.1); }
  100% { opacity: 0; transform: scale(1.5); }
}
/* Lone-caster-in-DE: opposite duel slot is fully hidden — no sprite, no card,
   no queue-fallback "next up" ghost combatant. Visibility:hidden preserves
   layout so the slot's grid cell doesn't collapse and shift the dome bg. */
#player-card.is-empty,
#enemy-card.is-empty,
#screen-battle .player-sprite.is-empty,
#screen-battle .enemy-sprite.is-empty {
  visibility: hidden;
}

/* Team slots — 6 black white-outlined squares on the roster screen.
   Filled slots show the picked character's PFP; empty slots stay dark. */
.team-slots-row {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  padding: 14px 0 4px;
}
.team-slot {
  width: 56px;
  height: 56px;
  border-radius: 6px;
  background: #0f0f14;
  background-size: cover;
  background-position: center top;
  border: 2px solid #fff;
  box-sizing: border-box;
  image-rendering: pixelated;
  flex-shrink: 0;
}
.team-slot.empty {
  background-image: none;
}
.team-slot:not(.empty) {
  cursor: pointer;
}
.team-slot:not(.empty):hover {
  outline: 2px solid #ff4747;
  outline-offset: 1px;
}

/* "Done" row — sits directly under the team slots so finalizing a team is an
   explicit, unmistakable action. Advances to next team or jumps to arena once
   the last team is finalized. */
/* Slot-budget counter chip at the end of each team's slot row. Shows the
   sum of picked characters' slot costs vs the match-type's total budget
   (20 for 2/3-team modes, 10 for 4-team). Red when over budget. */
.team-slot-counter {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  margin-left: 18px;
  padding: 8px 14px;
  background: rgba(20, 22, 30, 0.6);
  border: 2px solid #4a4a55;
  border-radius: 8px;
  font-family: "Press Start 2P", monospace;
  font-size: 11px;
  color: #d6cfa8;
  white-space: nowrap;
}
.team-slot-counter .tsc-num { color: #ffd84d; font-size: 14px; }
.team-slot-counter .tsc-sep { color: #888; margin: 0 1px; }
.team-slot-counter .tsc-max { color: #c7c1a8; }
.team-slot-counter .tsc-label { margin-left: 6px; font-size: 8px; color: #888; letter-spacing: 1px; }
.team-slot-counter.over-budget {
  border-color: #d94a3d;
  background: rgba(60, 20, 20, 0.6);
}
.team-slot-counter.over-budget .tsc-num { color: #ff5c5c; }

/* Post-battle winning-team roster row + MVP highlight (battle-end screen). */
.end-winning-team {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin: 10px 0 4px;
}
.end-winning-team-title {
  font-family: var(--font-pixel, monospace);
  font-size: 11px;
  letter-spacing: 2px;
  color: #ffd84d;
}
.end-winning-team-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}
.end-team-slot { position: relative; cursor: default; }
.end-team-slot:not(.is-mvp) { opacity: 0.82; }
.end-team-slot.is-mvp {
  border-color: #ffd84d;
  animation: end-mvp-glow 1.1s ease-in-out infinite alternate;
}
@keyframes end-mvp-glow {
  from { box-shadow: 0 0 8px rgba(255, 216, 77, 0.55), 0 0 16px rgba(255, 216, 77, 0.28); }
  to   { box-shadow: 0 0 14px rgba(255, 216, 77, 1.0), 0 0 30px rgba(255, 216, 77, 0.6); }
}
.end-mvp-badge {
  position: absolute;
  top: -9px; left: 50%; transform: translateX(-50%);
  background: #ffd84d;
  color: #1a1500;
  font-family: var(--font-pixel, monospace);
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 1px;
  padding: 1px 4px;
  border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  white-space: nowrap;
}
.end-mvp-caption {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-pixel, monospace);
  font-size: 11px;
  color: #f0e6c0;
  /* Clear break so the MVP reads as its OWN line below the winning-team row,
     not tucked against the portraits (the 8px flex gap alone looked attached). */
  width: 100%;
  margin-top: 10px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 216, 77, 0.18);
}
.end-mvp-caption b { color: #ffd84d; }
/* MVP tag — moved OFF the portrait row to sit inline with the MVP info line below
   the winning-team slots, so the team and the MVP are on cleanly separate lines. */
.end-mvp-tag {
  background: #ffd84d;
  color: #1a1500;
  font-family: var(--font-pixel, monospace);
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 1px;
  padding: 2px 6px;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
/* MVP portrait + the rotating, twinkling star burst behind it. */
.end-mvp-pfp-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px; height: 40px;
  flex: 0 0 auto;
  margin-right: 2px;
}
.end-mvp-pfp {
  position: relative;
  z-index: 1;
  width: 36px; height: 36px;
  border-radius: 50%;
  background-color: #222;
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
  border: 2px solid #ffd84d;
  box-shadow: 0 0 6px rgba(255, 216, 77, 0.6);
}
.end-mvp-star {
  position: absolute;
  z-index: 0;
  left: 50%; top: 50%;
  width: 58px; height: 58px;
  margin: -29px 0 0 -29px;
  background: radial-gradient(closest-side, #fffdf0 0%, #ffe27a 38%, rgba(255, 216, 77, 0) 72%);
  /* 4-point sparkle */
  clip-path: polygon(50% 0%, 59% 41%, 100% 50%, 59% 59%, 50% 100%, 41% 59%, 0% 50%, 41% 41%);
  pointer-events: none;
  will-change: transform, opacity;
  animation: end-mvp-star-spin 7s linear infinite, end-mvp-star-twinkle 1.5s ease-in-out infinite alternate;
}
@keyframes end-mvp-star-spin { to { transform: rotate(360deg); } }
@keyframes end-mvp-star-twinkle { from { opacity: 0.5; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .end-mvp-star { animation: none; opacity: 0.85; } }

/* Floating SFX mute toggle — fixed top-right, available on every screen. */
.sfx-toggle {
  position: fixed;
  top: 10px;
  right: 10px;
  z-index: 100000;
  width: 34px;
  height: 34px;
  padding: 0;
  font-size: 16px;
  line-height: 1;
  border-radius: 8px;
  border: 2px solid rgba(255, 255, 255, 0.5);
  background: rgba(20, 24, 34, 0.85);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
  transition: border-color 0.12s ease, opacity 0.12s ease;
}
.sfx-toggle:hover { border-color: #ffd84d; }
.sfx-toggle.is-muted { opacity: 0.6; border-color: rgba(255, 92, 92, 0.7); }
/* "Tap for sound" cue — shown only before the first gesture, while the browser
   keeps audio locked (notably every page load on iOS). The button pulses and a
   small hint bubble sits to its left so the title-screen silence reads as
   "tap to begin," not "broken." Both clear the instant audio unlocks. */
.sfx-toggle.is-locked {
  border-color: #ffd84d;
  animation: sfxLockedPulse 1.4s ease-in-out infinite;
}
@keyframes sfxLockedPulse {
  0%, 100% { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 0 rgba(255, 216, 77, 0.5); }
  50%      { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 7px rgba(255, 216, 77, 0); }
}
.sfx-toggle.is-locked::after {
  content: 'tap for sound ♪';
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  white-space: nowrap;
  font-family: var(--font-pixel, monospace);
  font-size: 8px;
  letter-spacing: 0.3px;
  color: #ffd84d;
  background: rgba(20, 24, 34, 0.92);
  border: 1px solid rgba(255, 216, 77, 0.6);
  border-radius: 6px;
  padding: 4px 6px;
  pointer-events: none;
  animation: sfxHintBob 1.4s ease-in-out infinite;
}
@keyframes sfxHintBob {
  0%, 100% { opacity: 0.85; transform: translateY(0); }
  50%      { opacity: 1;    transform: translateY(2px); }
}
/* Blinking countdown shown under the 🔊 button after the first tap on mobile,
   while the AudioContext is resuming asynchronously (~3s). Clears on unlock. */
.sfx-unlock-countdown {
  position: fixed;
  top: 50px;
  right: 10px;
  z-index: 100000;
  width: 34px;
  text-align: center;
  font-family: var(--font-pixel, monospace);
  font-size: 14px;
  font-weight: bold;
  color: #ffd84d;
  text-shadow: 0 0 8px rgba(255, 216, 77, 0.9), 0 0 18px rgba(255, 216, 77, 0.5);
  pointer-events: none;
  animation: sfxCountdownBlink 0.6s ease-in-out infinite;
}
@keyframes sfxCountdownBlink {
  0%, 100% { opacity: 1;   transform: scale(1); }
  50%      { opacity: 0.3; transform: scale(1.15); }
}

/* Controller toggle (HUMAN / AI) — sits next to the slot counter on each
   team's row. Click to cycle. Color-coded so the player's team (default
   HUMAN) reads green; AI teams read red. */
.team-ctrl-toggle {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-left: 10px;
  padding: 7px 12px;
  background: rgba(20, 22, 30, 0.6);
  border: 2px solid #4a4a55;
  border-radius: 8px;
  cursor: pointer;
  font-family: "Press Start 2P", monospace;
  transition: transform 80ms ease, border-color 80ms ease;
}
.team-ctrl-toggle:hover { transform: translateY(-2px); }
.team-ctrl-toggle .tct-label { font-size: 7px; color: #888; letter-spacing: 1px; }
.team-ctrl-toggle .tct-value { font-size: 13px; letter-spacing: 1px; }
.team-ctrl-toggle.ctrl-human {
  border-color: #4caf50;
}
.team-ctrl-toggle.ctrl-human .tct-value { color: #5ed479; }
.team-ctrl-toggle.ctrl-ai {
  border-color: #d94a3d;
}
.team-ctrl-toggle.ctrl-ai .tct-value { color: #ff5c5c; }

.team-done-row {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 4px 0 10px;
}
.team-done-row:empty {
  display: none;
}
.team-done-btn {
  padding: 12px 20px;
  background: var(--accent, #d94a3d);
  color: #fff9e6;
  border: 2px solid #fff;
  border-radius: 6px;
  font-family: "Press Start 2P", monospace;
  font-size: 11px;
  cursor: pointer;
  white-space: nowrap;
  letter-spacing: 1px;
  box-shadow: 0 3px 0 rgba(0,0,0,0.35);
  transition: transform 80ms ease, background 80ms ease;
}
.team-done-btn:hover:not(:disabled) {
  transform: translateY(-2px);
  background: #ff5a4d;
}
.team-done-btn:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: 0 1px 0 rgba(0,0,0,0.35);
}
.team-done-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* Completed-teams banner — visible above the roster grid once at least one
   team has been finalized. Each prior team shows as a labeled row of PFPs. */
.completed-teams-banner {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 6px 14px 10px;
}
.completed-teams-banner:empty {
  display: none;
}
.completed-team-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: rgba(15,15,20,0.55);
  border: 1px solid #4a4a55;
  border-radius: 4px;
}
.completed-team-label {
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
  color: #c7c1a8;
  letter-spacing: 1px;
  min-width: 70px;
}
.completed-team-pfp {
  width: 32px;
  height: 32px;
  border-radius: 4px;
  border: 1px solid #fff;
  background: #0f0f14;
  background-size: cover;
  background-position: center top;
  image-rendering: pixelated;
}

/* Turn queue — horizontal strip of character PFPs in EVA-sorted order.
   Blinking white outline marks the active character. Shell tile (added in
   Ship B) replaces the caster's slot from the escapees' POV. */
.turn-queue {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;          /* overflow valve: extra tiles wrap to a 2nd row instead of clipping off-frame */
  gap: 10px;
  padding: 8px 14px;
  min-height: 64px;
  max-width: 100%;
  box-sizing: border-box;
  background: var(--cream-2);
  border-top: var(--border);
}
/* Even-split wrap (renderTurnQueue, game.js): once a battle's queue actually
   needs a 2nd row, JS switches to an explicit grid with a computed column
   count so the split is as even as possible (10 -> 5/5, 9 -> 5/4) instead of
   flex-wrap's greedy fill (10 -> 9-then-1-orphan). Below the wrap threshold
   this class is never applied and .turn-queue's original flex centering
   (above) is untouched. */
.turn-queue.tq-grid {
  display: grid;
  grid-template-columns: repeat(var(--tq-cols, 1), auto);
  justify-content: center;
  align-content: center;
}
.tq-tile {
  width: 48px;
  height: 48px;
  border-radius: 6px;
  position: relative;
  flex-shrink: 0;
  /* Thin checkered border: the checkerboard is the tile's own background; the
     PFP (::before, inset by the 3px border thickness) covers the centre, so a
     single ring of checker squares shows around the edge. */
  background-image: repeating-conic-gradient(#000 0% 25%, #fff 0% 50%);
  background-size: 6px 6px;
}
.tq-tile::before {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 3px;
  background-color: #0f0f14;
  background-image: var(--tq-pfp, none);
  background-size: cover;
  background-position: center top;
  image-rendering: pixelated;
}
.tq-tile.shell::before {
  background-color: #1a1a22;
}
/* Active character's tile pulsates */
.tq-tile.active {
  animation: tq-pulse 0.9s ease-in-out infinite;
}
@keyframes tq-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.16); }
}
/* Defeated character: tile stays in queue but darkened, shrunken, and
   topped with a skull emoji to signal KO at a glance. Overrides passed/
   active states so death visual always wins. */
.tq-tile.tq-dead {
  transform: scale(0.6);
  opacity: 0.55;
  filter: grayscale(1) brightness(0.45) contrast(1.05);
  animation: none !important;       /* kill the active pulse if KO'd mid-turn */
  position: relative;
}
.tq-tile.tq-skull,
.tq-tile .tq-skull {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 24px;
  line-height: 1;
  text-shadow: 0 0 4px rgba(0, 0, 0, 0.85), 0 0 2px rgba(0, 0, 0, 1);
  pointer-events: none;
  z-index: 3;
  filter: brightness(2);            /* counter the parent dimming so skull stays readable */
}

/* HP-band ring on each turn-queue PFP (set by renderTurnQueue from current HP):
   green 100–76% · yellow 75–51% · orange 50–26% · red 25–1%. A 2px box-shadow
   ring hugs the tile edge (no layout shift). */
.tq-tile.tq-hp-green  { box-shadow: 0 0 0 2px #45c057; }
.tq-tile.tq-hp-yellow { box-shadow: 0 0 0 2px #e8c61f; }
.tq-tile.tq-hp-orange { box-shadow: 0 0 0 2px #f0851f; }
.tq-tile.tq-hp-red    { box-shadow: 0 0 0 2px #e23b34; }
/* Active domain caster / trapped character (and the shell that proxies a caster)
   → a blinking purple glowing border. Rendered on ::after so it layers over the
   HP ring and coexists with the active-tile scale pulse (different property). */
.tq-tile.tq-domain::after {
  content: ''; position: absolute; inset: 0; border-radius: 6px;
  pointer-events: none; z-index: 2;
  animation: tq-domain-pulse 0.85s ease-in-out infinite;
}
@keyframes tq-domain-pulse {
  0%, 100% { box-shadow: 0 0 0 2px #8a3dff, 0 0 4px 1px rgba(160, 90, 255, 0.55); }
  50%      { box-shadow: 0 0 0 2px #c79bff, 0 0 13px 4px rgba(184, 120, 255, 0.95); }
}
/* C3: tiles whose character has already acted this round shrink to 50% and
   desaturate slightly. Resets on round rollover when turnIndex returns to 0
   and renderTurnQueue strips the data-passed attribute from all tiles. */
.tq-tile[data-passed="true"] {
  opacity: 0.45;
  filter: grayscale(0.65) brightness(0.75);
  transition: opacity 220ms ease, filter 220ms ease;
}

/* BUG-229 option-C(a): constant, cheap "this tile just took a hit" feedback —
   fires for EVERY landed hit (displayed or undisplayed victim, incl. AoE
   secondaries), from queueTileHitFx (game.js, wired in applyDamage). One-shot
   white ring flash, no loop — the default candidate from tools/queue-fx-audition.html
   ("Off-hit-1"). Class name kept swappable (single selector) so Joe can pick a
   different candidate later without touching the JS trigger site.
   Layered on ::after (z-index 4) so it sits above the HP ring / domain glow. */
.tq-tile.tq-hit-flash::after {
  content: ''; position: absolute; inset: 0; border-radius: 6px;
  pointer-events: none; z-index: 4;
  animation: tq-hit-flash 0.4s ease-out;
}
@keyframes tq-hit-flash {
  0%   { box-shadow: 0 0 0 3px #ffffff, 0 0 14px 4px rgba(255,255,255,0.9); }
  100% { box-shadow: 0 0 0 2px rgba(255,255,255,0); }
}
/* Compact rising damage number above the queue tile — smaller sibling of the
   sprite-slot .dmg-number, scaled down for the 48px tile footprint. */
.tq-dmg-number {
  position: absolute;
  left: 50%;
  top: -14px;
  transform: translateX(-50%);
  font-family: var(--font-pixel);
  font-size: 10px;
  color: #fff;
  text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000;
  pointer-events: none;
  z-index: 5;
  white-space: nowrap;
  line-height: 1;
  animation: tq-dmg-float 0.7s ease-out forwards;
}
.tq-dmg-number.crit { color: #ffd23f; font-size: 12px; }
@keyframes tq-dmg-float {
  0%   { opacity: 1; transform: translateX(-50%) translateY(0)    scale(1.2); }
  20%  { opacity: 1; transform: translateX(-50%) translateY(-4px) scale(1.0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-22px) scale(0.85); }
}
@media (prefers-reduced-motion: reduce) {
  .tq-tile.tq-hit-flash::after { animation: none !important; box-shadow: none !important; }
  .tq-dmg-number { animation: none !important; opacity: 1; transform: translateX(-50%) translateY(-10px); }
}

/* Battle VFX reduced-motion gate. Disables the vestibular-triggering motion —
   arena/screen shake, the INFINITE domain grow-shake, sprite lunge, impact burst,
   crit ring, and dodge slide — while KEEPING informational damage/miss numbers
   (they fade in place, no vertical drift, so hit feedback survives). The JS-driven
   crit-tumble is guarded in game.js (triggerCritTumble). !important + placement
   after the base defs guarantees the override regardless of source order. */
@keyframes dmgFadeRM { 0% { opacity: 1; } 72% { opacity: 1; } 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .battle-arena.shake-light,
  .battle-arena.shake-med,
  .battle-arena.shake-heavy,
  .battle-arena.de-grow-shake,
  .battle-sprite.lunge-up,
  .battle-sprite.lunge-down,
  .miss-dodge        { animation: none !important; }
  .impact-burst,
  .crit-ring,
  .heal-glow         { animation: none !important; opacity: 0 !important; }
  .dmg-number        { animation: dmgFadeRM 0.88s ease-out forwards !important; }
  .miss-number       { animation: dmgFadeRM 0.82s ease-out forwards !important; }
  .heal-number       { animation: dmgFadeRM 0.88s ease-out forwards !important; }
}

/* Bottom UI */
.battle-bottom {
  display: grid;
  background: var(--cream-2);
  border-top: var(--border);
}
body:not(.layout-portrait) .battle-bottom {
  flex-shrink: 0;
  /* BUG-286-adjacent (U7, improvement — no bug row): was a flat 264px, so a
     turn-queue that wraps to a 2nd row (large team, e.g. 6v6) grew taller
     and the arena above (the only flex:1 sibling) absorbed 100% of that
     extra height, shrinking the battle art for no reason — the action
     window itself never gave up any of its own fixed space. renderTurnQueue
     (game.js) now writes the queue's live rendered height to --tq-h; past a
     single row's ~64px this budget gives it back, shrinking the action
     window instead, down to a 180px floor so the buttons stay usable. */
  height: max(180px, calc(264px - max(0px, var(--tq-h, 64px) - 64px)));
  grid-template-columns: 1fr 2fr;
  gap: 14px;
  padding: 14px;
}

.event-log {
  background: var(--cream);
  border: var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  box-shadow: var(--shadow);
  overflow-y: auto;
  font-size: 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.log-event {
  display: flex;
  gap: 8px;
  padding: 6px 8px;
  /* Box outline around every event so each one reads as a discrete entry. */
  border: 1px solid rgba(236, 230, 221, 0.16);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.025);
  /* Never let a long unbroken string (e.g. the damage-breakdown formula) push
     past the box and clip — wrap it inside the event instead. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.log-event:last-child { border-bottom: 1px solid rgba(236, 230, 221, 0.16); }
.log-num {
  flex: 0 0 26px;
  height: 22px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-pixel);
  font-size: 9px;
  color: #fff5e8;
  font-weight: bold;
  margin-top: 2px;
  /* BUG-247: a flex-centered circle has no default nowrap on its text — once the
     event index reaches 3 digits ("146") it can word-break across two lines
     inside the fixed 26x22px pill. Same failure mode as BUG-223's round-tag
     wrap, different element (that fix was scoped to .log-body p.round-tag only). */
  white-space: nowrap;
}
.log-num-player { background: #4a8cd9; }                    /* blue */
.log-num-enemy  { background: var(--accent); }              /* red  */
.log-num-system { background: var(--cream-3); color: var(--navy-2); }
.log-body { flex: 1; min-width: 0; overflow-wrap: anywhere; word-break: break-word; }
.log-body p { margin: 0 0 2px 0; line-height: 1.35; overflow-wrap: anywhere; }
.log-body p:last-child { margin-bottom: 0; }
/* Round divider — centered, all-caps, BOLD so each round transition pops in
   the event log without needing a separate HUD chip. */
.log-body p.round-tag {
  text-align: center;
  font-family: var(--font-pixel, monospace);
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 5px;
  color: #ffd84d;
  text-shadow: 0 0 8px rgba(255, 216, 77, 0.55), 0 1px 0 rgba(0, 0, 0, 0.4);
  padding: 4px 0;
  margin: 0;
  /* BUG-223: .log-body/.log-body p both carry overflow-wrap:anywhere +
     word-break:break-word for normal event text — neither rule is number-aware,
     so a 2-3 digit round number ("— Round 100 —") could legally split mid-digit
     at narrow widths. The round tag is a short, centered banner (never needs
     mid-word wrapping), so force it back to a single unbreakable line. */
  white-space: nowrap;
  overflow-wrap: normal;
  word-break: normal;
}
/* Narrow viewports (portrait phone): 5px letter-spacing at 14px can outgrow
   the log column width even on one line once the round hits 3 digits. Tighten
   spacing/size so "— Round 100 —" still fits without wrapping or clipping. */
@media (max-width: 480px) {
  .log-body p.round-tag { font-size: 12px; letter-spacing: 2px; }
}
/* The round-divider event itself gets a gold-tinted box so round transitions
   stand out from regular event boxes (the per-event outline is shared). */
.log-event:has(p.round-tag),
.log-event:has(p.round-tag-meta) {
  border-color: rgba(255, 216, 77, 0.4);
  background: rgba(20, 20, 30, 0.35);
  justify-content: center;
}
.log-event:has(p.round-tag) .log-num,
.log-event:has(p.round-tag-meta) .log-num { display: none; }
.log-event:has(p.round-tag) .log-body,
.log-event:has(p.round-tag-meta) .log-body { flex: 0 1 auto; }
/* AI difficulty meta line directly above each Round tag. */
.log-body p.round-tag-meta {
  text-align: center;
  font-family: var(--font-pixel, monospace);
  font-size: 9px;
  letter-spacing: 3px;
  color: rgba(255, 216, 77, 0.85);
  padding: 0;
  margin: 0 0 2px 0;
}
.log-body p.crit { color: var(--accent); font-weight: bold; }
.log-body p.bf { color: #ffd84d; font-weight: bold; }
/* Damage-breakdown legibility line — dim, indented, monospace so the formula
   reads as a sub-note under the "loses X HP" line rather than a headline. */
.log-body p.breakdown {
  color: var(--navy-2);
  font-size: 0.92em;
  opacity: 0.82;
  padding-left: 10px;
  border-left: 2px solid rgba(168, 85, 247, 0.35);
}

.action-area {
  background: var(--cream);
  border: var(--border);
  border-radius: var(--radius);
  padding: 10px;
  box-shadow: var(--shadow);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 0;
  position: relative; /* anchor for .ai-thinking-overlay */
}
/* BUG-254: #de-banner-overlay hosts every full-screen cinematic banner (DE
   flash, trap-roll reveal, domain-clash, max-tech unleash) but it lives
   inside .battle-arena while the action menu lives in .battle-bottom — NOT
   siblings, so a plain `~` combinator can't reach across. `:has()` lets the
   shared ancestor (#screen-battle) react to the overlay's `.active` state and
   gate a non-sibling descendant, all in CSS: dim + pointer-events:none the
   grid so a click can't land mid-cinematic (e.g. during the clash banner).
   No JS class-toggle needed — see CLAUDE.md's game.js/screens.js scope note
   for why this couldn't just be done overlay-side instead. */
#screen-battle:has(#de-banner-overlay.active) .action-area {
  pointer-events: none;
  opacity: 0.35;
  filter: grayscale(0.5);
  transition: opacity 0.2s ease, filter 0.2s ease;
}

/* Task 48: AI cursor visualization. When the AI is mid-decision the action
   grid is visible (humans see what humans see) and a virtual cursor lights
   up each button via the .ai-cursor-hover class — same look as :hover so the
   simulation is indistinguishable from a real player at the controls. */
.ct-tile.ai-cursor-hover,
.target-tile.ai-cursor-hover,
.action-btn.ai-cursor-hover,
.tc-confirm.ai-cursor-hover {
  outline: 2px solid rgba(255, 216, 77, 0.85);
  outline-offset: 2px;
  box-shadow: 0 0 12px rgba(255, 216, 77, 0.45);
  filter: brightness(1.08);
  transition: outline 0.08s ease, box-shadow 0.08s ease, filter 0.08s ease;
}

/* AI POV badge — small chip identifying which AI's turn we're observing.
   Body-level + fixed so submenu re-renders inside #action-area don't wipe it. */
.ai-pov-badge {
  position: fixed;
  top: 6px;
  right: 8px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  background: rgba(20, 20, 30, 0.85);
  border: 1px solid rgba(255, 216, 77, 0.4);
  border-radius: 6px;
  z-index: 11;
  pointer-events: none;
  font-family: var(--font-pixel, monospace);
  font-size: 9px;
  letter-spacing: 1.5px;
  color: #ffd84d;
}
.ai-pov-pfp {
  width: 18px;
  height: 18px;
  border-radius: 4px;
  background-size: cover;
  background-position: center top;
  background-color: #222;
  border: 1px solid rgba(255, 255, 255, 0.3);
}
.ai-pov-name { white-space: nowrap; }

/* AI THINKING overlay — legacy. Now hidden by default (the cursor flow IS
   the indicator). Retained for any future fallback need. */
.ai-thinking-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 15, 20, 0.78);
  border-radius: var(--radius);
  z-index: 10;
  pointer-events: none;
}
.ai-thinking-text {
  font-family: var(--font-pixel);
  font-size: 18px;
  color: #fff;
  letter-spacing: 3px;
  text-shadow: 0 0 14px rgba(168, 85, 247, 0.5);
}
.ai-thinking-text .ai-dot {
  display: inline-block;
  animation: ai-dot-fade 1.2s ease-in-out infinite;
}
.ai-thinking-text .ai-dot-1 { animation-delay: 0s; }
.ai-thinking-text .ai-dot-2 { animation-delay: 0.18s; }
.ai-thinking-text .ai-dot-3 { animation-delay: 0.36s; }
@keyframes ai-dot-fade {
  0%, 100% { opacity: 0.25; }
  50%      { opacity: 1; }
}

.action-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  flex: 1;
  min-height: 0;
}
.action-btn {
  font-family: var(--font-pixel);
  font-size: 10px;
  background: var(--cream-2);
  border: 2px solid var(--navy);
  border-radius: 6px;
  padding: 6px;
  cursor: pointer;
  letter-spacing: 1px;
  transition: all 0.1s;
}
.action-btn:hover:not(:disabled) {
  background: var(--accent-2);
  color: #fff5e8;
  border-color: var(--accent-2);
  transform: translateY(-1px);
  box-shadow: var(--glow);
}
.action-btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* Action area inner header: Back btn + Turn Timer */
.action-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.action-header .back-btn { margin: 0; }
/* layout + mute controls that sit next to CHOOSE ACTION (replacing the fixed
   top-right toggles during battle) */
.action-header-ctrls { display: flex; align-items: center; gap: 6px; }
.hdr-ctrl {
  width: 30px; height: 30px; padding: 0; flex-shrink: 0;
  font-size: 15px; line-height: 1;
  border: 2px solid var(--navy); border-radius: 7px;
  background: var(--cream-2); color: var(--navy); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.hdr-ctrl:hover { border-color: #c9a23a; background: var(--cream); }
.hdr-ctrl.is-muted { opacity: 0.6; border-color: rgba(196, 60, 60, 0.7); }
/* during battle the controls are in the header — hide the fixed corner copies */
body.in-battle #layout-toggle, body.in-battle #sfx-toggle { display: none; }
/* glossary has its own [B] BACK control — hide the fixed top-right toggles so
   they don't collide with it (the back button sits in that corner). */
body:has(#screen-glossary.active) #layout-toggle,
body:has(#screen-glossary.active) #sfx-toggle { display: none; }
.turn-timer {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--cream-2);
  border: 3px solid var(--navy);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-pixel);
  font-size: 11px;
  color: var(--navy);
  flex-shrink: 0;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.turn-timer.warn { background: var(--accent); color: var(--cream); border-color: var(--accent); }
.turn-timer.hidden { visibility: hidden; }

/* Unified action tile grid (Attack & Support menus) — sized to ALWAYS fit, never scrolls */
.ct-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: 6px;
  margin-bottom: 6px;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.ct-tile {
  background: var(--cream-2);
  border: 2px solid var(--navy);
  border-radius: 6px;
  padding: 4px 6px;
  cursor: pointer;
  text-align: center;
  transition: all 0.1s;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.ct-tile:hover:not(:disabled) { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); box-shadow: var(--glow); }
.ct-tile:disabled { opacity: 0.4; cursor: not-allowed; background: #c8c0a4; }
/* GJ-10: soft-disable — same dim look as the hard-disabled state above, but
   NOT the native :disabled pseudo-class (that would swallow the click event
   the lockedReason toast depends on). cursor stays 'help' (the tile's title
   already carries the AP/CE/Bind detail on hover) instead of 'not-allowed'. */
.ct-tile.locked { opacity: 0.4; cursor: help; background: #c8c0a4; }
.ct-tile.locked:hover { background: #c8c0a4; color: inherit; border-color: var(--navy); box-shadow: none; }
.ct-tile .tile-name {
  font-family: var(--font-pixel);
  font-size: 9px;
  line-height: 1.2;
  word-break: break-word;
  overflow-wrap: anywhere;
  hyphens: auto;
  max-width: 100%;
}
.ct-tile.empty { background: #aaa; cursor: not-allowed; }
.ct-tile.special { border-color: var(--accent); }

/* C5: Target picker — when active, .ct-grid switches from the 2-col tile
   grid into a vertical stack of team rows. Each row labeled + horizontal
   strip of clickable PFP tiles. Allies tinted green, enemies red, dead
   chars dimmed + uninteractable. */
.ct-grid.target-picker-mode {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 4px;
  position: relative;
  /* Multi-team battles with N=6 per team can push the picker rows past the
     action-area height. Scroll vertically so all rows stay reachable. */
  overflow-y: auto;
  min-height: 0;
}
.target-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(20, 22, 30, 0.45);
  border: 1px solid #4a4a55;
  border-radius: 6px;
  padding: 4px 8px;
  flex: 1 1 auto;
  min-height: 0;
}
.target-row[data-team-index="0"] {
  border-color: #4caf50;
  background: rgba(40, 60, 40, 0.5);
}
.target-row-label {
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 1px;
  color: #d6cfa8;
  min-width: 78px;
  white-space: nowrap;
}
.target-row-tiles {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;                       /* let the strip shrink inside the row so it can scroll */
  /* A team's portraits scroll LEFT-TO-RIGHT instead of wrapping down the page —
     on a narrow phone screen a 6-wide team otherwise stacked into several rows
     and pushed the picker off-screen. nowrap + horizontal scroll keeps each
     team on a single swipeable strip. */
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;  /* momentum scroll on iOS */
  scrollbar-width: thin;
  padding-bottom: 2px;
}
.target-tile {
  width: 44px;
  height: 44px;
  border-radius: 4px;
  border: 2px solid #888;
  /* Longhand background-image — NOT the `background` shorthand. iOS/WebKit fails
     to substitute a var() that resolves to a url() inside the `background`
     shorthand, leaving the tile an empty square (the turn-queue tile worked
     because it already used longhand `background-image: var(--tq-pfp)`). */
  background-color: #0f0f14;
  background-image: var(--target-pfp, none);
  background-position: center top;
  background-size: cover;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  cursor: pointer;
  padding: 0;
  transition: transform 100ms ease, box-shadow 100ms ease, border-color 100ms ease;
  flex-shrink: 0;
}
.target-tile.is-enemy { border-color: #d94a3d; }
.target-tile.is-ally  { border-color: #4caf50; }
.target-tile.is-invalid { opacity: 0.35; cursor: not-allowed; filter: grayscale(0.8); }
.target-tile.is-dead    { opacity: 0.25; cursor: not-allowed; filter: grayscale(1) brightness(0.5); }
/* Hover grow gated to real-hover (mouse) devices only. On touch, iOS "sticks" the
   :hover state after a tap, so the tapped tile stayed scaled 1.18 with the glow
   spilling outside its cell — reading as "the PFP is too big / clipped outside the
   border." @media (hover: hover) keeps the grow on desktop and off on phones. */
@media (hover: hover) {
  .target-tile:not(:disabled):hover {
    transform: scale(1.18);
    box-shadow: 0 0 0 2px #fff, 0 0 12px 4px currentColor;
  }
  .target-tile.is-enemy:not(:disabled):hover { color: rgba(217, 74, 61, 0.7); }
  .target-tile.is-ally:not(:disabled):hover  { color: rgba(76, 175, 80, 0.7); }
}

/* Confirm overlay — slides over the picker; cancel removes it leaving picker. */
.target-confirm-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  animation: tc-slide-up 180ms ease-out;
  padding: 8px;
}
@keyframes tc-slide-up {
  from { transform: translateY(20px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
.tc-card {
  background: var(--cream);
  border: 2px solid var(--navy);
  border-radius: 8px;
  padding: 10px 14px;
  width: 100%;
  max-width: 460px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.6);
}
/* Compact yes/no variant — combatant info already shown in the duel HUD,
   so the confirm card is just a binary prompt. Smaller, tighter, bigger
   title for legibility. */
.tc-card.tc-card-mini {
  max-width: 320px;
  padding: 14px 18px;
}
.tc-card.tc-card-mini .tc-title {
  font-size: 14px;
  margin-bottom: 14px;
  letter-spacing: 1px;
  color: var(--navy);
  text-transform: none;
}
/* D1: closed-form forecast line on the confirm card (hit% + expected damage) —
   the tap-reachable surface for phones where hover forecasts never show. */
.tc-forecast {
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 1px;
  color: var(--accent-2);
  margin: -6px 0 12px;
  line-height: 1.5;
}
.tc-title {
  font-family: var(--font-pixel);
  font-size: 10px;
  letter-spacing: 2px;
  color: var(--accent);
  text-align: center;
  margin-bottom: 8px;
}
.tc-body {
  display: flex;
  gap: 12px;
  align-items: center;
}
.tc-pfp {
  width: 76px;
  height: 76px;
  border-radius: 6px;
  border: 2px solid var(--navy);
  background-size: cover;
  background-position: center top;
  image-rendering: pixelated;
  flex-shrink: 0;
}
.tc-info { flex: 1 1 auto; min-width: 0; }
.tc-name {
  font-family: var(--font-pixel);
  font-size: 12px;
  color: var(--navy);
  margin-bottom: 2px;
}
.tc-grade {
  font-size: 11px;
  color: var(--navy-2);
  margin-bottom: 6px;
}
.tc-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 10px;
  margin-bottom: 3px;
}
.tc-bar-label { font-family: var(--font-pixel); font-size: 9px; color: var(--navy); min-width: 20px; }
.tc-bar-track {
  flex: 1 1 auto;
  height: 8px;
  background: #2a2a30;
  border: 1px solid var(--navy);
  border-radius: 3px;
  overflow: hidden;
}
.tc-bar-fill { height: 100%; transition: width 200ms ease; }
.tc-bar-fill.hp { background: #4caf50; }
.tc-bar-fill.ce { background: #a855f7; }
.tc-bar-num { font-family: var(--font-pixel); font-size: 9px; color: var(--navy); min-width: 80px; text-align: right; }
.tc-meta {
  font-size: 10px;
  color: var(--navy-2);
  margin-top: 4px;
  font-style: italic;
}
.tc-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 10px;
}
.tc-btn {
  padding: 8px 16px;
  font-family: var(--font-pixel);
  font-size: 10px;
  border: 2px solid var(--navy);
  border-radius: 6px;
  cursor: pointer;
  letter-spacing: 1px;
  box-shadow: 0 2px 0 rgba(0,0,0,0.3);
  transition: all 0.1s;
}
.tc-btn.tc-confirm { background: var(--accent); color: #fff5e8; border-color: var(--accent-2); }
.tc-btn.tc-cancel  { background: var(--cream-2); color: var(--navy); }
.tc-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 0 rgba(0,0,0,0.3); }
.tc-btn:active { transform: translateY(1px); box-shadow: none; }

.detail-row {
  font-size: 12px;
  padding: 4px 8px;
  background: var(--cream-2);
  border: 1px solid var(--navy);
  border-radius: 4px;
  min-height: 28px;
  flex-shrink: 0;
}

.back-btn {
  font-family: var(--font-pixel);
  font-size: 9px;
  margin-top: 6px;
  padding: 4px 8px;
  background: var(--cream-2);
  border: 2px solid var(--navy);
  border-radius: 4px;
  cursor: pointer;
}

/* In-battle Stats panel — compact 2-column */
.stats-panel {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.stats-panel-block {
  background: var(--cream-2);
  border: 2px solid var(--navy);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 13px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.stats-panel-name { font-family: var(--font-pixel); font-size: 9px; letter-spacing: 0.5px; padding-bottom: 4px; border-bottom: 1px solid var(--navy); margin-bottom: 2px; }
.stats-panel-row { font-size: 12px; line-height: 1.35; }
.stats-panel-row b { color: var(--navy); }
.stats-panel-statuses { color: var(--accent); font-size: 11px; }
.stats-panel-passives { font-size: 11px; opacity: 0.85; }
/* Tap/click-a-stat explanations. Buttons sit inline in a non-flex row (plain
   block text), so the flex-min-width shrink/ellipsis cluster doesn't apply —
   still set white-space:nowrap defensively so a 2-3 char abbreviation never
   mid-word-wraps on a narrow portrait card. */
.stat-tip-btn {
  font-family: inherit;
  font-size: inherit;
  font-weight: 700;
  color: var(--navy);
  background: var(--cream);
  border: 1px solid var(--navy);
  border-radius: 4px;
  padding: 3px 7px;
  min-height: 24px;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}
.stat-tip-btn:hover { background: var(--cream-2); }
.stat-tip-btn.active,
.stat-tip-btn:active { background: var(--accent); color: var(--cream); border-color: var(--accent); }
.stats-panel-tip {
  margin-top: 8px;
  background: var(--cream-2);
  border: 2px solid var(--accent);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 12px;
  line-height: 1.4;
}

/* ============================================================
   BATTLE END SCREEN
   ============================================================ */
#screen-battle-end { align-items: center; justify-content: center; padding: 30px; overflow-y: auto; max-height: 100vh; }
.end-panel {
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  padding: 30px;
  box-shadow: var(--shadow);
  width: 600px;
  max-width: 100%;
  text-align: center;
  /* Whole panel scrolls when content overflows viewport (added Casualties
     timeline + longer battle log can easily exceed the screen). max-height
     caps at viewport minus the outer 60px padding so the panel itself can
     internally scroll. */
  max-height: calc(100vh - 60px);
  overflow-y: auto;
}
.end-banner {
  font-family: var(--font-pixel);
  font-size: 28px;
  margin-bottom: 14px;
  letter-spacing: 4px;
  color: var(--accent);
  text-shadow: 3px 3px 0 var(--navy);
}
.end-banner.defeat { color: var(--navy); text-shadow: 3px 3px 0 var(--accent); }

.end-art { height: 120px; margin: 14px auto; font-size: 11px; }
/* Themed victory illustration (post-battle screen). Fills the .end-art slot as a
   4:3 hero banner sized to the panel width. */
.end-art.end-art-img {
  height: auto;
  width: 100%;
  max-width: 540px;
  aspect-ratio: 4 / 3;
  margin: 16px auto;
  border: var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  font-size: 0;
}

.end-section { margin-top: 16px; text-align: left; }
.end-section h3 { font-family: var(--font-pixel); font-size: 11px; margin-bottom: 6px; display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.end-section ul { list-style: none; padding-left: 0; }
.end-section li { padding: 3px 0; font-size: 16px; }
/* Battle-log download (txt/json). Small pixel-style buttons matching
   .end-actions' register but compact enough to sit inline in the h3 row.
   min-width:0 on the flex-wrapping h3 lets this shrink/wrap instead of
   forcing "Battle Log" to clip (flex-min-width cluster, BUGLOG_INDEX.md). */
.end-log-download { display: flex; gap: 6px; min-width: 0; flex-wrap: wrap; }
.end-log-download-btn {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 0.3px;
  padding: 6px 8px;
  min-height: 26px;
  background: var(--cream);
  border: var(--border);
  border-radius: 4px;
  cursor: pointer;
  white-space: nowrap;
}
.end-log-download-btn:hover { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); }

/* End-screen battle log — scrollable, height-capped so the action buttons
   below stay visible without pushing the panel off-screen. Inherits the
   in-battle .event-log styling. */
.end-log-section .end-event-log {
  max-height: 320px;
  overflow-y: auto;
  font-size: 13px;
}
/* Casualties timeline on the post-battle screen. Each row reads as a single
   line: "R3 · T18  Jogo  defeated by Gojo's Hollow Purple (627 dmg)" — turn
   and round on the left, victim, then cause. ol counter is suppressed because
   the R/T tag carries the order. */
.end-ko-list { list-style: none; padding-left: 0; counter-reset: ko-counter; max-height: 240px; overflow-y: auto; }
.end-ko-list li.ko-row {
  display: grid;
  grid-template-columns: 88px 160px 1fr;
  gap: 10px;
  padding: 4px 0;
  font-size: 14px;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
}
.end-ko-list li.ko-row:last-child { border-bottom: none; }
.end-ko-list .ko-when {
  font-family: var(--font-pixel);
  font-size: 10px;
  color: var(--accent);
  align-self: center;
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1.2;
}
/* Both round and turn signifiers always render on their own line, regardless
   of digit count. Without this, single-digit rounds (R6, T63) fit on one line
   while double-digit ones (R10, T105) wrap — producing inconsistent row heights
   in the casualty list. Stacking them explicitly keeps every row uniform. */
.end-ko-list .ko-when-r,
.end-ko-list .ko-when-t {
  display: block;
}
.end-ko-list .ko-victim {
  font-weight: bold;
  color: #f4d264;
}
.end-ko-list .ko-cause i { color: #d94a3d; font-style: normal; }
.end-ko-list .ko-cause b { color: #fff; font-weight: bold; }
/* DE context: when the killing blow happened inside an active closed dome,
   a second line shows the dome name. The whole row gets a subtle purple
   left-border to flag "domain kill" at-a-glance. */
.end-ko-list .ko-de-context {
  font-size: 11px;
  margin-top: 3px;
  color: #c79bff;
  letter-spacing: 0.5px;
}
.end-ko-list .ko-de-context b {
  color: #e9d5ff;
  font-weight: bold;
}
.end-ko-list li.ko-row.inside-de {
  border-left: 3px solid #a259ff;
  padding-left: 8px;
}
.end-ko-list .ko-empty {
  font-style: italic;
  opacity: 0.6;
  padding: 6px 0;
}

.end-actions { display: flex; gap: 10px; justify-content: center; margin-top: 22px; }
.end-actions button {
  font-family: var(--font-pixel);
  font-size: 11px;
  padding: 10px 18px;
  background: var(--cream);
  border: var(--border);
  border-radius: 6px;
  cursor: pointer;
}
.end-actions button:hover { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); box-shadow: var(--glow); }

/* generic small button */
button {
  font-family: var(--font-pixel);
  font-size: 10px;
  padding: 8px 14px;
  background: var(--cream-2);
  color: var(--navy);
  border: 2px solid var(--navy);
  border-radius: 4px;
  cursor: pointer;
  letter-spacing: 1px;
}
button:hover { background: var(--accent-2); color: #fff5e8; border-color: var(--accent-2); box-shadow: var(--glow); }

/* ============================================================
   LAYOUT MODE — Desktop (default, fluid fill) ↔ Mobile (portrait,
   DS-stacked). Manual toggle (#layout-toggle), persisted. Desktop
   is untouched; Mobile centers a phone-shaped 'stage' (letterboxed)
   and reflows the battle + menus into a single vertical column.
   ============================================================ */
body.layout-portrait {
  display: flex; align-items: center; justify-content: center;
  background: #05070b;
}
body.layout-portrait #app {
  aspect-ratio: 9 / 16;
  /* BUG (Joe playtest, iOS Brave): 100vh includes the area under the
     collapsing address-bar chrome, so the stage sized taller than the
     actual VISIBLE viewport — the top row (enemy name) rendered off-screen
     above the fold. 100vh stays as the fallback for browsers with no
     dvh support; 100dvh (the small/dynamic viewport height, tracks the
     bar's current collapsed/expanded state) overrides it everywhere modern
     mobile Safari/Brave/Chrome support it. */
  height: 100vh; height: 100dvh; width: auto; max-width: 100vw;
  position: relative; overflow: hidden;
  box-shadow: 0 0 0 2px rgba(255,255,255,0.07), 0 10px 50px rgba(0,0,0,0.7);
}
/* layout toggle button (sits left of the 🔊 mute toggle) */
.layout-toggle {
  position: fixed; top: 10px; right: 52px; z-index: 100000;
  width: 34px; height: 34px; padding: 0; font-size: 15px; line-height: 1;
  border-radius: 8px; border: 2px solid rgba(255, 255, 255, 0.5);
  background: rgba(20, 24, 34, 0.85); color: #fff; cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.layout-toggle:hover { border-color: #ffd84d; }

/* ---- PORTRAIT: battle screen reflow (DS-stacked) ---- */
/* DS dual-screen split: arena (top) ~46% / action area (bottom) ~54%. The log
   is intentionally a SHORT scroll strip so it can't starve the action area —
   the previous 1fr log ate the bottom and smooshed the buttons. overflow:hidden
   + min-height:0 keep the two halves from bleeding into each other / the sprites. */
/* Only style the battle stage when it's the ACTIVE screen — without `.active`
   this forced #screen-battle visible on every menu (battle chrome leaked behind
   the roster/mode screens in portrait). `.screen.active { display:flex }` still
   drives show/hide; we only add the column/overflow stage styling here. */
body.layout-portrait #screen-battle.active { display: flex; flex-direction: column; overflow: hidden; }
body.layout-portrait .battle-arena {
  max-height: none; flex: 1 1 46%; min-height: 0; overflow: hidden;
  grid-template-rows: 1fr 1fr; padding: 7px; gap: 6px;
}
/* SLIM NAMEPLATE (Pokémon-style): the boxy desktop card is compacted to a short
   plate — tight padding, thin bars, HP/CE labels, one-line status row — so the
   HUD stops dominating the arena. Card + sprite center vertically in each row;
   the freed space goes to bigger sprites (wider column + more height). */
/* Corner the HUDs (matches desktop's start/end): the enemy pair aligns to the
   TOP so the enemy plate hugs the top-left corner, and the player pair aligns
   to the BOTTOM so the player plate hugs the bottom-right corner — the slim
   nameplates tuck into the arena's diagonal corners instead of floating in the
   middle. (Card is the left column for enemy, right column for player.) */
:is(body.layout-portrait, body.layout-landscape) .combatant.enemy  { grid-template-columns: minmax(0, 1fr) 124px; gap: 8px; align-items: start; }
:is(body.layout-portrait, body.layout-landscape) .combatant.player { grid-template-columns: 124px minmax(0, 1fr); gap: 8px; align-items: end; }
/* 25% narrower HUD cards: the short bars leave dead black space on the wide
   side, so cap the card at 75% of its grid cell and tuck it to the OUTER edge
   (enemy left, player right) — the freed space becomes transparent arena, not
   black card. */
/* 90% (was 67.5%): a narrower card couldn't hold [bar + HP/CE number + ACC/EVA stat]
   on one line once a stat gained a (±N) buff/debuff suffix — the bar-text overflowed
   the bar-wrap and painted OVER the stat ("1140" colliding with "ACC 3 (+2)"). The
   wider card fits the row; the sprite is in a separate 124px grid column, so this only
   uses the card column's slack and never crowds the sprite. */
:is(body.layout-portrait, body.layout-landscape) .char-card { width: 90%; max-width: 90%; min-width: 0; font-size: 11px; padding: 3px 8px; }
/* Safety net: if a stat row still can't fit (very long name + big suffix), the stat
   wraps below the bar (card grows TALLER) instead of overlapping it. */
:is(body.layout-portrait, body.layout-landscape) .card-row:not(.card-status-row) { flex-wrap: wrap; row-gap: 0; }
:is(body.layout-portrait, body.layout-landscape) .card-row .bar-wrap { min-width: min-content; }
:is(body.layout-portrait, body.layout-landscape) .combatant.enemy  .char-card { justify-self: start; }
:is(body.layout-portrait, body.layout-landscape) .combatant.player .char-card { justify-self: end; }
/* The desktop card row is ~380px (160px bar + 90px text + 100px stat) — far too
   wide for the narrow plate, so ACC/EVA overflowed behind the sprite. Flex the
   bar and strip the min-widths so everything fits. */
/* HUD card header on mobile: the long grade ("Special Grade - Regular/Plus")
   collided with the name on one line — the name got crushed to "M" / "Go...".
   STACK them (name on top at full width, grade below in smaller text) so neither
   clips, regardless of how long the name or rank is. */
:is(body.layout-portrait, body.layout-landscape) .card-header {
  font-size: 8px; padding-bottom: 2px; margin-bottom: 2px; letter-spacing: 0;
  flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 0;
}
:is(body.layout-portrait, body.layout-landscape) .card-header .char-name { flex-shrink: 0; min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:is(body.layout-portrait, body.layout-landscape) .card-header .char-grade { flex-shrink: 0; white-space: normal; text-align: left; font-size: 7px; line-height: 1.15; opacity: 0.8; }
:is(body.layout-portrait, body.layout-landscape) .card-row { gap: 7px; margin: 1px 0; }
:is(body.layout-portrait, body.layout-landscape) .bar-wrap { gap: 4px; flex: 1 1 auto; min-width: 0; }
/* HP / CE labels (bar color already distinguishes; the text completes the
   nameplate look). :has() picks the label off the bar's existing class. */
:is(body.layout-portrait, body.layout-landscape) .bar-wrap::before {
  font-family: var(--font-pixel); font-size: 7px; color: var(--navy);
  flex: 0 0 auto; letter-spacing: 0.5px;
}
:is(body.layout-portrait, body.layout-landscape) .bar-wrap:has(.hp-bar)::before { content: "HP"; }
:is(body.layout-portrait, body.layout-landscape) .bar-wrap:has(.ce-bar)::before { content: "CE"; }
/* HTML .bar-label spans duplicate the ::before labels above in portrait — hide them */
:is(body.layout-portrait, body.layout-landscape) .bar-label { display: none; }
/* The "1140 / 1140" value must NEVER flex-shrink below its content width —
   a shrunken box lets the nowrap digits paint over the ACC/EVA column.
   The text is rigid (flex-basis auto); the BAR absorbs all squeeze instead,
   shrinking down to a 28px floor on the narrowest cards. */
:is(body.layout-portrait, body.layout-landscape) .bar { flex: 1 1 40%; width: auto; min-width: 28px; max-width: 40%; height: 7px; }
:is(body.layout-portrait, body.layout-landscape) .bar-text { min-width: 0; flex: 0 0 auto; font-size: 8px; }
:is(body.layout-portrait, body.layout-landscape) .stat-inline { min-width: 0; font-size: 8px; flex: 0 0 auto; }
/* compact, single-line status row (AP dots + status chips) */
/* Status row wraps so accumulating status tags BUILD UP (grow the card) instead
   of clipping — the card only gets taller as tags stack. */
:is(body.layout-portrait, body.layout-landscape) .card-status-row { margin: 1px 0 0; gap: 6px; flex-wrap: wrap; row-gap: 3px; }
:is(body.layout-portrait, body.layout-landscape) .ap-dots { font-size: 9px; letter-spacing: 1px; }
/* The narrow portrait card can't hold a full-width chip row. Let the chips area
   take the whole card width and wrap, and shrink long chips (notably the domain
   tags "DE: …" / "TRAPPED") so they ellipsize to the card instead of spilling out
   of the HUD. overflow:visible here so a wrapped chip is never clipped mid-text. */
:is(body.layout-portrait, body.layout-landscape) .status-chips { flex: 1 1 100%; max-width: 100%; overflow: visible; gap: 3px; }
:is(body.layout-portrait, body.layout-landscape) .status-chip { font-size: 6px; padding: 1px 3px; letter-spacing: 0.2px; }
/* Full STATS panel had zero portrait rules — the desktop 2-column grid stayed
   fixed at 1fr 1fr on narrow viewports with no min-width guard, squeezing both
   blocks. Stack to one column (matches the rest of the portrait HUD reflow) and
   shrink text to fit. */
:is(body.layout-portrait, body.layout-landscape) .stats-panel { grid-template-columns: 1fr; gap: 6px; }
:is(body.layout-portrait, body.layout-landscape) .stats-panel-block { min-width: 0; font-size: 11px; padding: 6px 8px; }
:is(body.layout-portrait, body.layout-landscape) .stats-panel-row { font-size: 10px; }
:is(body.layout-portrait, body.layout-landscape) .stats-panel-statuses,
:is(body.layout-portrait, body.layout-landscape) .stats-panel-passives { font-size: 9px; }
/* Grown sprites — the slimmer HUD frees room, so the sprite column is wider and
   the sprites taller; the empty arena gap shrinks ("arena breathes"). Explicit
   height because the sprite is a background-image div (auto would collapse it). */
:is(body.layout-portrait, body.layout-landscape) .battle-sprite { height: 132px; width: 100%; max-width: 116px; min-height: 0; }
:is(body.layout-portrait, body.layout-landscape) .combatant.player .battle-sprite { height: 146px; max-width: 122px; }
:is(body.layout-portrait, body.layout-landscape) .combatant.enemy  .battle-sprite { justify-self: end;   margin-right: 4px; }
:is(body.layout-portrait, body.layout-landscape) .combatant.player .battle-sprite { justify-self: start; margin-left: 4px; }
:is(body.layout-portrait, body.layout-landscape) .turn-queue { flex: 0 0 auto; padding: 5px 6px; gap: 6px; min-height: 0; row-gap: 6px; }
/* Portrait: shrink the turn-order tiles so a full 3v3 (+ shikigami) fits the
   narrow column; wrap (inherited) catches any overflow beyond one row. The
   active-tile pulse scale is reduced so it can't bulge past the frame edge. */
:is(body.layout-portrait, body.layout-landscape) .tq-tile { width: 38px; height: 38px; }
:is(body.layout-portrait, body.layout-landscape) .tq-tile::before { inset: 2px; }
:is(body.layout-portrait, body.layout-landscape) .tq-tile.active { animation: tq-pulse-sm 0.9s ease-in-out infinite; }
@keyframes tq-pulse-sm { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }
/* Portrait: shrink the domain-expansion trap/escape-roll banner so its d20
   chips fit the narrow column without spilling off-frame. The strip already
   wraps; these just scale every element down to phone proportions. */
:is(body.layout-portrait, body.layout-landscape) .de-trap-roll-banner { padding: 12px 12px !important; gap: 10px !important; min-width: 0; max-width: 96vw; }
:is(body.layout-portrait, body.layout-landscape) .trap-roll-title { font-size: 14px; letter-spacing: 2px; text-align: center; }
:is(body.layout-portrait, body.layout-landscape) .trap-roll-strip { gap: 8px; }
:is(body.layout-portrait, body.layout-landscape) .trap-roll-chip { min-width: 60px; padding: 6px 7px; gap: 4px; }
:is(body.layout-portrait, body.layout-landscape) .trap-roll-pfp { width: 42px; height: 42px; }
:is(body.layout-portrait, body.layout-landscape) .trap-roll-name { font-size: 8px; letter-spacing: 0.5px; }
:is(body.layout-portrait, body.layout-landscape) .trap-roll-num { font-size: 20px; }
body.layout-portrait .trap-roll-vs { font-size: 8px; }
body.layout-portrait .trap-roll-result-label { font-size: 9px; min-height: 11px; }
body.layout-portrait .battle-bottom {
  grid-template-columns: 1fr; grid-template-rows: auto 1fr;  /* log capped / action fills */
  height: auto; flex: 1 1 54%; min-height: 0; gap: 6px; padding: 8px;
}
body.layout-portrait .event-log { font-size: 11px; max-height: 84px; min-height: 44px; }
body.layout-portrait .action-area { min-height: 0; overflow: hidden; }
/* BUG (Joe playtest): with 5+ action buttons (ATTACK/SUPPORT/VOWS/STATS/…) the
   grid's content height can exceed the shrunken .battle-bottom panel (U7's
   180px floor), and .action-area's inherited overflow:hidden clipped the
   overflow with no way to reach the last button(s). The list itself now
   scrolls internally — U7's height contract on .battle-bottom is untouched
   (desktop-only formula, not selected here), and .action-grid's own
   `height:100%` still caps it to the panel, it just gains a scroll path
   instead of clipping. -webkit-overflow-scrolling gives it iOS momentum. */
/* BUG-340 (Joe's proposal, 2026-08-01): portrait now uses the SAME 2-column grid
   desktop has always used, instead of a single tall column. Measured at 375x700
   (a phone with the URL bar expanded, i.e. the constrained case that matters):
   the 1-column stack needed 216px inside a 173px area — 43px below the fold,
   reachable only by scrolling — while 2 columns need exactly 173px and fit with
   zero overflow. The buttons get BIGGER doing this, not smaller (335x48 -> 164x83),
   because the grid rows stretch to fill the freed height, so touch targets improve
   as well. `overflow-y:auto` below is KEPT as the safety net for 5-6 buttons or an
   even shorter viewport — BUG-329's scroll path must survive this change, not be
   replaced by it. */
body.layout-portrait .action-grid {
  grid-template-columns: 1fr 1fr; gap: 8px; height: 100%;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
}
body.layout-portrait .action-btn { min-height: 48px; font-size: 14px; }
body.layout-portrait .ct-grid { gap: 6px; }
/* Target picker portrait overrides: rows take natural height (flex: 0 0 auto) so the
   picker scrolls vertically when rows overflow; tiles shrink to 36px to pack more per
   strip; strip stretches to the row height so tiles fill the row (no wasted gap above/
   below); label shrinks to match the slimmer tiles. */
body.layout-portrait .ct-grid.target-picker-mode { overflow-y: auto; }
body.layout-portrait .target-row { flex: 0 0 auto; align-items: stretch; padding: 4px 6px; }
body.layout-portrait .target-row-tiles { align-items: stretch; gap: 4px; }
/* W3a item 6: bumped 36px→44px — below the ~44px minimum comfortable touch target. */
body.layout-portrait .target-tile { width: 44px; height: auto; min-height: 44px; }
body.layout-portrait .target-row-label { font-size: 8px; min-width: 58px; letter-spacing: 0.5px; align-self: center; }

/* tap-to-preview: the armed tile (first tap) is highlighted so it's obvious a
   second tap commits. Portrait/touch only. */
/* Two-tap ability preview: in mobile layout the FIRST tap previews (shows the
   damage forecast); the previewed tile pulses gold and shows a "TAP AGAIN" bar so
   the second-tap-to-fire reads as intentional, not a dead button. */
body.layout-portrait .ct-tile { position: relative; }
body.layout-portrait .ct-tile.tile-previewed {
  border-color: #ffd84d;
  animation: ct-previewed-pulse 1.05s ease-in-out infinite;
}
body.layout-portrait .ct-tile.tile-previewed::after {
  content: '▶ TAP AGAIN';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  background: rgba(255, 216, 77, 0.95);
  color: #1a1206;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 0.4px;
  line-height: 1;
  padding: 2px 0 3px;
  text-align: center;
  pointer-events: none;
}
@keyframes ct-previewed-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(255, 216, 77, 0.6), 0 0 10px rgba(255, 216, 77, 0.35); }
  50%      { box-shadow: 0 0 0 3px rgba(255, 216, 77, 0.95), 0 0 18px rgba(255, 216, 77, 0.7); }
}
body.layout-portrait .detail-row { font-size: 11px; }
/* tuck the global toggles into the very corner so they clip the sprite less */
body.layout-portrait .layout-toggle { width: 28px; height: 28px; font-size: 13px; top: 6px; right: 40px; opacity: 0.85; }
body.layout-portrait .sfx-toggle { transform: scale(0.82); transform-origin: top right; opacity: 0.85; }

/* ---- PORTRAIT: tutorial / coach overlay reflow ----
   Desktop floats the coach head bottom-left with a tail to a side bubble; the
   narrow 9/16 stage has no room, so dock the bubble as a bottom band (DS-style)
   and inset the head into a reserved left gutter — a visual-novel layout that's
   robust to any text length. Cutout / chevron / arrow are positioned by JS
   against the live target (host-relative), so they need no portrait overrides. */
:is(body.layout-portrait, body.layout-landscape) #tutorial-card .tc-bubble {
  top: auto; bottom: 8px; left: 8px; right: 8px;
  max-width: none; min-width: 0; width: auto;
  font-size: 14px; line-height: 1.34; padding: 10px 14px 30px 92px;
}
:is(body.layout-portrait, body.layout-landscape) #tutorial-card .tc-bubble::before { display: none; }
:is(body.layout-portrait, body.layout-landscape) #tutorial-card .tc-head {
  top: auto; bottom: 16px; left: 12px;
  width: 72px; height: 72px;
}
:is(body.layout-portrait, body.layout-landscape) #tutorial-card .tc-coach-name { font-size: 7px; }
:is(body.layout-portrait, body.layout-landscape) #tutorial-card .tc-next-btn { font-size: 8px; }
:is(body.layout-portrait, body.layout-landscape) #tutorial-card .tc-why { font-size: 11px; margin-top: 6px; padding-top: 4px; }
body.layout-portrait #tutorial-card .tc-why-tag { font-size: 6px; }

/* ---- PORTRAIT: menus — single column, scrollable ---- */
/* .mode-controls-pane is display:grid (2 cols on desktop) — flex-direction is a
   no-op on a grid, so the columns clipped on mobile. Collapse to one column. */
body.layout-portrait .mode-controls-pane { grid-template-columns: 1fr; }
body.layout-portrait .roster-full,
body.layout-portrait .mode-controls-pane { overflow-y: auto; }
/* BUG-222: the post-battle/results screen had no real portrait layout — only
   this no-op max-width (100% was already the desktop default). Mirror the
   roster/mode screens' portrait treatment: tighter panel padding, smaller
   banner/art/section text, and a KO-row grid that actually fits 375px instead
   of the desktop 88px/160px/1fr split (which alone exceeds the viewport). */
#screen-battle-end { padding: 10px; }
body.layout-portrait .end-panel {
  max-width: 100%;
  padding: 14px 10px;
  max-height: calc(100vh - 20px);
}
body.layout-portrait .end-banner { font-size: 18px; letter-spacing: 2px; margin-bottom: 8px; }
body.layout-portrait .end-art.end-art-img { max-width: 100%; margin: 10px auto; }
body.layout-portrait .end-winning-team-title { font-size: 9px; letter-spacing: 1px; }
body.layout-portrait .end-team-slot { transform: scale(0.85); }
body.layout-portrait .end-section { margin-top: 10px; }
body.layout-portrait .end-section h3 { font-size: 9px; }
body.layout-portrait .end-section li { font-size: 12px; padding: 2px 0; }
body.layout-portrait .end-log-section .end-event-log { max-height: 200px; font-size: 11px; }
body.layout-portrait .end-section h3 { flex-direction: column; align-items: flex-start; gap: 6px; }
body.layout-portrait .end-log-download-btn { min-height: 30px; padding: 7px 10px; }
/* KO list: desktop's fixed 88px/160px/1fr grid alone is 248px+gaps — too wide
   for a 375px viewport with panel padding. Collapse the "when" + "victim" +
   "cause" cells to stack instead of sitting in strict columns. */
body.layout-portrait .end-ko-list { max-height: 180px; }
body.layout-portrait .end-ko-list li.ko-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 12px;
  padding: 6px 0;
}
body.layout-portrait .end-ko-list .ko-when { flex-direction: row; gap: 6px; font-size: 9px; }
body.layout-portrait .end-actions { flex-direction: column; gap: 8px; margin-top: 16px; }
body.layout-portrait .end-actions button { width: 100%; font-size: 10px; padding: 10px 12px; }
/* Portrait roster polish: the 6-slot team row (~396px) overflowed the 375px
   viewport and clipped on both edges. Shrink the slots + allow wrap so the full
   team is always visible; tame the wide "DONE → NEXT TEAM" strip text too. */
/* Portrait roster: tighter padding on the panel itself */
body.layout-portrait .roster-full { padding: 10px 8px; }
/* Team slot row: smaller squares, tighter gap */
body.layout-portrait .team-slots-row { gap: 4px; padding: 6px 0 4px; flex-wrap: wrap; justify-content: center; }
body.layout-portrait .team-slot { width: 30px; height: 30px; border-width: 1px; border-radius: 4px; }
/* Slot-budget counter: compact chip */
body.layout-portrait .team-slot-counter { padding: 3px 7px; font-size: 9px; margin-left: 4px; border-radius: 5px; }
body.layout-portrait .team-slot-counter .tsc-num { font-size: 11px; }
body.layout-portrait .team-slot-counter .tsc-label { font-size: 7px; margin-left: 3px; }
/* CTRL toggle: shrink so it sits inline with the small slots */
body.layout-portrait .team-ctrl-toggle { padding: 3px 7px; margin-left: 4px; gap: 1px; border-radius: 5px; }
body.layout-portrait .team-ctrl-toggle .tct-label { font-size: 5px; }
body.layout-portrait .team-ctrl-toggle .tct-value { font-size: 9px; }
body.layout-portrait .team-done-row,
body.layout-portrait .completed-teams-banner { font-size: 11px; letter-spacing: 0; }
body.layout-portrait .team-done-row > * { white-space: normal; max-width: 100%; }
/* Header: 3-row grid — BACK top-right, title centered, match-type centered */
body.layout-portrait .roster-header {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "back" "title" "matchtype";
  gap: 4px;
}
body.layout-portrait .roster-header h2 {
  grid-area: title; text-align: center; grid-column: unset;
  font-size: 9px; letter-spacing: 0; line-height: 1.3; margin-bottom: 0; padding-bottom: 4px;
}
body.layout-portrait .roster-header .match-type-selector {
  grid-area: matchtype; justify-self: center; transform: none;
}
body.layout-portrait .roster-header .corner-btn {
  grid-area: back; justify-self: end;
  font-size: 9px; padding: 5px 10px;
}
body.layout-portrait .ai-difficulty-carousel { padding: 3px 6px; margin: 5px auto 0; gap: 6px; }
body.layout-portrait .ai-difficulty-label { font-size: 9px; min-width: 80px; }
/* Roster tiles: no names in portrait — portrait art fills the whole tile */
body.layout-portrait .roster-tile .tile-label { display: none; }
/* Switch to flex so the last partial row centers instead of snapping left */
body.layout-portrait .roster-grid-wide { display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; }
body.layout-portrait .roster-grid-wide .roster-tile { flex: 0 0 52px; width: 52px; }
/* Center season toggle buttons; remove asymmetric scroll padding */
body.layout-portrait .seasons-container { padding-right: 0; gap: 4px; }
body.layout-portrait .season-section { margin-bottom: 0; }
body.layout-portrait .season-toggle { margin-bottom: 0; }
body.layout-portrait .season-section { text-align: center; }
body.layout-portrait .season-toggle { display: inline-block; text-align: center; }

/* Portrait MODE screen (Joe's v4): stack top→bottom — art, a VERTICALLY-SHORTENED
   SELECT MODE card (fit to the mode list; the dead space came from `.mode-controls-pane
   flex:1` stretching to fill + the empty `.mode-footer`), then the 4 utility buttons in
   a single vertical stack below it. `#screen-mode` keeps its base flex-column + adds
   overflow-y so the stack is reachable by normal scroll (no display override → base
   `.screen{display:none}` inactive-hide preserved, avoids the bleed bug). */
body.layout-portrait #screen-mode { overflow-y: auto; }
body.layout-portrait .mode-art-pane { flex: 0 0 auto; max-height: 42vh; }
body.layout-portrait .mode-controls-pane { flex: 0 0 auto; min-height: auto; }   /* fit content → shortens the card */
body.layout-portrait .mode-list-area { min-height: auto; }
body.layout-portrait .mode-list { flex: 0 0 auto; }
body.layout-portrait .mode-footer { display: none; }         /* empty divider/spacer under the list */
body.layout-portrait .mode-details-area { display: none; }   /* remove Practice / description on mobile */
body.layout-portrait .mode-utility-row {
  display: flex;
  flex-direction: column;   /* vertical stack below the card */
  gap: 10px;
}

/* ---- PORTRAIT: Choose Arena screen — fit the phone width ----
   Was overflowing: the 24px pane margin + 3-col grid pushed the third column and
   the Back button off the right edge, and the empty "Select an arena" detail box
   (flex:1) became a giant black void. */
body.layout-portrait #screen-arena { overflow-y: auto; }
body.layout-portrait .arena-pane { margin: 8px; padding: 12px; }
body.layout-portrait .arena-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 12px; }
body.layout-portrait .arena-tile { aspect-ratio: 3 / 2; font-size: 8px; padding: 4px; }
body.layout-portrait .arena-tile .arena-label { padding: 3px 4px; line-height: 1.2; white-space: normal; }
body.layout-portrait .arena-tile.art-placeholder::after { display: none; }   /* hide dev asset-id chip */
body.layout-portrait .arena-detail { flex: 0 0 auto; min-height: 50px; font-size: 13px; padding: 10px 12px; }
body.layout-portrait #screen-arena .roster-actions { flex-wrap: wrap; gap: 8px; }
body.layout-portrait #screen-arena .roster-actions button { flex: 1 1 auto; }

/* ---- Character detail pop-up (roster modal) — NARROW-SCREEN reformat ----
   Scoped to a VIEWPORT media query (not the layout-portrait class) so the modal
   fits a phone screen even if the game is in Desktop layout / auto-detect didn't
   fire. The modal is a self-contained popup, so this doesn't touch the battle
   stage's deliberate no-media-query layout. Fixes: (1) bio was a cramped 140px
   nested-scroll; (2) the 2-col stats|sprite grid let the 220px square sprite
   overflow the right edge; (3) the five ability tabs clipped off-screen. */
@media (max-width: 540px) {
  .modal-card { padding: 16px; max-height: 94vh; overflow-y: auto; }
  /* Bio: more room + slightly smaller text → far less nested scrolling. */
  .char-bio { max-height: 28vh; font-size: 13px; line-height: 1.45; }
  /* Stats then sprite, STACKED — sprite centered at a clean fixed size, never
     spilling past the card edge; stats get the full width (more legible). */
  .stats-sprite-row { grid-template-columns: 1fr; gap: 10px; }
  .sprite-col { display: flex; justify-content: center; }
  .sprite-col .char-sprite-wrap { height: auto; margin-bottom: 0; }
  .sprite-col .char-sprite { height: 190px; width: 190px; min-height: 0; aspect-ratio: 1 / 1; }
  /* Ability tabs: short one-word labels (PASSIVES / ATTACKS / SUPPORTS / TOOLS /
     OBJECTS) fit one readable line each at equal width — no more 6px two-line
     wrapping. */
  .ability-tabs { gap: 2px; }
  .ability-tabs .tab-btn { flex: 1 1 0; min-width: 0; font-size: 6.5px; padding: 7px 1px; letter-spacing: 0; text-align: center; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* MOBILE-ONLY label swap: hide the full descriptive label, show the compact
     one-word variant so all five tabs fit one line at this width. Desktop keeps
     the full labels (this block is a viewport media query, never applied wide). */
  .ability-tabs .tab-lbl-full { display: none; }
  .ability-tabs .tab-lbl-mini { display: inline; }
  /* Action buttons (Pick / Cancel) stack-friendly + always reachable. */
  .modal-actions { flex-wrap: wrap; gap: 8px; }
}

/* Portrait-specific modal overrides — tighter than the generic 540px breakpoint.
   Puts stats + sprite side-by-side (saves ~150px vs stacked) and trims padding
   so the Pick / Cancel buttons are always reachable without scrolling far. */
body.layout-portrait .modal-backdrop { padding: 8px; }
body.layout-portrait .modal-card {
  padding: 12px 14px;
  max-height: 96vh;
  /* Constrain the modal to the 9:16 portrait STAGE width, not the full viewport.
     The backdrop is fixed to the whole screen, so without this the card balloons
     to its 640px desktop max-width and overflows the narrow mobile stage on wide
     screens. min() also caps it to the real viewport on actual phones. */
  max-width: min(calc(100vh * 9 / 16 - 16px), calc(100vw - 16px));
}
/* Mobile-layout tabs: same compact sizing + label swap as the narrow-viewport
   media query, so the toggle-driven mobile layout matches even on a wide screen
   (where @media max-width:540px does not fire). */
body.layout-portrait .ability-tabs { gap: 2px; }
body.layout-portrait .ability-tabs .tab-btn { flex: 1 1 0; min-width: 0; font-size: 6.5px; padding: 7px 1px; letter-spacing: 0; text-align: center; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
body.layout-portrait .ability-tabs .tab-lbl-full { display: none; }
body.layout-portrait .ability-tabs .tab-lbl-mini { display: inline; }
body.layout-portrait .char-bio { max-height: 14vh; font-size: 12px; line-height: 1.4; }
body.layout-portrait .stats-sprite-row {
  grid-template-columns: 1fr 108px;
  gap: 8px; margin: 8px 0 8px;
}
body.layout-portrait .sprite-col {
  display: flex; align-items: flex-start; justify-content: center;
}
body.layout-portrait .sprite-col .char-sprite-wrap { height: auto; margin-bottom: 0; }
body.layout-portrait .sprite-col .char-sprite {
  height: 140px; width: 108px; min-height: 0; aspect-ratio: unset;
}
body.layout-portrait .stat-block.compact .stat-row { font-size: 11px; }
body.layout-portrait .modal-actions { flex-direction: column; gap: 6px; }
body.layout-portrait .modal-actions button { width: 100%; }

/* ============================================================
   SCREEN SHAKE — jitter the whole arena on impact so hits land
   physically. Three intensities (chip / solid / big-hit·KO·BF),
   chosen in triggerShake() by damage vs target max HP. The arena
   has overflow:hidden; a few px of edge reveal during the ~0.2-0.4s
   shake is imperceptible.
   ============================================================ */
.battle-arena.shake-light { animation: arenaShakeL 0.16s ease-in-out; }
.battle-arena.shake-med   { animation: arenaShakeM 0.26s ease-in-out; }
.battle-arena.shake-heavy { animation: arenaShakeH 0.40s ease-in-out; }
@keyframes arenaShakeL {
  0%,100% { transform: translate(0,0); }
  25% { transform: translate(-2px, 1px); }
  50% { transform: translate(2px, -1px); }
  75% { transform: translate(-1px, 1px); }
}
@keyframes arenaShakeM {
  0%,100% { transform: translate(0,0); }
  20% { transform: translate(-4px, 2px); }
  40% { transform: translate(4px, -2px); }
  60% { transform: translate(-3px, -1px); }
  80% { transform: translate(2px, 2px); }
}
@keyframes arenaShakeH {
  0%,100% { transform: translate(0,0); }
  15% { transform: translate(-7px, 3px); }
  30% { transform: translate(6px, -4px); }
  45% { transform: translate(-5px, -3px); }
  60% { transform: translate(5px, 3px); }
  75% { transform: translate(-3px, 2px); }
  90% { transform: translate(2px, -1px); }
}

/* ============================================================
   MOVE ANIMATION — attacker lunges toward the opponent and snaps
   back; a warm impact burst sparks on the struck combatant. The
   sprite is position:relative so the burst anchors to it.
   ============================================================ */
.battle-sprite { position: relative; }
@keyframes lungeUp   { 0% { transform: translate(0,0); } 38% { transform: translate(16px,-20px) scale(1.04); } 100% { transform: translate(0,0); } }
@keyframes lungeDown { 0% { transform: translate(0,0); } 38% { transform: translate(-16px,20px) scale(1.04); } 100% { transform: translate(0,0); } }
.battle-sprite.lunge-up   { animation: lungeUp   0.28s ease-out; }
.battle-sprite.lunge-down { animation: lungeDown 0.28s ease-out; }

.impact-burst {
  position: absolute; left: 50%; top: 44%; width: 46px; height: 46px;
  margin: -23px 0 0 -23px; border-radius: 50%; pointer-events: none; z-index: 7;
  background: radial-gradient(circle, rgba(255,248,224,0.95) 0%, rgba(255,196,96,0.55) 45%, rgba(255,140,60,0) 72%);
  animation: impactBurst 0.34s ease-out forwards;
}
@keyframes impactBurst { 0% { transform: scale(0.25); opacity: 1; } 60% { opacity: 0.9; } 100% { transform: scale(1.9); opacity: 0; } }

/* ---- Floating damage numbers ---- */
@keyframes dmgFloat {
  0%   { opacity: 1; transform: translateX(-50%) translateY(0)     scale(1.3); }
  18%  { opacity: 1; transform: translateX(-50%) translateY(-10px) scale(1.0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-72px) scale(0.85); }
}
/* C6 (Joe, 2026-07-13): damage numbers mirror the MISS cue's pop-in float —
   same keyframe shape as missFloat — but in RED, escalating by damage tier.
   Crit keeps its gold identity. */
.dmg-number {
  position: absolute;
  top: 22%;
  font-family: var(--font-pixel);
  font-size: 13px;
  font-weight: 800;
  color: #ff5252;
  text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000, 0 0 7px rgba(255, 70, 70, 0.55);
  pointer-events: none;
  z-index: 30;
  white-space: nowrap;
  line-height: 1;
  animation: dmgFloatPop 0.88s ease-out forwards;
}
@keyframes dmgFloatPop {
  0%   { opacity: 0; transform: translateY(6px) scale(0.8); }
  22%  { opacity: 1; transform: translateY(-3px) scale(1.1); }
  100% { opacity: 0; transform: translateY(-26px) scale(1); }
}
.dmg-number.chip    { color: #ff9e9e; font-size: 10px; }
.dmg-number.heavy   { color: #ff3b30; font-size: 15px; }
.dmg-number.massive { color: #e01810; font-size: 19px; }
/* GJ-2: crit is a 4th tier — gold, largest, overrides the fraction-based ones. */
.dmg-number.crit { color: #ffd23f; font-size: 21px; text-shadow: 1px 1px 0 #7a4a00, -1px 1px 0 #7a4a00, 1px -1px 0 #7a4a00, -1px -1px 0 #7a4a00; }
.dmg-number-crit-label {
  position: absolute;
  top: 12%;
  font-family: var(--font-pixel);
  font-size: 11px;
  color: #ffd23f;
  text-shadow: 1px 1px 0 #7a4a00, -1px 1px 0 #7a4a00, 1px -1px 0 #7a4a00, -1px -1px 0 #7a4a00;
  pointer-events: none;
  z-index: 31;
  white-space: nowrap;
  line-height: 1;
  transform: translateX(-50%);
  animation: dmgFloat 0.88s ease-out forwards;
}
/* GJ-2: gold starburst ring on the target sprite — a crit's own escalation
   beat, distinct from the plain flinchFlash every hit already gets. */
.crit-ring { animation: crit-ring-burst 300ms ease-out; }
@keyframes crit-ring-burst {
  0%   { filter: drop-shadow(0 0 0 rgba(255, 210, 63, 0)) brightness(1); }
  25%  { filter: drop-shadow(0 0 16px rgba(255, 210, 63, 1)) brightness(1.4); }
  100% { filter: drop-shadow(0 0 0 rgba(255, 210, 63, 0)) brightness(1); }
}

/* Quick-win juice batch (2026-07-14, item 3): heal/RCT feedback, mirrors the
   dmg-number float (dmgFloatPop) and the crit-ring sprite pulse — same shape,
   green instead of red/gold, spawned via spawnHealNumber + spritePulse(...,
   'heal-glow', null) in game.js. #4ade80 matches the codebase's established
   "positive" green (trap-roll escape / clash-roll win, styles.css ~2240/2502). */
.heal-number {
  position: absolute;
  top: 22%;
  font-family: var(--font-pixel);
  font-size: 14px;
  font-weight: 800;
  color: #4ade80;
  text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000, 0 0 8px rgba(74, 222, 128, 0.7);
  pointer-events: none;
  z-index: 30;
  white-space: nowrap;
  line-height: 1;
  animation: dmgFloatPop 0.88s ease-out forwards;
}
.heal-glow { animation: heal-glow-burst 400ms ease-out; }
@keyframes heal-glow-burst {
  0%   { filter: drop-shadow(0 0 0 rgba(74, 222, 128, 0)) brightness(1); }
  30%  { filter: drop-shadow(0 0 14px rgba(74, 222, 128, 0.95)) brightness(1.25); }
  100% { filter: drop-shadow(0 0 0 rgba(74, 222, 128, 0)) brightness(1); }
}

/* Miss whiff cue (BUG-067): a grey "MISS" floats up + the target sprite sidesteps.
   Gives a missed attack its own feedback now that the ability VFX/SFX is suppressed
   on a miss, instead of the strike silently showing nothing on the target. */
.miss-number {
  position: absolute;
  top: 26%;
  font-family: var(--font-pixel);
  font-size: 13px; font-weight: 800;
  color: #b8c4d6;
  text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000, 0 0 7px rgba(180, 200, 230, 0.55);
  pointer-events: none;
  z-index: 30; white-space: nowrap; line-height: 1;
  animation: missFloat 0.82s ease-out forwards;
}
@keyframes missFloat {
  0%   { opacity: 0; transform: translateY(6px) scale(0.8); }
  22%  { opacity: 1; transform: translateY(-3px) scale(1.05); }
  100% { opacity: 0; transform: translateY(-24px) scale(1); }
}
.miss-dodge { animation: missDodge 0.34s ease-in-out; }
@keyframes missDodge {
  0%   { transform: translateX(0); }
  40%  { transform: translateX(13px) skewX(-6deg); }
  100% { transform: translateX(0); }
}

/* ============================================================
   GLOSSARY SCREEN
   ============================================================ */
/* Glossary — in-game codex styled to the game's dark/red/purple palette
   (game tokens only; the old cyan/#00d4ff + hot-pink #ff006e scheme is gone).
   Two scrolling panels (term list / detail) that fill remaining height via a
   3-row grid; stacks to a single column in portrait. */
#screen-glossary {
  flex-direction: column;
  background: var(--cream);
  color: var(--navy);
  overflow: hidden;
}

.glossary-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 22px clamp(16px, 4vw, 40px);
  gap: 16px;
}

.glossary-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: var(--font-pixel);
  font-size: 16px;
  padding-bottom: 12px;
  border-bottom: 3px solid var(--navy);
}

.glossary-header h2 {
  margin: 0;
  color: var(--navy);
  letter-spacing: 2px;
  text-shadow: 0 2px 0 #000;
}

.glossary-content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto minmax(0, 1fr);
  gap: 14px 20px;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.glossary-search { grid-column: 1 / -1; }

.glossary-search input {
  width: 100%;
  padding: 11px 15px;
  background: var(--cream-3);
  border: 2px solid rgba(236, 230, 221, 0.45);
  border-radius: 8px;
  color: var(--navy);
  font-family: var(--font-text);
  font-size: 19px;
  letter-spacing: 0.5px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.glossary-search input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}
.glossary-search input::placeholder { color: var(--navy-2); }

.glossary-count {
  grid-column: 1 / -1;
  font-family: var(--font-text);
  font-size: 16px;
  letter-spacing: 1px;
  color: var(--navy-2);
  text-transform: uppercase;
}

.glossary-list {
  grid-row: 3;
  grid-column: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px;
  background: var(--cream-2);
  border: 2px solid rgba(236, 230, 221, 0.5);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.glossary-term {
  padding: 10px 12px;
  margin: 6px 0;
  border-radius: 8px;
  background: var(--cream-3);
  border-left: 3px solid var(--navy-2);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.08s;
}
.glossary-term:hover { background: #26222e; transform: translateX(2px); }
/* selected term — solid JJK-red fill, matching the mode-select highlight */
.glossary-term.active {
  background: var(--accent);
  border-left-color: #ec6a5c;
  box-shadow: 0 0 0 1px #ec6a5c, var(--shadow);
}
.glossary-term.active .gt-name      { color: #fff5e8; }
.glossary-term.active .glossary-short { color: rgba(255, 245, 232, 0.9); }
.glossary-term.active .gt-chip      { color: #fff5e8 !important; border-color: rgba(255, 245, 232, 0.7) !important; }
/* per-group left accent (overridden by .active) */
.glossary-term[data-grp="core"]   { border-left-color: rgba(168, 85, 247, 0.6); }
.glossary-term[data-grp="combat"] { border-left-color: rgba(217, 74, 61, 0.6); }
.glossary-term[data-grp="effect"] { border-left-color: rgba(240, 200, 74, 0.6); }
.glossary-term.active { border-left-color: #ec6a5c; }
/* BUG-302: keyboard cursor highlight — matches .roster-tile.kbd-cursor's purple glow */
.glossary-term.kbd-cursor {
  border-color: var(--accent);
  box-shadow:
    0 0 4px 1px rgba(170, 110, 255, 0.6),
    0 0 12px 4px rgba(170, 110, 255, 0.45);
}

.gt-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.gt-name {
  font-family: var(--font-pixel);
  font-size: 10px;
  letter-spacing: 0.5px;
  color: var(--navy);
}

/* category chip — pill outlined in its group color */
.gt-chip {
  flex: 0 0 auto;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 999px;
  border: 1px solid currentColor;
  white-space: nowrap;
}
.glossary-term[data-grp="core"]   .gt-chip,
.gt-chip[data-grp="core"]   { color: var(--accent-2); }
.glossary-term[data-grp="combat"] .gt-chip,
.gt-chip[data-grp="combat"] { color: var(--accent); }
.glossary-term[data-grp="effect"] .gt-chip,
.gt-chip[data-grp="effect"] { color: var(--hp-yellow); }

.glossary-short {
  display: block;
  margin-top: 6px;
  font-family: var(--font-text);
  font-size: 16px;
  line-height: 1.25;
  color: var(--navy-2);
}

.glossary-detail {
  grid-row: 3;
  grid-column: 2;
  min-height: 0;
  overflow-y: auto;
  padding: 20px 22px;
  background: var(--cream-2);
  border: 2px solid rgba(236, 230, 221, 0.5);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-family: var(--font-text);
  font-size: 19px;
  line-height: 1.6;
  color: var(--navy);
}
.glossary-detail p { margin: 0; color: var(--navy); }

.glossary-term-header {
  font-family: var(--font-pixel);
  font-size: 16px;
  color: var(--navy);
  letter-spacing: 1px;
  text-shadow: 0 2px 0 #000;
  margin-bottom: 12px;
}

.gt-chip-lg { display: inline-block; font-size: 8px; padding: 4px 10px; }

.gloss-divider {
  height: 2px;
  margin: 14px 0;
  background: linear-gradient(90deg, var(--accent), transparent);
  border-radius: 2px;
}

/* Battle-screenshot block (glossary battle-shot system, 2026-07-23) — real
   in-game capture shown above the definition text. min-width:0 guard per the
   flex-min-width-cluster landmine (BUGLOG_INDEX.md) even though this wrapper
   isn't flex itself, for consistency with the rest of the panel. Missing-file
   case is handled in JS (onerror removes the wrapper), not CSS. */
.glossary-shot-wrap {
  margin: 10px 0 16px;
  min-width: 0;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid rgba(0, 0, 0, 0.25);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
.glossary-shot {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
}

/* Tutorial-snapshot block (glossary revamp, 2026-07-22) — visually distinct card
   inside the detail pane, min-width:0 on the flex-ish text children per the
   flex-min-width-cluster landmine (BUGLOG_INDEX.md); this block itself is a plain
   block element (not flex), so no shrink issue, but child text still gets the
   guard for consistency with the rest of the panel's discipline. */
.glossary-snapshot {
  margin-top: 16px;
  padding: 14px 16px;
  background: rgba(255, 216, 110, 0.08);
  border: 1px solid rgba(255, 216, 110, 0.35);
  border-left: 3px solid var(--accent, #ffd86e);
  border-radius: 8px;
  min-width: 0;
}
.gloss-snap-head {
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 1.5px;
  color: var(--accent, #ffd86e);
  text-shadow: 0 1px 0 #000;
  margin-bottom: 8px;
}
.gloss-snap-ref {
  font-family: var(--font-text);
  font-size: 13px;
  font-weight: 600;
  color: var(--navy-2);
  margin-bottom: 8px;
  min-width: 0;
}
.gloss-snap-ref--standalone { color: var(--navy-2); opacity: 0.85; font-style: italic; }
.gloss-snap-text {
  margin: 0;
  font-family: var(--font-text);
  font-size: 16px;
  line-height: 1.55;
  color: var(--navy);
  min-width: 0;
}

.glossary-placeholder,
.glossary-empty {
  color: var(--navy-2);
  text-align: center;
  padding: 40px 20px;
  font-family: var(--font-text);
  font-size: 18px;
}

/* dark scrollbars to match the panels */
.glossary-list::-webkit-scrollbar,
.glossary-detail::-webkit-scrollbar { width: 10px; }
.glossary-list::-webkit-scrollbar-thumb,
.glossary-detail::-webkit-scrollbar-thumb { background: rgba(236, 230, 221, 0.3); border-radius: 6px; }
.glossary-list::-webkit-scrollbar-track,
.glossary-detail::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.3); }

/* Portrait / narrow: stack list over detail in a single scrolling column */
body.layout-portrait .glossary-content {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
body.layout-portrait .glossary-list { flex: 0 0 auto; max-height: 38vh; }
body.layout-portrait .glossary-detail { flex: 1 1 auto; }
body.layout-portrait .glossary-container { padding: 14px; gap: 11px; }
body.layout-portrait .glossary-header { font-size: 13px; }

@media (max-width: 760px) {
  .glossary-content {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .glossary-list { flex: 0 0 auto; max-height: 38vh; }
  .glossary-detail { flex: 1 1 auto; }
}

/* ============================================================
   FONT SCALE — applied via JS to :root --font-scale
   Scale-sensitive elements multiply their base size by this var.
   ============================================================ */
.event-log .log-entry {
  font-size: calc(12px * var(--font-scale));
}
.detail-row {
  font-size: calc(11px * var(--font-scale));
}
.ct-tile .tile-name {
  font-size: calc(7px * var(--font-scale));
}
.char-bio, .char-hp-bar, .stat-val {
  font-size: calc(10px * var(--font-scale));
}

/* ============================================================
   OPTIONS PANEL
   ============================================================ */
.options-panel {
  position: absolute;
  top: 40px;
  right: 8px;
  width: 260px;
  background: var(--cream-2);
  border: var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow), var(--glow);
  padding: 12px;
  z-index: 100;
  font-family: var(--font-text);
  font-size: 14px;
  color: var(--navy);
}

.options-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: var(--font-pixel);
  font-size: 10px;
  color: var(--navy);
  letter-spacing: 1px;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 2px solid var(--navy);
}

.opts-close-btn {
  background: var(--cream-3);
  border: 2px solid var(--navy);
  border-radius: 6px;
  color: var(--navy);
  font-family: var(--font-pixel);
  font-size: 8px;
  padding: 3px 6px;
  cursor: pointer;
}

.opts-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid rgba(236, 230, 221, 0.12);
}

.opts-row:last-child {
  border-bottom: none;
}

.opts-scale-btns,
/* Save Data pair (2026-07-29) — same two-button cell shape as the scale row, so
   the label/value flex split and its min-width behaviour are already proven here. */
.opts-save-btns {
  display: flex;
  gap: 4px;
}

.opts-scale,
#opts-sfx-toggle,
#opts-dmg-toggle,
#opts-devmode-toggle,
#opts-save-export,
#opts-save-import {
  background: var(--cream-3);
  border: 2px solid var(--navy);
  border-radius: 6px;
  color: var(--navy);
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 1px;
  padding: 3px 7px;
  cursor: pointer;
  transition: all 0.1s;
}

.opts-scale:hover,
#opts-sfx-toggle:hover,
#opts-dmg-toggle:hover,
#opts-devmode-toggle:hover,
#opts-save-export:hover,
#opts-save-import:hover {
  border-color: var(--accent-2);
  color: var(--accent-2);
}

.opts-scale.active {
  background: var(--accent-2);
  border-color: var(--accent-2);
  color: var(--cream);
}

/* Carousel selector (Battle Speed / Title, 2026-07-12): ◀ value ▶. Replaces
   the side-by-side button rows that overflowed the options panel on narrow
   phones and caused a global horizontal scroll. Fixed-width value label keeps
   the arrows from shifting as the text changes. */
.opts-carousel {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.opts-car-arrow {
  background: var(--cream-3);
  border: 2px solid var(--navy);
  border-radius: 6px;
  color: var(--navy);
  font-family: var(--font-pixel);
  font-size: 8px;
  padding: 3px 6px;
  cursor: pointer;
  transition: all 0.1s;
}
.opts-car-arrow:hover {
  border-color: var(--accent-2);
  color: var(--accent-2);
}
.opts-car-value {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 1px;
  color: var(--accent-2);
  min-width: 88px;
  text-align: center;
  white-space: nowrap;
}

#opts-sfx-toggle,
#opts-dmg-toggle,
#opts-devmode-toggle {
  min-width: 40px;
  padding: 3px 8px;
}

.opts-hint {
  font-size: 11px;
  color: var(--navy-2);
  font-style: italic;
  line-height: 1.4;
}

/* ============================================================
   DAMAGE FORECAST in detail-row
   ============================================================ */
.detail-forecast {
  color: #ffd86e;
  font-weight: bold;
}

/* ============================================================
   TUTORIAL COACH CARD — a dismissible coaching popup anchored to the
   bottom of the screen (DS-style speech box). position:fixed so it
   overlays whatever screen is active. z-index above battle UI + overlays.
   ============================================================ */
/* ===== Hybrid Tutorial Overlay =====
   Anchored to #app (position:relative in both layouts), NOT the viewport — so in
   portrait (9/16 letterboxed) it glues to the phone stage instead of the black
   margin, and the cutout's full-bleed dim clips to #app's overflow:hidden box.
   JS (_position) converts target rects into #app-relative coords. */
#tutorial-card {
  position: absolute;
  inset: 0;
  z-index: 9000;
  pointer-events: none;
  overflow: hidden;
}

/* Spotlight cutout — box-shadow creates the full-screen dim around the target */
.tc-cutout {
  position: absolute;
  border-radius: 12px;
  box-shadow: 0 0 0 9999px rgba(4, 4, 10, 0.78);
  outline: 2px solid rgba(255, 216, 110, 0.9);
  transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1);
  z-index: 1;
  animation: tcCutoutGlow 1.1s ease-in-out infinite;
}
@keyframes tcCutoutGlow {
  0%, 100% { outline-color: rgba(255, 216, 110, 0.9); }
  50%       { outline-color: rgba(255, 216, 110, 1); box-shadow: 0 0 0 9999px rgba(4,4,10,.78), 0 0 0 4px rgba(255,216,110,.4); }
}

/* Bouncing chevron above target */
.tc-chev {
  position: absolute;
  z-index: 8;
  width: 32px;
  height: 32px;
  pointer-events: none;
  animation: tcChevBounce 0.8s ease-in-out infinite;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.6));
}
@keyframes tcChevBounce {
  0%, 100% { transform: rotate(var(--chev-flip, 0deg)) translateY(0); }
  50%       { transform: rotate(var(--chev-flip, 0deg)) translateY(-10px); }
}

/* Full-viewport SVG for the self-drawing arrow */
.tc-arrow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 7;
}
.tc-arrow path {
  stroke: #fffef8;
  stroke-width: 5;
  fill: none;
  stroke-linecap: round;
  filter: drop-shadow(2px 2px 0 #0d0d12) drop-shadow(-1px -1px 0 #0d0d12);
  stroke-dasharray: 400;
  stroke-dashoffset: 400;
  animation: tcDrawIn 0.5s 0.2s ease-out forwards;
}
@keyframes tcDrawIn { to { stroke-dashoffset: 0; } }

/* Manga coach head — bottom-left pop-in */
.tc-head {
  position: absolute;
  bottom: 64px;
  left: 14px;
  width: 130px;
  height: 130px;
  pointer-events: none;
  z-index: 11;                 /* above the bubble (9) — in portrait the head sits in the bubble's left gutter */
  animation: tcHeadPop 0.35s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
@keyframes tcHeadPop {
  from { transform: scale(0.4) rotate(-10deg); opacity: 0; }
  to   { transform: scale(1) rotate(-3deg); opacity: 1; }
}
.tc-burst {
  position: absolute;
  inset: -18px;
  z-index: -1;
  background:
    radial-gradient(circle at 30% 30%, rgba(0,0,0,.35) 1.4px, transparent 1.6px) 0 0/9px 9px,
    conic-gradient(from 0deg, #a855f7, #d94a3d, #a855f7);
  clip-path: polygon(50% 0%, 61% 12%, 78% 5%, 80% 23%, 98% 25%, 88% 40%, 100% 52%, 85% 60%, 92% 78%, 74% 76%, 70% 95%, 55% 83%, 42% 98%, 36% 80%, 17% 86%, 23% 67%, 3% 62%, 16% 49%, 2% 36%, 21% 32%, 13% 12%, 33% 17%, 38% 1%);
  animation: tcBurstSpin 9s linear infinite;
}
@keyframes tcBurstSpin { to { transform: rotate(360deg); } }
.tc-head img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 14px;
  border: 4px solid #0d0d12;
  box-shadow: 0 0 0 3px #fff, 0 12px 28px rgba(0,0,0,.6);
  background: #222;
  display: block;
}

/* Ink speech bubble */
.tc-bubble {
  position: absolute;
  bottom: 200px;
  left: 158px;
  max-width: 42vw;
  min-width: 210px;
  background: #fffef8;
  color: #16120a;
  pointer-events: auto;
  z-index: 9;
  border: 3.5px solid #0d0d12;
  border-radius: 22px;
  padding: 14px 18px 36px;
  font-size: 18px;
  line-height: 1.4;
  font-family: var(--font-text, 'VT323', monospace);
  box-shadow: 6px 6px 0 rgba(13,13,18,.85);
  animation: tcHeadPop 0.4s 0.08s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
.tc-bubble::before {
  content: '';
  position: absolute;
  left: -23px;
  bottom: 28px;
  width: 30px;
  height: 22px;
  background: #fffef8;
  border-left: 3.5px solid #0d0d12;
  border-bottom: 3.5px solid #0d0d12;
  clip-path: polygon(0 100%, 100% 0, 100% 100%);
}
/* T1: hides the floating-bubble's speech-tail whenever the JS narrow-bubble
   fallback is active (_position(), any host<480px width) — not just under
   body.layout-portrait. Portrait's #app is regularly <480px wide, so the
   narrow branch already fires there too (redundant with the portrait rule
   below, harmless), but it ALSO fires on a plain desktop window shrunk narrow
   with no portrait class present, where the portrait-scoped rule can't reach —
   that orphaned the triangle off the left edge of the now bottom-docked bubble. */
#tutorial-card .tc-bubble--narrow::before { display: none; }
.tc-bubble b { color: #d94a3d; }
.tc-bubble i { color: #6b5dd0; font-style: normal; }
.tc-coach-name {
  display: block;
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
  font-size: 8px;
  color: #6b5dd0;
  letter-spacing: 1px;
  margin-bottom: 8px;
}
/* WHY chip (BUG-280) — one-line rationale appended to guided DO-card copy by
   TutorialCoach._advance(). Styled as its own strip (dashed rule + gold tag)
   so it reads as a distinct "read this" affordance, not more body prose. */
.tc-why {
  display: block;
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1.5px dashed rgba(107, 93, 208, .35);
  font-size: 14px;
  line-height: 1.32;
  color: #4a4330;
}
.tc-why-tag {
  display: inline-block;
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
  font-size: 7px;
  color: #16120a;
  background: #ffd86e;
  border: 1.5px solid #0d0d12;
  border-radius: 4px;
  padding: 2px 4px;
  margin-right: 6px;
  vertical-align: middle;
  letter-spacing: .5px;
}
.tc-next-btn {
  position: absolute;
  right: 14px;
  bottom: 8px;
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
  font-size: 9px;
  color: #0d0d12;
  background: none;
  border: none;
  cursor: pointer;
  pointer-events: auto;
  animation: tcAdvBlink 1s steps(2) infinite;
}
@keyframes tcAdvBlink { 50% { opacity: 0; } }
.tc-next-btn:hover { color: #a855f7; animation: none; opacity: 1; }

/* ✕ Menu — quit-to-menu button, present on every coach card (top-right of bubble) */
.tc-exit-btn {
  position: absolute;
  top: 6px; right: 10px;
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
  font-size: 7px;
  letter-spacing: .5px;
  color: #9a8f78;
  background: none;
  border: none;
  padding: 2px 3px;
  cursor: pointer;
  pointer-events: auto;
  line-height: 1;
  z-index: 10;
  transition: color .12s;
}
.tc-exit-btn:hover { color: #d94a3d; }
.tc-exit-btn:focus-visible { outline: 2px solid #6b5dd0; outline-offset: 1px; }
body.layout-portrait #tutorial-card .tc-exit-btn { font-size: 6px; top: 5px; right: 7px; }

/* Pulsating action arrow — shown on DO-cards, lives outside #tutorial-card so it
   stays visible after the speech bubble collapses. Position is set inline (JS). */
#tc-pulse-arrow {
  position: absolute;
  z-index: 10005;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  transform: translateX(-50%);
}
.tc-pa-icon {
  font-size: 36px;
  line-height: 1;
  color: #ffd86e;
  filter: drop-shadow(0 0 8px rgba(255,216,110,.9));
  animation: tcPaBounce 0.55s ease-in-out infinite alternate;
}
.tc-pa-label {
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
  font-size: 7px;
  color: #ffd86e;
  background: #0d0d12;
  border: 1.5px solid #d94a3d;
  border-radius: 4px;
  padding: 3px 6px;
  margin-top: 4px;
  letter-spacing: 1px;
  white-space: nowrap;
  animation: tcPaGlow 0.55s ease-in-out infinite alternate;
}
@keyframes tcPaBounce {
  from { transform: translateY(0); }
  to   { transform: translateY(-10px); }
}
@keyframes tcPaGlow {
  from { box-shadow: 0 0 4px rgba(255,216,110,.5); }
  to   { box-shadow: 0 0 14px rgba(255,216,110,.9), 0 0 28px rgba(255,216,110,.35); }
}
/* Bubble fade after 2.5 s on DO cards — keeps spotlight + coach head visible */
.tc-bubble--hidden {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
body.layout-portrait .tc-pa-icon { font-size: 28px; }

/* ── MP RESULT MODAL ─────────────────────────────────────────────────────────
   Full-screen fixed overlay after an online battle ends or on forfeit. */
#mp-result-modal {
  position: fixed;
  inset: 0;
  z-index: 9200;
  background: rgba(10, 10, 13, 0.88);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  box-sizing: border-box;
}
.mpr-panel {
  background: var(--cream-2);
  border: 2px solid var(--navy);
  padding: 20px 24px 24px;
  max-width: 380px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  box-sizing: border-box;
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
}
.mpr-banner {
  font-size: 14px;
  letter-spacing: 2px;
  text-align: center;
  padding: 8px 12px;
  border: 2px solid currentColor;
  width: 100%;
  box-sizing: border-box;
}
.mpr-banner.mpr-victory { color: var(--hp-yellow); border-color: var(--hp-yellow); }
.mpr-banner.mpr-defeat  { color: var(--hp-red);    border-color: var(--hp-red); }
.mpr-banner.mpr-draw    { color: var(--navy-2);    border-color: var(--navy-2); }
.mpr-forfeit {
  font-size: 7px;
  color: var(--navy-2);
  text-align: center;
  letter-spacing: 0.5px;
  margin-top: -8px;
}
.mpr-vs {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  justify-content: center;
}
.mpr-side { flex: 1; text-align: center; }
.mpr-handle { font-size: 8px; color: var(--hp-yellow); margin-bottom: 4px; word-break: break-all; }
.mpr-char   { font-size: 7px; color: var(--navy-2); }
.mpr-sep    { font-size: 8px; color: var(--navy-2); flex-shrink: 0; }
.mpr-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
}
.mpr-meta-item { font-size: 7px; color: var(--navy-2); }
.mpr-meta-dot  { font-size: 9px; color: var(--navy-2); opacity: 0.4; }
/* Sorcerer-grade tier result line (2026-07-21) — js/ranked-grade.js is the single source
   of the band mapping/colors; this is presentation-only. */
.mpr-grade-meta {
  font-family: var(--font-text, 'VT323', monospace);
  font-size: 15px;
  color: var(--navy-2);
  display: flex;
  align-items: center;
  gap: 6px;
  justify-content: center;
  text-align: center;
}
.mpr-grade-dot { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex-shrink: 0; }
.mpr-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; width: 100%; }
.mpr-btn {
  font-family: var(--font-pixel, 'Press Start 2P', monospace);
  font-size: 7px;
  color: var(--navy);
  background: transparent;
  border: 2px solid var(--navy);
  padding: 7px 12px;
  cursor: pointer;
  letter-spacing: 0.5px;
  transition: background 0.12s, color 0.12s;
}
.mpr-btn:hover  { background: var(--navy); color: var(--cream); }
.mpr-btn-primary {
  color: var(--cream);
  background: var(--hp-yellow);
  border-color: var(--hp-yellow);
  width: 100%;
  padding: 10px 12px;
  font-size: 8px;
  margin-top: 2px;
}
.mpr-btn-primary:hover { background: #d4aa2e; border-color: #d4aa2e; color: var(--cream); }
.mpr-copy-fb {
  font-size: 7px;
  color: var(--hp-yellow);
  text-align: center;
  min-height: 14px;
}

/* ============================================================
   STORY MODE (Tower) — in-SPA screen (js/tower.js). All selectors
   namespaced under #screen-tower / .tw-* so the prototype's generic
   class names (.grid/.tile/.btn/.modal/.badge/.arena) can't collide
   with the battle/menu UI. Ported from tower-prototype.html.
   ============================================================ */
#screen-tower { overflow-y: auto; }
.tw-view { min-height: 100%; display: flex; align-items: flex-start; justify-content: center; padding: 20px; }
.tw-panel { background: #0a0a0d; border: 1px solid rgba(236,230,221,0.16); border-radius: 10px; overflow: hidden; width: 100%; max-width: 560px; }
.tw-phead { display: flex; align-items: center; padding: 12px 15px; border-bottom: 1px solid rgba(236,230,221,0.12); }
.tw-phead .tw-h { font-size: 12px; color: #ece6dd; }
.tw-phead .tw-s { font-size: 16px; color: rgba(236,230,221,0.5); margin-left: auto; }
.tw-grid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; padding: 15px; }
.tw-tile { position: relative; aspect-ratio: 1; border: 1px solid rgba(236,230,221,0.14); border-radius: 9px; background: rgba(236,230,221,0.03); display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 7px; cursor: pointer; transition: border-color .12s, background .12s; }
.tw-tile:hover { border-color: rgba(168,85,247,0.7); background: rgba(168,85,247,0.07); }
.tw-tile.lock { background: #070708; border-color: rgba(236,230,221,0.07); }
.tw-tile.sel { border-color: #d94a3d; background: rgba(217,74,61,0.12); }
.tw-tile.sel:hover { border-color: #a855f7; }
.tw-tile.done { border-color: rgba(95,220,95,0.45); }
.tw-tile.done:hover { border-color: #5fdc5f; }
.tw-tnum { position: absolute; top: 4px; left: 8px; font-size: 13px; }
.tw-badge { position: absolute; top: 4px; right: 5px; font-size: 12px; border-radius: 8px; padding: 0 6px; }
.tw-tpfp { width: 48px; height: 48px; border-radius: 7px; object-fit: cover; border: 1px solid rgba(236,230,221,0.25); }
.tw-lk { font-size: 22px; color: rgba(236,230,221,0.26); }
.tw-tlabel { font-size: 15px; color: #fff5e8; line-height: 1.05; margin-top: 4px; }
.tw-ll { font-size: 14px; color: rgba(236,230,221,0.3); margin-top: 4px; }
.tw-treward { font-size: 12px; color: #f0c84a; line-height: 1.05; margin-top: 4px; opacity: .92; }
.tw-tile.lock .tw-treward { color: rgba(240,200,74,0.5); }
.tw-pfoot { display: flex; align-items: center; gap: 9px; padding: 11px 15px; border-top: 1px solid rgba(236,230,221,0.12); font-size: 15px; color: rgba(236,230,221,0.45); }
.tw-pfoot .tw-btnB { margin-left: auto; }
/* briefing modal */
.tw-backdrop { position: fixed; inset: 0; background: rgba(5,5,7,0.74); display: none; align-items: center; justify-content: center; padding: 18px; z-index: 60; }
.tw-backdrop.open { display: flex; }
.tw-modal { background: #111014; border: 1px solid rgba(236,230,221,0.2); border-radius: 12px; overflow: hidden; width: 100%; max-width: 400px; font-family: 'VT323', monospace; }
.tw-mhead { display: flex; align-items: center; gap: 7px; padding: 11px 14px; border-bottom: 1px solid rgba(236,230,221,0.13); }
.tw-mhead .tw-b { font-size: 10px; color: rgba(236,230,221,0.5); }
.tw-mhead .tw-t { font-size: 18px; color: #fff5e8; }
.tw-mx { margin-left: auto; color: rgba(236,230,221,0.45); font-size: 18px; cursor: pointer; line-height: 1; }
.tw-arena { position: relative; height: 108px; background: #070708; border-bottom: 1px solid rgba(236,230,221,0.13); }
.tw-arena img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tw-ph { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #3a4250; }
.tw-where { position: absolute; left: 0; right: 0; bottom: 0; padding: 5px 12px; background: linear-gradient(transparent, rgba(7,7,8,0.92)); font-size: 16px; }
.tw-mbody { padding: 12px 14px; }
.tw-lbl { font-size: 13px; letter-spacing: 1px; color: rgba(236,230,221,0.45); text-transform: uppercase; margin-bottom: 1px; }
.tw-txt { font-size: 17px; line-height: 1.22; color: #ece6dd; margin-bottom: 10px; }
.tw-fighters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 11px; }
.tw-fc { display: flex; align-items: center; gap: 6px; }
.tw-fp { width: 38px; height: 38px; border-radius: 7px; object-fit: cover; border: 1px solid rgba(236,230,221,0.2); flex: none; background: #1a1a1f; }
.tw-fp.tw-curse { display: flex; align-items: center; justify-content: center; color: #cf8a82; font-size: 18px; }
.tw-fn { font-size: 16px; }
#screen-tower .tw-fn.hero { color: #5db4e8; }
#screen-tower .tw-fn.villain { color: #d94a3d; }
.tw-vs { color: rgba(236,230,221,0.4); font-size: 15px; }
.tw-lockbar { margin: 2px 0 10px; padding: 7px 10px; border: 1px solid rgba(236,230,221,0.14); border-radius: 7px; background: rgba(236,230,221,0.03); font-size: 15px; color: rgba(236,230,221,0.7); }
.tw-standbar { margin: 2px 0 10px; padding: 6px 10px; border: 1px solid rgba(240,200,74,0.3); border-radius: 7px; background: rgba(240,200,74,0.06); font-size: 14px; color: #f0c84a; }
.tw-rewardbar { margin: 2px 0 10px; padding: 7px 10px; border: 1px solid rgba(240,200,74,0.35); border-radius: 7px; background: rgba(240,200,74,0.07); font-size: 15px; color: #f0c84a; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.tw-rl { font-size: 11px; letter-spacing: 1px; border: 1px solid rgba(240,200,74,0.5); border-radius: 5px; padding: 1px 5px; }
.tw-rchip { display: inline-flex; align-items: center; gap: 4px; background: rgba(240,200,74,0.12); border-radius: 6px; padding: 2px 8px 2px 2px; color: #ffe9a8; }
.tw-rp { width: 24px; height: 24px; border-radius: 5px; object-fit: cover; border: 1px solid rgba(240,200,74,0.3); }
.tw-mfoot { display: flex; gap: 9px; justify-content: flex-end; flex-wrap: wrap; padding: 11px 14px; border-top: 1px solid rgba(236,230,221,0.13); }
.tw-btn { font-size: 16px; font-family: 'VT323', monospace; color: #ece6dd; border: 1px solid rgba(236,230,221,0.32); border-radius: 6px; padding: 5px 13px; cursor: pointer; transition: background .12s, border-color .12s, color .12s; }
.tw-btn:hover { background: #a855f7; border-color: #a855f7; color: #fff5e8; }
.tw-btn.disabled { color: rgba(236,230,221,0.3); border-color: rgba(236,230,221,0.12); cursor: not-allowed; }
.tw-btn.disabled:hover { background: transparent; color: rgba(236,230,221,0.3); border-color: rgba(236,230,221,0.12); }
.tw-btnB { font-size: 16px; font-family: 'VT323', monospace; color: rgba(236,230,221,0.55); border: 1px solid rgba(236,230,221,0.18); border-radius: 6px; padding: 5px 12px; cursor: pointer; transition: background .12s, border-color .12s, color .12s; }
.tw-btnB:hover { background: #a855f7; border-color: #a855f7; color: #fff5e8; }
/* story overlay (prelude + battle-end panel) */
.tw-story { position: fixed; inset: 0; z-index: 65; display: none; background: #050507; }
.tw-story.open { display: block; animation: twFade .25s ease; }
@keyframes twFade { from { opacity: 0; } to { opacity: 1; } }
.tw-so-img { position: absolute; inset: 0; background-size: cover; background-position: center; background-color: #0a0a0d; }
.tw-so-img::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 30%, rgba(5,5,7,.6) 52%, rgba(5,5,7,.97)); }
.tw-so-panel { position: absolute; left: 0; right: 0; bottom: 0; max-height: 82%; overflow-y: auto; padding: 20px clamp(18px,6vw,70px) 28px; z-index: 1; }
.tw-so-inner { max-width: 620px; margin: 0 auto; }
.tw-so-kicker { font-family: 'Press Start 2P', monospace; font-size: 12px; letter-spacing: 2px; color: #ec6a5c; margin-bottom: 10px; }
.tw-so-title { font-family: 'Press Start 2P', monospace; font-size: 14px; color: #fff5e8; margin-bottom: 13px; line-height: 1.4; }
.tw-so-body { font-family: 'VT323', monospace; font-size: 20px; line-height: 1.3; color: #ece6dd; margin-bottom: 18px; }
.tw-so-cont { font-family: 'Press Start 2P', monospace; font-size: 11px; color: #ece6dd; background: rgba(168,85,247,.15); border: 1px solid #a855f7; border-radius: 7px; padding: 11px 20px; cursor: pointer; transition: background .12s, color .12s; }
.tw-so-cont:hover { background: #a855f7; color: #fff5e8; }
/* battle-end panel (GDD spec) — rendered into the story body */
.tw-ep-summary { font-size: 17px; color: rgba(236,230,221,.82); margin-bottom: 14px; }
.tw-ep-sec { margin-bottom: 12px; }
.tw-ep-h { font-family: 'Press Start 2P', monospace; font-size: 9px; letter-spacing: 1px; color: rgba(236,230,221,.45); text-transform: uppercase; margin-bottom: 5px; }
.tw-ep-hi { font-size: 18px; color: #ece6dd; }
.tw-party-row { display: flex; align-items: center; gap: 8px; font-size: 17px; margin-bottom: 3px; }
.tw-party-name { flex: 0 0 34%; color: #ece6dd; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-party-bar { flex: 1 1 auto; height: 8px; background: rgba(236,230,221,.12); border-radius: 4px; overflow: hidden; }
.tw-party-bar > span { display: block; height: 100%; }
.tw-party-hp { flex: 0 0 auto; font-size: 15px; color: rgba(236,230,221,.7); min-width: 62px; text-align: right; }
.tw-ep-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
/* return-to-tower confirm */
.tw-confirm { position: fixed; inset: 0; z-index: 70; display: none; align-items: center; justify-content: center; background: rgba(5,5,7,.72); }
.tw-confirm.open { display: flex; animation: twFade .18s ease; }
.tw-cr-card { background: #0d0d12; border: 1px solid rgba(168,85,247,.55); border-radius: 12px; padding: 26px 30px 24px; max-width: 380px; width: 88%; box-shadow: 0 14px 50px rgba(0,0,0,.6); }
.tw-cr-kick { font-family: 'Press Start 2P', monospace; font-size: 9px; letter-spacing: 2px; color: #ec6a5c; margin-bottom: 12px; }
.tw-cr-title { font-family: 'Press Start 2P', monospace; font-size: 14px; color: #fff5e8; line-height: 1.45; margin-bottom: 12px; }
.tw-cr-text { font-family: 'VT323', monospace; font-size: 19px; line-height: 1.25; color: rgba(236,230,221,.78); margin-bottom: 22px; }
.tw-cr-row { display: flex; gap: 11px; justify-content: flex-end; }
.tw-cr-btn { font-family: 'Press Start 2P', monospace; font-size: 10px; border-radius: 7px; padding: 11px 16px; cursor: pointer; transition: background .12s, border-color .12s, color .12s; }
.tw-cr-stay { color: rgba(236,230,221,.7); background: transparent; border: 1px solid rgba(236,230,221,.22); }
.tw-cr-stay:hover { background: rgba(236,230,221,.1); color: #fff5e8; }
.tw-cr-go { color: #fff5e8; background: rgba(168,85,247,.2); border: 1px solid #a855f7; }
.tw-cr-go:hover { background: #a855f7; }

/* ═══ Local hot-seat handoff (BUG-162) — pass-the-device interstitial ═══
   Full-viewport privacy beat between two HUMAN teams' turns. Lives inside
   #screen-battle so leaving the battle screen hides it (same ancestor-display
   rule the Tower postlude relies on). Game palette per the design-token rule
   (cream/navy/crimson — NOT the cyan drift). */
#hs-handoff {
  position: fixed; inset: 0; z-index: 12000;
  display: none;
  align-items: center; justify-content: center;
  background: rgba(10, 10, 16, 0.96);
}
#hs-handoff.open { display: flex; }
.hs-card {
  text-align: center; padding: 34px 44px;
  border: 2px solid var(--navy); border-radius: 12px;
  background: var(--cream);
  box-shadow: 0 12px 60px rgba(0,0,0,0.6);
  max-width: min(480px, 88vw);
}
.hs-kicker {
  font-family: var(--font-pixel); font-size: 11px; letter-spacing: 2px;
  color: var(--accent, #c33);
  margin-bottom: 14px;
}
.hs-player {
  font-family: var(--font-pixel); font-size: 26px; color: var(--navy);
  margin-bottom: 8px;
}
.hs-actor {
  font-family: var(--font-text); font-size: 20px; color: var(--navy-2, #444);
  margin-bottom: 4px;
}
.hs-sub {
  font-family: var(--font-text); font-size: 16px; color: rgba(40,40,60,0.65);
  margin-bottom: 22px;
}
.hs-ready {
  font-family: var(--font-pixel); font-size: 13px;
  padding: 14px 26px; cursor: pointer; border-radius: 8px;
  color: var(--cream); background: var(--navy); border: 2px solid var(--navy);
  transition: transform .1s, background .12s;
}
.hs-ready:hover { background: #c33; border-color: #c33; transform: translateY(-1px); }

/* ============================================================================
   DISCLAIMER CAROUSEL (2026-07-29, Joe's ask) — the unofficial-fan-game notice
   plates shown between PRESS START and mode-select. Built by js/disclaimer.js.

   z-index 13000 sits above every screen and the #hs-handoff overlay (12000) but
   deliberately BELOW .sfx-toggle (100000), so a player can still mute during the
   ~30s sequence. A tap on that corner control therefore does not count toward
   the tap-to-dismiss total, which is the correct trade.

   The plates are 2:3 portrait art in a landscape viewport, so they use
   `contain` on black — same letterbox treatment as .title-bg-art, never `cover`,
   which would crop the legal text off the edges.
   ========================================================================== */
.disclaimer-overlay {
  position: fixed;
  inset: 0;
  z-index: 13000;
  background: #000;
  cursor: pointer;
  overflow: hidden;
  touch-action: manipulation;   /* no double-tap zoom stealing the 2nd tap */

  /* FAIL-SAFE VISIBILITY (learned the hard way, 2026-07-29): this element is
     opaque by DEFAULT and the fade is a pure enhancement layered on top. The
     first implementation started at opacity:0 and transitioned up, which meant
     the notice rendered INVISIBLE whenever the frame loop was throttled (hidden
     tab, some mobile webviews) — observed live. A legal notice that silently
     shows nothing while the player taps through it is the worst outcome this
     screen can produce, so its visibility must never depend on an animation
     actually running. Same reasoning as the no-rAF note in js/disclaimer.js. */
  opacity: 1;
}
/* There is deliberately NO fade-IN. A second attempt tried `animation: fade-in`
   with no fill, reasoning that an un-run animation leaves the element at its
   base opacity:1 — but a THROTTLED animation is worse than an un-run one: it is
   active and frozen at its 0% keyframe, so the notice still rendered invisible.
   Verified against a genuinely hidden tab (document.visibilityState === 'hidden',
   rAF never firing). A hard cut is the only presentation that cannot fail, and
   for a legal notice that is the right trade — the fade bought nothing.
   The fade-OUT is kept because its failure mode is benign: if it doesn't run the
   overlay simply stays opaque until the node is removed a beat later. */
.disclaimer-overlay.is-closing { animation: disclaimer-fade-out .26s ease forwards; }

@keyframes disclaimer-fade-out { from { opacity: 1; } to { opacity: 0; } }

.disclaimer-stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.disclaimer-plate {
  position: absolute;
  inset: 0;
  background-color: #000;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}
.disclaimer-plate.is-current { opacity: 1; }

/* Screen-reader-only copy: the notice itself is baked into the JPEGs, so without
   this the whole legal screen is invisible to assistive tech. */
.disclaimer-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* The carousel: outgoing plate slides off left with a jitter; incoming slides in
   from the right, OVERSHOOTS ~3% past its resting position, then recoils back to
   0. Timings match TRANSITION_MS (420ms) in js/disclaimer.js — change both. */
@keyframes disclaimer-leave {
  0%   { transform: translateX(0)      translateY(0)    rotate(0deg); opacity: 1; }
  18%  { transform: translateX(-4%)    translateY(-6px) rotate(-.5deg); }
  36%  { transform: translateX(-14%)   translateY(5px)  rotate(.45deg); }
  58%  { transform: translateX(-42%)   translateY(-4px) rotate(-.35deg); }
  80%  { transform: translateX(-78%)   translateY(3px)  rotate(.25deg); }
  100% { transform: translateX(-108%)  translateY(0)    rotate(0deg); opacity: .85; }
}
@keyframes disclaimer-enter {
  0%   { transform: translateX(108%)  translateY(0)    rotate(0deg); }
  22%  { transform: translateX(58%)   translateY(6px)  rotate(.5deg); }
  45%  { transform: translateX(22%)   translateY(-5px) rotate(-.45deg); }
  68%  { transform: translateX(-3.2%) translateY(4px)  rotate(.3deg); }   /* overshoot */
  84%  { transform: translateX(1.4%)  translateY(-2px) rotate(-.15deg); } /* recoil back */
  93%  { transform: translateX(-.5%)  translateY(1px)  rotate(.06deg); }  /* settle */
  100% { transform: translateX(0)     translateY(0)    rotate(0deg); }
}

.disclaimer-plate.is-leaving {
  opacity: 1;
  animation: disclaimer-leave .42s cubic-bezier(.36, .07, .19, .97) both;
}
.disclaimer-plate.is-entering {
  animation: disclaimer-enter .42s cubic-bezier(.34, 1.26, .64, 1) both;
}

/* Tap acknowledgement — a 3-tap dismiss with no feedback reads as a frozen
   screen on taps 1 and 2. A brief inset flash confirms the input landed. */
@keyframes disclaimer-tap-ack {
  0%   { box-shadow: inset 0 0 0 0 rgba(255, 255, 255, 0); }
  30%  { box-shadow: inset 0 0 0 6px rgba(255, 255, 255, .22); }
  100% { box-shadow: inset 0 0 0 0 rgba(255, 255, 255, 0); }
}
.disclaimer-overlay.tap-ack { animation: disclaimer-tap-ack .22s ease-out; }

/* Reduced motion: js/disclaimer.js already skips the slide classes, but belt-and
   -braces in case a class lands — collapse everything to a plain crossfade. */
@media (prefers-reduced-motion: reduce) {
  .disclaimer-plate { transition: opacity .3s ease; }
  .disclaimer-plate.is-leaving,
  .disclaimer-plate.is-entering { animation: none; transform: none; }
  .disclaimer-overlay.tap-ack { animation: none; }
}

/* ============================================================
   LAYOUT MODE — Mobile LANDSCAPE (BUG-343, Joe's ask 2026-08-01).
   Applied by applyLayoutMode() when the layout is 'mobile' AND the
   browser reports (orientation: landscape) — i.e. a phone turned on
   its side. Mutually exclusive with .layout-portrait; desktop carries
   neither class. Joe's ruling on the arrangement: battle arena on the
   LEFT, event log + action menu stacked in a RIGHT column, and the
   turn queue VERTICAL on the far right edge.
   NOTE ON SCOPING: desktop rules scoped `body:not(.layout-portrait)`
   (HUD card columns, sprite sizes, .battle-bottom's fixed-height
   formula) DO match this mode too — the overrides below are placed at
   the end of the stylesheet precisely so they win that cascade at
   equal specificity. ============================================== */

/* The stage fills the whole viewport — the portrait mode's centered
   9:16 letterbox makes no sense sideways. dvh per BUG-339 (both the
   container chain and the stage must track the DYNAMIC viewport). */
body.layout-landscape { display: block; }
body.layout-landscape #app {
  width: 100%;
  height: 100vh; height: 100dvh;
  max-width: none;
  margin: 0;
  position: relative;
  overflow: hidden;
}

/* Battle screen becomes a 2-row grid: banner strip across the top,
   then arena | log+actions | vertical queue. minmax(0,1fr) on the
   content row so the arena can SHRINK below its content height —
   without it the grid row grows past the viewport (the classic
   min-content trap) and the bottom clips. */
body.layout-landscape #screen-battle.active {
  display: grid;
  grid-template-areas:
    "banner banner banner"
    "arena  bottom queue";
  grid-template-columns: minmax(0, 1fr) minmax(240px, 34%) auto;
  grid-template-rows: auto minmax(0, 1fr);
  padding: 0; gap: 0;
}
body.layout-landscape .battle-mode-banner { grid-area: banner; }
body.layout-landscape .battle-arena {
  grid-area: arena;
  min-height: 0; min-width: 0;
  height: 100%; max-height: none;
}

/* Turn queue: vertical strip, top-aligned, scrolls internally when a
   big match (Quick Match can field 24) outgrows the column. Same 38px
   tiles as portrait so the two mobile modes read at one scale. */
body.layout-landscape .turn-queue {
  grid-area: queue;
  flex-direction: column;
  flex-wrap: nowrap;
  justify-content: flex-start;
  gap: 6px;
  padding: 8px 6px;
  min-height: 0;
  min-width: 0;
  width: auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  border-left: var(--border);
}
body.layout-landscape .tq-tile { width: 38px; height: 38px; flex: 0 0 auto; }
body.layout-landscape .tq-tile.active { animation: tq-pulse-sm 0.9s ease-in-out infinite; }

/* Right column: log on top (short, scrolls), action area filling the
   rest. Overrides the desktop .battle-bottom height formula (that rule
   is body:not(.layout-portrait)-scoped, so it matches here too). */
body.layout-landscape .battle-bottom {
  grid-area: bottom;
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto minmax(0, 1fr);
  height: 100% !important;   /* the desktop formula sets a fixed calc() height; !important beats its specificity without duplicating the whole selector chain */
  min-height: 0;
  gap: 6px; padding: 8px;
  border-top: none;
  border-left: var(--border);
}
body.layout-landscape .event-log { font-size: 11px; max-height: 96px; min-height: 40px; }
body.layout-landscape .action-area { min-height: 0; overflow: hidden; }
body.layout-landscape .action-grid {
  grid-template-columns: 1fr 1fr;   /* same 2x2 as portrait (BUG-340) */
  gap: 8px; height: 100%;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
}
body.layout-landscape .action-btn { min-height: 44px; font-size: 12px; }

/* HUD cards inside the arena: the desktop rules pin a 410px column, which
   cannot fit an arena that is now ~55% of a phone's width. Handled by SCOPE,
   not duplication — the portrait card/sprite/stats internals (styles.css
   ~4106-4200) are rescoped `:is(body.layout-portrait, body.layout-landscape)`
   so both mobile orientations share the one proven card treatment. Only the
   desktop sprite vertical offset needs neutralising here (portrait never sets
   `top`, so the desktop 24px offset would otherwise leak through). */
body.layout-landscape .battle-sprite { top: 0; }
body.layout-landscape .combatant.player .battle-sprite { top: 6px; }

/* ---- LANDSCAPE: mode select (BUG-344, from Joe's on-device screenshots).
   The desktop layout stacks art above controls in a COLUMN; on a ~350px-tall
   rotated phone the art pane swallowed the height, the mode list started
   below the fold, and the utility row painted OVER the list rows. Landscape
   splits side-by-side instead: art left, list+details right, utility row
   spanning the bottom. `.active` must stay in the selector — the base
   `.screen { display:none }` hide loses to an ID-carrying selector, so an
   unscoped display:grid would reveal the screen permanently. */
body.layout-landscape #screen-mode.active {
  display: grid;
  grid-template-areas: "art controls" "util util";
  grid-template-columns: minmax(0, 44%) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 8px; padding: 8px 12px;
  overflow: hidden;
}
body.layout-landscape .mode-art-pane { grid-area: art; min-height: 0; padding: 10px; }
body.layout-landscape .mode-controls-pane {
  grid-area: controls; min-height: 0; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px 14px;
}
body.layout-landscape .mode-utility-row {
  grid-area: util; display: flex; flex-direction: row;
  justify-content: center; gap: 10px;
}
body.layout-landscape #screen-mode h2 { font-size: 11px; margin-bottom: 8px; }
body.layout-landscape .mode-footer { display: none; }

/* ---- LANDSCAPE: Tower battle grid (BUG-344). The 560px panel cap left ~40%
   of a rotated phone black, while 3 columns of aspect-ratio:1 tiles were huge
   relative to the short viewport. Joe's ask: smaller tiles, use the width.
   auto-fill at a fixed minimum derives the column count from the real width
   (~6 columns at 812px) instead of hardcoding one. */
/* .tw-view is a flex CHILD of #screen-tower (.screen.active is display:flex),
   so without an explicit width it shrink-to-fits its content — which collapses
   the auto-fill grid to its min-content (2 columns in a 362px view, measured).
   Fill the row first; THEN the panel's 96% and auto-fill resolve as intended. */
body.layout-landscape .tw-view { width: 100%; }
body.layout-landscape .tw-panel { max-width: 96%; }
body.layout-landscape .tw-grid {
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
  gap: 8px; padding: 10px;
}
body.layout-landscape .tw-tile { padding: 5px; border-radius: 7px; }
