/* measure/ink.css — THE INK BEAT (measure/ink.js, docs/action-language-spec.md).
 *
 * Two node families, both `position:absolute` inside `.ms-ink` (mounted in `world`, world-
 * scoped like THE CUT-IN — never the band, L9):
 *   `.ms-ink-fx`      — a standalone overlay mark/streak. Never touches a figure card.
 *                       Centred on its own `left`/`top` (set once, in JS, at spawn) via the
 *                       standalone `translate` CSS property — NOT `transform` — so the WAAPI
 *                       `transform` keyframes ink.js plays layer on top of the centring
 *                       instead of overwriting it (the two are independent CSS properties).
 *   `.ms-ink-tell-*`  — a class TOGGLED on the struck body's own `.ms-world-figure` card
 *                       (never a node of its own) for the three `onBody` registers (crit/
 *                       primeTick/break). THE FIGURE TELL COMPOSITION LAW (figures.css
 *                       header, point 3 — "explicit compound selectors", the strategy this
 *                       file's own brief calls "the fifth"): each rule below lists BOTH this
 *                       tell's own keyframe AND idle/critical's, at `.ms-fig-idle.ms-ink-
 *                       tell-*`/`.ms-fig-critical.ms-ink-tell-*` specificity — a bare
 *                       `.ms-ink-tell-* .ms-figure-body { animation: ... }` would instead WIN
 *                       outright over idle/critical's own rule (same element, same
 *                       `animation` shorthand — the single-winner problem figures.css's
 *                       header explains) and silently cancel the ambient bob/pulse for as
 *                       long as the tell plays. Proof this composes for real (not just
 *                       "shouldn't collide"): measure-ink.test.js's own
 *                       `figure-tell/*-composes-with-idle-and-critical` scenarios, built on
 *                       the SAME getComputedStyle().animationName technique
 *                       measure-board.test.js's `figure-state/idle-and-critical-compose-
 *                       with-the-attack-tell` scenario already uses.
 *
 * Animated properties throughout: `transform`/`opacity`/`filter` ONLY, per R2/L2 — a node's
 * `left`/`top` (and any `--ms-ink-*` custom property feeding a keyframe) are written ONCE,
 * synchronously, at spawn (ink.js `_buildNode`), never per-frame. No rAF anywhere.
 */

/* THE WAVE'S OWN TOKENS (docs/enemy-turn-spec.md E5) — a status's mark is NEVER
 * `--ms-burn`/`--ms-shock`/`--ms-rupture` (measure.css's own element vars) — see
 * measure/ink.js's WAVE_STATUS_TOKEN header comment for why `burn` in particular is the one
 * that would be easy to get wrong. `--ms-wave-tether` reuses `--ms-kinetic` on purpose: that
 * var's own measure.css comment already carves it out as "kinetic element + the Conduit
 * shield/tether mark" — TETHER inflicts no status on anyone, so it was never a candidate for
 * a NEW token, only a rename of the one that already meant this. */
:root {
  --ms-wave-status-burn: #ff9d52;
  --ms-wave-status-slow: #7fa8c9;
  --ms-wave-status-delay: #d7b34a;
  --ms-wave-mark: #c94b4b;       /* the no-status wave mark (OVERRUN/SPALL/RING BURST) */
  --ms-wave-telegraph: #ffb020;  /* THE STANDING TELEGRAPH's own alarm tone */
  --ms-wave-tether: var(--ms-kinetic);
}

.ms-ink {
  position: absolute;
  inset: 0;
  /* Above THE WORLD's figures (z 1) and below THE CUT-IN (z 3) — an ink mark reads as
     happening IN the scene, never on top of a live sweep (which suppresses this layer's
     travel legs outright while it plays — see ink.js `cast()`'s `isCutinLive()` check). */
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
}

.ms-ink-fx {
  position: absolute;
  /* THE ELEMENT MOTION CHANNEL (action-language-spec.md §9 F5, Phase 2 Task 2) — the base
     centring offset (-50%,-50%) PLUS `--ms-ink-elem-x`/`-y`, the two custom properties
     ink.js's `_applyElementMotion` drives for shock (jitter, discrete steps) and rupture
     (converge, a smooth pull to 0). Both default to 0px so every other element/verb — and
     every existing test — sees plain centring, unchanged. This is the SAME "independent
     property" composition strategy the file header already documents: `translate` here never
     collides with the WAAPI `transform` keyframes the shape/verb own, because they are two
     different CSS properties on the same element. */
  translate: calc(-50% + var(--ms-ink-elem-x, 0px)) calc(-50% + var(--ms-ink-elem-y, 0px));
  pointer-events: none;
  will-change: transform, opacity;
}

