/* measure/figures.css — THE FIGURE STATE LAYER (docs/measure-stage-spec.md build record
 * 2026-08-29). Playtest: "the enemies should have states: Idle, attack, taking damage,
 * critical" + "make the enemies a bit larger to show off their states."
 *
 * TWO of those four states already exist and are NOT reimplemented here — measure.css's own
 * `.ms-foe-hit`/`.ms-foe-crit`/`.ms-face-hurt`/`.ms-impact-flash` (taking damage) and
 * `.ms-foe-acting`/`.ms-attacker-rim` (attack). This file owns the two that did not: a
 * PERSISTENT idle ambient (`.ms-fig-idle`) and a PERSISTENT critical read (`.ms-fig-critical`)
 * — both painted from measure/scene.js's `figureStateClasses()`, a pure function of the
 * enemy's own hp/maxHp spread onto the figure CARD (`.ms-world-figure`) at render time, so
 * they survive board.js's `worldFigures.innerHTML = ''` rebuild for free.
 *
 * ── HOW THIS STAYS COLLISION-FREE (read this before touching anything below) ──────────────
 * CSS's `animation` shorthand does not MERGE across two matching rules on one element — only
 * the higher-specificity (or later-in-cascade, on a tie) rule's `animation` list survives.
 * Two rules from two different files, each independently deciding "this element should
 * animate," will silently cancel one or the other rather than both playing. This file avoids
 * that five ways, deliberately, in order of how much of it is used:
 *
 *   1. DIFFERENT ELEMENT. Every hit/attack tell measure.css owns lives on the CARD itself
 *      (`.ms-foe-hit`/`.ms-foe-crit`/`.ms-foe-acting`/`.ms-foe-dying` all animate `transform`
 *      on `.ms-world-figure`) or on a ::after pseudo (`.ms-impact-flash`) or on `.ms-face`
 *      (`.ms-face-hurt`). This file's idle bob + critical bob/pulse live on `.ms-figure-body`
 *      — a CHILD of the card — so a hit-shake's card-level `transform` and this file's
 *      figure-body-level `transform` are two DIFFERENT elements' `transform` properties: they
 *      compose automatically (a child's transform multiplies with its parent's), never
 *      collide, and need no special-casing. Same reasoning for the HP flicker below, which
 *      lives on `.ms-hp-fill` — untouched by any measure.css animation.
 *   2. STATIC, NOT ANIMATED, where a static value will do. Critical's desaturated/darkened
 *      base look is a plain `filter` DECLARATION (like `.ms-foe-shielded .ms-figure-body`'s
 *      own static `saturate()` already does in measure.css) — a static property value never
 *      collides with another rule's `animation`, since only the `animation` SHORTHAND itself
 *      has the single-winner problem, not the individual properties keyframes end up
 *      painting. Only the WARNING PULSE half of critical needs real motion.
 *   3. NO OVERLAP LEFT TO COMPOUND. `.ms-attacker-rim` used to animate a `box-shadow` ring on
 *      `.ms-figure-body` — the same element idle/critical/broken move — and needed compound
 *      `.ms-fig-*.ms-attacker-rim` rules listing both animations. Since 2026-10-04 (THE
 *      SILHOUETTE, NOT THE BOX, measure.css) the tell lives on the drawn IMG and the walk
 *      wrapper's `::after`, two elements nothing here animates, so rule 1 covers it and the
 *      compound rules are gone. Guard: measure-board.test.js
 *      `figure-state/idle-and-critical-compose-with-the-attack-tell`.
 *      `.ms-hitstop, .ms-hitstop *` (measure.css) already pauses EVERY animation on a struck
 *      figure via `animation-play-state: paused !important` — idle/critical included, for
 *      free, no special-casing needed there.
 *   4. DEATH NEEDS NO COMPOSITION AT ALL. `figureStateClasses()` (scene.js) returns `[]` for
 *      any enemy with `hp <= 0` — a dying/dead body carries NEITHER `.ms-fig-idle` NOR
 *      `.ms-fig-critical`, so `.ms-foe-dying`'s kill-collapse (card-level `transform: scale`)
 *      never has an idle/critical class to fight in the first place. Nothing to compose,
 *      nothing to prove.
 *
 *   5. THE WALK CYCLE ANIMATES A FOURTH ELEMENT, ON A PROPERTY ITS OWNER DOES NOT TOUCH.
 *      THE PACE (2026-09-01, docs/measure-approach-spec.md; Jeff: "i want the enemies to
 *      walk over to the fateforged they are attacking, and vice versa") inserts a WRAPPER,
 *      `.ms-figure-walk`, between the CARD and `.ms-figure-body`. measure/pace.css owns
 *      that wrapper's TRAVEL and states its own half of this law: the travel is a CSS
 *      *transition* on the INDEPENDENT `translate:` and `scale:` properties, and it "writes
 *      `transform` on NOTHING, ever." The gait below is therefore an `animation` on
 *      `transform` on that same wrapper, and the two compose rather than collide, for two
 *      separate reasons, both of which must hold:
 *        (a) DIFFERENT MECHANISMS. A transition and an animation are independent timelines;
 *            the single-winner problem in this file's header is a property of the `animation`
 *            SHORTHAND cascading against another `animation` shorthand on one element. There
 *            is no second `animation` on `.ms-figure-walk` in ANY stylesheet — pace.css's
 *            own header commits to that, and the only rules that touch the wrapper elsewhere
 *            set `transition`, `translate`, `scale`, `filter` and `will-change`.
 *        (b) DIFFERENT PROPERTIES, AND THEY MULTIPLY. `translate`, `rotate`, `scale` and
 *            `transform` are four separate animatable properties; the engine composes them
 *            into one matrix in that fixed order (translate -> rotate -> scale -> transform),
 *            so the wrapper's transitioned `translate`/`scale` and this file's animated
 *            `transform` are two factors of one matrix, never two writers of one value.
 *            Same guarantee, one level down, as point 1's card-vs-body composition.
 *      That is why the gait may live on the wrapper, and why it may not be written as
 *      `translate:` (which WOULD overwrite the travel) or moved onto `.ms-figure-body`
 *      (whose `transform` is already spoken for by the idle/critical bobs above). It runs
 *      ONLY while pace.js has marked the wrapper `.ms-walking`, and while it runs the
 *      idle/critical bobs' own AMPLITUDES are zeroed through their custom properties — a
 *      plain value write, never a competing `animation` — so a walking body has exactly one
 *      gait, while critical's filter pulse and the HP flicker keep running untouched.
 *
 * Every ambient animation here (idle bob, critical bob+pulse, the HP flicker, the walk cycle)
 * is fully neutralised under `prefers-reduced-motion: reduce` at the bottom of this file — critical
 * stays readable through its STATIC filter/rim alone (point 2, above), which the
 * reduced-motion block explicitly keeps instead of merely turning everything off.
 *
 * Timing/threshold values mirror `config.measure.figure` (core/config.js) — every `var(...)`
 * fallback below is that config's own number, so a card that never gets the CSS custom
 * property written onto it (a follow-up wiring step, see below) still renders at the
 * designed feel, exactly the idiom `--ms-impact-flash-ms`/`--ms-hit-shake-ms` already use in
 * measure.css. FOLLOW-UP WIRING OWED (board.js, not this file — see this pass's own PR/report
 * for the exact call sites): `_worldFigureCard` should
 *   (a) spread `figureStateClasses(foe)` into the card's className alongside its existing
 *       `ms-tier-*`/`ms-foe-boss` classes,
 *   (b) `card.style.setProperty('--ms-fig-phase', String(pos.phase))` beside its existing
 *       `--ms-fig-scale` write (pos = `placeFigure(...)`'s return, which now carries `phase`).
 * Until that wiring lands, this file is dormant (no card ever carries `.ms-fig-idle`/
 * `.ms-fig-critical`) — safe to ship ahead of it, exactly like a MARQUEE naming a story slot
 * with `comic: null`.
 */

