/*
 * Shared keyframes + selectors for level-map animated foreground layers.
 *
 * Consumed by both the in-game runtime (src/components/game/level-map/
 * animated-map-layer.tsx) and tools/walk-poc.html. Per memory rule
 * feedback_walk_poc_game_parity: this file is the load-bearing parity
 * guarantee. Both surfaces apply the SAME selectors and SAME CSS
 * custom properties — no duplicated keyframes anywhere.
 *
 * Element contract: <div class="anim-layer" data-anim="<preset>"
 *                        data-trigger="idle|click"
 *                        style="--anim-amplitude: …; --anim-duration: …;
 *                               --anim-delay: …;  (+ --move-* for `moving`
 *                               and `cruise`, which also names its generated
 *                               keyframes in --cruise-keyframes)">
 *
 * Flipbook layers (MapLayer.frames > 1) nest ONE extra child that carries the
 * sheet:
 *
 *   <div class="anim-layer" …><div class="anim-frames"
 *        style="background-image: url(<sheet>); background-size: N00% 100%;
 *               background-position-x: …"></div></div>
 *
 * It has to be a child, not the same element: the layer's own `animation`
 * shorthand is already taken (`moving` uses two entries, `none` zeroes it), so
 * a same-element frame cycle would be overwritten. Splitting them lets a layer
 * flap its wings *while* flying. The parent then carries no background-image.
 * The frame cycle itself is NOT a keyframe here — it is stepped from JS (see
 * `.anim-frames` below); this file only owns the child's geometry.
 *
 * `--anim-amplitude` is UNITLESS — each preset applies its own unit via
 * calc() (deg for wiggle/sway, px for float/bounce/shake, percent for pulse).
 *
 * A layer may also carry the `data-rock` MODIFIER (+ `--rock-deg`,
 * `--rock-duration`, `--rock-delay`), which adds an occasional left/right
 * swing on the independent `rotate` property on top of whatever the preset
 * is doing — or `data-spin` (+ `--spin-deg`, `--spin-duration`,
 * `--spin-delay`), which does the same thing with a full revolution instead
 * of a swing. The two are mutually exclusive (both write `rotate`).
 *
 * Every preset is defined for BOTH triggers so no editor combination is a
 * dead cell:
 *   - data-trigger="idle"  → animation loops continuously on mount.
 *   - data-trigger="click" → plays once each time `.anim-playing` is toggled
 *                            on (pointerdown), cleared on `animationend`.
 * The exception is `cruise`, which is trigger-agnostic: it never rests, so
 * there is no one-shot reading of it to give the click cell.
 */

.anim-layer {
  position: absolute;
  background-repeat: no-repeat;
  background-position: top left;
  background-size: contain;
  pointer-events: none;
  --anim-amplitude: 4;
  --anim-duration: 2s;
  --anim-delay: 0s;
}

.anim-layer[data-trigger="click"] {
  pointer-events: auto;
  cursor: pointer;
}

/* Flipbook child: geometry only. The sheet is N frames wide and one frame tall,
   so background-size (N × 100%) makes each cell fill the layer box, and
   background-position-x selects the cell — 0% is the first, 100% the last.
   background-size and the position are set inline and STEPPED FROM JS, not by a
   keyframe here: `frameMs` and `holdMs` are independent knobs and a static
   keyframe has only one (its duration), since its shape lives in percentages
   that can't read a custom property. See animated-map-layer.tsx's LayerArt.
   Inherit pointer-events so a decorative layer stays click-through and a click
   layer stays clickable — the handler lives on the parent either way. */
.anim-frames {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  pointer-events: inherit;
}

/* Per-preset transform origins — apply to both the idle loop and the click
   one-shot so rotation/scale pivots stay consistent across triggers. */
.anim-layer[data-anim="wiggle"],
.anim-layer[data-anim="pop-in"] { transform-origin: 50% 100%; }
.anim-layer[data-anim="sway"]   { transform-origin: 50% 95%; }
.anim-layer[data-anim="pulse"],
.anim-layer[data-anim="moving"] { transform-origin: 50% 50%; }

/* -------- Keyframes (declared once, shared by idle + click) ----------- */

