/* measure/pace.css — THE TRAVEL LAYER (measure/pace.js). Playtest 2026-09-01:
   "i want the enemies to walk over to the fateforged they are attacking, and vice versa."

   WHAT THIS FILE OWNS, and nothing else:
     .ms-figure-walk         the walk WRAPPER — one element, sitting between the figure CARD
                             (`.ms-world-figure`/`.ms-member`) and `.ms-figure-body`. It owns
                             the INDEPENDENT `translate:` and `scale:` properties, driven off
                             three custom properties approach.js writes (`--ms-walk-x`,
                             `--ms-walk-y`, `--ms-walk-scale`) through a CSS TRANSITION.
     .ms-walking             the wrapper is mid-beat (a hook for the walk-cycle layer).
     .ms-walk-close /
     .ms-walk-brace          which of L7's travel profiles this beat is (close walks the whole
                             berth, brace takes a short committed step and plants).
     .ms-walk-strike         the ARRIVAL pose — held for the planted beat while the hit lands.
     .ms-walk-instant        approach.js's own no-transition escape hatch (»» SKIP, and the
                             re-time snapshot) — never authored on a card by anyone else.
     .ms-walk-lifted         THE LIFT (spec §5b) — the one class this file authors on the
                             figure CARD rather than the wrapper, marking a body that is
                             currently drawn IN FRONT of the ones it walks past. It is a
                             MARKER: the z-index itself is a plain inline write pace.js makes
                             (`liftedZIndex()`), because the number is the card's own stacking
                             value plus config's WALK_Z_LIFT and CSS cannot compute it. Both
                             go on together and come off together in `finish()`.

   WHAT THIS FILE DOES *NOT* OWN, on the same element: THE WALK CYCLE. The gait is an
   `animation` on `transform` on this same wrapper, owned by figures.css (see its header
   point 5 for the composition proof). This file authors NO `animation` anywhere, which is
   exactly what makes that safe — see the proof below.

   ── HOW THIS STAYS COLLISION-FREE (the three-way proof; read before touching anything) ────
   figures.css's header states the law this file is bound by: a CSS `animation` SHORTHAND does
   not merge across two rules on one element — one silently wins. Fate's fix is a division of
   labour by ELEMENT and by PROPERTY, and THE PACE is the third participant in it:

     1. THE CARD (`.ms-world-figure`) — `transform`, owned by measure.css. Every card-level
        tell (`.ms-foe-hit`/`-acting`/`-crit`/`-dying`, `.ms-member-hit`) animates the card
        through the `--ms-fx-*` channel folded into measure.css's ONE `transform` declaration.
        NOTHING IN THIS FILE WRITES `transform` ON THE CARD.
     2. THE BODY (`.ms-figure-body`) — `transform`, owned by figures.css. The idle bob and the
        critical bob/pulse animate the body. NOTHING IN THIS FILE WRITES `transform` ON THE
        BODY. (Both of those selectors are DESCENDANT selectors — `.ms-fig-idle .ms-figure-body`
        — so inserting this wrapper between card and body does not break either of them.)
     3. THE WRAPPER (`.ms-figure-walk`) — this file, and ONLY through `translate:`/`scale:`,
        which are separate animatable properties from `transform`. The rendering engine
        composes them (translate, then rotate, then scale, then `transform`) into one matrix,
        so a walk offset MULTIPLIES with the card's shake and the body's bob rather than
        replacing either. And because the walk is a TRANSITION on those two properties — never
        an `animation` shorthand — the walk-cycle bob that the figure layer animates on this
        SAME wrapper composes too: a transition and an animation on DIFFERENT properties of one
        element are two independent things, with no single-winner problem between them.

   That is why the offset is a transition and not an animation, and why it lives on a third
   element rather than on either of the two that already have owners.

   Timing/easing values mirror `config.measure.approach` (core/config.js) — every `var(...)`
   below carries a fallback equal to that block's own value, and approach.js writes the live
   per-leg duration and easing into `--ms-walk-dur`/`--ms-walk-ease` (so a fast-forwarded
   beat's TRANSITION is genuinely shorter, not merely cut short by a faster JS timer while the
   transition keeps running, and so each of the five legs gets its own authored curve —
   EASE_WIND leaning off the heel, EASE_OUT planting, EASE_BACK brisk and even).
   CONFIG WINS; these fallbacks exist so the layer still reads correctly if a var is missing.

   Nothing here paints a colour, and nothing here is a click target: the wrapper is a plain
   layout-neutral box that inherits its card's own pointer behaviour untouched. */