/* ── THE IDLE BOB — cheap, always-on, desynchronised per figure ─────────────────────────── */
@keyframes ms-fig-idle-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(calc(-1 * var(--ms-fig-idle-amp, 3px))); }
}
/* A breathing contact shadow reads as weight shifting even before the eye catches the body's
 * own bob — cheap (opacity + scale, no layout) and lives on an element (`.ms-figure-shadow`)
 * nothing else in either stylesheet ever animates, so it needs no compound-selector guard. */
@keyframes ms-fig-shadow-breathe {
  0%, 100% { opacity: 0.55; transform: translateX(-50%) scale(1); }
  50%      { opacity: 0.34; transform: translateX(-50%) scale(0.86); }
}
.ms-fig-idle .ms-figure-body {
  animation: ms-fig-idle-bob var(--ms-fig-idle-ms, 3000ms) ease-in-out infinite;
  animation-delay: calc(var(--ms-fig-phase, 0) * -1 * var(--ms-fig-idle-ms, 3000ms));
}
.ms-fig-idle .ms-figure-shadow {
  animation: ms-fig-shadow-breathe var(--ms-fig-idle-ms, 3000ms) ease-in-out infinite;
  animation-delay: calc(var(--ms-fig-phase, 0) * -1 * var(--ms-fig-idle-ms, 3000ms));
}