/* ── Shape sizing/silhouette — one rule per `ms-ink-js` shape token ─────────────────────── */
/* Every rule below that declares a static `filter: drop-shadow(...)` ALSO multiplies in
   `brightness(var(--ms-ink-elem-tone, 1))` — burn's ramp channel (ink.js `_applyElementMotion`,
   'ramp' case). Defaults to 1 (a no-op) for every other element, so nothing here changes for
   shock/rupture/kinetic or for a mark with no element at all. */

.ms-ink-lob, .ms-ink-jabStreak, .ms-ink-thickBolt, .ms-ink-overshoot {
  width: 34px; height: 10px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 55%, #fff 90%, transparent);
  border-radius: 6px;
  filter: drop-shadow(0 0 6px var(--ms-ink-color)) brightness(var(--ms-ink-elem-tone, 1));
}
.ms-ink-slash {
  width: 46px; height: 6px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 40%, #fff 85%, transparent);
  border-radius: 3px;
  filter: drop-shadow(0 0 5px var(--ms-ink-color)) brightness(var(--ms-ink-elem-tone, 1));
}
.ms-ink-thickBolt { height: 14px; width: 52px; }

/* THE FIRE BEAT (2026-09-26) — a GUN's mark is a TRACER: long, thin, a hot white head, a
   coloured tail — the FPS's own tracer read at board scale, and nothing like the melee
   slash (46×6, peaking mid-flight). A tracer is what says "shot" on a board with no gun
   model; the origin half is `.ms-ink-muzzle` below. */
.ms-ink-tracer {
  width: 72px; height: 3px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 35%, var(--ms-ink-color) 70%, #fff 92%, #fff);
  border-radius: 2px;
  filter: drop-shadow(0 0 5px var(--ms-ink-color)) brightness(var(--ms-ink-elem-tone, 1));
}
/* The muzzle flash: a small hot disc with a four-point flare, at the SHOOTER, gone in
   ink.MUZZLE_MS (ink.js `_spawnMuzzle` animates transform/opacity only). */
.ms-ink-muzzle {
  position: absolute; width: 18px; height: 18px; pointer-events: none;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--ms-ink-color) 38%, transparent 70%);
  box-shadow: 0 0 12px 3px color-mix(in srgb, var(--ms-ink-color) 60%, transparent);
  will-change: transform, opacity;
}
.ms-ink-muzzle::before, .ms-ink-muzzle::after {
  content: ''; position: absolute; left: 50%; top: 50%; background: #fff;
  transform: translate(-50%, -50%); opacity: 0.9;
}
.ms-ink-muzzle::before { width: 30px; height: 2px; }
.ms-ink-muzzle::after  { width: 2px; height: 30px; }
.ms-ink-muzzle.ms-ink-static { opacity: 0.7; }

.ms-ink-wideFan {
  width: 64px; height: 40px;
  background: conic-gradient(from -35deg at 0% 50%, var(--ms-ink-color), transparent 70deg);
  opacity: 0.85;
  filter: brightness(var(--ms-ink-elem-tone, 1));
}

.ms-ink-groundRing {
  width: 72px; height: 72px;
  border-radius: 50%;
  border: 3px solid var(--ms-ink-color);
  box-shadow: 0 0 14px var(--ms-ink-color) inset, 0 0 10px var(--ms-ink-color);
  filter: brightness(var(--ms-ink-elem-tone, 1));
}

.ms-ink-beam {
  width: 4px; height: 90px;
  background: linear-gradient(180deg, transparent, var(--ms-ink-color) 30%, var(--ms-ink-color) 70%, transparent);
  filter: drop-shadow(0 0 6px var(--ms-ink-color)) brightness(var(--ms-ink-elem-tone, 1));
}

.ms-ink-descendMark {
  width: 16px; height: 16px;
  clip-path: polygon(50% 0%, 100% 60%, 70% 60%, 85% 100%, 50% 65%, 15% 100%, 30% 60%, 0% 60%);
  background: var(--ms-ink-color);
  filter: drop-shadow(0 0 6px var(--ms-ink-color)) brightness(var(--ms-ink-elem-tone, 1));
}

.ms-ink-reverseBloom, .ms-ink-casterBloom, .ms-ink-allyBloom {
  width: 46px; height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--ms-ink-color) 0%, transparent 72%);
  filter: brightness(var(--ms-ink-elem-tone, 1));
}

.ms-ink-shellCrack {
  width: 40px; height: 40px;
  clip-path: polygon(50% 0%, 62% 30%, 100% 38%, 68% 55%, 78% 100%, 50% 74%, 22% 100%, 32% 55%, 0% 38%, 38% 30%);
  background: var(--ms-ink-color);
}

.ms-ink-threadSnap {
  width: 60px; height: 4px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color), transparent);
}