@keyframes anim-wiggle {
  0%, 100% { transform: rotate(calc(var(--anim-amplitude) * -1deg)); }
  50%      { transform: rotate(calc(var(--anim-amplitude) * 1deg)); }
}

/* Same shape as anim-wiggle but kept separate so sway can diverge in
   curve/easing later without coupled edits (it already differs in origin). */
@keyframes anim-sway {
  0%, 100% { transform: rotate(calc(var(--anim-amplitude) * -1deg)); }
  50%      { transform: rotate(calc(var(--anim-amplitude) * 1deg)); }
}

/* Uses the independent `translate` property, not `transform: translateY()`, so
   it can run alongside the `rotate`-based rock modifier. With `transform` the
   browser applies rotate FIRST and the translation inherits the rotation — the
   bob would swing round in a circle mid-rock instead of staying vertical. */
@keyframes anim-float {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 calc(var(--anim-amplitude) * -1px); }
}

@keyframes anim-pop-in {
  0%   { transform: scale(0.6); opacity: 0; }
  60%  { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

@keyframes anim-slide-in-then-settle {
  0%   { transform: translateY(40px); opacity: 0; }
  70%  { transform: translateY(-4px); opacity: 1; }
  100% { transform: translateY(0); opacity: 1; }
}

@keyframes anim-bounce {
  0%, 100% { transform: translateY(0); }
  30%      { transform: translateY(calc(var(--anim-amplitude) * -3px)); }
  60%      { transform: translateY(0); }
  80%      { transform: translateY(calc(var(--anim-amplitude) * -1px)); }
}

@keyframes anim-shake {
  0%, 100%      { transform: translateX(0); }
  10%, 30%, 50% { transform: translateX(calc(var(--anim-amplitude) * -1px)); }
  20%, 40%, 60% { transform: translateX(calc(var(--anim-amplitude) * 1px)); }
  70%           { transform: translateX(calc(var(--anim-amplitude) * -0.5px)); }
  80%           { transform: translateX(calc(var(--anim-amplitude) * 0.5px)); }
}

/* Pulse — amplitude is percent grow (15 → scales to 1.15×). */
@keyframes anim-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(calc(1 + var(--anim-amplitude) * 0.01)); }
}

/* Shadow — for a layer that IS a ground shadow, sitting under a `float` layer
   and breathing in counter-phase with it. `anim-float` is at its lowest at
   0%/100% and at the top of the bob at 50%, so this one is at full size and
   darkness at 0%/100% and at its peak values at 50%: biggest and darkest when
   the art is DOWN, smallest and faintest when it is UP. That inverse pairing is
   the blob-shadow convention every platformer uses, and it is what makes a bob
   read as art lifting off the ground rather than sliding up the screen.

   Note this animates ART — ks2's frog shadow is a sprite the designer drew, cut
   out of the island PDF like any other layer. The preset only makes it breathe;
   its size, softness and placement are the artwork's own, which is why there
   are no knobs for those here.

   Every 50% stop is a separate authorable knob (MapLayer.shadow), because
   "follows the bob" is three independent things and tuning one should not drag
   the other two:
     --shadow-peak         how much SMALLER at the top   (shadow.peakScale)
     --shadow-peak-opacity how much FAINTER at the top   (shadow.peakOpacity)
     --shadow-shift-x/y    how far it SLIDES at the top  (shadow.peakShift)
   Each rides its own property — `scale`, `opacity`, `translate` — so they
   compose in one keyframe with nothing overwriting anything else. Scale pivots
   at the centre (the default origin), so the shadow tightens onto its own
   middle rather than crawling sideways; `shift` is what moves it, applied on
   top, and a positive Y walks it away from the art as the art rises.

   Like `glow`, it animates alpha, so the layer's constant `opacity` can't also
   be inline — the rest level comes from --shadow-rest instead (see shadowVars
   in animated-map-layer.tsx).

   It takes its clock from the layer's own --anim-duration / --anim-delay, which
   the author sets to MATCH the floating layer above it. Nothing enforces that
   pairing: they are two independent layers, so a shadow given a different
   duration will visibly drift against its owner. */