/* Per-tier tempo — config.measure.figure.IDLE_TIER (core/config.js), mirrored here 1:1 (see
 * this file's header for why CSS can't read that object directly). A boss breathes slower
 * and heavier; chaff quicker and shallower — "size IS the threat readout" extended to tempo,
 * not just scale. Declared as plain custom properties (not `animation`), so these compose
 * with everything above/below for free — see header point 2. Ordered chaff -> boss so the
 * later, `.ms-foe-boss` rule wins any tie (a boss whose roster `tier` field also happens to
 * read `elite`/etc. must still get the boss row, never the tier row). */
.ms-tier-chaff .ms-figure-body {
  --ms-fig-idle-ms: 2600ms; --ms-fig-idle-amp: 2.5px;
  --ms-fig-critical-ms: 1450ms; --ms-fig-critical-amp: 1.3px;
}
.ms-tier-line .ms-figure-body {
  --ms-fig-idle-ms: 3000ms; --ms-fig-idle-amp: 3px;
  --ms-fig-critical-ms: 1650ms; --ms-fig-critical-amp: 1.5px;
}
.ms-tier-specialist .ms-figure-body {
  --ms-fig-idle-ms: 3200ms; --ms-fig-idle-amp: 3.5px;
  --ms-fig-critical-ms: 1750ms; --ms-fig-critical-amp: 1.7px;
}
.ms-tier-elite .ms-figure-body {
  --ms-fig-idle-ms: 3500ms; --ms-fig-idle-amp: 4px;
  --ms-fig-critical-ms: 1900ms; --ms-fig-critical-amp: 2px;
}
.ms-foe-boss .ms-figure-body {
  --ms-fig-idle-ms: 5200ms; --ms-fig-idle-amp: 6px;
  --ms-fig-critical-ms: 2850ms; --ms-fig-critical-amp: 3px;
}

/* ── THE CRITICAL READ — a continuous "this one is dying" signal, never just a flash ────── */
/* The warning pulse: a slow filter breathe, INDEPENDENT of the bob above (a different period
 * — CRITICAL_PULSE_MS vs the tier's own CRITICAL_BOB — so the two read as two signals, a
 * heartbeat and a wounded breath, never one thing doubled). The attack tell's silhouette
 * glow is a filter too, but on the child IMG — a child's filter composes under this one. */
@keyframes ms-fig-critical-pulse {
  0%, 100% { filter: saturate(0.60) brightness(0.80) drop-shadow(0 0 4px rgba(255,74,48,0.35)); }
  50%      { filter: saturate(0.78) brightness(1.00) drop-shadow(0 0 10px rgba(255,74,48,0.80)); }
}
@keyframes ms-fig-critical-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(calc(-1 * var(--ms-fig-critical-amp, 1.5px))); }
}
.ms-fig-critical .ms-figure-body {
  /* Static fallback (also THE reduced-motion state, below): desaturated, darkened, a hot
   * rim — readable even with every animation on this page turned off, per this file's own
   * accessibility contract. Deliberately NOT `.ms-foe-shielded`'s language (a flat
   * `saturate(0.6)`, no hue push, no rim) and NOT primed's gold-dot language — a critical
   * body reads as WOUNDED, not tethered or primed. */
  filter: saturate(0.68) brightness(0.88) drop-shadow(0 0 6px rgba(255,74,48,0.55));
  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;
  animation-delay: calc(var(--ms-fig-phase, 0) * -1 * var(--ms-fig-critical-ms, 1650ms)), 0s;
}
.ms-fig-critical .ms-figure-shadow {
  animation: ms-fig-shadow-breathe var(--ms-fig-critical-ms, 1650ms) ease-in-out infinite;
  animation-delay: calc(var(--ms-fig-phase, 0) * -1 * var(--ms-fig-critical-ms, 1650ms));
}
/* The boss reads differently at critical — a boss at critical is the fight ending, so its
 * pulse swings harder (wider brightness/glow range) rather than just running the same curve
 * bigger. Higher specificity than the plain rule above (2 classes vs 1), so it wins on any
 * boss regardless of source order. */
