/* styles/combat.css — combat screen (mockup: docs/mockups/combat-screen.svg) */

.combat {
  --safe-top-local: calc(env(safe-area-inset-top, 0px) / var(--ui-zoom, 1));
  --safe-right-local: calc(env(safe-area-inset-right, 0px) / var(--ui-zoom, 1));
  --safe-bottom-local: calc(env(safe-area-inset-bottom, 0px) / var(--ui-zoom, 1));
  --safe-left-local: calc(env(safe-area-inset-left, 0px) / var(--ui-zoom, 1));
  --safe-edge-local: calc(2px / var(--ui-zoom, 1));
  flex: 1; display: flex; flex-direction: column; position: relative; overflow: hidden;
}

/* ---------- act backdrop ---------- */
/* One rendered plate per act (tools/backdrops-blender.py → assets/bg/*.webp,
   ~4 KB each; tools/bundle.mjs base64s them into the standalone build). The
   plate is geometry only — the bloom around the Goldbough is the ::after
   radial-gradient below, so it stays perfectly smooth, costs no bytes, and
   picks up the player's accent colour. Sits behind everything, never eats
   clicks. */
.backdrop {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-position: center 38%; background-size: cover; background-repeat: no-repeat;
  opacity: 0.55;
}
.backdrop::after {
  content: ''; position: absolute; left: 50%; top: 20%; width: 46rem; height: 46rem;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(var(--accent-rgb), 0.30), rgba(var(--accent-rgb), 0.10) 38%, transparent 68%);
  animation: backdropGlow 7s ease-in-out infinite alternate;
}
@keyframes backdropGlow { from { opacity: 0.75; } to { opacity: 1; } }
.reduced-motion .backdrop::after { animation: none; }
:root[data-ambient='off'] .backdrop::after { animation: none; opacity: 0.7; }
.backdrop.act-1 { background-image: url('../assets/bg/bg_act1.webp'); }
.backdrop.act-2 { background-image: url('../assets/bg/bg_act2.webp'); }
.backdrop.act-3 { background-image: url('../assets/bg/bg_act3.webp'); }
/* Keep the board legible over art: a floor-to-mid scrim under the combatants. */
.backdrop::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to bottom, rgba(13, 11, 8, 0.30) 0%, rgba(13, 11, 8, 0.10) 38%, rgba(13, 11, 8, 0.82) 100%);
}