@keyframes anim-shadow {
  0%, 100% { scale: 1;
             opacity: var(--shadow-rest, 1);
             translate: 0 0; }
  50%      { scale: var(--shadow-peak, 0.85);
             opacity: calc(var(--shadow-rest, 1) *
                           var(--shadow-peak-opacity, var(--shadow-peak, 0.85)));
             translate: var(--shadow-shift-x, 0px) var(--shadow-shift-y, 0px); }
}

/* Glow — the one preset that animates ALPHA instead of a transform: the layer
   fades up from invisible to --glow-peak, STAYS lit, then fades back, which is
   how a lit-window mask laid over the island art reads as a city switching its
   lights on and off (ks3). Peak comes from MapLayer.opacity via the runtime,
   not from --anim-amplitude: amplitude defaults to 4, and 4% would be an
   invisible ceiling. Cheap to run — opacity composites, so nothing repaints.

   The 1:3:3:1 split (fade up : lit : fade down : dark) is fixed, like
   anim-rock's and anim-spin's splits and for the same reason: a keyframe's
   shape lives in percentages and those can't read a custom property. So
   --anim-duration scales all four together — at 8s the cycle is 1s up, 3s lit,
   3s down, 1s dark. The fade DOWN is deliberately as long as the lit hold: at
   the 1:3:1:1 this rule used to carry, the lights snapped off next to how
   slowly they had sat lit. BOTH plateaus are the point: without them the layer
   only touches full brightness (and full dark) for an instant, and the city reads as
   throbbing rather than as lights that switch ON, stay on, and go off.
   ease-in-out applies per interval, so the two ramps ease and the plateaus are
   flat, and 100% meets 0% at the same value so the loop is seamless. */
/* Three stops across the open stretch instead of a flat hold, so a layer can
   set the two SHOULDERS below the peak (MapLayer.glowMid) and ramp
   0 -> mid -> peak -> mid -> 0. `--glow-mid` is a fraction of the peak and
   defaults to 1, which makes all three stops equal: the stretch is then flat
   and this keyframe is value-for-value the two-stop original, so ks3's glow
   layer does not move.

   The stop offsets stay glow's own (12.5 / 31.25 / 50) rather than an
   even 25/50/75, because a keyframe's shape lives in percentages that cannot
   read a custom property — one variable can retune the VALUES for every layer,
   the timing is shared. */
@keyframes anim-glow {
  0%                    { opacity: 0; }
  12.5%                 { opacity: calc(var(--glow-peak, 1) * var(--glow-mid, 1)); }
  31.25%                { opacity: var(--glow-peak, 1); }
  50%                   { opacity: calc(var(--glow-peak, 1) * var(--glow-mid, 1)); }
  87.5%, 100%           { opacity: 0; }
}

/* Rock modifier (MapLayer.rock). Rides on the independent `rotate` property so
   it layers over a preset that drives translate/scale — a star pulses AND
   rocks, a planet bobs AND rocks. The swing (+deg → -deg → level) is squeezed
   into the tail of the cycle and the layer sits level for the rest, which is
   what makes it read as "every so often"; both halves scale together with
   --rock-duration.

   It deliberately does NOT turn full circle: art with a fixed "up" (a ringed
   planet, a crescent moon) reads as somersaulting rather than spinning once it
   passes 90°. `wiggle`/`sway` also rotate, but they are presets — non-stop,
   pivoting at the foot, and they consume the whole `animation` slot.

   Because `animation` is a shorthand, a rocking layer needs a rule naming BOTH
   animations — hence the explicit combinations below. Adding rock to another
   preset means adding one more rule here. */
/* Level is --layer-rot, not 0deg: a layer may carry a static base rotation
   (MapLayer.rotation, the designer's own angle for that placement, kept out of
   the sprite so one image serves every placement of it). Adding the swing to it
   means the layer rocks around ITS OWN rest pose — which is the whole premise
   of rock, since art with a fixed "up" is exactly what it's for. Absent, the
   fallback is 0deg and this is the original keyframe. */
@keyframes anim-rock {
  0%, 55% { rotate: var(--layer-rot, 0deg); }
  70%     { rotate: calc(var(--layer-rot, 0deg) + var(--rock-deg, 12deg)); }
  85%     { rotate: calc(var(--layer-rot, 0deg) - var(--rock-deg, 12deg)); }
  100%    { rotate: var(--layer-rot, 0deg); }
}