@keyframes ms-fig-critical-pulse-boss {
  0%, 100% { filter: saturate(0.55) brightness(0.74) drop-shadow(0 0 7px rgba(255,74,48,0.45)); }
  50%      { filter: saturate(0.85) brightness(1.08) drop-shadow(0 0 16px rgba(255,74,48,0.95)); }
}
/* Overrides ONLY `animation-name` (not the full shorthand) — `animation-duration`/
 * `-timing-function`/`-iteration-count`/`-delay` still come from the plain
 * `.ms-fig-critical .ms-figure-body` rule above (lower specificity, so this wins on just the
 * one longhand it names), reused positionally: index 0 (bob) is untouched, index 1 (pulse ->
 * pulse-boss) swaps the keyframes while keeping the same duration/delay slot. */
.ms-foe-boss.ms-fig-critical .ms-figure-body {
  filter: saturate(0.62) brightness(0.82) drop-shadow(0 0 9px rgba(255,74,48,0.65));
  animation-name: ms-fig-critical-bob, ms-fig-critical-pulse-boss;
}

/* A shielded body that is ALSO critical: `.ms-foe-shielded .ms-figure-body` (measure.css) sets
 * a static `filter: saturate(0.6)`, which would otherwise be entirely replaced (not merged)
 * by critical's own static filter above — same single-winner rule as `animation`, just for
 * `filter` this time. Named explicitly so both reads survive at once. */
.ms-foe-shielded.ms-fig-critical .ms-figure-body {
  filter: saturate(0.6) brightness(0.88) drop-shadow(0 0 6px rgba(255,74,48,0.5));
}

/* The HP bar itself flickers under critical — a third, faster signal, living on `.ms-hp-fill`
 * (nothing else in either stylesheet ever animates it, so no compound rule is needed here). */
@keyframes ms-fig-critical-hp-flicker {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}
.ms-fig-critical .ms-hp-fill {
  animation: ms-fig-critical-hp-flicker var(--ms-fig-critical-hp-ms, 900ms) ease-in-out infinite;
}

/* ── THE WALK CYCLE (THE PACE, 2026-09-01) ────────────────────────────────────────────
 * The gait a travelling body needs, on the walk WRAPPER measure/pace.css owns the travel
 * of — see point 5 of this file's header for the full proof that an `animation` on
 * `transform` here composes with that file's `transition` on `translate`/`scale`.
 *
 * ONE FOOTFALL PER ITERATION (config.measure.pace's own ruling: "One footfall per
 * WALK_BOB_MS, so a stride is two") — the body rises off its heel, leans a degree into the
 * step, and settles; at the ~340-420 ms out leg that reads as roughly a stride and a half.
 * The rotation returns to 0 at both ends of the cycle, so a loop restart never snaps.
 *
 * Values mirror `config.measure.pace` 1:1 (WALK_BOB_MS 260, WALK_BOB_PX 3.2,
 * WALK_BOB_TIER, PHONE.WALK_BOB_PX 2.4) — CONFIG WINS; these are the `var(...)` fallbacks
 * the same way every other number in this file mirrors `config.measure.figure`. They are
 * split into a per-PLATFORM base and a per-TIER MULTIPLIER rather than one flat per-tier
 * table (which is how the idle bob above is authored) for one reason: the phone overrides
 * only the base amplitude, and a flat table would need a phone rule per tier whose
 * specificity would then out-rank the tier rules themselves. Multiplying keeps the two axes
 * independent, exactly as the config table does (`WALK_BOB_TIER` multiplies WALK_BOB_PX). */