/* Registered so the browser TWEENS them as real typed values rather than hard-cutting between
   token strings — the same reasoning measure.css's own `@property --ms-fx-x` block gives, and
   the reason approach.js can read a mid-flight offset back out for re-timing. */
@property --ms-walk-x { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --ms-walk-y { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --ms-walk-scale { syntax: '<number>'; inherits: false; initial-value: 1; }
/* The arrival POSE's own channel, kept separate from the travel offset so the two ADD instead
   of one overwriting the other — approach.js writes `--ms-walk-y` INLINE, and an inline custom
   property beats any class rule, so a pose that tried to re-declare `--ms-walk-y` would simply
   never apply. This is that bug, designed out. */
@property --ms-walk-lift { syntax: '<length>'; inherits: false; initial-value: 0px; }
/* THE WINDUP's own channel (THE WAVE STRIKES, docs/enemy-turn-spec.md §7) — a third addend on
   the same translate, so a wind-up lean, the travel offset and the arrival pose SUM instead
   of any one overwriting another (the same reasoning as `--ms-walk-lift` above). */
@property --ms-walk-windup-lift { syntax: '<length>'; inherits: false; initial-value: 0px; }

:root {
  /* Mirrors of config.measure.approach's duration knobs — approach.js overrides `--ms-walk-dur`
     per leg at runtime; these are the resting values a bare wrapper reads. */
  --ms-pace-out-ms: 420ms;    /* config.measure.approach.CLOSE.outMs */
  --ms-pace-ease: cubic-bezier(.22,.61,.36,1);   /* …EASE_OUT — the default leg curve */
  --ms-pace-strike-lift: 2.5px; /* how far the arrival pose leans in, on its own
                                       `--ms-walk-lift` channel — a pose, never a second
                                       travel (owed to config; see approach.js's OWED table) */
}

/* THE WRAPPER. `display: contents` is deliberately NOT used (a `display:contents` box cannot
   be transformed at all); this is a plain block that inherits the card's flex alignment, adds
   no box of its own worth speaking of, and is transform-origin'd at the feet so a walk that
   also scales grows UP from the ground the way a body standing on a floor does. */
.ms-figure-walk {
  display: flex;
  flex-direction: column;
  align-items: center;
  transform-origin: bottom center;
  translate: var(--ms-walk-x, 0px) calc(var(--ms-walk-y, 0px) + var(--ms-walk-lift, 0px) + var(--ms-walk-windup-lift, 0px));
  scale: var(--ms-walk-scale, 1);
  transition: translate var(--ms-walk-dur, var(--ms-pace-out-ms, 420ms)) var(--ms-walk-ease, var(--ms-pace-ease, ease)),
              scale var(--ms-walk-dur, var(--ms-pace-out-ms, 420ms)) var(--ms-walk-ease, var(--ms-pace-ease, ease));
  /* The only two properties that ever change here — declared so the compositor can promote the
     wrapper once instead of re-rasterising the figure each beat. */
  will-change: translate, scale;
}

/* approach.js's escape hatch: the zeroing write at the start of a beat, the »» SKIP collapse,
   and the snapshot half of a mid-flight re-time all need the value to land with no tween.
   `!important` because it must beat the profile rules below regardless of source order. */
.ms-figure-walk.ms-walk-instant { transition: none !important; }

/* ── THE PROFILES (approach.js's L7 / spec §3) ────────────────────────────────────────────
   The distances, durations and per-leg curves are all the PLAN's — computed from real rects
   and config rows, written as custom properties — so these two classes are markers, not a
   second timing table. What they legitimately differ on is how far the arrival pose leans: a
   `close` body swung in someone's face, a `brace` body only planted and firing. They are also
   the hook figures.css and any later layer selects a profile by. */
.ms-figure-walk.ms-walk-close { --ms-pace-strike-lift: 3px; }
.ms-figure-walk.ms-walk-brace { --ms-pace-strike-lift: 1.5px; }

/* ── THE ARRIVAL POSE ─────────────────────────────────────────────────────────────────────
   Held for the planted beat, while the caller's hit FX / ink cast lands (approach.js fires
   `onStrike` as this class goes on, `strikeAt` into the OUT leg — arrival, never departure).
   Deliberately NOT motion of its own: a static lean on the `--ms-walk-lift` channel, which the
   travel's own `translate` already sums in, so it composes with the walk instead of
   introducing a fourth animated property. figures.css PAUSES the gait for the same beat. `filter` is safe here — no other layer animates
   `filter` on this element (measure.css's own `saturate()`/`brightness()` reads sit on the
   card and on `.ms-figure-body`). */
.ms-figure-walk.ms-walk-strike {
  --ms-walk-lift: calc(-1 * var(--ms-pace-strike-lift, 2px));
  filter: brightness(1.06);
}

/* ── THE WINDUP (THE WAVE STRIKES, docs/enemy-turn-spec.md §7) ────────────────────────────
   Worn by the wrapper through the WIND + OUT legs and taken off as the strike lands (pace.js
   `fireStrike`), one class per data/action-language.js WAVE_WINDUP_LANGUAGE key: the pose
   CLASS of the move about to land. Same discipline as the arrival pose — a static lean on its
   own `--ms-walk-windup-lift` channel (summed into the translate above, tweened by the leg's
   own transition) plus `filter`, which no other layer animates on this element. NOTHING here
   writes `transform`, `scale`, `translate` or an `animation` — the three-way proof above holds.
     lunge        a body closing the distance coils DOWN first (the OUT leg releases it)
     braced       a shoulder set — a plant, barely a lean
     bracedHigh   the barrel goes HIGH for a lob (the body rises)
     bracedSweep  the sweep's plant, a touch hotter (the yaw is the stage's to show)
     telegraph    the big attack REARS — a heavy crouch, lit up
     armsOut      the Conduit reaching to its own side — no lean, a brightening */
.ms-figure-walk.ms-walk-windup-lunge       { --ms-walk-windup-lift: 2.5px; }
.ms-figure-walk.ms-walk-windup-braced      { --ms-walk-windup-lift: -1px;  filter: brightness(1.04); }
.ms-figure-walk.ms-walk-windup-bracedHigh  { --ms-walk-windup-lift: -4px;  filter: brightness(1.04); }
.ms-figure-walk.ms-walk-windup-bracedSweep { --ms-walk-windup-lift: -1px;  filter: brightness(1.06) saturate(1.1); }
.ms-figure-walk.ms-walk-windup-telegraph   { --ms-walk-windup-lift: 4px;   filter: brightness(1.14) contrast(1.05); }
.ms-figure-walk.ms-walk-windup-armsOut     { filter: brightness(1.08); }

/* ── THE LIFT (spec §5b) ──────────────────────────────────────────────────────────────────
   A crossing is SANCTIONED — no deployment of a two-rank wave plus a three-body party makes
   every straight path clear — so the walker is drawn OVER what it passes for the length of its
   walk, exactly as a JRPG does. The stacking value is pace.js's (the card's own z + config's
   `WALK_Z_LIFT`), written inline; this rule exists so the state is selectable and so nothing
   else in the mode can quietly re-declare a z-index on a lifted card. `z-index` is NOT an
   animated property here and must never become one: that would put a fourth writer on the
   card, which is measure.css's element (the three-way proof above).

   The class therefore carries NO declaration of its own, deliberately: it is a state marker
   (for this comment, for the guard, and for any later layer that wants to tint or shadow a
   passing body), and giving it paint here would be inventing a look nobody asked for. */

/* A wrapper that is not mid-beat costs nothing: no transition target, no promoted layer, no
   animation. THE MEASURE never has anything running between beats (L5). */
.ms-figure-walk:not(.ms-walking) { will-change: auto; }

/* ── REDUCED MOTION (L4) ──────────────────────────────────────────────────────────────────
   Full neutralisation: the wrapper never travels, never scales, never leans. approach.js's
   own plan already degrades to a near-instant in-place beat (so `onStrike` still fires at the
   right point in the sequence and the caller needs no branch) — this block is the CSS half of
   the same promise, and it holds even if a plan is somehow played with the flag unset. */
@media (prefers-reduced-motion: reduce) {
  .ms-figure-walk {
    transition: none !important;
    translate: none !important;
    scale: none !important;
    will-change: auto;
  }
  .ms-figure-walk.ms-walk-strike { filter: brightness(1.06); }   /* the read survives, statically */
}