.anim-layer[data-rock] {
  /* Swing about the middle, whatever origin the preset wanted for its own
     pivot. Every preset combined below already uses 50% 50%. */
  transform-origin: 50% 50%;
  --rock-deg: 12deg;
  --rock-duration: 8s;
  --rock-delay: 0s;
}

.anim-layer[data-trigger="idle"][data-anim="none"][data-rock] {
  animation: anim-rock var(--rock-duration) ease-in-out var(--rock-delay) infinite;
}
.anim-layer[data-trigger="idle"][data-anim="float"][data-rock] {
  animation: anim-float var(--anim-duration) ease-in-out var(--anim-delay) infinite,
             anim-rock  var(--rock-duration) ease-in-out var(--rock-delay) infinite;
}
.anim-layer[data-trigger="idle"][data-anim="pulse"][data-rock] {
  animation: anim-pulse var(--anim-duration) ease-in-out var(--anim-delay) infinite,
             anim-rock  var(--rock-duration) ease-in-out var(--rock-delay) infinite;
}

/* Spin modifier (MapLayer.spin). Rock's sibling for art with no fixed "up": one
   FULL revolution every so often instead of a left/right swing. Same rules as
   rock — rides the independent `rotate` property so it layers over a preset
   driving translate/scale (ks1's stars pulse AND spin), turn squeezed into the
   tail of the cycle with the layer level for the rest, both halves scaling with
   --spin-duration.

   The 65/35 rest/turn split is deliberately NOT authorable: a keyframe's shape
   lives in percentages, which can't read a custom property, and a star only
   needs one knob (--spin-duration) to read right. `ease-in-out` on the turn is
   what makes it a twirl rather than a constant rotation.

   --spin-deg is the whole revolution, so the sign picks the direction: 360deg
   clockwise, -360deg anticlockwise. It is NOT a peak like --rock-deg.

   These rules come AFTER the rock ones on purpose: both write `rotate`, so a
   layer carrying rock and spin together resolves to spin rather than to
   whichever the author happened to list first. They're documented as mutually
   exclusive (see MapLayer.spin) and walk-poc's checkboxes enforce it. */
/* Rest is --layer-rot for the same reason as anim-rock above — the turn is
   relative to the layer's static base rotation, not to level. For a full 360
   the endpoint is visually identical either way, but a star that rests at 10deg
   must not snap to 0 when its turn begins. */
@keyframes anim-spin {
  0%, 65% { rotate: var(--layer-rot, 0deg); }
  100%    { rotate: calc(var(--layer-rot, 0deg) + var(--spin-deg, 360deg)); }
}

.anim-layer[data-spin] {
  /* Turn about the middle — a star pivoting at its foot would orbit instead. */
  transform-origin: 50% 50%;
  --spin-deg: 360deg;
  --spin-duration: 8s;
  --spin-delay: 0s;
}

.anim-layer[data-trigger="idle"][data-anim="none"][data-spin] {
  animation: anim-spin var(--spin-duration) ease-in-out var(--spin-delay) infinite;
}
.anim-layer[data-trigger="idle"][data-anim="float"][data-spin] {
  animation: anim-float var(--anim-duration) ease-in-out var(--anim-delay) infinite,
             anim-spin  var(--spin-duration) ease-in-out var(--spin-delay) infinite;
}
.anim-layer[data-trigger="idle"][data-anim="pulse"][data-spin] {
  animation: anim-pulse var(--anim-duration) ease-in-out var(--anim-delay) infinite,
             anim-spin  var(--spin-duration) ease-in-out var(--spin-delay) infinite;
}

/* Moving — ping-pong between endpoint A (the element's anchor) and endpoint
   B (anchor + (--move-dx, --move-dy)), composing translate + scaleX in one
   keyframe so a single element handles both motion and the facing flip:
   A→B facing --move-fwd, instant flip at B, B→A facing --move-back, then it
   flips back at the A endpoint on loop. scaleX mirrors the box in place
   (origin centre) before the translate, so position is unaffected by the
   flip. */