@keyframes ms-walk-bob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(calc(-1 * var(--ms-walk-bob-amp, 3.2px)))
                        rotate(var(--ms-walk-bob-tilt, 1.1deg)); }
}
.ms-figure-walk {
  --ms-walk-bob-base-ms: 260ms;  /* config.measure.pace.WALK_BOB_MS */
  --ms-walk-bob-base-px: 3.2px;  /* config.measure.pace.WALK_BOB_PX */
  --ms-walk-bob-mult: 1;         /* config.measure.pace.WALK_BOB_TIER (per tier, below) */
  --ms-walk-bob-tilt: 1.1deg;
  --ms-walk-bob-amp: calc(var(--ms-walk-bob-base-px, 3.2px) * var(--ms-walk-bob-mult, 1));
  --ms-walk-bob-ms: calc(var(--ms-walk-bob-base-ms, 260ms) * var(--ms-walk-bob-mult, 1));
}
/* Per-tier gait — a heavy body strides slower AND higher, chaff quicker and flatter (the same
 * tempo-carries-threat idea IDLE_TIER ships above, applied to gait). One multiplier drives
 * both channels, exactly as config.measure.pace.WALK_BOB_TIER does. Ordered chaff -> boss
 * so the `.ms-foe-boss` rule wins any tie, same as the idle table above. */
.ms-tier-chaff      .ms-figure-walk { --ms-walk-bob-mult: 0.82; --ms-walk-bob-tilt: 1.4deg; }
.ms-tier-line       .ms-figure-walk { --ms-walk-bob-mult: 1.00; }
.ms-tier-specialist .ms-figure-walk { --ms-walk-bob-mult: 1.10; }
.ms-tier-elite      .ms-figure-walk { --ms-walk-bob-mult: 1.25; --ms-walk-bob-tilt: 0.9deg; }
.ms-foe-boss        .ms-figure-walk { --ms-walk-bob-mult: 1.55; --ms-walk-bob-tilt: 0.7deg; }
/* The phone's smaller scene gets a shorter footfall lift and nothing else — config's own
 * `pace.PHONE` patch names exactly one gait key (WALK_BOB_PX 2.4), so this rule names
 * exactly one too. `.ms-desktop` is the platform class board.js already toggles on the stage
 * (measure.css: "a platform class, not a width query"), so this needs no media query. */
.ms-stage:not(.ms-desktop) .ms-figure-walk { --ms-walk-bob-base-px: 2.4px; }

/* The gait runs ONLY while pace.js has the wrapper mid-beat. A wrapper that is not
 * `.ms-walking` carries no animation at all (pace.css's L5, NO IDLE COST, holds here). */
.ms-figure-walk.ms-walking {
  animation: ms-walk-bob var(--ms-walk-bob-ms, 260ms) ease-in-out infinite;
}
/* PLANTED. The arrival pose is a body that has STOPPED — the gait pauses (rather than being
 * removed, which would snap the pose back to the keyframe origin mid-blow) for the held beat
 * the hit lands in, and resumes for the walk home. Same idiom measure.css's `.ms-hitstop`
 * already uses on every other animation on a struck figure. */
.ms-figure-walk.ms-walking.ms-walk-strike { animation-play-state: paused; }
/* pace.js's no-transition escape hatch (»» SKIP and the re-time snapshot) means the body
 * is being placed, not walking — no gait during either. */
.ms-figure-walk.ms-walk-instant { animation: none; }

/* THE ONE GAIT RULE: while the wrapper walks, the idle/critical bobs' own vertical amplitudes
 * go to zero, so a travelling body bobs once (here) rather than twice at two periods. Written
 * as plain custom-property values — never a competing `animation` — so critical's filter
 * pulse, the contact shadow's breathe and the HP flicker all keep running exactly as they do
 * standing still. Declared AFTER the per-tier idle table above, at equal specificity, so it
 * wins on source order the way that table's own boss row does. */
.ms-walking .ms-figure-body { --ms-fig-idle-amp: 0px; --ms-fig-critical-amp: 0px; }

/* ── prefers-reduced-motion: every animation above off; critical stays readable through its
 * static filter alone (already declared, unconditionally, above — nothing to re-declare here
 * except cancelling the motion layered on top of it). ─────────────────────────────────────── */