.ms-ink-sigil, .ms-ink-elementTick, .ms-ink-bounceMark {
  width: 18px; height: 18px;
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
  background: var(--ms-ink-color);
  filter: drop-shadow(0 0 4px var(--ms-ink-color)) brightness(var(--ms-ink-elem-tone, 1));
}

.ms-ink-starburst {
  width: 30px; height: 30px;
  clip-path: polygon(
    50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
  background: var(--ms-ink-color);
}

/* ═══════════════════════════════════════════════════════════════════════════════════════
 * THE NINE MARKS (docs/enemy-turn-spec.md §4) — one silhouette per WAVE_MOVE_KINDS
 * `moveToken` (measure/ink.js). `sweepEmber`/`ringDownFile` are the two `spanReach` shapes
 * (E3 — sized off `--ms-ink-span-w`/`-h`, set once in `_buildNode`, never per-target);
 * `reinforceLink` is the one `isLink` shape (sized off `--ms-ink-link-len`, the REAL on-
 * screen distance between the two enemies). Every other shape rides the ordinary
 * `--ms-ink-arc-*`/`--ms-ink-angle` travel channel the archetype shapes above already use.
 * ═══════════════════════════════════════════════════════════════════════════════════════ */

.ms-ink-dashClose { /* OVERRUN — fast closing dash */
  width: 40px; height: 9px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 45%, #fff 88%, transparent);
  clip-path: polygon(0 30%, 70% 30%, 70% 0%, 100% 50%, 70% 100%, 70% 70%, 0 70%);
  filter: drop-shadow(0 0 6px var(--ms-ink-color));
}

.ms-ink-brittleScatter { /* SPALL — already looks unstable */
  width: 16px; height: 16px;
  clip-path: polygon(50% 0%, 62% 38%, 100% 30%, 70% 58%, 88% 100%, 50% 74%, 12% 100%, 30% 58%, 0% 30%, 38% 38%);
  background: var(--ms-ink-color);
  filter: drop-shadow(0 0 5px var(--ms-ink-color));
}

.ms-ink-lobEmber { /* ACID ARC — lob + ember afterglow */
  width: 20px; height: 20px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--ms-ink-color) 45%, transparent 75%);
  filter: drop-shadow(0 0 7px var(--ms-ink-color));
}

.ms-ink-sweepEmber { /* GROUND SPRAY — one sweep drawn ACROSS the whole rank */
  width: var(--ms-ink-span-w, 90px); height: 13px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 20%, var(--ms-ink-color) 80%, transparent);
  filter: drop-shadow(0 0 8px var(--ms-ink-color));
}