/* `moving` is two composed animations on one element with INDEPENDENT
   durations, so travel speed and wobble speed are separate knobs:
   - anim-move-travel drives the straight A→B→A trip via the `translate`
     property + the facing flip via the `scale` property (duration =
     --anim-duration). Instant flip at B and A.
   - anim-move-wobble drives a perpendicular bob via the `transform` property
     (duration = --move-wobble-duration), looping independently.
   The browser composes `translate`/`scale`/`transform` (Transforms L2), so
   the net path is a ~~~~ sine whose hump count = travel ÷ wobble duration.
   The bob is along the unit vector perpendicular to travel (--move-perp-x/y),
   so it waves correctly whatever direction the layer flies. */
@keyframes anim-move-travel {
  0%     { translate: 0 0;                                 scale: var(--move-fwd, 1) 1; }
  50%    { translate: var(--move-dx, 0) var(--move-dy, 0); scale: var(--move-fwd, 1) 1; }
  50.01% { translate: var(--move-dx, 0) var(--move-dy, 0); scale: var(--move-back, -1) 1; }
  100%   { translate: 0 0;                                 scale: var(--move-back, -1) 1; }
}
@keyframes anim-move-wobble {
  0%, 100% { transform: translate(0, 0); }
  25%      { transform: translate(calc(var(--move-perp-x, 0) * var(--move-wobble, 0px)),      calc(var(--move-perp-y, 0) * var(--move-wobble, 0px))); }
  75%      { transform: translate(calc(var(--move-perp-x, 0) * var(--move-wobble, 0px) * -1), calc(var(--move-perp-y, 0) * var(--move-wobble, 0px) * -1)); }
}
/* One-way moving: A→B once and HOLD at B (no return leg). Paired with
   animation-fill-mode: forwards in the selectors below. Facing stays --move-fwd
   the whole trip (no flip — there's no return). */
@keyframes anim-move-oneway {
  0%   { translate: 0 0;                                 scale: var(--move-fwd, 1) 1; }
  100% { translate: var(--move-dx, 0) var(--move-dy, 0); scale: var(--move-fwd, 1) 1; }
}

/* -------- Idle: loops continuously on mount (data-trigger="idle") ----- */

.anim-layer[data-trigger="idle"][data-anim="wiggle"] { animation: anim-wiggle var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="sway"]   { animation: anim-sway   var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="float"]  { animation: anim-float  var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="bounce"] { animation: anim-bounce var(--anim-duration) ease-out     var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="shake"]  { animation: anim-shake  var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="pulse"]  { animation: anim-pulse  var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="glow"]   { animation: anim-glow   var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="shadow"] { animation: anim-shadow var(--anim-duration) ease-in-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="moving"]:not([data-move-oneway]) {
  animation: anim-move-travel var(--anim-duration) linear var(--anim-delay) infinite,
             anim-move-wobble var(--move-wobble-duration, 1.2s) ease-in-out var(--anim-delay) infinite;
}
/* One-way idle: flies A→B once on mount and holds at B (fill forwards). */
.anim-layer[data-trigger="idle"][data-anim="moving"][data-move-oneway] {
  animation: anim-move-oneway var(--anim-duration) linear var(--anim-delay) 1 forwards,
             anim-move-wobble var(--move-wobble-duration, 1.2s) ease-in-out var(--anim-delay) infinite;
}
/* Cruise — `moving`'s one-directional sibling: an endless flight ACROSS the
   island rather than a there-and-back. The route may bend, so its keyframe is
   the one thing this file doesn't own: stop percentages have to be each leg's
   share of the total distance (that's what keeps the speed constant through a
   corner), and CSS percentages can't be computed from custom properties. The
   runtime generates the rule per layer and names it in --cruise-keyframes; see
   cruiseKeyframes() in src/lib/game/cruise-path.ts for what it emits.

   Its translate values are relative to the layer's anchor, where the element is
   positioned — so if the var never arrives this whole declaration is invalid at
   computed-value time and the layer simply rests where the designer put it.
   The wrap from 100% back to 0% happens with the art fully off-canvas at both
   ends, so nothing is seen to teleport. Applies at any trigger: a cruise never
   rests, so there's no click state to distinguish. --anim-delay is set NEGATIVE
   by the runtime to start each layer at its authored anchor. */
.anim-layer[data-anim="cruise"] {
  animation: var(--cruise-keyframes) var(--anim-duration) linear var(--anim-delay) infinite,
             anim-move-wobble var(--move-wobble-duration, 1.2s) ease-in-out var(--anim-delay) infinite;
}