/* ── THE BROKEN BOSS (2026-09-26, docs/playtest-r13.md §7 — "a new look for the boss being
 * staggered") — the THIRD persistent figure state, and the first that is not read off HP.
 * board.js hands `figureStateClasses()` the break (`state.boss.brokenRounds > 0` for the body
 * carrying `bossUid`) and the body wears `ms-fig-broken` INSTEAD of idle/critical for those
 * rounds. The look: a slow, heavy REEL — the body sags a few px and sways through ±4° from
 * the feet, like a fighter who has lost the floor — drained of colour and lit from inside by
 * a burn-orange rim (the same hue the plate's bar and label turn; one colour for one fact).
 * The shadow flattens and widens under it. Same element contract as idle/critical: the
 * `transform`/`filter`/`animation` live on `.ms-figure-body`, the card's own FX channel and
 * the walk wrapper are untouched (the attack tell rides the img + the wrapper's ::after). Period + halo period are
 * config's (`config.measure.figure.BROKEN_REEL_MS`/`BROKEN_HALO_MS`, read into
 * `--ms-fig-broken-ms`/`--ms-fig-broken-halo-ms` by board.js; fallbacks pinned to them). */
@keyframes ms-fig-broken-reel {
  0%, 100% { transform: translateY(3px) rotate(-4deg); }
  50%      { transform: translateY(1px) rotate(4deg); }
}
@keyframes ms-fig-broken-glow {
  0%, 100% { filter: saturate(0.42) brightness(0.84) contrast(1.08) drop-shadow(0 0 8px rgba(255,138,64,0.55)); }
  50%      { filter: saturate(0.55) brightness(0.96) contrast(1.08) drop-shadow(0 0 16px rgba(255,138,64,0.95)); }
}
.ms-fig-broken .ms-figure-body {
  transform-origin: bottom center;
  /* Static fallback — also THE reduced-motion state (below): drained, orange-rimmed, leaning. */
  filter: saturate(0.45) brightness(0.88) contrast(1.08) drop-shadow(0 0 10px rgba(255,138,64,0.75));
  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-fig-broken .ms-figure-shadow { opacity: 0.4; transform: translateX(-50%) scale(1.2, 0.8); }
/* A broken body never critical-flickers its bar — the bar is saying BROKEN. */
.ms-fig-broken .ms-hp-fill { animation: none; }

/* ── THE STATE SHEETS (docs/measure-stage-spec.md §18, 2026-10-03) ────────────────────────
 * The FOURTH channel, and the one that owns NO transform at all: a figure's STATE is now a
 * different IMAGE (measure/portrait.js renders a sheet of poses from the real mesh — ready /
 * fire / lance / deploy / brand / superCast / hurt / critical / down for a Fateforged, idle /
 * windup / hurt / broken for a foe) and a state change is an `img.src` swap, booked by
 * board.js's `_holdHeroState`/`_holdFoeState`. It writes `src`, `width`/`height` and an inline
 * `height` PERCENTAGE (a kneel is drawn SHORTER than the standing frame, because every frame of
 * one body renders under one camera — THE INK BOX sizes by height, so the ratio is the truth),
 * and it never touches `transform`, `translate`, `scale` or an `animation` — so every tell above
 * (`.ms-member-hit`'s recoil, `.ms-foe-acting`'s step, the idle/critical bobs, the walk) keeps
 * playing over the swapped frame with nothing to collide against. `data-sheet-state` on the
 * `<img>` names the frame standing (the suite reads it; nothing here styles off it, by design —
 * the state IS the picture). The only motion added is the height settle below, a transition on
 * a property no other rule animates. */
.ms-world-figure .ms-figure-body img[data-sheet-state] { transition: height 140ms ease-out; }

@media (prefers-reduced-motion: reduce) {
  .ms-fig-broken .ms-figure-body {
    animation: none !important;
    transform: translateY(3px) rotate(-4deg);   /* the reel's own lean, held — the read survives */
  }
  .ms-fig-idle .ms-figure-body,
  .ms-fig-idle .ms-figure-shadow,
  .ms-fig-critical .ms-figure-body,
  .ms-fig-critical .ms-figure-shadow,
  .ms-foe-boss.ms-fig-critical .ms-figure-body,
  .ms-fig-critical .ms-hp-fill,
  .ms-figure-walk.ms-walking,
  .ms-figure-walk.ms-walking.ms-walk-strike {
    animation: none !important;
  }
  /* THE STATE SHEETS' one motion — the frame-height settle — goes too; the swap itself is a
   * still image replacing a still image, which is exactly what reduced motion asks for. */
  .ms-world-figure .ms-figure-body img[data-sheet-state] { transition: none !important; }
  /* The attack tell's reduced-motion state (a static silhouette outline, no stamp) is
   * measure.css's own, under this same media query; nothing to repeat. */
}