.ms-ink-pipStreak { /* PIP SHOT — the Runner's quick tracer dot (THE CHAFF SHOOTS) */
  width: 30px; height: 2px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 45%, #fff 90%, #fff);
  border-radius: 1px;
  filter: drop-shadow(0 0 4px var(--ms-ink-color));
}
.ms-ink-lobPip { /* LOB PIP — the Swarmer's slow lobbed dot, no afterglow */
  width: 10px; height: 10px; border-radius: 50%;
  background: radial-gradient(circle at 40% 40%, #fff 0%, var(--ms-ink-color) 45%, transparent 75%);
  filter: drop-shadow(0 0 5px var(--ms-ink-color));
}

.ms-ink-tautSnap { /* PINNING SHOT — snaps taut, drag mark stays */
  width: 44px; height: 4px;
  background: linear-gradient(90deg, transparent, var(--ms-ink-color) 30%, #fff 60%, var(--ms-ink-color) 85%, transparent);
}

.ms-ink-hardSpike { /* LANCE — the landing IS a single hard spike, the loudest single-target mark */
  width: 12px; height: 46px;
  clip-path: polygon(50% 0%, 100% 55%, 68% 55%, 68% 100%, 32% 100%, 32% 55%, 0% 55%);
  background: var(--ms-ink-color);
  filter: drop-shadow(0 0 9px var(--ms-ink-color));
}

.ms-ink-reinforceLink { /* TETHER — a held beam, enemy -> ENEMY */
  width: var(--ms-ink-link-len, 60px); height: 6px;
  background: repeating-linear-gradient(90deg,
    var(--ms-ink-color) 0, var(--ms-ink-color) 8px, transparent 8px, transparent 13px);
  filter: drop-shadow(0 0 6px var(--ms-ink-color));
}

.ms-ink-ringDownFile { /* RING BURST — one ring drawn DOWN the file it will sweep */
  width: 26px; height: var(--ms-ink-span-h, 120px);
  background:
    repeating-linear-gradient(180deg,
      var(--ms-ink-color) 0, var(--ms-ink-color) 5px, transparent 5px, transparent 18px);
  border-left: 2px solid var(--ms-ink-color);
  border-right: 2px solid var(--ms-ink-color);
  opacity: 0.9;
}

.ms-ink-heavyPush { /* SHOVE — heavy, BLUNT displacing push (wide/flat vs LANCE's spike) */
  width: 52px; height: 26px;
  border-radius: 50%;
  background: radial-gradient(ellipse, #fff 0%, var(--ms-ink-color) 40%, transparent 78%);
  filter: drop-shadow(0 0 8px var(--ms-ink-color));
}

/* ═══════════════════════════════════════════════════════════════════════════════════════
 * THE WAVE STRIKES (docs/enemy-turn-spec.md §7; measure/ink.js `_spawnWaveBody`) — the
 * eleven move BODIES, one silhouette per `data/action-language.js WAVE_MOVE_BODY[].body`
 * token. A body is the thing that CROSSES the gap; the `.ms-ink-<moveToken>` marks above are
 * the silhouette it leaves. Every body node is `position:absolute` in `.ms-ink` (world-scoped,
 * never the band — L9, nothing here reflows the cockpit), centred by its own `translate`
 * (independent of the WAAPI `transform` keyframes ink.js plays — the same two-property
 * composition `.ms-ink-fx` uses), and tinted by `--ms-ink-color` = the move's STATUS token
 * (E5) or `--ms-wave-mark`. `.ms-ink-body-hot` marks a body whose bright spike THE FLASH
 * BUDGET granted (≤ 3/s; ink.js `_takeFlash`) — over budget the body draws without it.
 * The two ARC bodies (`-pip`/`-glob`) ride `offset-path` (set inline by ink.js) and so carry
 * NO centring translate: `offset-anchor` already puts the node's centre on the path.
 * ═══════════════════════════════════════════════════════════════════════════════════════ */
.ms-ink-body-group { position: absolute; inset: 0; pointer-events: none; }
.ms-ink-body {
  position: absolute;
  translate: -50% -50%;
  pointer-events: none;
  will-change: transform, opacity;
}
.ms-ink-body-pip, .ms-ink-body-glob { translate: none; offset-anchor: center; }

.ms-ink-body-burst { /* OVERRUN — the contact burst at the berth */
  width: 34px; height: 34px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--ms-ink-color) 32%, transparent 70%);
  filter: drop-shadow(0 0 8px var(--ms-ink-color));
}
.ms-ink-body-shard { /* SPALL — one of three brittle shards */
  width: 11px; height: 11px;
  clip-path: polygon(50% 0%, 100% 40%, 72% 100%, 18% 86%, 0% 34%);
  background: linear-gradient(135deg, #fff 0%, var(--ms-ink-color) 45%, var(--ms-ink-color) 100%);
  filter: drop-shadow(0 0 4px var(--ms-ink-color));
}
.ms-ink-body-head { /* PIP SHOT — the tracer's hot head */
  width: 7px; height: 7px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, #fff 40%, var(--ms-ink-color) 70%, transparent 100%);
  filter: drop-shadow(0 0 5px var(--ms-ink-color));
}
.ms-ink-body-pip { /* LOB PIP — the Swarmer's lobbed dot */
  width: 9px; height: 9px; border-radius: 50%;
  background: radial-gradient(circle at 40% 40%, #fff 0%, var(--ms-ink-color) 50%, transparent 80%);
  filter: drop-shadow(0 0 4px var(--ms-ink-color));
}
.ms-ink-body-glob { /* ACID ARC — the glob, burn-tinted by its STATUS */
  width: 16px; height: 16px; border-radius: 50% 50% 46% 54%;
  background: radial-gradient(circle at 42% 38%, #fff 0%, var(--ms-ink-color) 42%, color-mix(in srgb, var(--ms-ink-color) 70%, #000) 100%);
  filter: drop-shadow(0 0 7px var(--ms-ink-color));
}
.ms-ink-body-scorch { /* what a landing LEAVES on the floor — an ellipse in perspective */
  width: 38px; height: 12px; border-radius: 50%;
  background: radial-gradient(ellipse, var(--ms-ink-color) 0%, color-mix(in srgb, var(--ms-ink-color) 55%, transparent) 55%, transparent 80%);
  filter: drop-shadow(0 0 6px var(--ms-ink-color));
}
.ms-ink-body-scorch-wide { width: var(--ms-ink-span-w, 90px); height: 10px; }
.ms-ink-body-ember { /* GROUND SPRAY — the one ember head sweeping the rank */
  width: 18px; height: 18px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--ms-ink-color) 40%, transparent 72%);
  filter: drop-shadow(0 0 8px var(--ms-ink-color));
}
.ms-ink-body-line { /* PINNING SHOT — the taut line that STAYS (the slow status) */
  width: var(--ms-ink-link-len, 60px); height: 2px;
  background: linear-gradient(90deg, var(--ms-ink-color) 0%, #fff 50%, var(--ms-ink-color) 100%);
  filter: drop-shadow(0 0 4px var(--ms-ink-color));
}
.ms-ink-body-beam { /* LANCE — the telegraph line, then the beam, on one node */
  width: var(--ms-ink-link-len, 60px); height: 5px; border-radius: 3px;
  background: linear-gradient(90deg, var(--ms-ink-color) 0%, #fff 35%, #fff 65%, var(--ms-ink-color) 100%);
  filter: drop-shadow(0 0 9px var(--ms-ink-color));
}
.ms-ink-body-pulse { /* TETHER — the pulse running along the link */
  width: 10px; height: 10px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--ms-ink-color) 55%, transparent 85%);
  filter: drop-shadow(0 0 6px var(--ms-ink-color));
}
.ms-ink-body-ring { /* RING BURST — the floor ring from the Major's feet, down the file */
  width: var(--ms-ink-ring-w, 160px); height: var(--ms-ink-ring-h, 68px);
  border-radius: 50%;
  border: 3px solid var(--ms-ink-color);
  box-shadow: 0 0 10px var(--ms-ink-color), inset 0 0 10px color-mix(in srgb, var(--ms-ink-color) 50%, transparent);
  background: transparent;
}
.ms-ink-body-wedge { /* SHOVE — the shock-wedge on contact, pointed the way the shove went */
  width: 44px; height: 30px;
  clip-path: polygon(0% 50%, 100% 0%, 78% 50%, 100% 100%);
  background: linear-gradient(90deg, var(--ms-ink-color) 0%, #fff 60%, var(--ms-ink-color) 100%);
  filter: drop-shadow(0 0 8px var(--ms-ink-color));
}
.ms-ink-body-hot { filter: drop-shadow(0 0 12px #fff) drop-shadow(0 0 8px var(--ms-ink-color)); }
.ms-ink-body.ms-ink-static { opacity: 0.85; }
@media (prefers-reduced-motion: reduce) {
  .ms-ink-body { animation: none !important; }
}

/* E8 — reduced motion's static substitute NAMES the move (never just tints a shape) — see
   ink.js `_buildNode`'s `data-ms-wave-label` comment for why this is set unconditionally but
   only PAINTED here, under `.ms-ink-static`. */
.ms-ink-static[data-ms-wave-label]::after {
  content: attr(data-ms-wave-label);
  position: absolute;
  left: 50%; top: 100%;
  translate: -50% 4px;
  white-space: nowrap;
  font: 700 9px/1.1 var(--ms-font, sans-serif);
  letter-spacing: 0.02em;
  color: var(--ms-ink-color);
  text-shadow: 0 1px 2px rgba(0,0,0,0.7);
  pointer-events: none;
}

/* THE SCATTER SPRAY GROUP (ink.js `_runScatter`) — a plain positioning wrapper, no size/
   visual of its own; every submunition mark inside it is its own full `.ms-ink-fx`. */
.ms-ink-fx-group { position: absolute; inset: 0; pointer-events: none; }

/* reduced motion — every WAAPI animation was never started by ink.js in this branch (it adds
   `.ms-ink-static` and skips `.animate()` entirely); this is defense in depth for a node
   whose class list somehow still carries a browser-native CSS animation from a future shape
   rule, matching fx.css's own posture. */
@media (prefers-reduced-motion: reduce) {
  .ms-ink-fx { animation: none !important; }
}
.ms-ink-static { opacity: 0.9; }

/* ═══════════════════════════════════════════════════════════════════════════════════════
 * THE FIGURE TELL — onBody registers (crit/primeTick/break) toggle these on the struck
 * card. See the file header for the composition law; the idle/critical keyframe names/vars
 * referenced below are figures.css's OWN globals (`ms-fig-idle-bob`, `ms-fig-critical-bob`/
 * `-pulse`, their `--ms-fig-*` duration vars) — referenced by name, never redeclared, so a
 * retune to figures.css's own timing is picked up here for free. This tell's OWN three
 * durations (`--ms-ink-crit-ms`/`-accent-ms`/`-class-ms`) are read as `var(..., FALLBACK)`
 * with the fallback PINNED to config.measure.ink's CRIT_MS/ACCENT_MS/CLASS_MS at time of
 * writing (160/150/420ms) rather than wired from JS: the tell class lands on `beat.figureEl`
 * (inside `worldFigures`), a SIBLING of ink.js's own `.ms-ink` mount, not its descendant, so
 * a custom property set on `.ms-ink` would never inherit there — routing it through the
 * shared board root instead was judged not worth the extra board.js coupling for three
 * numbers that already match exactly. A retune to any of those three config keys must update
 * the matching fallback below too — named here so it is not a silent drift trap.
 * ═══════════════════════════════════════════════════════════════════════════════════════ */

@keyframes ms-ink-tell-starburst   { 0%, 100% { filter: brightness(1); } 45% { filter: brightness(2.1) saturate(1.3); } }
@keyframes ms-ink-tell-elementTick { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.5); } }
@keyframes ms-ink-tell-shellCrack  {
  0%, 100% { filter: brightness(1); }
  30%      { filter: brightness(1.9) contrast(1.2); }
}

/* Base rule — the tell alone, no idle/critical on the card. */
.ms-ink-tell-starburst .ms-figure-body   { animation: ms-ink-tell-starburst var(--ms-ink-crit-ms, 160ms) ease-out; }
.ms-ink-tell-elementTick .ms-figure-body { animation: ms-ink-tell-elementTick var(--ms-ink-accent-ms, 150ms) ease-out; }
.ms-ink-tell-shellCrack .ms-figure-body  { animation: ms-ink-tell-shellCrack var(--ms-ink-class-ms, 420ms) ease-out; }

/* THE COMPOSING RULES — idle/critical PLUS the tell, one `animation:` shorthand listing all
   the keyframes the plain single-state rules above would otherwise fight over (the same
   single-winner problem figures.css's header documents; explicit compound selectors are the
   fix, not a coincidence of specificity). */
.ms-fig-idle.ms-ink-tell-starburst .ms-figure-body {
  animation: ms-fig-idle-bob var(--ms-fig-idle-ms, 3000ms) ease-in-out infinite,
             ms-ink-tell-starburst var(--ms-ink-crit-ms, 160ms) ease-out;
}
.ms-fig-idle.ms-ink-tell-elementTick .ms-figure-body {
  animation: ms-fig-idle-bob var(--ms-fig-idle-ms, 3000ms) ease-in-out infinite,
             ms-ink-tell-elementTick var(--ms-ink-accent-ms, 150ms) ease-out;
}
.ms-fig-idle.ms-ink-tell-shellCrack .ms-figure-body {
  animation: ms-fig-idle-bob var(--ms-fig-idle-ms, 3000ms) ease-in-out infinite,
             ms-ink-tell-shellCrack var(--ms-ink-class-ms, 420ms) ease-out;
}
.ms-fig-critical.ms-ink-tell-starburst .ms-figure-body {
  animation: ms-fig-critical-bob var(--ms-fig-critical-ms, 1650ms) ease-in-out infinite,
             ms-fig-critical-pulse var(--ms-fig-critical-pulse-ms, 1100ms) ease-in-out infinite,
             ms-ink-tell-starburst var(--ms-ink-crit-ms, 160ms) ease-out;
}
.ms-fig-critical.ms-ink-tell-elementTick .ms-figure-body {
  animation: ms-fig-critical-bob var(--ms-fig-critical-ms, 1650ms) ease-in-out infinite,
             ms-fig-critical-pulse var(--ms-fig-critical-pulse-ms, 1100ms) ease-in-out infinite,
             ms-ink-tell-elementTick var(--ms-ink-accent-ms, 150ms) ease-out;
}
.ms-fig-critical.ms-ink-tell-shellCrack .ms-figure-body {
  animation: ms-fig-critical-bob var(--ms-fig-critical-ms, 1650ms) ease-in-out infinite,
             ms-fig-critical-pulse var(--ms-fig-critical-pulse-ms, 1100ms) ease-in-out infinite,
             ms-ink-tell-shellCrack var(--ms-ink-class-ms, 420ms) ease-out;
}

/* THE BROKEN BOSS (2026-09-26) — the third figure state gets the same three compounds, so a
   hit landing on a reeling boss flashes its tell and KEEPS reeling (figures.css owns the
   `ms-fig-broken-reel`/`-glow` names and `--ms-fig-broken-ms`; referenced, never redeclared). */
.ms-fig-broken.ms-ink-tell-starburst .ms-figure-body {
  animation: ms-fig-broken-reel var(--ms-fig-broken-ms, 1700ms) ease-in-out infinite,
             ms-fig-broken-glow var(--ms-fig-broken-ms, 1700ms) ease-in-out infinite,
             ms-ink-tell-starburst var(--ms-ink-crit-ms, 160ms) ease-out;
}
.ms-fig-broken.ms-ink-tell-elementTick .ms-figure-body {
  animation: ms-fig-broken-reel var(--ms-fig-broken-ms, 1700ms) ease-in-out infinite,
             ms-fig-broken-glow var(--ms-fig-broken-ms, 1700ms) ease-in-out infinite,
             ms-ink-tell-elementTick var(--ms-ink-accent-ms, 150ms) ease-out;
}
.ms-fig-broken.ms-ink-tell-shellCrack .ms-figure-body {
  animation: ms-fig-broken-reel var(--ms-fig-broken-ms, 1700ms) ease-in-out infinite,
             ms-fig-broken-glow var(--ms-fig-broken-ms, 1700ms) ease-in-out infinite,
             ms-ink-tell-shellCrack var(--ms-ink-class-ms, 420ms) ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .ms-fig-broken.ms-ink-tell-starburst .ms-figure-body,
  .ms-fig-broken.ms-ink-tell-elementTick .ms-figure-body,
  .ms-fig-broken.ms-ink-tell-shellCrack .ms-figure-body,
  .ms-ink-tell-starburst .ms-figure-body,
  .ms-ink-tell-elementTick .ms-figure-body,
  .ms-ink-tell-shellCrack .ms-figure-body,
  .ms-fig-idle.ms-ink-tell-starburst .ms-figure-body,
  .ms-fig-idle.ms-ink-tell-elementTick .ms-figure-body,
  .ms-fig-idle.ms-ink-tell-shellCrack .ms-figure-body,
  .ms-fig-critical.ms-ink-tell-starburst .ms-figure-body,
  .ms-fig-critical.ms-ink-tell-elementTick .ms-figure-body,
  .ms-fig-critical.ms-ink-tell-shellCrack .ms-figure-body {
    animation: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════
 * THE STANDING TELEGRAPH (docs/enemy-turn-spec.md E4) — a PERSISTENT class
 * (`ms-ink-standing-<moveToken>`, `measure/ink.js _standingTelegraphClass`) toggled on the
 * winding-up body's OWN `.ms-world-figure` card for the whole round it winds up — recomputed
 * every render off `enemy.telegraph` (state-driven, not a timer — see that function's own
 * header for why). Only the three telegraphed moves ever carry this: LANCE (hardSpike),
 * RING BURST (ringDownFile), SHOVE (heavyPush).
 *
 * Two independent tells, on two independent elements, so NEITHER needs the compound-selector
 * treatment on its own — but the PULSE (below) still lands on `.ms-figure-body`, the SAME
 * element idle/critical's own ambient bob/pulse already animates, so IT alone needs the
 * explicit-compound-selector strategy (L7's 3rd, `.ms-ink-tell-*`'s own precedent):
 *   1. THE LABEL — the move's NAME (E1), a `::after` pseudo-element on the CARD itself, a
 *      DIFFERENT element from `.ms-figure-body` entirely (L7 strategy 1) — cannot collide
 *      with ANY existing animation no matter what plays on the body inside it.
 *   2. THE ALARM PULSE — one shared `ms-ink-standing-pulse` keyframe (a brightening breathe
 *      on `filter`) reused across all three moveTokens — E4 does not require three DISTINCT
 *      standing silhouettes (the nine LANDING marks already carry that job); a standing
 *      telegraph's job is simpler: "this body is winding up, and here is its name," so ONE
 *      alarm read plus the label does the work without competing with the eventual landing
 *      mark's own distinctness. Composes with idle/critical below exactly like the tell block
 *      above.
 * ═══════════════════════════════════════════════════════════════════════════════════════ */

/* THE NAMED SHOT (playtest-r15 §3) adds the Marksman's PINNING SHOT (`tautSnap`) to the three
 * standing marks below — it telegraphs now, so it winds up the same visible way. */
@keyframes ms-ink-standing-pulse {
  0%, 100% { filter: brightness(1) saturate(1); }
  50%      { filter: brightness(1.55) saturate(1.35); }
}

/* --ms-ink-standing-pulse-ms — OWED TO config.measure.wave (no key exists for it yet; the
 * three rules below read the fallback via the SAME `var(..., FALLBACK)` idiom the tell block
 * above already uses for its own three durations). Not gated by BEAT_MS on purpose: unlike
 * every other beat in this file, the standing pulse's own cadence is not "how long a beat
 * takes" — it loops for an entire round — so it is not naturally one of BEAT_MS's classes. */

/* Base rule — the standing pulse alone, no idle/critical on the card. */
.ms-ink-standing-hardSpike .ms-figure-body,
.ms-ink-standing-ringDownFile .ms-figure-body,
.ms-ink-standing-heavyPush .ms-figure-body,
.ms-ink-standing-tautSnap .ms-figure-body {
  animation: ms-ink-standing-pulse var(--ms-ink-standing-pulse-ms, 1400ms) ease-in-out infinite;
  outline: 2px solid var(--ms-wave-telegraph);
  outline-offset: 2px;
}

/* THE COMPOSING RULES — idle/critical PLUS the standing pulse, one `animation:` shorthand. */
.ms-fig-idle.ms-ink-standing-hardSpike .ms-figure-body,
.ms-fig-idle.ms-ink-standing-ringDownFile .ms-figure-body,
.ms-fig-idle.ms-ink-standing-heavyPush .ms-figure-body,
.ms-fig-idle.ms-ink-standing-tautSnap .ms-figure-body {
  animation: ms-fig-idle-bob var(--ms-fig-idle-ms, 3000ms) ease-in-out infinite,
             ms-ink-standing-pulse var(--ms-ink-standing-pulse-ms, 1400ms) ease-in-out infinite;
}
.ms-fig-critical.ms-ink-standing-hardSpike .ms-figure-body,
.ms-fig-critical.ms-ink-standing-ringDownFile .ms-figure-body,
.ms-fig-critical.ms-ink-standing-heavyPush .ms-figure-body,
.ms-fig-critical.ms-ink-standing-tautSnap .ms-figure-body {
  animation: ms-fig-critical-bob var(--ms-fig-critical-ms, 1650ms) ease-in-out infinite,
             ms-fig-critical-pulse var(--ms-fig-critical-pulse-ms, 1100ms) ease-in-out infinite,
             ms-ink-standing-pulse var(--ms-ink-standing-pulse-ms, 1400ms) ease-in-out infinite;
}

/* THE LABEL — E1, "the move is named, so name it": a caller sets
   `card.dataset.msTelegraphMove = enemy.telegraph.name` alongside the standing class. A
   different element (`::after` on the CARD, never `.ms-figure-body`) — L7 strategy 1 — so it
   never needs the compound-selector treatment the pulse above does. */
.ms-ink-standing-hardSpike[data-ms-telegraph-move]::after,
.ms-ink-standing-ringDownFile[data-ms-telegraph-move]::after,
.ms-ink-standing-heavyPush[data-ms-telegraph-move]::after,
.ms-ink-standing-tautSnap[data-ms-telegraph-move]::after {
  content: attr(data-ms-telegraph-move);
  position: absolute;
  left: 50%; top: -14px;
  translate: -50% 0;
  white-space: nowrap;
  font: 800 10px/1 var(--ms-font, sans-serif);
  letter-spacing: 0.04em;
  color: var(--ms-wave-telegraph);
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  pointer-events: none;
}

/* E8/E6 — reduced motion (and a SKIP, which never touches this state-driven class at all —
   see `_standingTelegraphClass`'s own header) keep the outline + label; only the pulse
   animation itself is neutralised, exactly like the tell block above. */
@media (prefers-reduced-motion: reduce) {
  .ms-ink-standing-hardSpike .ms-figure-body,
  .ms-ink-standing-ringDownFile .ms-figure-body,
  .ms-ink-standing-heavyPush .ms-figure-body,
  .ms-ink-standing-tautSnap .ms-figure-body,
  .ms-fig-idle.ms-ink-standing-hardSpike .ms-figure-body,
  .ms-fig-idle.ms-ink-standing-ringDownFile .ms-figure-body,
  .ms-fig-idle.ms-ink-standing-heavyPush .ms-figure-body,
  .ms-fig-idle.ms-ink-standing-tautSnap .ms-figure-body,
  .ms-fig-critical.ms-ink-standing-hardSpike .ms-figure-body,
  .ms-fig-critical.ms-ink-standing-ringDownFile .ms-figure-body,
  .ms-fig-critical.ms-ink-standing-heavyPush .ms-figure-body,
  .ms-fig-critical.ms-ink-standing-tautSnap .ms-figure-body {
    animation: none !important;
  }
}

/* ── THE ARCHETYPE SILHOUETTE / ACCENT (cutin.js) ──────────────────────────────────────────
 * cutin.js's sweep gains a per-ARCHETYPE accent stripe alongside its existing class
 * silhouette — the fix for action-language-spec finding 1 ("five identical sweeps"). One
 * class per archetype, applied to `.ms-cutin-panel`; colour still comes from the caster's
 * CLASS_ACCENT (unchanged — the cut-in has always been class-coloured, not element-coloured,
 * since it fires before an element is necessarily resolved for a hybrid ability), only the
 * ACCENT SHAPE differs now. */
.ms-cutin-panel[data-ms-archetype="thrown"] .ms-cutin-lines    { clip-path: polygon(0 40%, 100% 10%, 100% 30%, 0 60%); }
.ms-cutin-panel[data-ms-archetype="strike"] .ms-cutin-lines    { clip-path: polygon(0 0, 60% 0, 100% 100%, 40% 100%); }
.ms-cutin-panel[data-ms-archetype="deployed"] .ms-cutin-lines  { clip-path: circle(60% at 30% 50%); }
.ms-cutin-panel[data-ms-archetype="channeled"] .ms-cutin-lines { clip-path: inset(42% 0 42% 0); }