/* pop-in / slide are entrance shapes; as idle they replay on a loop so the
   editor still shows visible motion for the combination. */
.anim-layer[data-trigger="idle"][data-anim="pop-in"] { animation: anim-pop-in var(--anim-duration) ease-out var(--anim-delay) infinite; }
.anim-layer[data-trigger="idle"][data-anim="slide-in-then-settle"] { animation: anim-slide-in-then-settle var(--anim-duration) ease-out var(--anim-delay) infinite; }

/* -------- Click: plays once per tap (.anim-playing toggled by JS) ----- */

.anim-layer[data-trigger="click"][data-anim="wiggle"].anim-playing { animation: anim-wiggle var(--anim-duration) ease-in-out 1; }
.anim-layer[data-trigger="click"][data-anim="sway"].anim-playing   { animation: anim-sway   var(--anim-duration) ease-in-out 1; }
.anim-layer[data-trigger="click"][data-anim="float"].anim-playing  { animation: anim-float  var(--anim-duration) ease-in-out 1; }
.anim-layer[data-trigger="click"][data-anim="bounce"].anim-playing { animation: anim-bounce var(--anim-duration) ease-out 1; }
.anim-layer[data-trigger="click"][data-anim="shake"].anim-playing  { animation: anim-shake  var(--anim-duration) ease-in-out 1; }
.anim-layer[data-trigger="click"][data-anim="pulse"].anim-playing  { animation: anim-pulse  var(--anim-duration) ease-in-out 1; }
/* Click glow is one pass of the same keyframe: dark → lit → dark, then the
   layer drops back to its inline opacity and rests there. Present so the
   preset has no dead editor cell; the mask it exists for is an idle layer. */
.anim-layer[data-trigger="click"][data-anim="glow"].anim-playing   { animation: anim-glow   var(--anim-duration) ease-in-out 1; }
/* Click shadow is one pass of the same keyframe. Present so the preset has no
   dead editor cell; the shadow it exists for is an idle layer under a bob. */
.anim-layer[data-trigger="click"][data-anim="shadow"].anim-playing { animation: anim-shadow var(--anim-duration) ease-in-out 1; }
.anim-layer[data-trigger="click"][data-anim="moving"]:not([data-move-oneway]).anim-playing {
  animation: anim-move-travel var(--anim-duration) linear 1,
             anim-move-wobble var(--move-wobble-duration, 1.2s) ease-in-out infinite;
}
/* One-way click: flies A→B once on tap and holds at B (fill forwards). The
   runtime leaves .anim-playing on for one-way so the forwards fill persists. */
.anim-layer[data-trigger="click"][data-anim="moving"][data-move-oneway].anim-playing {
  animation: anim-move-oneway var(--anim-duration) linear 1 forwards,
             anim-move-wobble var(--move-wobble-duration, 1.2s) ease-in-out infinite;
}
.anim-layer[data-trigger="click"][data-anim="pop-in"].anim-playing { animation: anim-pop-in var(--anim-duration) ease-out 1; }
.anim-layer[data-trigger="click"][data-anim="slide-in-then-settle"].anim-playing { animation: anim-slide-in-then-settle var(--anim-duration) ease-out 1; }

/* -------- Journey flight (multi-step layers; see animated-map-layer.tsx) ----
   A journey layer rests at a stop playing its normal `animation` preset via the
   idle selectors above (data-trigger="idle"), and flies between stops via a
   left/top CSS transition set inline by the runtime. While flying it carries
   data-trigger="flying" so NO parked idle selector matches (motion is the
   transition, not a keyframe). `straight` flight adds no keyframe; `wobble`
   flight adds the shared perpendicular sine — the runtime sets --move-wobble +
   --move-perp-x/y for the active leg, exactly like `moving`. */
.anim-layer[data-trigger="flying"] { pointer-events: none; }
.anim-layer[data-trigger="flying"].journey-wobble {
  animation: anim-move-wobble var(--move-wobble-duration, 1.2s) ease-in-out infinite;
}

/* `none` preset — present so the editor dropdown has a no-op default. */
.anim-layer[data-anim="none"] { animation: none; }