/* Approved environment paintings are selected with an SVG atlas viewBox. */
.environment-backdrop { opacity: 1; overflow: hidden; background: #17130f; top: var(--environment-top, 0); bottom: auto; height: var(--environment-height, 65%); }
.environment-backdrop > svg { position: absolute; inset: 0; width: 100%; height: 100%; transform:scale(1.06); transform-origin:center; }
@media (max-width:600px) { .environment-backdrop > svg { transform:scale(1.02); } }
/* WGS1: once the battlefield stage has fitted the plate (SceneLayerModel), its
   viewBox already carries the bleed and the floor alignment; a CSS zoom about
   the centre would move the painted ground off the floor band. */
.environment-backdrop[data-scene-fit] > svg { transform: none; }
.environment-backdrop[data-skyline='off'] > svg { display: none; }
.environment-backdrop::after { display: none; }
.environment-backdrop::before {
  z-index: 1;
  background: linear-gradient(to bottom, rgba(13, 11, 8, 0.22), rgba(13, 11, 8, 0.06) 40%, rgba(13, 11, 8, 0.22) 90%, #17130f 100%);
}

/* A pale alpha-shaped edge plus dark separation reads on snow and charcoal.
   Keep this on the art host, so frame-level target glows and pose auras compose. */
.combat .combatant .sprite {
  --combatant-edge: drop-shadow(0.7px 0 0 #f4e6c4) drop-shadow(-0.7px 0 0 #f4e6c4)
    drop-shadow(0 0.7px 0 #f4e6c4) drop-shadow(0 -0.7px 0 #f4e6c4)
    drop-shadow(0 0 2px #100e0c);
}
.combat .combatant .pose-frame, .combat .combatant .defeated-frame { filter: var(--combatant-edge); }
.combat .combatant .sprite::before {
  content: ''; position: absolute; bottom: -3px; left: 12%; right: 12%;
  height: 12px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(ellipse, rgba(10, 8, 6, .7), rgba(10, 8, 6, .28) 45%, transparent 72%);
}

/* ---------- battlefield ---------- */
/* The combatants sit ON the backdrop, so the field is a positioned layer above
   it and the rows are pulled up out of the old dead space at the top. */
/* The battlefield is the flexible row between two intrinsic bands. Its default
   flex min-height is its combatants' min-content height. At 320x640 and
   1200x730 with Text XL that prevented it from yielding a single pixel and
   pushed the hand/card band below the viewport. Let this one flexible row
   yield only when main.js's existing fit decider says the viewport is not
   short. The short-screen branch keeps the min-content premise measured by the
   upright refusal; this selector reads that answer and defines no breakpoint. */
.field {
  --battlefield-edge-gap: clamp(0.35rem, 1.2%, 0.8rem);
  flex: 1; display: flex; align-items: stretch; position: relative; z-index: 1;
  box-sizing: border-box;
  padding-block: var(--battlefield-hud-clearance, 0) var(--battlefield-action-clearance, 0);
  padding-inline-end: var(--quick-corner);
}
:root[data-short='false'] .field { min-height: 0; }
.player-zone {
  width: 32%; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding-block: var(--battlefield-edge-gap); gap: 0.6rem;
}
.enemy-row {
  flex: 1; display: flex; align-items: center; justify-content: space-evenly;
  padding-block: var(--battlefield-edge-gap); gap: 1rem;
}
.field[data-stage-safe-corridor='true'] :is(.player-zone, .enemy-row) { padding-block: 0; }

/* Combatant Inspector — the expanded reading is an ordinary Folding Tray,
   not a second tooltip implementation. Player details own the left edge;
   enemy details own the right. The host ignores the playfield except where the
   tray itself is painted, so a folded rail cannot steal combat targeting. */
.combatant-inspector-host {
  position: absolute; z-index: 18; top: var(--battlefield-hud-clearance, 0);
  bottom: var(--battlefield-action-clearance, 0);
  display: flex; width: min(calc(var(--combatant-inspector-width-rem, 20) * 1rem), 34%);
  min-width: 0; pointer-events: none;
}
.combatant-inspector-host[hidden] { display: none; }
.combatant-inspector-host.left { left: 0; justify-content: flex-start; }
.combatant-inspector-host.right {
  right: calc(var(--tap-floor) + (var(--hud-quick-edge-gap, 4px) / var(--ui-zoom, 1)));
  justify-content: flex-end;
}
.combatant-inspector-host > .folding-tray {
  pointer-events: auto; width: 100%; height: 100%; padding: 0.65rem 0.75rem;
  border: 1px solid var(--line); border-radius: var(--r-2);
  background: color-mix(in srgb, var(--panel) 96%, transparent);
  box-shadow: 0 0.5rem 1.5rem rgba(0,0,0,0.45);
}
.combatant-inspector-host > .folding-tray[data-collapsed='1'] { width: var(--tap-floor); }
.combatant-inspector-host .tray-content { padding-top: 0.5rem; }

.combatant.inspectable:not(.targetable) { cursor: help; }

:root[data-layout='narrow'] .combatant-inspector-host {
  width: min(var(--combatant-inspector-mobile-vw, 62vw), calc(var(--combatant-inspector-width-rem, 20) * 1rem));
}
:root[data-layout='narrow'] .combatant-inspector-host.right { right: 0; }

/* Arcane visual evidence is a host-snapshot pose, not a client reconstruction.
   Remove unrelated player/action chrome so all conditional enemy states remain
   readable in one phone still. This selector is unreachable in ordinary play. */
:root[data-shot-arcane='matrix'] .combat.coop .topbar,
:root[data-shot-arcane='matrix'] .combat.coop .player-zone,
:root[data-shot-arcane='matrix'] .combat.coop .hand-area { display: none; }
:root[data-shot-arcane='matrix'] .combat.coop .field { overflow: visible; padding: 1rem; }
:root[data-shot-arcane='matrix'] .combat.coop .enemy-row {
  width: 100%; min-width: 0; height: 100%; padding: 0; align-items: center;
}

.combatant {
  display: flex; flex-direction: column; align-items: center; position: relative;
  width: 100%; height: 100%; min-height: 0;
}
.combatant[data-ui-component='combatant-frame'] {
  display: block;
  overflow: visible;
}
.combatant-stack {
  position: absolute;
  left: 50%;
  top: var(--combatant-stage-center, 50%);
  width: 100%;
  height: var(--combatant-stack-height, auto);
  transform: translate(-50%, -50%);
}
.combatant-leading {
  position: absolute; inset: 0 0 auto;
  display: flex; align-items: flex-end; justify-content: center;
  flex: 0 0 auto; min-height: 0; width: 100%;
}
.combatant-leading:empty { display: none; }
.combatant-card {
  position: absolute;
  top: var(--combatant-card-offset, 0);
  left: 50%;
  display: flex; flex: 0 0 auto; flex-direction: column; align-items: center;
  justify-content: flex-end; gap: 0.4rem; width: 100%; min-height: 0;
  margin: 0;
  transform: translateX(-50%) scale(var(--combatant-card-scale, 1));
  transform-origin: top center;
}
.combatant .sprite { position: relative; display: flex; align-items: flex-end; justify-content: center; min-height: 0; }
/* Bring the figures forward without enlarging their names or resource bars.
   The stage still fits the complete stack inside its safe battlefield area. */
.combat .combatant-card > .sprite {
  zoom:var(--stage-sprite-zoom,1);
  /* Fitting reads zoom synchronously. The global reduced-motion rule gives
     every property a tiny transition; even that interpolates these measures. */
  transition-property: none !important;
}
/* The under-model strip. Its bars are the SAME component as the main HUD's —
   the old .hpbar/.poisebar pair here was two more homes for one grammar, and
   they are gone rather than kept in parallel (Law 0 clause 4). What survives is
   the track width, which is this surface's own container. */
.combatant .meters { width: 13rem; display: flex; flex-direction: column; gap: 0.3rem; }

/* M5: a matched tag-scoped vulnerability lights the boosted preview number in
   the status row's tint. */
.card.tag-boost .ctext .val.up { color: var(--boost-tint); }

.combatant.dead { opacity: 0.25; filter: grayscale(1); pointer-events: none; }

/* Likely-target glow: a tinted, slightly-larger clone of the sprite sits behind
   the real one (which is kept on top), so the target reads as a colored,
   glowing silhouette — red for an enemy, blue for a self/buff target. */
.combatant .sprite > *:not(.aim-silho) { position: relative; z-index: 1; }
.aim-silho {
  position: absolute; left: 50%; bottom: 0; z-index: 0; pointer-events: none;
  transform: translateX(-50%) scale(1.16); transform-origin: center bottom;
  filter: blur(2px); opacity: 0.9;
  animation: aimGlow 900ms ease-in-out infinite alternate;
}
.aim-silho > * { box-shadow: none !important; }
@keyframes aimGlow { from { opacity: 0.5; } to { opacity: 0.95; } }
.reduced-motion .aim-silho { animation: none; filter: blur(1px); opacity: 0.8; }
.combatant.player.armed { cursor: pointer; }

.enemy .sprite { cursor: pointer; }
/* WGC4: eligible targets are dashed, the active one (pointer or keyboard) is
   solid. Width and offset come from TargetLayerModel via the battlefield stage,
   held at a physical minimum under the sprite's depth zoom. */
.enemy.targetable .sprite { outline: var(--target-outline-width, 2px) dashed var(--gold); outline-offset: var(--target-outline-offset, 6px); border-radius: var(--r-3); }
.enemy.hover-target .sprite, .enemy.targetable:is(.gp-focus, :focus-visible) .sprite { outline-style: solid; }

/* ---------- hand + controls ---------- */
/* #295 — `--action-row-drop` IS THE ONE HOME FOR HOW FAR THE ACTION ROW HANGS
   BELOW THE HAND, AND IT HAS TWO CONSUMERS THAT USED TO AGREE BY ACCIDENT.
   `.combat-action-row` is bottom-anchored to `.hand-area` at a NEGATIVE offset,
   so it hangs into the band underneath — and nothing in the column reserved that
   band. It stayed on screen only because the hint strip happened to be the last
   in-flow row of `.combat` and happened to be about that tall. That is a fact
   written in two places with nothing checking it: move the strip and the whole
   action row drops off the bottom of the viewport (measured: 47/52/62 px of new
   `.combat` overflow, and hintstrip H5 goes from `ok 0 px` to `unknown` at all
   four wide cells — it does not go RED, which is the second finding).
   The offset and its reservation are now the same token, so they cannot drift,
   and `short-wide`'s deeper drop below overrides ONE value instead of leaving a
   6.4rem hang against a 5.2rem reservation that was never written down. */
/* The row's tallest cell and the deepest fanned card need a real separation
   band. These measured reservations keep that band intact in the portrait
   884x1326 matrix and in the compressed short-wide composition below. */
/* 6.4rem -> 7.6rem (#679). That PR grew the row from four cells to six AND gave
   every cell a 44px tap floor, so the tallest cell got taller — but this token,
   which the comment above calls a MEASURED reservation for exactly that cell
   plus the deepest fanned card, was not re-measured with it. The band went
   ~4.3px short and the hand's lowest cards came to rest ON the Draw and End Turn
   buttons at 1200x730, every text size (hintstrip H1). The overlap is near
   constant in px across text sizes, which is the signature of the absolute
   44px floor rather than anything that scales with text.
   Re-measured here rather than reverting the row: six cells and a 44px tap
   target are both deliberate, so the reservation follows the row.
   7.6rem, not the 7.2rem that merely clears: 7.2 leaves 2.94/3.70/5.24 px at
   1200x730 S/M/XL, which is thinner than the band this token exists to hold and
   is exactly how much slack #679 needed to break it. 7.6 measures 6.53/7.70/
   10.04 px there and still costs nothing above: hintstrip H5 reports 0 px of
   column overflow at every wide cell, so the extra reservation is absorbed by
   the column rather than pushing the row toward the viewport edge. */
.combat { --action-row-drop: 7.6rem; }
.hand-area {
  height: 23rem; flex-shrink: 0; position: relative;
  display: flex; align-items: flex-end; justify-content: center;
  margin-bottom: var(--action-row-drop);
}
.hand-overlay {
  width: min(72rem, calc(100% - 2rem)); height: 100%; min-width: 0;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "hand";
  align-items: end; column-gap: 0.8rem;
}
.hand-overlay[data-paging='true'] {
  grid-template-columns: var(--tap-floor) minmax(0, 1fr) var(--tap-floor);
  grid-template-areas: "prev hand next";
}
/* THE PAGER ARROWS ARE THE KIT'S IconButtons NOW, AND THIS IS THEIR PLACEMENT:
   the three asks the old painted `.hand-page` block carried alongside its paint,
   kept because each one is measured and none of them is decoration.
   • `grid-area` names their columns, so the overlay's "prev hand next" holds
     whatever order the markup is written in.
   • `align-self: center` with `z-index: 70` puts them ON TOP of the strip, at
     its midline. The cards spread on a -1.8rem margin, so a hand of eight is
     WIDER than its own column and paints over both pager columns: with the
     arrows sitting at the strip's bottom instead, `elementFromPoint` at the
     centre of `.hand-next` (916,616 at 1200x730, ?shot=combat&shotHand=8)
     returned a CARD — the pager could not be tapped at all, and a click there
     selected that card and armed targeting. That is the finding
     tools/hybrid-input-parity.mjs reports as "Previous/Next moves the real hand
     focus"; on the same fixture the arrow reads as itself at 916,485.
   • The wide-shape lift keeps the right-hand arrow off the wide End Turn's hit
     surface at large text sizes — one ergonomic target, not a nudge.
   Nothing here paints: the box, the border, the hover and the glyph are the
   kit's IconButton. */
.hand-page { align-self: center; z-index: 70; }
.hand-prev { grid-area: prev; }
.hand-next { grid-area: next; }
:root:not([data-layout='narrow']) .hand-page {
  transform: translateY(calc(-1 * var(--tap-floor)));
}
/* padding-top RESERVES THE FAN'S LIFT, and the number is not written here.
   components/hand.js anchors the fan on its DEEPEST card and publishes the lift
   it used as `--fan-lift` on this element (FAN_LIFT_PROP — one home, Law 0
   clause 4). Absent -> 0px, so a stylesheet that forgets to reserve it SHOWS the
   clipping rather than hiding it (Law 0 clause 5).
   ⚠ `.hand-area`'s `height: 23rem` above is a BOX in text units — Law 4 clause 4,
   inside the known 400-of-548 set, and NOT converted here (ratchet, not sweep).
   This act LEANS on it: the wide fan's clearance comes out of that box's slack,
   so at Text S the slack shrinks with the cards and not with the 6 px step.
   Measured, not assumed: tools/hintstrip.mjs walks Text S and XL at both
   shapes. */
.hand { grid-area: hand; min-width: 0; display: flex; align-items: flex-end; justify-content: center; height: 100%; padding-bottom: 1rem; padding-top: var(--fan-lift, 0px); }
.hand .card { margin: 0 -1.8rem; transform-origin: bottom center; }
.hand .card:hover, .hand .card.selected {
  transform: translateY(-56px) scale(1.32) !important; z-index: 50 !important;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.8);
}
/* The selected card's glow is the shared WCF3 filter (kit.css); no second outline. */
.hand .card.selected { border-color: var(--gold); }

/* #198: drag aiming deliberately reuses the click-selected `.aim-silho`
   visual above. The ghost names whether the current point can commit; enemies
   do not acquire a second outline/label vocabulary for the same target fact. */
.card-drag-ghost { border: 3px solid var(--gold) !important; }
.card-drag-ghost[data-drop-state='illegal'] {
  border-style: dashed !important; filter: grayscale(0.75) brightness(0.7);
}
.card-drag-ghost .drop-verdict {
  position: absolute; left: 50%; top: -2.4rem; transform: translateX(-50%);
  min-width: max-content; padding: 0.25rem 0.65rem; border-radius: var(--r-2);
  color: #120f0c; background: var(--gold); border: 1px solid #120f0c;
  font-size: 0.9rem; font-weight: 900; letter-spacing: 0.08em;
}
.card-drag-ghost[data-drop-state='illegal'] .drop-verdict {
  color: var(--parchment); background: var(--blood); border-style: dashed;
}

/* The hold-to-inspect copy (armInspect, components/holdconfirm.js). It lives
   on <body>, not in the hand — geometry (position, size, scale) is written
   inline in LOCAL px by the one conversion home (fx.js), so nothing here may
   restate it; this block is only the surface. `pointer-events: none` is also
   set inline — Law 4: the copy may cover pixels, never a tap floor, and it
   must vanish on release leaving the hand byte-for-byte untouched. Appears at
   full size, no transition: the expansion is STATE, not an animation, so it
   survives reduced-motion the same way the hold fill does (rule 5). */
.card-inspect {
  border-color: var(--parchment);
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.85);
}

/* THE ACTION ROW IS A KIT ButtonRow (`.as-btnrow[data-size="fill"]`); this
   rule only says WHERE it hangs — bottom-anchored under the hand by the one
   `--action-row-drop` token — and lets the taps through around it.
   `.combat` IS IN THE SELECTOR so this outranks the kit's `width: 100%`: the
   row is absolutely positioned between two insets, so its width is the
   distance between them and a declared 100% would hang it off the right edge
   by exactly the left inset (measured: right 1216 in a 1200 px viewport).

   Z-INDEX ABOVE THE LIFTED CARD, and that number is not decoration. A selected
   card takes `z-index: 50` and rises 56 px out of the fan; the row's cells now
   span the full width (a kit ButtonRow has no spacer tracks), so at the armed
   state the lifted card landed ON draw and End Turn and took 8 of their 45
   sample hits — measured, tools/combat-action-row.mjs. The row is a control
   surface the player must always be able to press, so it outranks the fan. */
.combat > .hand-area > .combat-action-row {
  position: absolute; inset-inline: 1.6rem; bottom: calc(-1 * var(--action-row-drop)); z-index: 60;
  width: auto;
  pointer-events: none;
}
.combat-action-row > * { pointer-events: auto; }


/* ---------- targeting arrow ---------- */
#target-arrow { position: fixed; z-index: 400; pointer-events: none; }

/* ---------- fx ---------- */
.fx-layer { position: absolute; inset: 0; pointer-events: none; z-index: 300; overflow: hidden; }
.float-num {
  /* `left` is the float's CENTRE; this takes off its own half-width, so the
     width is never a number anyone maintains (#69). Standalone `translate`
     rather than `transform`, because num-pop animates transform and the two
     must compose. */
  translate: -50% 0;
  position: absolute; font-family: var(--font-display); font-weight: 800; font-size: 2.5rem;
  color: var(--parchment);
  /* A dark outline keeps numbers legible over any sprite. */
  text-shadow: 0 0 3px #000, 0 0 3px #000, 0 2px 5px #000;
  transform-origin: center bottom;
  animation: num-pop 540ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}
.float-num.dmg { color: #ff8f6b; }
.float-num.small { font-size: 2rem; }
.float-num.heavy { font-size: 3.4rem; }
.float-num.crit { font-size: 4.4rem; color: #ffc861; text-shadow: 0 0 4px #000, 0 0 10px rgba(201, 162, 39, 0.7), 0 2px 6px #000; }
.float-num.blk { color: var(--frost); }
.float-num.heal { color: var(--green); }
.float-num.burst { color: var(--ember); font-size: 3.6rem; }
/* Reduced motion: a plain rise, no bounce/overshoot. */
.reduced-motion .float-num { animation: float-up 260ms ease-out forwards; }
.fx-banner {
  position: absolute; left: 50%; top: 34%; transform: translate(-50%, -50%);
  font-family: var(--font-display); font-size: 3rem; letter-spacing: 0.3em; color: var(--gold);
  text-shadow: 0 0 30px rgba(201, 162, 39, 0.7); animation: banner-pop 300ms ease-out forwards;
}
.fx-banner.blood { color: var(--ember); }
.fx-banner.turn { font-size: 2.4rem; color: var(--parchment); text-shadow: 0 0 24px rgba(230, 220, 195, 0.5); }
.combat.shake { animation: shake 200ms; }

/* ---------- paced playback: one actor at a time (SPEC §7.4) ---------- */
/* Attack lunge: enemies step left toward the player, the player steps right. */
.enemy .sprite.act-attack { animation: lunge-left 280ms ease-out; }
.enemy-pose-attack { visibility: hidden; }
.facing[data-state-active="true"] > :is(.enemy-pose-idle, .enemy-pose-attack) { visibility: hidden !important; }
.enemy .sprite:is(.act-attack, .enemy-attack-pose) [data-attack-ready="true"] > .enemy-pose-idle { visibility: hidden; }
.enemy .sprite:is(.act-attack, .enemy-attack-pose) [data-attack-ready="true"] > .enemy-pose-attack { visibility: visible; }
/* Keep the idle drawing for anticipation and recovery. The attack drawing
   covers the family's impact at 55%, using the actor's playback duration. */
.enemy .sprite.enemy-attack-pose { transform-origin: 50% 100%; }
.enemy .sprite.enemy-attack-pose [data-attack-ready="true"] > .enemy-pose-idle {
  animation: enemy-idle-frames var(--enemy-attack-duration, 280ms) steps(1, end) both;
}
.enemy .sprite.enemy-attack-pose [data-attack-ready="true"] > .enemy-pose-attack {
  animation: enemy-attack-frames var(--enemy-attack-duration, 280ms) steps(1, end) both;
}
@keyframes enemy-idle-frames {
  0%, 80%, 100% { visibility: visible; }
  25% { visibility: hidden; }
}
@keyframes enemy-attack-frames {
  0%, 80%, 100% { visibility: hidden; }
  25% { visibility: visible; }
}
.player .sprite.act-attack { animation: lunge-right 280ms ease-out; }
.sprite.act-move { animation: act-step 260ms ease-out; }
@keyframes lunge-left {
  0% { transform: none; }
  35% { transform: translateX(-26px) scale(1.06); filter: brightness(1.25); }
  100% { transform: none; }
}
@keyframes lunge-right {
  0% { transform: none; }
  35% { transform: translateX(26px) scale(1.06); filter: brightness(1.25); }
  100% { transform: none; }
}
@keyframes act-step {
  0% { transform: none; }
  40% { transform: translateY(-8px) scale(1.05); filter: brightness(1.35) drop-shadow(0 0 12px rgba(201, 162, 39, 0.55)); }
  100% { transform: none; }
}
.reduced-motion .sprite.act-attack,
.reduced-motion .sprite.act-move { animation: none; }

/* Action families move existing sprites; they do not require new pose art.
   Signed distances come from the actor's bounded reach and facing direction. */
.sprite.act-attack[data-action-motion="sweep"] { animation-name: family-slash; }
.sprite.act-attack[data-action-motion="lunge"] { animation-name: family-thrust; }
.sprite.act-attack[data-action-motion="impact"] { animation-name: family-strike; }
.sprite.act-attack[data-action-motion="release"] { animation-name: family-projectile; }
.sprite.act-move[data-action-motion="cast"] { animation-name: family-spell; }
.sprite.act-move[data-action-motion="brace"] { animation-name: family-guard; }
.sprite.act-move[data-action-motion="sidestep"] { animation-name: family-dodge; }
@keyframes family-slash {
  0%, 100% { transform: none; filter: none; }
  22% { transform: translateX(var(--action-recoil, -12px)) rotate(var(--action-windup-tilt, -8deg)); }
  55% { transform: translate(var(--action-travel, 26px), var(--action-lift, -8px)) rotate(var(--action-tilt, 12deg)); filter: brightness(1.25); }
}
@keyframes family-thrust {
  0%, 100% { transform: none; filter: none; }
  30% { transform: translateX(var(--action-recoil, -12px)); }
  55%, 65% { transform: translateX(var(--action-travel, 26px)); filter: brightness(1.25); }
}
@keyframes family-strike {
  0%, 100% { transform: none; filter: none; }
  30% { transform: translateY(var(--action-lift, -8px)) rotate(var(--action-windup-tilt, -8deg)); }
  55% { transform: translateY(7px) scaleY(0.96); filter: brightness(1.4); }
}
@keyframes family-projectile {
  0%, 100% { transform: none; filter: none; }
  40% { transform: none; filter: brightness(1.25); }
  55% { transform: translateX(var(--action-recoil, -12px)) rotate(var(--action-windup-tilt, -8deg)); }
}
@keyframes family-spell {
  0%, 100% { filter: none; }
  55% { filter: brightness(1.4) drop-shadow(0 0 10px var(--gold)); }
}
@keyframes family-guard {
  0%, 100% { filter: none; }
  35%, 70% { filter: brightness(1.2) drop-shadow(0 0 5px var(--parchment)); }
}
@keyframes family-dodge {
  0%, 100% { transform: none; opacity: 1; }
  40%, 60% { transform: translate(var(--action-recoil, -12px), var(--action-lift, -8px)); opacity: 0.65; }
}
.reduced-motion .sprite[data-action-motion].act-attack,
.reduced-motion .sprite[data-action-motion].act-move { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .sprite[data-action-motion].act-attack,
  .sprite[data-action-motion].act-move { animation: none; }
}

/* ---------- ability / relic / status fx ---------- */
/* Hit reaction: the victim flashes AND recoils away from the blow — enemies are
   knocked right (struck from the player's side), the player left. A heavy hit
   (≥15, the same threshold as screen shake) recoils further with a tilt. */
.sprite.hitflash > :first-child { animation: hitflash var(--hurt-duration, 220ms) linear; }
.enemy .sprite.hitflash > :first-child { animation-name: hit-enemy; }
.player .sprite.hitflash > :first-child { animation-name: hit-player; }
.enemy .sprite.hitflash.hit-heavy > :first-child { animation-name: hit-enemy-heavy; }
.player .sprite.hitflash.hit-heavy > :first-child { animation-name: hit-player-heavy; }
@keyframes hitflash {
  0%, 32.99% { filter: none; }
  33%, 66.99% { filter: sepia(1) saturate(8) hue-rotate(315deg); }
  67%, 100% { filter: none; }
  0%, 100% { transform: none; }
  30% { transform: translateX(4px); }
}
@keyframes hit-enemy {
  0%, 32.99% { filter: none; }
  33%, 66.99% { filter: sepia(1) saturate(8) hue-rotate(315deg); }
  67%, 100% { filter: none; }
  0%, 100% { transform: none; }
  30% { transform: translateX(13px); }
}
@keyframes hit-player {
  0%, 32.99% { filter: none; }
  33%, 66.99% { filter: sepia(1) saturate(8) hue-rotate(315deg); }
  67%, 100% { filter: none; }
  0%, 100% { transform: none; }
  30% { transform: translateX(-13px); }
}
@keyframes hit-enemy-heavy {
  0%, 32.99% { filter: none; }
  33%, 66.99% { filter: sepia(1) saturate(8) hue-rotate(315deg); }
  67%, 100% { filter: none; }
  0%, 100% { transform: none; }
  30% { transform: translateX(26px); }
}
@keyframes hit-player-heavy {
  0%, 32.99% { filter: none; }
  33%, 66.99% { filter: sepia(1) saturate(8) hue-rotate(315deg); }
  67%, 100% { filter: none; }
  0%, 100% { transform: none; }
  30% { transform: translateX(-26px); }
}

/* Attack impact: a slashing arc sweeping across the victim. */
.fx-slash {
  position: absolute; width: 6.4rem; height: 6.4rem; margin: -3.2rem 0 0 -3.2rem;
  border-radius: var(--r-round); border-top: 3px solid #f3e9cf; border-right: 3px solid transparent;
  pointer-events: none; filter: drop-shadow(0 0 6px rgba(201, 162, 39, 0.8));
  animation: slash-sweep 260ms ease-out forwards;
}
@keyframes slash-sweep {
  from { transform: rotate(calc(var(--rot, 0deg) - 90deg)) scale(0.55); opacity: 0; }
  25% { opacity: 1; }
  to { transform: rotate(calc(var(--rot, 0deg) + 70deg)) scale(1.2); opacity: 0; }
}

/* Cast flourish (skills/powers/buff moves) + stagger star. */
.fx-glyph {
  position: absolute; margin-left: -1.1rem; pointer-events: none;
  color: var(--grace); font-size: 2.2rem; text-shadow: 0 0 10px currentColor;
  animation: glyph-pop 430ms ease-out forwards;
}
@keyframes glyph-pop {
  from { transform: scale(0.4); opacity: 0; }
  30% { opacity: 1; }
  to { transform: scale(1.6) translateY(-14px); opacity: 0; }
}

/* Fully blocked hit: frost spark, armor held. */
.fx-spark {
  position: absolute; margin-left: -1rem; pointer-events: none;
  color: var(--frost); font-size: 2rem; text-shadow: 0 0 8px currentColor;
  animation: spark-pop 300ms ease-out forwards;
}
@keyframes spark-pop {
  from { transform: scale(0.5); opacity: 0; }
  40% { opacity: 1; }
  to { transform: scale(1.5); opacity: 0; }
}

/* Stagger: the figure teeters. */
.sprite.wobble > :first-child { animation: wobble 550ms ease-in-out; }
@keyframes wobble {
  0%, 100% { transform: rotate(0); }
  20% { transform: rotate(6deg); }
  40% { transform: rotate(-5deg); }
  60% { transform: rotate(3deg); }
  80% { transform: rotate(-2deg); }
}
.relic.proc { animation: relicProc 300ms ease-out; }
@keyframes relicProc {
  0% { transform: scale(1.45); box-shadow: 0 0 18px var(--gold); }
  100% { transform: none; }
}

@keyframes orbPulse {
  0% { transform: scale(1.18); box-shadow: 0 0 34px rgba(201, 162, 39, 0.6); }
  100% { transform: none; }
}
.card-ghost {
  position: fixed; z-index: 700; pointer-events: none;
  transition: transform 220ms ease-in, opacity 220ms ease-in;
}
.stance-flare {
  position: absolute; width: 18rem; height: 18rem; border-radius: var(--r-round);
  pointer-events: none; animation: flare 300ms ease-out forwards;
}
@keyframes flare {
  0% { opacity: 0.7; transform: scale(0.4); }
  100% { opacity: 0; transform: scale(1.4); }
}

@keyframes float-up { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-42px); } }
/* Punchy number: snap in with an overshoot, settle, then rise and fade. */
@keyframes num-pop {
  0% { opacity: 0; transform: translateY(6px) scale(0.5); }
  16% { opacity: 1; transform: translateY(0) scale(1.22); }
  30% { transform: translateY(-2px) scale(1); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-48px) scale(1); }
}
@keyframes banner-pop { 0% { opacity: 0; letter-spacing: 0.6em; } 30% { opacity: 1; } 100% { opacity: 0; letter-spacing: 0.25em; } }
@keyframes shake { 0%, 100% { transform: translate(0); } 25% { transform: translate(4px, -3px); } 50% { transform: translate(-4px, 2px); } 75% { transform: translate(3px, 3px); } }
@keyframes pulse-gold { from { box-shadow: 0 0 6px rgba(201, 162, 39, 0.3); } to { box-shadow: 0 0 22px rgba(201, 162, 39, 0.8); } }

/* ---- rendered sprites: idle bob + death fade (solo + co-op boards) -------- */
@keyframes sprite-idle { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.combatant .sprite > img { animation: sprite-idle 3.1s ease-in-out infinite; will-change: transform; }
/* A fallen combatant holds still: the idle bob kept every dead sprite on the
   compositor's per-frame list for the rest of the fight. */
.combatant.dead .sprite > img { animation: none; }
.player .sprite.reaver-attack-playing { overflow: visible; }
.combatant .sprite > img.reaver-attack-sequence {
  width: 17rem;
  height: 17rem;
  max-width: none;
  flex: 0 0 auto;
  object-fit: contain;
  object-position: center bottom;
  margin-bottom: -1.1rem;
  pointer-events: none;
  animation: none;
  filter: drop-shadow(0 0.55rem 0.5rem rgba(0, 0, 0, 0.58));
}
.combatant.enemy .sprite > img { animation-duration: 2.6s; }
/* WCF3: one glow over the whole selected assembly — intent, the delayed
   inspect control, art, name and lower stack — never one per child. */
.combatant.context-selected > .combatant-stack {
  filter: var(--selection-glow, drop-shadow(0 0 calc(0.35 * max(16px / var(--ui-zoom, 1), 1rem)) var(--gold)));
}
.combatant.context-selected .nm { color: var(--gold); }
.combatant:nth-child(2) .sprite > img { animation-delay: -0.9s; }
.combatant:nth-child(3) .sprite > img { animation-delay: -1.7s; }
.combatant:nth-child(4) .sprite > img { animation-delay: -2.3s; }
.reduced-motion .combatant .sprite > img { animation: none; }
.combatant.dead .sprite > img, .combatant.down .sprite > img { transition: filter 400ms, opacity 400ms; }

/* ---------- short-wide combat composition (#27) ---------------------------
   main.js selects this composition only when the wide board still owns its
   full 1200 local px and the ordinary vertical composition would raise the
   upright refusal. The stylesheet follows that derived answer; it does not
   measure a viewport or restate either threshold.

   The normal board already contains enough flexible height once the refusal's
   `data-short` answer is false. What it still needs is a shorter hand band so
   that the battlefield receives a real row rather than collapsing to zero.
   Card faces keep their complete information hierarchy, while box dimensions
   shrink only in this wide, height-constrained composition. */
:root[data-composition='short-wide'] .combat.coop .hand { padding-bottom: 2rem; }
:root[data-composition='short-wide'] .combat.coop .hand .card {
  --card-name-h: 1.3rem; --card-art-h: 2.2rem; --card-band-h: 1.1rem;
  --card-row: 0.75rem; --card-gap: 0.2rem;
  width: 8rem; height: 10rem; margin: 0 -0.8rem; padding: 1.3rem 0.3rem 0.3rem;
  transform: translateY(calc(-1 * var(--fan-lift, 0px))) !important;
}
:root[data-composition='short-wide'] .combat.coop .hand .card .cost {
  top: -0.6rem; left: -0.6rem; width: 2.4rem; height: 2.4rem;
}
:root[data-composition='short-wide'] .combat.coop .hand .card .mana-cost {
  top: -0.4rem; right: -0.4rem; min-width: 2.8rem; height: 2rem;
}
:root[data-composition='short-wide'] .combat.coop .hand .card .cname {
  font-size: 0.75rem; min-height: 1.3rem;
}
:root[data-composition='short-wide'] .combat.coop .hand .card .art {
  width: 6.8rem; height: 2.2rem; margin: 0.1rem 0; font-size: 1.7rem;
}
:root[data-composition='short-wide'] .combat.coop .hand .card .ctype {
  width: 6.8rem; padding: 0.1rem 0; font-size: 0.65rem;
}
:root[data-composition='short-wide'] .combat.coop .hand .card .ctext {
  margin-top: 0.15rem; font-size: 0.68rem; line-height: 1.15;
}
:root[data-composition='short-wide'] .combat.coop .hand .card:hover,
:root[data-composition='short-wide'] .combat.coop .hand .card.selected {
  transform: translateY(-12px) scale(1.1) !important;
}
:root[data-composition='short-wide'] .combat { --action-row-drop: 11.2rem; }
:root[data-composition='short-wide'] .player-zone,
:root[data-composition='short-wide'] .enemy-row { padding-block: 0.4rem; }
:root[data-composition='short-wide'] .field[data-stage-safe-corridor='true'] :is(.player-zone, .enemy-row) { padding-block: 0; }
:root[data-composition='short-wide'] .combatant { gap: 0.15rem; }

:root[data-composition='short-wide'] .combatant .sprite > img.reaver-attack-sequence {
  width: 9rem;
  height: 9rem;
  max-width: none;
  margin-bottom: -0.6rem;
}
:root[data-composition='short-wide'] .combatant .meters { width: 8rem; }

/* ==========================================================================
   PROTOTYPE (EldenSpire#23, track B) — the narrow combat board.
   ==========================================================================
   Fires when main.js's resolveLayout() writes data-layout="narrow" on <html>.
   THE CONDITION IS NOT RESTATED HERE ON PURPOSE. This block used to say "when
   #app has 520 LOCAL px or fewer", which was true, and which was a second copy
   of a rule that lives in resolveLayout() with nothing keeping the two in
   agreement — the same shape as the defect that made this block a container
   query and locked out three tablet shapes (#24). A comment that restates a
   condition is a decider that cannot be executed and therefore cannot be
   tested. Read the function.

   What is safe to say here, because it is about THIS file: the block is inert
   wherever the wide layout is on screen, and that is checked by running
   tools/mobilefit.mjs at those shapes rather than by this sentence.

   WHAT REFLOW MEANS HERE, against what "scaling" would have done: the hand
   stops being a fan of overlapping cards under an absolutely-positioned button
   and becomes a horizontal strip with the controls in their own row beneath
   it. END TURN is no longer over the hand at all — not moved a little, not
   raised above it, out of it. That is the difference between shrinking a
   1200px layout and laying out for 430.

   ~~NOT DONE HERE, deliberately: `touch-action` is not set on the strip~~ —
   RESOLVED BY #22, which is the card this note was deferring to. The rule
   lives below with the deferral kept for its reasoning: which gestures the
   browser claims IS the design decision, and it is now made, per-axis.
   - No `clamp()`. It was pitched for type and spacing, and the useful form of
     it is `clamp(a, Nvw, b)` — a vw length, which under `body { zoom }`
     resolves in the unzoomed room and is the same defect as everything else on
     this branch. `clamp()` over rem is just a min and a max, and this block
     already has exact numbers for the one baseline it targets. It would have
     been decoration. */

  /* ---- top bar: two rows instead of one clipped one ---- */
  /* #22's design answer for the narrow hand, per-axis: the strip is a HORIZONTAL
   scroller and a card drag is a VERTICAL aim, so the axes are split rather than
   fought over. `pan-x` on the cards: a horizontal touch is the browser's (the
   hand scrolls — through a card, which is where every touch starts, because
   cards fill the strip), and a vertical touch stays with the JS drag (the
   browser may not claim pan-y, so no cancel fires and the aim survives).
   Vira measured the fight this replaces: five drag attempts on dev fired ZERO
   card pointerdowns once the scroller owned the follow-through — the player
   scrolled while trying to aim. Scoped to narrow: the wide hand is not a
   scroller and its default behaviour is not in question. */
:root[data-layout='narrow'] .hand .card { touch-action: pan-x; }
/* The OVERLAP arm of balance.ui.handLayout (C2): when the layout word says
   overlap, the narrow hand is not a scroller — the whole hand fits its width
   by derived negative margins (renderHand) and there is nothing legitimate
   for the browser to claim, which is the WIDE argument below, arriving on a
   narrow shape because the word changed the composition. Leaving pan-x here
   would let Chrome cancel a real aimed drag against a strip with zero travel.
   Scoped to the word: in 'paging' (the default) this rule does not exist and
   the pan-x above is untouched. */
:root[data-layout='narrow'][data-hand-layout='overlap'] .hand .card { touch-action: none; }
/* And the WIDE half, found by the probe going red at 1200x730 where 390x844
   passed: with default `touch-action: auto` Chrome claims ANY touch drag as a
   potential scroll and fires pointercancel — even where nothing scrolls — so a
   real aimed drag died mid-flight on the layout with no scroller at all. Wide
   has nothing legitimate for the browser to claim on a card, so the answer is
   none. NOT merged with the rule above into one `none`: on narrow the hand
   scroller is real, and pan-x THROUGH the cards is how it stays reachable. */
.hand .card { touch-action: none; }

  /* The hint bar was hidden here. It is now hidden GLOBALLY for the narrow
     layout in styles/ui.css — it was never a fact about combat (#29 slice 0). */

  /* ---- battlefield: give the enemies the width ---- */
  :root[data-layout='narrow'] .field {
    padding-block: var(--battlefield-hud-clearance, 0) var(--battlefield-action-clearance, 0);
    padding-inline: 1.2rem calc(1.2rem + var(--quick-corner));
  }

  :root[data-layout='narrow'] .player-zone { width: 32%; padding-block: 0.6rem; }
  :root[data-layout='narrow'] .field[data-stage-safe-corridor='true'] :is(.player-zone, .enemy-row) { padding-block: 0; }
  :root[data-layout='narrow'] .player-zone .combatant,
  :root[data-layout='narrow'] .enemy-row .combatant { height: 100%; max-height: 100%; }
  :root[data-layout='narrow'] .player-zone .combatant .sprite,
  :root[data-layout='narrow'] .enemy-row .combatant .sprite { flex: 1 1 auto; overflow: hidden; }
  :root[data-layout='narrow'] .player-zone .combatant .sprite > img,
  :root[data-layout='narrow'] .enemy-row .combatant .sprite > img {
    max-height: 100%; width: auto; object-fit: contain;
  }
  /* The player sprite is sized from balance.ui.spriteTiers and does not know
     about the zone it stands in; at 390x844 it ran 9px past the left edge of
     the screen. Capped by width, height auto so it does not squash. */
  :root[data-layout='narrow'] .player-zone .combatant .sprite > img { max-width: 11rem; height: auto; }
  :root[data-layout='narrow'] .player-zone .combatant .sprite.reaver-attack-playing { overflow: visible; }
  :root[data-layout='narrow'] .player-zone .combatant .sprite > img.reaver-attack-sequence {
    width: 12rem;
    height: 12rem;
    max-width: none;
    margin-bottom: -0.75rem;
  }
  :root[data-layout='narrow'] .enemy-row { padding-block: 0.6rem; gap: 0.4rem; }
  /* The complete combatant frame compacts as one component on phones. Intent,
     art, name, meters and statuses therefore keep their order and stay inside
     the flexible battlefield instead of painting beneath either fixed HUD. */
  :root[data-layout='narrow'] .combatant { gap: 0; }

  :root[data-layout='narrow'] .combatant .meters { width: 8rem; gap: 0; }

  :root[data-layout='narrow'] .backdrop::after { width: 30rem; height: 30rem; }

  /* ---- hand + controls: the actual reflow ----
     Two rows. The hand is a strip that scrolls sideways; every persistent
     action destination is a cell in the one row beneath it. */
  :root[data-layout='narrow'] .hand-area {
    height: auto; flex-shrink: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "hand" "actions";
    align-items: center; gap: 0.8rem; padding: 0 1rem 1rem;
  }
  :root[data-layout='narrow'] .hand-overlay {
    grid-area: hand; width: 100%; height: auto;
  }
  /* Same reservation, and the phone is the shape that PROVED it was needed: the
     fan's lift at the shipped hand of five is 12 px and this rule's own 1.2rem
     is 12 px, so the centre card's top landed exactly on the border edge —
     clearance 0.00, measured. At six cards the lift is 15 and the scroller
     starts eating the card it is featuring. The added room costs the phone up to
     `--fan-lift` of height, which Law 5 clause 4 spends on purpose. */
  :root[data-layout='narrow'] .hand {
    grid-area: hand; height: auto; padding: calc(1.2rem + var(--fan-lift, 0px)) 0 1.6rem;
    min-width: 0; margin-inline: 0;
    justify-content: flex-start; overflow-x: auto; gap: 0.6rem;
    scrollbar-width: thin;
  }
  /* The overlay owns the pager columns at 8+ and collapses them completely at
     0-7. The control row below therefore never pays for a pager it does not use. */
  /* No negative margin: cards in a scroller must not overlap, or the one on
     top eats the taps meant for the one beneath — the same mechanism as #21,
     one level down. */
  /* THE CARD'S BOX IS THE CARD'S (styles/kit.css §THE CARD): its height is
     spelled out from its own landmarks, so a phone may set the card's WIDTH —
     five faces have to fit a 390 px scroller — and must not retype its height
     or its landmarks. The old block did (`height: 17.4rem` plus four inner
     sizes) and the kit face then overflowed its own box by 21 px, which the
     hand's scroller reported as 12 px of vertical travel (hintstrip H6,
     measured). One home per fact: the width here, the box there. */
  :root[data-layout='narrow'] .hand .card { margin: 0; --card-w: 11.6rem; }
  /* A lift of 56px inside an overflow container is clipped, and the container
     has to clip or it cannot scroll. So the selected card grows in place. */
  :root[data-layout='narrow'] .hand .card:hover,
:root[data-layout='narrow'] .hand .card.selected {
    transform: translateY(-4px) scale(1.04) !important;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.8);
  }

  /* Narrow puts the row back in the flow as a grid child of .hand-area, so
     there is no hang to reserve — the token's consumer is gone and so is the
     margin. Both halves are dropped together, in one place. */
  :root[data-layout='narrow'] .hand-area { margin-bottom: 0; }
  :root[data-layout='narrow'] .combat-action-row {
    grid-area: actions; position: static; inset: auto; width: 100%;
  }


.hand-area {
  margin-bottom: calc(var(--action-row-drop) + var(--safe-bottom-local));
}
.combat-action-row {
  inset-inline-start: max(1.6rem, calc(var(--safe-left-local) + var(--safe-edge-local)));
  inset-inline-end: max(1.6rem, calc(var(--safe-right-local) + var(--safe-edge-local)));
  bottom: calc(var(--safe-bottom-local) - var(--action-row-drop));
}
:root[data-layout='narrow'] .hand-area {
  margin-bottom: 0;
  padding-inline-start: max(1rem, var(--safe-left-local));
  padding-inline-end: max(1rem, var(--safe-right-local));
  padding-bottom: calc(1rem + var(--safe-bottom-local));
}
:root[data-layout='narrow'] .combat-action-row {
  inset-inline: auto;
  bottom: auto;
}

/* ---------- boss splash beats (components/intro.js) --------------------------
   The card itself is kit atoms (styles/kit.css); these are its FX, and they
   live here with every other animation this screen plays. */
.boss-intro { animation: bi-veil 480ms ease-out; background: radial-gradient(ellipse 70% 50% at 50% 46%, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.88)); }
.boss-intro .bi-stack { animation: bi-rise 700ms cubic-bezier(0.2, 0.8, 0.3, 1); }
.boss-intro .bi-rule { animation: bi-rule 900ms ease-out; }
.boss-intro.bi-out { animation: bi-fade 460ms ease-in forwards; }
@keyframes bi-veil { from { opacity: 0; } }
@keyframes bi-rise { from { opacity: 0; transform: translateY(16px) scale(0.97); } }
@keyframes bi-rule { from { transform: scaleX(0.2); opacity: 0; } }
@keyframes bi-fade { to { opacity: 0; } }

/* Animated class figure (src/ui/services/PoseAnimator.js).
   The layer is a canvas-shaped box positioned so the pose art's floor line sits
   on the bottom of the figure box; frames are placed inside it by percentage, so
   a swap keeps the feet planted. A lunge reaches past the box on purpose, which
   is why nothing here clips. */
.class-sprite.animated { overflow: visible; }
.pose-stage { position: relative; width: 100%; height: 100%; }
.pose-layer { position: absolute; left: 50%; }
.pose-frame { position: absolute; image-rendering: auto; pointer-events: none; }

/* Six stable bottom controls; the final column always opens Potions. */
.combat > .hand-area > .combat-action-row {
  grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-flow: row;
  bottom: calc(-1 * var(--action-row-drop) + max(1rem, var(--safe-bottom-local, 0px)));
}
.combat .combat-action-row > .wide { grid-column: auto; }
.combat-action-row > button { min-height: max(44px, calc(44px / var(--ui-zoom, 1))); }
.combat-action-row .spent small { font-size: 0.9em; white-space: nowrap; }
.combat-art-choice { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.combat-art-choice > .card { align-self: center; }
.combat-art-choice > button { min-height: max(44px, calc(44px / var(--ui-zoom, 1))); white-space: normal; }
.combat-potion-menu .as-option { width: 100%; min-height: max(44px, calc(44px / var(--ui-zoom, 1))); }
@media (max-width: 480px) {
  .combat > .hand-area > .combat-action-row { inset-inline: 8px; gap: 4px; }
  .combat-action-row > button { padding-inline: 2px; font-size: 1.1rem; }
}

/* Finite feedback uses opacity/light, preserving the hand fan and hit boxes. */
.hand .card.card-drawn { animation: card-arrive 240ms ease-out; }
@keyframes card-arrive { from { opacity: 0.35; filter: brightness(1.6); } to { opacity: 1; filter: none; } }
.combat .pile.spent.pile-received { animation: pile-receive 320ms ease-out; }
@keyframes pile-receive { 35% { box-shadow: inset 0 0 16px var(--gold); } }
.combat .pile.spent[data-card-outcome="exhaust"] { border-bottom-color: var(--ember); }
.combat .pile.spent[data-card-outcome="discard"] { border-bottom-color: var(--gold); }
.reduced-motion .hand .card.card-drawn,
.reduced-motion .combat .pile.spent.pile-received { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .hand .card.card-drawn, .combat .pile.spent.pile-received { animation: none; }
}

/* Readable cards and controls occupy separate flow rows.
   BOTH BOARDS (owner, 2026-10-01): the co-op board mounts this same footer
   (components/combatActionRow.js), so the footer and hand-area rules below
   that were solo-only (`:not(.coop)`) now read `:is(.coop,:not(.coop))`:
   every combat board, at the SAME specificity the solo-only selector had, so
   the solo cascade is unchanged. */
:root .combat:is(.coop,:not(.coop)) > .hand-area {
 height: auto; margin-bottom: 0; display: grid;
 grid-template-areas: "hand" "actions"; grid-template-columns: minmax(0, 1fr);
 gap: calc(20px / var(--ui-zoom,1)); padding: 0 12px max(16px, var(--safe-bottom-local));
}
:root .combat:not(.coop) .hand-overlay { width: 100%; height: auto; grid-area: hand; }
:root .combat:not(.coop) .hand {
 height: auto; justify-content: safe center; align-items: end;
 padding: calc(48px / var(--ui-zoom,1)) 24px calc(12px / var(--ui-zoom,1)); gap: 0; overflow-x: auto; scrollbar-width: thin;
}
:root .combat:not(.coop) .hand .card {
 --card-w: max(178px, calc(178px / var(--ui-zoom, 1))); --card-name-h: max(32px, calc(32px / var(--ui-zoom, 1))); --card-art-h: max(70px, calc(70px / var(--ui-zoom, 1)));
 --card-band-h: max(22px, calc(22px / var(--ui-zoom, 1))); --card-row: max(22px, calc(22px / var(--ui-zoom, 1))); --card-pad: max(8px, calc(8px / var(--ui-zoom, 1))); --card-gap: max(5px, calc(5px / var(--ui-zoom, 1)));
 margin: 0; touch-action: pan-x;
}
:root .combat:is(.coop,:not(.coop)) > .hand-area > .combat-action-row {
 position: relative; inset: auto; grid-area: actions; width: 100%;
  --combat-control-size: max(64px, calc(64px / var(--ui-zoom, 1)));
 grid-template-columns: var(--combat-control-size) minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1fr) var(--combat-control-size); align-items: center; gap: 6px;
}
:root .combat:is(.coop,:not(.coop)) .combat-action-row > :is(.end-turn, .energy-orb, .combat-potions) { height: var(--combat-control-size); min-height: var(--combat-control-size); box-sizing: border-box; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row > :is(.energy-orb, .combat-potions) { width: var(--combat-control-size); min-width: 0; border-radius: 50%; padding: 4px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row > .combat-potions { font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); }
:root .combat:is(.coop,:not(.coop)) .combat-action-row .spent { display: flex; flex-direction: column; justify-content: center; }
/* Focus the figure, never paint the full-height combatant layout box. */
.combat:not(.coop) .combatant { border: 0 !important; outline: none !important; box-shadow: none !important; background: transparent !important; }
.combat .combatant:focus-visible .sprite { filter: drop-shadow(0 0 3px var(--gold)); }
:root .combat:not(.coop) .hand .card:hover,
:root .combat:not(.coop) .hand .card.inspection-selected,
:root .combat:not(.coop) .hand .card.selected { transform: translateY(-28px) !important; z-index: 50 !important; }
:root .combat:not(.coop) .hand { padding-top:calc(84px / var(--ui-zoom,1)); }
.hand .card.beat-hold[data-hold='holding']::after { content:''; position:absolute; inset:0; z-index:6; pointer-events:none; border-radius:inherit; background:linear-gradient(0deg,rgba(72,166,98,.42),rgba(121,204,132,.22)); clip-path:inset(calc((1 - var(--hold,0)) * 100%) 0 0 0); }
.hand .card > .hold-hint { position:absolute; bottom:10px; left:0; right:0; margin:0; text-align:center; pointer-events:none; }

@media (max-width: 480px) {
  :root .combat:not(.coop) .hand { padding-inline: 16px; }
  :root .combat:not(.coop) .hand .card { --card-w: calc(146.5px / var(--ui-zoom, 1)); }
}

/* Give the enemy group the right edge; stage fitting reserves each fighter's cell. */
:root .combat:not(.coop) .field { padding-inline: 8px; gap: 6px; }
:root .combat:not(.coop) .player-zone { width: 28%; flex: 0 0 28%; min-width:0; }
:root .combat:not(.coop) .enemy-row { justify-content: center; min-width: 0; gap: 6px; }
:root .combat:not(.coop) .enemy-row > .combatant { flex: 1 1 0; min-width: 0; max-width:calc(180px / var(--ui-zoom,1)); }
:root .combat:not(.coop) .combatant-card { width: max-content; }
:root .combat:not(.coop) .combatant .sprite { flex: 0 0 auto; min-width: max-content; overflow: visible; }

/* Normal encounters fit the viewport. Only four or more enemies may scroll. */
@media(max-width:640px) {
  /* The shared fitter still owns the row. Art may cross adjacent cells a
     little; scale only the figures so meters, labels and intent stay readable. */
  :root .combat:not(.coop) .combatant .sprite { scale:0.9; transform-origin:bottom center; }
  :root .combat:not(.coop) .combatant-card { width:80px; }
  :root .combat:not(.coop) .combatant .meters { width:80px; }
  :root .combat:not(.coop) .combatant .sprite { width:80px; min-width:0; }
  :root .combat:not(.coop) .combatant .sprite > :first-child { flex-shrink:0; }
  :root .combat:not(.coop) .field:not(:has(.enemy-row > .combatant:nth-child(4))) { overflow-x:clip; overflow-clip-margin:4px; }
  :root .combat:not(.coop) .field:has(.enemy-row > .combatant:nth-child(4)) { overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; }
  :root .combat:not(.coop) .field:has(.enemy-row > .combatant:nth-child(4)) .enemy-row { flex:0 0 auto; min-width:max-content; }
  :root .combat:not(.coop) .field:has(.enemy-row > .combatant:nth-child(4)) .enemy-row > .combatant { min-width:calc(86px / var(--ui-zoom,1)); }
}

/* A hand card has one portrait profile; viewport changes never resize one axis
   alone. The profile itself is authored once, in card.json, and reaches here as
   --card-ratio / --card-height-per-width (src/ui/models/CardSizeModel.js): the
   1.4 that used to sit in this calc was 7/5 written out by hand. */
:root .combat:not(.coop) .hand .card {
  height: calc(var(--card-w) * var(--card-height-per-width)); aspect-ratio: var(--card-ratio);
  --card-art-h: calc(var(--card-w) * 0.30);
  --card-name-h: calc(var(--card-w) * 0.18);
}
:root .combat:not(.coop) .hand .card .art { overflow: hidden; contain: layout paint; }
:root .combat:not(.coop) .hand .card .art img { width: 100%; height: 100%; object-fit: contain; }

/* Scale the complete playing face, including costs and text, in one step. */
:root .combat:not(.coop) .hand .card {
  zoom:var(--hand-card-zoom,1); --card-w:178px; width:var(--card-w); padding:20px 8px 8px;
  --card-name-h:32px; --card-art-h:53.4px; --card-band-h:17px;
  --card-row:22px; --card-pad:8px; --card-gap:4px;
}
:root .combat:not(.coop) .hand .card .cname {
  font-size:16px; line-height:16px; white-space:normal; overflow-wrap:anywhere;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2;
}
:root .combat:not(.coop) .hand .card .ctext { font-size:16px; line-height:20px; }
:root .combat:not(.coop) .hand .card .card-costs {
  display:flex; align-items:center; gap:4px; position:absolute; top:-10px; left:-6px; right:22px;
}
:root .combat:not(.coop) .hand .card .card-costs > :is(.cost,.mana-cost,.stamina-cost) {
  position:static; flex:0 0 auto; box-sizing:border-box; margin:0; height:24px;
  min-width:24px; padding:0 5px; font-size:12px; line-height:1;
}
:root .combat:not(.coop) .hand .card .card-costs > .cost { width:24px; padding:0; }
:root .combat:not(.coop) .hand .card .ctype { font-size:10px; }
:root .combat:not(.coop) .hand .card .ctag { font-size:10px; }
:root .combat:not(.coop) .hand .card .art { width:100%; height:var(--card-art-h); margin:0; }

/* A short landscape viewport gets a scrollable battle, not miniature cards. */
@media(max-height:620px) {
  :root .combat:not(.coop) { overflow-y:auto; }
  :root .combat:not(.coop) .field { flex:1 0 calc(300px / var(--ui-zoom,1)); min-height:calc(300px / var(--ui-zoom,1)); }
  :root .combat:not(.coop) > .hand-area { flex:0 0 auto; }
}

/* Increase the reading inside the existing five fixed HUD tracks. */
:root .combat:is(.coop,:not(.coop)) .combat-action-row .sp-k { font-size: max(0.85rem, calc(10px / var(--ui-zoom, 1))); line-height: 1.1; letter-spacing: 0; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row .sp-v { font-size: max(1.5rem, calc(17px / var(--ui-zoom, 1))); line-height: 1.1; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row > .end-turn { font-size: max(1.38rem, calc(14px / var(--ui-zoom, 1))); line-height: 1.1; }
/* The round Potions face is --combat-control-size wide; at End Turn's size the
   word overran the circle and read "Potion" with a sliver. It keeps its own
   smaller step (declared above) and never wraps or clips. */
:root .combat:is(.coop,:not(.coop)) .combat-action-row > .combat-potions { line-height: 1.1; letter-spacing: 0; padding-inline: 2px; white-space: nowrap; overflow: visible; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row .spent { padding-inline: 2px; font-size: max(1.1rem, calc(11px / var(--ui-zoom, 1))); line-height: 1.15; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row .spent small { font-size: inherit; }
:root .combat:is(.coop,:not(.coop)) .combat-action-row :is(.hold-hint, .et-key) { font-size: max(0.85rem, calc(10px / var(--ui-zoom, 1))); line-height: 1; }

/* Painted frames own their alpha-shaped glow; do not add the generic gold or
   white cast halo around their parent (which also contains HUD elements). */
.sprite.act-move[data-action-motion="cast"]:has(.painted-stage),
.sprite.act-move[data-action-motion="brace"]:has(.painted-stage) { animation-name: none; }

/* Selection reveals the same card without rebuilding or relocating the fan. */
:root .combat:not(.coop) .hand .card.selected { transform: var(--card-fan-transform) !important; z-index: 100 !important; }
:root .combat:not(.coop) .hand:has(.selected) .card:not(.selected):hover { transform: var(--card-fan-transform) !important; z-index: auto !important; }
.combat .hand .card.drag-source { opacity: .2; }
.combat .combatant.player.skill-selected .sprite { filter: drop-shadow(0 0 .6rem var(--gold)); }
.combat .hand .card { touch-action: none; }

/* Progress sits above the opaque face and uses the shared hold timer. */
.combat .hand .card > .card-hold-progress {
  display: none; position: absolute; left: 6px; right: 6px; bottom: 6px;
  height: max(6px, calc(6px / var(--ui-zoom, 1)));
  z-index: 9; pointer-events: none; border-radius: 2px;
  background: #17120e; box-shadow: 0 0 0 1px var(--gold);
}
.combat .hand .card[data-hold="holding"] > .card-hold-progress { display: block; }
.combat .hand .card > .card-hold-progress::before {
  content: ''; display: block; width: 100%; height: 100%;
  background: repeating-linear-gradient(135deg, #ffe3a0 0 8px, #917025 8px 16px);
  transform: scaleX(var(--hold, 0)); transform-origin: left;
}

/* A horizontal body needs room to the right of the leftmost solo player. */
@media (max-width: 600px) {
  .combat:not(.coop) .player .defeated-frame { left: 100% !important; }
}

/* Stable combat composition. Percent tracks share one viewport-sized parent;
   disappearing cards, status chips and turn banners cannot resize the field. */
:root .combat[data-layout='formation'] {
  flex: none; height: calc(100dvh / var(--ui-zoom, 1)); min-height: 0;
  display: grid; grid-template-rows: 10% 45% 45%; overflow: clip;
}
:root .combat[data-layout='formation'] > .topbar { grid-row: 1; min-height: var(--w4-band-hud, 0px); height: auto; box-sizing: border-box; overflow: visible; }
:root .combat[data-layout='formation'] > .field {
  grid-row: 2; height: 100%; min-height: 0; padding: 0; display: block; overflow: visible;
}
:root .combat[data-layout='formation'] .field > :is(.player-zone,.enemy-row) {
  display: contents; width: auto; min-width: 0; padding: 0;
}
:root .combat[data-layout='formation'] .field .combatant {
  position: absolute; top: 0; bottom: 0; height: 100%; min-width: 0; max-width: none;
  transform: translateX(-50%); pointer-events: none; border: 0; background: none;
}
:root .combat[data-layout='formation'] .combatant-stack {
  height: auto; transform: translateX(-50%); transition: none;
}
:root .combat[data-layout='formation'] .combatant-card {
  position: relative; inset: auto; transform: none; width: 100%; gap: calc(3px / var(--ui-zoom,1));
}
:root .combat[data-layout='formation'] .combatant-leading {
  top: auto; bottom: calc(100% + 3px / var(--ui-zoom,1)); pointer-events: auto;
}
:root .combat[data-layout='formation'] .combatant .sprite {
  width: max-content; min-width: 0; scale: none; flex: none; pointer-events: auto;
  transition: none !important;
}
:root .combat[data-layout='formation'] .combatant.enemy-target-hitbox::after {
  content: ''; position: absolute;
  left: var(--enemy-hit-x); top: var(--enemy-hit-y);
  width: calc(44px / var(--ui-zoom, 1)); height: calc(44px / var(--ui-zoom, 1));
  transform: translate(-50%, -50%); z-index: 900; pointer-events: auto;
}
:root .combat[data-layout='formation'] .combatant .sprite > :first-child { flex-shrink: 0; }
:root .combat[data-layout='formation'] .combatant :is(.nm,.meters) {
  width: 100%; min-width: 0; max-width: none; pointer-events: auto;
}
:root .combat[data-layout='formation'] .combatant .nm {
  height: calc(20px / var(--ui-zoom,1)); line-height: calc(20px / var(--ui-zoom,1));
  font-size: calc(12px / var(--ui-zoom,1)); text-align: center; overflow: hidden;
  white-space: nowrap; text-overflow: ellipsis; flex: none;
}
:root .combat[data-layout='formation'] .combatant .nm :is(.as-label,.label) { font: inherit; line-height: inherit; }
:root .combat[data-layout='formation'] .combatant .meters { gap: calc(3px / var(--ui-zoom,1)); }
:root .combat[data-layout='formation'] .combatant .meters .bar { height: calc(10px / var(--ui-zoom,1)); min-height: 0; }
:root .combat[data-layout='formation'] .combatant .meters .bar .label { font-size: calc(10px / var(--ui-zoom,1)); line-height: 1; }
:root .combat[data-layout='formation'] .combatant :is(.status-row,.stance-chip,.foundation-evade,.dodge-receipt) { pointer-events: auto; }
:root .combat[data-layout='formation'] .combatant [data-tip-attached], .combat .turn-ribbon { pointer-events: auto; }
.combat-ability-badge { cursor: pointer; }
.combat-ability-badge:focus-visible { outline: var(--focus-ring); outline-offset: 3px; }
:root .combat[data-layout='formation'] .combatant:is(:focus-within,.hover-target,.context-selected) .combatant-leading { z-index: 1200; }
:root .combat[data-layout='formation'] > .hand-area {
  grid-row: 3; height: 100%; min-height: 0; margin: 0; padding: 0;
  display: grid; grid-template-rows: 2fr 1fr; grid-template-areas: 'hand' 'actions'; gap: 0;
}
:root .combat[data-layout='formation'] .hand-overlay { grid-area: hand; height: 100%; width: 100%; min-height: 0; }
:root .combat[data-layout='formation'] .hand {
  grid-area: hand; width: 100%; height: 100%; min-height: 0; box-sizing: border-box;
  align-items: center; justify-content: center; padding: calc(18px / var(--ui-zoom,1)) calc(16px / var(--ui-zoom,1)) calc(10px / var(--ui-zoom,1));
  gap: 0; overflow: visible;
}
:root .combat[data-layout='formation'] .hand .card {
  /* 178px is the hand's own pinned width (SPEC §12's 150-180 band); the HEIGHT
     is the card's shape applied to it, not a second 249.2px to keep in step. */
  box-sizing: border-box; flex: 0 0 178px; min-width: 178px; max-width: 178px;
  width: 178px;
  height: calc(178px * var(--card-height-per-width));
  min-height: calc(178px * var(--card-height-per-width));
  max-height: calc(178px * var(--card-height-per-width));
  aspect-ratio: var(--card-ratio);
}
:root .combat[data-layout='formation'] .hand .card .ctext { min-height: 0; overflow: hidden; }
:root .combat[data-layout='formation'][data-turn='enemy'] .hand {
  filter: grayscale(.85); opacity: .55; pointer-events: none;
}
:root .combat[data-layout='formation'] .combat-action-row {
  grid-area: actions; position: relative; inset: auto; align-self: stretch; box-sizing: border-box;
  width: 100%; padding: calc(8px / var(--ui-zoom,1)); margin: 0; align-items: center;
}
:root .combat[data-layout='formation'] .combat-action-row { --combat-control-size: calc(52px / var(--ui-zoom,1)) !important; }
/* THE POTIONS MINIS stand over the footer's Potions control, outside the row's
   grid. The row is wider than its grid on a wide screen, so combat.js
   (placePotionTray) writes where the control is. The tray is the shared icon
   tray (styles/kit.css § ICON TRAY): --icon-tray-slots of its own icons wide
   (wireframeUi.iconTray.footerPotionIcons), its right edge on the control's,
   and what does not fit is its +N tile, which opens the Potions list. */
:root .combat[data-layout='formation'] .combat-action-row > .combat-potion-tray {
  position: absolute; inset: auto var(--potion-tray-right, 0px) calc(100% + 0.3rem) auto; margin: 0; z-index: 1;
  width: calc(var(--icon-tray-slots, 4) * (var(--icon-tray-size, calc(25px / var(--ui-zoom,1))) + var(--icon-tray-gap, calc(3px / var(--ui-zoom,1)))) - var(--icon-tray-gap, calc(3px / var(--ui-zoom,1))));
  max-width: 100%; height: auto; max-height: none; justify-content: flex-end; align-self: auto;
  visibility: hidden;
  transition: visibility 0s linear var(--potion-reveal-fade, 160ms);
}
:root .combat[data-layout='formation'] .combat-action-row > .combat-potion-tray > * {
  opacity: 0; transform: translateY(0.7rem);
  transition: opacity var(--potion-reveal-fade, 160ms) ease, transform var(--potion-reveal-fade, 160ms) ease;
}
:root .combat[data-layout='formation'] .combat-potion-tray > *:nth-last-child(1) { --potion-stagger: 0ms; }
:root .combat[data-layout='formation'] .combat-potion-tray > *:nth-last-child(2) { --potion-stagger: 40ms; }
:root .combat[data-layout='formation'] .combat-potion-tray > *:nth-last-child(3) { --potion-stagger: 80ms; }
:root .combat[data-layout='formation'] .combat-potion-tray > *:nth-last-child(4) { --potion-stagger: 120ms; }
/* Keyboard focus exposes the minis on every device. :focus-visible keeps a
   pointer click (and focus restored after closing the list) from pinning them
   open as though the pointer were still hovering the control. */
:root .combat[data-layout='formation'] .combat-action-row:has(> .combat-potions:focus-visible, > .combat-potion-tray :focus-visible) > .combat-potion-tray {
  visibility: visible; transition-delay: var(--potion-focus-delay, 500ms);
}
:root .combat[data-layout='formation'] .combat-action-row:has(> .combat-potions:focus-visible, > .combat-potion-tray :focus-visible) > .combat-potion-tray > * {
  opacity: 1; transform: none;
  transition-delay: calc(var(--potion-focus-delay, 500ms) + var(--potion-stagger, 0ms));
}
/* A touchscreen can latch :hover after tapping. The touch route is the large
   Potions button and its full list, so hover reveal belongs only to a real
   fine hover pointer. Keeping the tray itself in the selector lets the pointer
   cross into a mini and use it. */
@media (hover: hover) and (pointer: fine) {
  :root .combat[data-layout='formation'] .combat-action-row:has(> .combat-potions:hover, > .combat-potion-tray:hover) > .combat-potion-tray {
    visibility: visible; transition-delay: var(--potion-reveal-delay, 500ms);
  }
  :root .combat[data-layout='formation'] .combat-action-row:has(> .combat-potions:hover, > .combat-potion-tray:hover) > .combat-potion-tray > * {
    opacity: 1; transform: none;
    transition-delay: calc(var(--potion-reveal-delay, 500ms) + var(--potion-stagger, 0ms));
  }
}
:root body.reduced-motion .combat[data-layout='formation'] .combat-action-row > .combat-potion-tray > * {
  transform: none; transition-property: opacity;
}
@media (prefers-reduced-motion: reduce) {
  :root .combat[data-layout='formation'] .combat-action-row > .combat-potion-tray > * {
    transform: none; transition-property: opacity;
  }
}
/* Narrow: the fan reaches down to the row beside the control, so there is no
   room above it. The minis ride inside the control's top edge, as wide as the
   control (its +N opens the list), and the control's label moves down. */
:root[data-layout='narrow'] .combat[data-layout='formation'] .combat-action-row > .combat-potion-tray {
  top: calc(var(--potion-control-top, 0px) + 2px); bottom: auto;
  width: var(--potion-control-width, var(--combat-control-size)); justify-content: center;
}
:root[data-layout='narrow'] .combat[data-layout='formation'] .combat-action-row > .combat-potions {
  justify-content: flex-end; padding-bottom: calc(6px / var(--ui-zoom, 1));
}
/* THE DESKTOP ACTION ROW (owner, 2026-09-11): Draw and Discard were stretched
   to a third of a 1920 window each, and Actions / End Turn / Potions stood
   52-64 px tall in a row that owns 15% of the viewport. The row keeps the
   contract tools/combat-action-row.mjs states — Actions and Potions pinned to
   the edges, Draw / End Turn / Discard a centre cluster, End Turn the wide
   one — and sizes it from the viewport instead of the window's width: the
   round controls and End Turn stand ~13vh tall, the piles ~60% of that, the
   cluster is 15vw / 30vw / 15vw with the leftover width spent between the
   edges and the cluster rather than inside the piles. The `/ var(--ui-zoom)`
   is the shape every viewport length wears under the body zoom (base.css).
   Below 600px tall the formation's own 52/42 px rule above still stands. */
.turn-ribbon {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%); z-index: 40; pointer-events: none;
  padding: calc(3px / var(--ui-zoom,1)) calc(32px / var(--ui-zoom,1));
  font: small-caps bold calc(16px / var(--ui-zoom,1))/1.2 Georgia, serif; letter-spacing: .08em;
  white-space: nowrap; color: #ead294; border-block: 1px solid #ac854b;
  background: linear-gradient(90deg, transparent, #332515e8 20%, #332515e8 80%, transparent);
}
.turn-ribbon::before { content: '‹ ✧ '; } .turn-ribbon::after { content: ' ✧ ›'; }
/* THE VICTORY BEAT (components/victoryBeat.js): the fight's title over the
   battlefield for balance.ui.victoryBeat.ms, drawn in the ribbon's own ink so
   it reads as the same voice that said "Player Turn". Absolute inside .combat
   like the ribbon; px through --ui-zoom, never vw/vh. */
.victory-beat {
  position: absolute; inset: 0; z-index: 60; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(ellipse at center, #00000088, #000000cc);
  opacity: 0; transition: opacity 180ms ease;
}
.victory-beat.is-in { opacity: 1; }
.victory-beat-title {
  margin: 0; width: 100%; text-align: center;
  padding: calc(8px / var(--ui-zoom,1)) calc(40px / var(--ui-zoom,1));
  font: small-caps bold calc(34px / var(--ui-zoom,1))/1.2 Georgia, serif; letter-spacing: .14em;
  color: #ead294; border-block: 1px solid #ac854b;
  background: linear-gradient(90deg, transparent, #332515e8 20%, #332515e8 80%, transparent);
}
.victory-beat-title::before { content: '‹ ✧ '; } .victory-beat-title::after { content: ' ✧ ›'; }
[data-turn='enemy'] .turn-ribbon { color: #ffc0a4; border-color: #9f492f; background: linear-gradient(90deg, transparent, #5a2017ec 20%, #5a2017ec 80%, transparent); }
@media (max-height: 500px) {
  :root .combat[data-layout='formation'] .combat-action-row { --combat-control-size: calc(42px / var(--ui-zoom,1)) !important; padding-block: 0; }
  :root .combat[data-layout='formation'] > .topbar .hud-info-row { display: none; }
  .turn-ribbon { font-size: calc(12px / var(--ui-zoom,1)); padding-block: 1px; }
}

/* The snapshot party uses the same complete card face and fixed hand tracks. */
:root .combat.coop[data-layout='formation'] .hand .card {
 zoom:var(--hand-card-zoom,1); --card-w:178px; padding:20px 8px 8px;
 --card-name-h:32px; --card-art-h:53.4px; --card-band-h:17px;
 --card-row:22px; --card-pad:8px; --card-gap:4px;
}
:root .combat.coop[data-layout='formation'] .hand .card .cname { font-size:16px; }
:root .combat.coop[data-layout='formation'] .hand .card .ctext { font-size:18px; line-height:24px; }
:root .combat.coop[data-layout='formation'] .hand .card .ctype,
:root .combat.coop[data-layout='formation'] .hand .card .ctag { font-size:10px; }
:root .combat.coop[data-layout='formation'] > .coop-arm-host { position:absolute; bottom:0; left:25%; z-index:20; max-width:50%; }
:root .combat[data-layout='formation'] .hand[inert] .card { animation:none; }

:root .combat[data-layout='formation'] .combatant .nm {
 min-height:calc(44px / var(--ui-zoom,1)); height:calc(44px / var(--ui-zoom,1));
 display:flex; justify-content:center; align-items:center; padding:0;
}
:root .combat[data-layout='formation'] .combatant .nm .ls-label {
 font-size:calc(12px / var(--ui-zoom,1)); line-height:1.2; max-width:100%; overflow:hidden; text-overflow:ellipsis;
}
:root .combat[data-layout='formation'] .combatant .meters .m-value {
 font-size:calc(10px / var(--ui-zoom,1)); line-height:1;
}

:root .combat[data-layout='formation'] .combatant .nm {
 min-height:0 !important; height:calc(20px / var(--ui-zoom,1));
}
:root .combat[data-layout='formation'] .combatant .nm-inspect { cursor:pointer; }
:root .combat[data-layout='formation'] .combatant[data-formation-row='1'] .nm,
:root .combat[data-layout='formation'] .combatant[data-formation-row='1'] .meters {
 background:#17130fd9; border-radius:3px;
}

:root .combat[data-layout='formation'] > .hand-area { grid-template-columns:minmax(0,1fr); justify-content:stretch; }
:root .combat[data-layout='formation'] .hand .card { margin-right:0; }
:root .combat.coop[data-layout='formation'] .combatant { outline:none !important; box-shadow:none !important; border:0 !important; }
:root .combat.coop[data-layout='formation'] .combatant.selected-target .sprite { filter:drop-shadow(0 0 3px var(--gold)); }

/* Names stay above every silhouette, including the shallow rear row. Frames
   do not create stacking contexts; only sprite art participates in depth. */
:root .combat[data-layout='formation'] .field .combatant { transform:none; }
:root .combat[data-layout='formation'] .combatant-stack { left:0; transform:none; }
:root .combat[data-layout='formation'] .combatant :is(.nm,.meters,.statuses) { position:relative; z-index:1000; }
:root .combat[data-layout='formation'] .combatant .nm { background:#17130fa6; border-radius:3px; }

:root .combat[data-layout='formation'] > .hand-area { grid-template-rows:minmax(0,2fr) minmax(0,1fr); }
:root .combat[data-layout='formation'] .combat-action-row { min-height:0; }
:root .combat[data-layout='formation'] .combat-action-row > * { max-height:100%; }

/* The fitted fan has no scrollbars. A scrollbar appearing/disappearing when
   the enemy-turn filter changes would shift the vertically centered cards. */
:root .combat[data-layout='formation'] .hand { overflow:visible; }

/* Short landscape keeps meters inline in the left resource column.
   Relics remain below them; the compact band preserves battlefield space. */
@media (max-height:500px) {
 :root :is(.combat[data-layout='formation'], .w4-frame) > .topbar { padding:0 calc(8px / var(--ui-zoom,1)); }
 :root :is(.combat[data-layout='formation'], .w4-frame) .topbar .hud-top { width:100%; height:auto; justify-content:center; }
 :root :is(.combat[data-layout='formation'], .w4-frame) .topbar .resbars { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:calc(12px / var(--ui-zoom,1)); }
 :root :is(.combat[data-layout='formation'], .w4-frame) .topbar .resline { min-width:0; }
 :root :is(.combat[data-layout='formation'], .w4-frame) .topbar .topbar-btn { height:calc(32px / var(--ui-zoom,1)); min-height:0; width:calc(32px / var(--ui-zoom,1)); }
 /* INTENDED, measured in Chromium (#1368; D29 in docs/FINISH.md). The co-op
    board's `.hud-top` (src/ui/screens/coop.js) is not the shared run HUD: it
    has no `.shared-hud` ancestor, so kit.css's `.shared-hud > .hud-top` grid
    never applies and this row overrides nothing shared; without it the box is
    block flow. In short landscape block flow drops Leave to a second line and
    the band takes that height from the battlefield. This row keeps the bars
    (flex:1) and Leave on one row, fight label hidden: the compact band above.
    tools/coop-hud-top.mjs holds it (overlap, clipping, lost parts, horizontal
    overflow at 390x844, 844x390, 1280x800; one row at 844x390). */
 :root .combat.coop[data-layout='formation'] .topbar .hud-top { display:flex; flex-direction:row; align-items:center; }
 :root .combat.coop[data-layout='formation'] .topbar .resbars-host { flex:1; min-width:0; }
 :root .combat.coop[data-layout='formation'] .topbar .fight-label { display:none; }
 :root .combat.coop[data-layout='formation'] .topbar .coop-leave { height:calc(32px / var(--ui-zoom,1)); min-height:0; }
}

@media (max-height:500px) {
 :root .combat[data-layout='formation'] .combat-action-row .combat-potions { font-size:calc(10px / var(--ui-zoom,1)); }
 :root .combat[data-layout='formation'] .combat-action-row .energy-orb .sp-k { font-size:calc(7px / var(--ui-zoom,1)); }
}

/* A fixed-height StatPair must share the buttons' center, not stretch to the
   taller controls band. Flexible pile columns keep the five controls in one row. */
:root .combat[data-layout='formation'] > .hand-area > .combat-action-row > * {
  align-self:center; min-width:0; grid-row:1;
}
:root .combat[data-layout='formation'] .combat-action-row > .pile {
  width:100%; max-width:100%; box-sizing:border-box;
}

/* Status capacity is reserved even when empty; opening overflow does not reflow the HUD.
   THE RESERVED TRAY ANSWERS NO HIT. A back-row fighter's stack lands over the
   front row's nameplates, and this tray — 24 px tall with nothing in it — was
   what `elementFromPoint` returned at the centre of the second Blight Hound's
   name at 390x650 (tools/screenreach.mjs, red on dev since #788). Only the pips
   inside it are controls; the tray and its gaps pass the press through. */
/* The row, its icons and its +N tile are the shared icon tray (styles/kit.css
   § ICON TRAY); the card only reserves the tray's height and gives it the
   card's width. */
:root .combat[data-layout='formation'] .combatant .statuses {
 height:var(--icon-tray-size, calc(25px / var(--ui-zoom,1))); min-height:var(--icon-tray-size, calc(25px / var(--ui-zoom,1))); width:100%; margin:0;
}
.player .status-overflow-list { left:0; right:auto; }
/* Lite keeps the target color and selection affordance without cloning art. */
:root[data-performance='lite'] .aim-silho.aim-outline {
  inset: auto 8% -3%; width: auto; height: 12%; border: 2px solid var(--target-color);
  border-radius: 50%; filter: none; animation: none; opacity: 0.9;
  transform: scale(1.04); background: transparent; box-shadow: none;
}
:root[data-performance='lite'] .backdrop::after,
:root[data-performance='lite'] .sprite > img { animation: none; }
:root[data-performance='lite'] .sprite img,
:root[data-performance='lite'] .sprite .facing { filter: none !important; }

/* Grid geometry and actor placement share the formation model. */
.formation-grid {
  display: none; position: absolute; inset: 0; z-index: 8;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  pointer-events: none; user-select: none;
}
:root[data-formation-grid='true'] .formation-grid { display: grid; }
:root[data-formation-movement='true'] .formation-grid { display: grid; }
:root[data-formation-movement='true'] .formation-grid-cell:not(:disabled) { pointer-events: auto; cursor: pointer; }
.formation-grid-cell.formation-selected { --grid-stroke: var(--selection-color, #59bd75); }
.formation-grid-cell.formation-selected .formation-grid-outline > * { fill-opacity: .4; stroke-width: 3; }
.formation-move-controls { position: absolute; z-index: 2500; top: 36px; left: 50%; transform: translateX(-50%); display: flex; gap: 6px; }
.formation-move-controls[hidden] { display: none; }
.formation-move-controls button { min-height: 44px; background: #191710; white-space: nowrap; }
.formation-grid-cell {
  border: 0; border-radius: 0; padding: 0; background: transparent; color: inherit; opacity: 1; box-shadow: none;
  position: absolute;
  transform: translateY(-50%);
  min-width: 0; min-height: 0; box-sizing: border-box;
  --grid-stroke: var(--player-grid-color, #d5cc63);
}
.formation-grid-cell[hidden] { display: none !important; }
:root .formation-grid-cell, :root .formation-grid-cell:disabled, :root .formation-grid-cell:hover, :root .formation-grid-cell:focus-visible {
  background-color: transparent; border: 0; box-shadow: none; opacity: 1; filter: none; overflow: visible;
}
.formation-grid-cell::after {
  content: none; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%); color: #fff2bb; font-weight: bold;
}
.formation-grid-cell[data-side='enemy'] { --grid-stroke: var(--enemy-grid-color, #e1a679); }
.formation-grid-outline { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; transform: var(--tile-transform, none); transform-origin: center; }
.formation-grid-outline > * { display: none; fill: var(--grid-stroke); fill-opacity: .07; stroke: var(--grid-stroke); stroke-width: 1; }
.formation-grid[data-shape='rhombus'] polygon,
.formation-grid[data-shape='wide-rhombus'] polygon,
.formation-grid[data-shape='square'] rect,
.formation-grid[data-shape='rectangle'] rect,
.formation-grid[data-shape='circle'] ellipse,
.formation-grid[data-shape='ellipse'] ellipse { display: block; }
.formation-grid-cell[data-occupied='true'] .formation-grid-outline > * { stroke-width: 2; fill-opacity: .25; }
.formation-grid-cell span {
  position: absolute; left: 50%; top: 100%; transform: translateX(-50%); white-space: nowrap;
  display: inline-block; background: #14110de6; color: #fff2bb;
  font: 500 max(1rem, calc(11px / var(--ui-zoom, 1)))/1.2 var(--font-ui, sans-serif); padding: .1rem .25rem;
  border-radius: .2rem;
}
:root .combat .combatant .sprite { scale: none; translate: none; }

/* Overhead controls are screen-sized, independent of sprite zoom and animation. */
:root .combat[data-layout='formation'] .combatant-leading {
  top: var(--overhead-top, 0); bottom: auto;
  transform: translateY(-100%);
  display: flex; flex-direction: column; align-items: center;
  gap: calc(4px / var(--ui-zoom, 1)); z-index: 1100;
  pointer-events: none;
}
.combatant-leading .overhead-control {
  pointer-events: auto; cursor: pointer; flex: none;
  touch-action: manipulation;
}
/* Match the card Information motif; hidden controls leave no empty grid cell. */
.combatant:not(.context-selected) .combatant-leading > .combatant-info { display: none; }
.combatant-leading > .combatant-info { box-shadow: none; }
.combatant-leading .intent {
  --intent-accent: #d9bc74;
  border: calc(2px / var(--ui-zoom, 1)) solid var(--intent-accent);
  background: linear-gradient(#34313a, #16151d);
  box-shadow: inset 0 1px 0 #ffffff36, 0 2px 5px #0009;
  display: flex; align-items: center; justify-content: center;
  gap: calc(5px / var(--ui-zoom, 1));
  min-width: calc(52px / var(--ui-zoom, 1)); min-height: calc(48px / var(--ui-zoom, 1));
  padding: calc(4px / var(--ui-zoom, 1)) calc(9px / var(--ui-zoom, 1));
  border-radius: calc(12px / var(--ui-zoom, 1)) calc(4px / var(--ui-zoom, 1));
  border-color: var(--intent-accent); color: var(--intent-accent);
  font: 800 calc(28px / var(--ui-zoom, 1))/1.05 var(--font-ui, sans-serif);
  white-space: nowrap; letter-spacing: 0;
}
.combatant-leading .intent .as-glyph { font-size: inherit; }
.combatant-leading .intent.attack { --intent-accent: #ffac94; }
.combatant-leading .intent.block { --intent-accent: #a7defb; }
.combatant-leading .intent.debuff { --intent-accent: #d6afff; }
.combatant-leading .intent.staggered { font-size: calc(18px / var(--ui-zoom, 1)); }
.combatant-leading .intent.dashed { border-style: dashed; }
.combatant-leading .overhead-control:is(.tooltip-selected, :focus-visible) {
  outline: var(--focus-ring);
  outline-offset: calc(3px / var(--ui-zoom, 1));
  box-shadow: 0 0 12px #f8d98588, inset 0 1px 0 #ffffff55;
}
.combatant-leading .enemy-key { position: absolute; left: calc(100% + 3px); bottom: 0; }
@media (max-height: 700px) {
  :root .combat[data-layout='formation'] { grid-template-rows: 10% 55% 35%; }
}
@media (max-height: 480px) and (min-width: 600px) {
  /* Keep inspection beside the intent on short screens so selection does
     not consume another row above the fixed card-to-action gap. */
  :root .combat[data-layout='formation'] .combatant-leading {
    flex-direction: row; align-items: flex-end;
  }
  /* The expanded relic tray can extend beyond its HUD row. Only its actual
     controls should intercept a press on the battlefield below. */
  :root:has(.combat) .hud-relics { pointer-events: none; }
  :root:has(.combat) .hud-relics > * { pointer-events: auto; }
}
/* WCO1 headroom: on every host the relic row's box spans the band's width
   under the HUD row, over the enemies' intent and Inspect controls. Only the
   relics themselves answer a press. */
:root .combat[data-layout='formation'] .hud-relics { pointer-events: none; }
:root .combat[data-layout='formation'] .hud-relics > * { pointer-events: auto; }
