/* ============================================================
   LIVING EMBLEMS — a machine's own art, animated from the INSIDE
   ============================================================
   Every slot ships hand-drawn SVG art (slots/<id>.skin.js): the reel symbol
   ui/SlotEmblem.js uses as the machine's face, and the ceremonial crest the
   jackpot capsule throws. This file makes the PARTS of that art move — the
   vault handle spins and the door cracks open, the dragon breathes fire, the
   volcano erupts, the kraken's tentacles curl — instead of just wobbling the
   whole glyph around, which reads as a sticker being shaken rather than a
   machine coming to life.

   ------------------------------------------------------------
   THE GATE — how a part knows it is allowed to move
   ------------------------------------------------------------
   Emblems appear in dozens of places at a dozen sizes (16px chips in a card
   list, 56px lobby tiles, an 84px capsule payload). Sixteen of them animating
   at once would make the whole screen seethe, so motion is OFF by default and
   switched on only where the art is the subject:

     .emblem-alive          an explicit host — the jackpot capsule's payload
                            once it has landed (ui/JackpotCapsule.js), or any
                            <SlotEmblem alive /> caller.
     .emblem-hover-live     a hoverable host — the lobby's machine tiles and
                            Quick Play chips (ui/LobbyScreen.js). Parts wake
                            while the pointer is over the tile. There is no hover
                            on a touch screen, so on phones ui/LobbyScreen.js puts
                            `emblem-alive` on whichever card is nearest the middle
                            of the viewport instead — scrolling is the cue.
     .symbol-winning        a reel cell that just paid (ui/ReelComponents.js) —
                            the win is exactly when the symbol should come to
                            life. Only winning cells, never the whole board.

   The switch is `animation-name`, not `animation-play-state`, and that choice
   matters: a PAUSED animation freezes wherever it stopped, so a vault door
   would sit half-open forever once the pointer left. Dropping the name removes
   the animation outright and the art snaps back to exactly the static drawing.

   ------------------------------------------------------------
   THE CONTRACT — how to add motion to a machine
   ------------------------------------------------------------
   1. In slots/<id>.skin.js, tag the moving piece:
          <g className="em-part em-xx-hinge"> … </g>
      `em-part` opts the element into the gate below and gives it a sane pivot.
      `em-vh-<name>` is the machine-prefixed part class this file animates.

   2. Here, declare the part with LONGHAND animation properties and hand it its
      keyframes through the `--em-anim` custom property:

          @keyframes em-xx-hinge { … }
          .em-xx-hinge {
            --em-anim: em-xx-hinge;
            animation-duration: 3.6s;
            animation-timing-function: ease-in-out;
            animation-iteration-count: infinite;
          }

      NEVER use the `animation` shorthand in a part rule — the shorthand resets
      animation-name, which is the gate, and the part would then animate
      everywhere including 16px chips in a list.

   3. Keyframes MUST start and end at the static pose (`transform: none`, or
      whatever the art already draws) so switching the animation off is
      invisible. Anything that only exists while alive — a fire plume, a light
      spill, a puff of snow — is tagged `em-fx` as well and lives at opacity 0
      until its own keyframes fade it in.

   NOTE ON TRANSFORMS: a CSS animation on `transform` OVERRIDES an SVG
   `transform="rotate(…)"` presentation attribute on the same element. Art that
   already carries one must either be wrapped in a plain <g class="em-part …">
   (animate the wrapper, leave the attribute alone) or have that base transform
   restated in every keyframe step.
   ============================================================ */

/* ---- the gate ---------------------------------------------------------- */
/* Off by default. `.em-part` also normalises the pivot: fill-box means "this
   element's own bounding box", so 50% 50% is the part's own centre whatever
   the host viewBox is (48 for reel symbols, 96 for ceremonial crests). */
.em-part {
  animation-name: none;
  transform-box: fill-box;
  transform-origin: 50% 50%;
}
:is(.emblem-alive, .emblem-hover-live:hover, .symbol-winning) .em-part {
  animation-name: var(--em-anim, none);
}
/* Lobby tiles wake their emblem from the WRAPPER's hover, not the card's. The
   card lifts on hover and so moves out from under the pointer that triggered
   it; gating the art on that made it stutter on and off as the hover flickered.
   The wrapper does not move when the card lifts, so this is the stable signal.
   (styles.css carries the matching .lobby-tile rules and the reasoning.) */
.lobby-tile:hover .emblem-hover-live .em-part {
  animation-name: var(--em-anim, none);
}

/* Parts that are pure effect — they are not part of the static drawing and
   must be invisible until their keyframes bring them in. */
.em-fx { opacity: 0; }

/* Accessibility: a genuine reduced-motion preference always holds the art still. */
.reduced-motion .em-part { animation-name: none !important; }

/* Performance mode is DEFAULT ON for phones and tablets (game/App.js), and it
   exists to silence always-running decorative animation. That is the MANY-at-once
   case — a winning board can be fifteen cells all moving their own parts — so that
   is what it silences here.
   It deliberately does NOT silence the single focal emblem the player is actually
   looking at: one hovered/centred lobby tile, or the crest the jackpot capsule just
   threw. Suppressing those too is what left phones with no emblem motion at all,
   which is exactly the "icon appears not to animate on mobile" report. One emblem
   is not a performance problem; sixteen would be, and nothing here ever runs
   sixteen at once. */
.performance-mode .symbol-winning .em-part { animation-name: none !important; }

/* ============================================================
   PER-MACHINE MOTION
   Each block below is one machine, in the order they appear in the lobby.
   Prefixes: vh vault-heist · dv diamond-vault · ll lava-loot · kj king-jungle
   cd cosmic-drift · pg penguin-gold · dmb dead-mans-booty · hn high-noon-gold
   cwk clockwork-carnival · hm hive-of-midas · ml mad-lab · ab the-abyss
   dd dragon-dynasty · ph pharaohs-rift · ys yeti-summit · nn neon-nights
   vt valhalla-thunder · gs gilded-speakeasy · sb sultans-bazaar · ss sugar-spires
   (the last four ship static art for now — they have no motion block yet, which
   is the supported "no entry" case: the emblem simply holds still.)
   ============================================================ */

/* @@EMBLEM-MACHINE-BLOCKS@@ — tools/emblem-assemble.mjs rebuilds everything
   below this line from the authoring agents' returned blocks. It is a unique
   sentinel on purpose: the first cut of that script truncated on a banner rule
   of ===== characters, which also occurs inside the machine blocks, so every
   run appended instead of replacing and the file grew duplicate copies of all
   16 machines. Do not hand-edit below; edit the machine block and re-run. */

/* ============================================================
   vault-heist — THE VAULT DOOR SPINS AND OPENS
   ============================================================
   One choreographed break-in, played on both art pieces: the three-spoke gold
   turn-handle winds back, hauls round most of a revolution, the bolts throw and
   the door LEAF swings off its hinge while the vault's own gold-white light
   floods the recess behind it — then it all reseats. The red security field and
   the hex bolts run on their own slower clocks underneath, so the machine is
   never completely still but the heist itself still reads as a single EVENT.

   Two bodies in the art (see slots/vault-heist.skin.js): the FRAME — aura, the
   steel bezel rings, the bolts — holds station, and the LEAF — door face, dial
   ticks, handle, centre cap (plus the reel symbol's rim-light and the crest's
   gold edge trim, which belong to the door rather than the hole) — is the thing
   that moves. Leaving the bezel on the frame is what makes the swing read as a
   door coming out of a hole rather than the whole glyph shrinking.

   Reel-symbol parts are em-vh-*; the 96-viewBox ceremonial crest uses em-vh-k*
   ("k" for the crest) because its pivots are in a different coordinate space.
   Rotations and scales are unit-free, so the two share keyframes wherever the
   gesture is identical and only the pivot and the tempo differ — that is what
   makes the lobby tile and the jackpot crest feel like the same machine.
   ------------------------------------------------------------ */

/* THE HANDLE — the gesture the player asked for by name, so it is the one that
   must be unmistakable. It rests for the first seventh of the cycle, winds back
   28deg (a telegraph: the eye is on it before anything big happens), then hauls
   round 234deg in one decisive pull, holds while the bolts are thrown and the
   door is open, and finally spins back to true. It unwinds rather than
   completing the revolution because that is how a safe is actually closed, and
   because it lets the loop end on exactly `transform: none`. */
@keyframes em-vh-handle {
  0%, 14%, 100% { transform: none; }
  22%           { transform: rotate(-28deg); }
  48%           { transform: rotate(206deg); }
  58%, 84%      { transform: rotate(218deg); }
  95%           { transform: rotate(58deg); }
}

/* THE DOOR LEAF. A hinge, not a squash: transform-box:view-box moves the pivot
   off the leaf's own centre and onto the frame's left inner edge, so scaleX
   foreshortens the face toward that edge exactly as a real door does when it
   swings toward you, and the crescent of recess it uncovers grows on the right.
   The small negative rotate lifts the free edge a couple of units so the leaf
   reads as a slab tilting out of the hole instead of a flat X-scale. It waits
   until the handle's turn is complete (46%), opens hard, then reseats. */
@keyframes em-vh-door {
  0%, 46%, 100% { transform: none; }
  56%           { transform: scaleX(0.86) rotate(-1.2deg); }
  72%           { transform: scaleX(0.34) rotate(-4.5deg); }
  86%           { transform: scaleX(0.44) rotate(-3.6deg); }
}

/* The crest's leaf opens FURTHER — down to 22% and a 7deg tilt, near enough
   edge-on. It can afford to: the crest is thrown by the jackpot capsule and
   then held big on screen, so the reveal has time and pixels to play out,
   whereas the 48-viewBox reel symbol has to stay legible at 56px. */
@keyframes em-vh-kdoor {
  0%, 46%, 100% { transform: none; }
  56%           { transform: scaleX(0.8) rotate(-1.6deg); }
  72%           { transform: scaleX(0.22) rotate(-7deg); }
  88%           { transform: scaleX(0.3) rotate(-5.6deg); }
}

/* WHAT IS INSIDE. Pure effect (em-fx), so it is dead at opacity 0 until the
   door moves. It lights at half strength first — that is the glow escaping the
   0.7-unit seam between the leaf and the steel bezel while the bolts throw,
   which is the tell that something is about to open — and only reaches full
   brightness with the leaf wide, growing as it goes so the light feels like it
   is welling up out of the vault rather than switching on. */
@keyframes em-vh-spill {
  0%, 42%, 100% { opacity: 0; transform: scale(0.66); }
  52%           { opacity: 0.5; transform: scale(0.8); }
  72%           { opacity: 1; transform: scale(1); }
  88%           { opacity: 0.85; transform: scale(0.97); }
}

/* Shafts of that light lancing out past the frame, crest only. Volumetric rays
   are what sell "the door is standing open" instead of "the door got thin";
   they grow outward from the door's centre, peak with the widest point of the
   swing, and are gone before the leaf reseats. */
@keyframes em-vh-kshafts {
  0%, 50%, 100% { opacity: 0; transform: scaleX(0.3); }
  62%           { opacity: 0.55; transform: scaleX(0.78); }
  74%           { opacity: 1; transform: scaleX(1); }
  90%           { opacity: 0.3; transform: scaleX(1.06); }
}

/* The security field breathing — a slow swell out and a settle back in, with
   the opacity dipping as it expands so it scans rather than throbs. Deliberately
   on a clock that shares no factor with the heist so the two never lock up. */
@keyframes em-vh-aura {
  0%, 100% { transform: none; opacity: 1; }
  36%      { transform: scale(1.1); opacity: 0.6; }
  70%      { transform: scale(0.9); opacity: 1; }
}

/* Hex bolts catching a glint. At 2 units across on the reel symbol a pure scale
   would be invisible, so the pop is carried mostly by brightness; scale just
   gives the flare somewhere to happen. The saturate() is doing real work: gold
   is already near-clipped in red and green, so brightness alone drives it to a
   flat acid yellow — pulling saturation down at the same time bends the flare
   toward white instead, which is what metal actually does. Each bolt's delay is
   indexed off its position in the art, so the glint travels round the plate. */
@keyframes em-vh-bolt {
  0%, 60%, 100% { transform: none; filter: none; }
  70%           { transform: scale(1.35); filter: brightness(1.4) saturate(0.6); }
  82%           { transform: scale(1.1); filter: brightness(1.15) saturate(0.88); }
}

/* The keyhole cap flaring — the lock is live. Shortest cycle in the machine, so
   it fires two or three times per heist and keeps the art from ever looking
   frozen between the big gestures. */
@keyframes em-vh-cap {
  0%, 56%, 100% { transform: none; filter: none; }
  66%           { transform: scale(1.22); filter: brightness(1.5) saturate(0.82); }
  80%           { transform: scale(1.05); filter: brightness(1.18) saturate(0.94); }
}

/* ---- reel symbol / lobby face (48 viewBox) ---------------------------- */
/* Handle, leaf and light spill share 4.4s ON PURPOSE — they are one gesture in
   three parts and would fall apart as three loops of different lengths. The
   ambient parts below them each get their own tempo. */
.em-vh-handle {
  --em-anim: em-vh-handle;
  /* pivot on the hub, not the group's bbox centre: the spokes and knobs sit
     asymmetrically (one arm up, two down) so fill-box would put the origin
     ~2.5 units high and the handle would orbit instead of turning */
  transform-box: view-box;
  transform-origin: 24px 24px;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.5, 0, .28, 1);
  animation-iteration-count: infinite;
}
.em-vh-door {
  --em-anim: em-vh-door;
  /* the hinge: left inner edge of the steel bezel, x = 24 - 13 */
  transform-box: view-box;
  transform-origin: 11px 24px;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.4, 0, .22, 1);
  animation-iteration-count: infinite;
}
.em-vh-spill {
  --em-anim: em-vh-spill;
  animation-duration: 4.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-vh-aura {
  --em-anim: em-vh-aura;
  animation-duration: 2.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-vh-bolt {
  --em-anim: em-vh-bolt;
  animation-duration: 3.1s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  /* per-bolt animation-delay is set inline in the art, where the bolt's
     position round the plate is known */
}
.em-vh-cap {
  --em-anim: em-vh-cap;
  animation-duration: 1.9s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  animation-delay: 0.35s;
}

/* ---- ceremonial crest (96 viewBox) ------------------------------------ */
/* Same choreography, slower (5.2s) because the crest is held rather than
   glanced at — and slower than the reel symbol so a lobby tile and a landed
   jackpot crest on screen together are not marching in step. */
.em-vh-khandle {
  --em-anim: em-vh-handle;
  transform-box: view-box;
  transform-origin: 48px 48px;
  animation-duration: 5.2s;
  animation-timing-function: cubic-bezier(.5, 0, .28, 1);
  animation-iteration-count: infinite;
}
.em-vh-kdoor {
  --em-anim: em-vh-kdoor;
  /* the hinge: left inner edge of the crest's steel ring, x = 48 - 32 */
  transform-box: view-box;
  transform-origin: 16px 48px;
  animation-duration: 5.2s;
  animation-timing-function: cubic-bezier(.4, 0, .22, 1);
  animation-iteration-count: infinite;
}
.em-vh-kspill {
  --em-anim: em-vh-spill;
  animation-duration: 5.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-vh-kshafts {
  --em-anim: em-vh-kshafts;
  /* rays fan out from the door's centre, so the pivot is the hub */
  transform-box: view-box;
  transform-origin: 48px 48px;
  animation-duration: 5.2s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-vh-kaura {
  --em-anim: em-vh-aura;
  animation-duration: 3.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-vh-kbolt {
  --em-anim: em-vh-bolt;
  animation-duration: 3.7s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-vh-kcap {
  --em-anim: em-vh-cap;
  animation-duration: 2.3s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  animation-delay: 0.5s;
}

/* ============================================================
   DIAMOND VAULT (em-dv-*) — "the dial throws and the door dogs down"
   ------------------------------------------------------------
   One mechanical event per cycle, read as a sequence rather than a jiggle:
   the four-spoke handle back-winds onto its stop, throws a full turn and
   dogs home; the bolt circle counter-rotates and shoots outward into the
   frame; and the instant the door seats, the brilliant at the hub throws a
   hard specular flash. The crest plays the same event in jewellery language —
   the claws ratchet a quarter turn, the setting collar clamps, the stone
   flashes — so the reel symbol and the ceremonial crest read as ONE machine.

   WHY THE MECHANISM SHARES ONE 4.8s PERIOD (dial, bolts, flash, claws,
   collar): these parts are geared to each other. If they drifted apart the
   bolts would fire while the handle was still turning and the linkage would
   read as broken. Everything that ISN'T geared — the travelling specular,
   the gem's dispersion, the prism halo, the ice glints — runs on its own
   prime-ish period (1.7 / 2.3 / 2.9 / 3.1 / 3.4 / 3.9 / 6.8s) so the art
   never pulses in lockstep.

   The house style here is cold and machined: hard cubic-beziers that snap and
   hold, linear ramps for the flicker, no organic ease-in-out breathing.
   ============================================================ */

/* THE SIGNATURE — the four-spoke turn handle (reel symbol).
   Held dead still for the first third of the cycle so the throw lands as an
   EVENT: back-wind against the stop (41%), one hard full turn, then dogged
   home. 100% is rotate(360deg), not `none`: a full turn is geometrically the
   static pose, so the loop seam and the moment the gate switches off are both
   invisible — and unlike `none` it does not make the last segment whip
   backwards through 360 degrees. */
@keyframes em-dv-dial {
  0%, 34%   { transform: none; }
  41%       { transform: rotate(-28deg); }
  58%       { transform: rotate(212deg); }
  70%       { transform: rotate(348deg); }
  76%, 100% { transform: rotate(360deg); }
}
.em-dv-dial {
  --em-anim: em-dv-dial;
  animation-duration: 4.8s;
  animation-timing-function: cubic-bezier(.5, 0, .12, 1);
  animation-iteration-count: infinite;
}

/* The bolt circle dogging down. Starts moving at 62% — while the handle is
   still on its last few degrees — because the bolts are DRIVEN by the handle,
   and a clean hand-off would look like two unrelated animations. It counter-
   rotates (opposite the dial, or the door reads as one flat spinning disc) and
   scales out to 1.2, which throws the 12 bolt heads from r12.5 to r15 in art
   units: visibly into the door frame at 56px, still inside the plate rim. The
   snap back to `none` over the last 8% is the bolts withdrawing. */
@keyframes em-dv-bolts {
  0%, 62%, 100% { transform: none; }
  78%           { transform: rotate(-15deg) scale(1.2); }
  92%           { transform: rotate(-16deg) scale(1.2); }
}
.em-dv-bolts {
  --em-anim: em-dv-bolts;
  animation-duration: 4.8s;
  animation-delay: 0.05s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}

/* THE PAYOFF — the hard specular the stone throws as the door seats. A pure-FX
   star: invisible for 76% of the cycle, one frame-hard pop to full white, then
   it blooms outward and dies. Scale starts and ends at .35 so the return trip
   happens while opacity is already 0 and nothing is seen snapping back.
   Shared by both art pieces; the 0.12s delay puts it just BEHIND the bolts
   seating (reel) and the claws landing (crest) — the flash is the consequence. */
@keyframes em-dv-flash {
  0%, 76%, 100% { opacity: 0;   transform: scale(.35) rotate(-24deg); }
  84%           { opacity: 1;   transform: scale(1.15) rotate(4deg); }
  90%           { opacity: .55; transform: scale(1.6) rotate(16deg); }
  96%           { opacity: 0;   transform: scale(1.9) rotate(24deg); }
}
.em-dv-flash {
  --em-anim: em-dv-flash;
  animation-duration: 4.8s;
  animation-delay: 0.12s;
  animation-timing-function: cubic-bezier(.15, .85, .3, 1);
  animation-iteration-count: infinite;
}

/* The gem's fire. Four prismatic wedges spilling from under the girdle onto the
   metal, guttering fast on 1.7s — deliberately NOT a multiple of the 4.8s
   mechanism, so the stone is always alive even while the door is at rest and
   the two never fall into step. `linear` on purpose: the hard-edged ramps read
   as light being refracted through facets, where an ease-in-out would read as
   something breathing. */
@keyframes em-dv-fire {
  0%, 100% { opacity: 0;   transform: none; }
  16%      { opacity: .9;  transform: rotate(-8deg) scale(1.05); }
  33%      { opacity: .18; transform: rotate(-2deg) scale(1); }
  52%      { opacity: 1;   transform: rotate(7deg) scale(1.12); }
  68%      { opacity: .22; transform: rotate(3deg) scale(1.02); }
  84%      { opacity: .7;  transform: rotate(-4deg) scale(1.06); }
}
.em-dv-fire {
  --em-anim: em-dv-fire;
  animation-duration: 1.7s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* A specular running around the polished rim — the door plate on the reel, the
   setting collar on the crest. `transform` is declared only at 0% and 100%, so
   it interpolates as one uninterrupted linear sweep from -30deg to 300deg while
   opacity does its own thing on top: the light arrives, travels most of the way
   round, and is gone before it can reach the seam and look like it teleported.
   Ends at opacity 0, as every em-fx part must. */
@keyframes em-dv-sweep {
  0%   { opacity: 0;   transform: rotate(-30deg); }
  10%  { opacity: .95; }
  44%  { opacity: .6; }
  62%  { opacity: 0; }
  100% { opacity: 0;   transform: rotate(300deg); }
}
.em-dv-sweep {
  --em-anim: em-dv-sweep;
  animation-duration: 3.1s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* CREST — the four claws ratchet a quarter turn: wind back off the stop (60%),
   drive round, seat. Ends held at rotate(-90deg) rather than `none` because the
   prong set is four identical strokes at 90-degree spacing, so a quarter turn
   IS the static pose pixel for pixel — which makes both the loop seam and the
   gate switching off invisible, and turns the cycle into a repeating ratchet
   instead of an oscillation that would un-turn itself every time. */
@keyframes em-dv-claws {
  0%, 52%   { transform: none; }
  60%       { transform: rotate(11deg); }
  76%       { transform: rotate(-74deg); }
  83%, 100% { transform: rotate(-90deg); }
}
.em-dv-claws {
  --em-anim: em-dv-claws;
  animation-duration: 4.8s;
  animation-timing-function: cubic-bezier(.45, 0, .1, 1);
  animation-iteration-count: infinite;
}

/* CREST — the setting collar clamping onto the stone: the crest's answer to the
   door dogging down. Counter-rotates (which drags the dv-metal gradient round
   the bevel, so the polished band visibly shifts under the light) and tightens
   to 0.9 — about 3px of travel at 84px, enough to see, and as it shrinks it
   uncovers a little more of the spectral fan hidden beneath it, which is what
   sells the clamp rather than a plain scale. */
@keyframes em-dv-collar {
  0%, 70%, 100% { transform: none; }
  80%           { transform: rotate(-13deg) scale(.9); }
  92%           { transform: rotate(-15deg) scale(.915); }
}
.em-dv-collar {
  --em-anim: em-dv-collar;
  animation-duration: 4.8s;
  animation-delay: 0.07s;
  animation-timing-function: cubic-bezier(.3, .8, .2, 1);
  animation-iteration-count: infinite;
}

/* CREST — the stone's fire fan flaring and guttering as one body. Opacity alone
   could only ever DIM it (the drawing is already at full opacity), so a
   brightness/saturate filter carries the flare upward past the static pose;
   `filter: none` at 0%/100% is the identity, so the off state is exact. */
@keyframes em-dv-fan {
  0%, 100% { opacity: 1;   filter: none; }
  16%      { opacity: .4;  filter: none; }
  38%      { opacity: 1;   filter: brightness(1.85) saturate(1.25); }
  56%      { opacity: .52; filter: none; }
  76%      { opacity: .95; filter: brightness(1.5); }
}
.em-dv-fan {
  --em-anim: em-dv-fan;
  animation-duration: 2.9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* CREST — the prism halo, the one part that never stops. Rotating the ring
   drags its left-to-right spectral gradient around with it, so the colours
   travel without animating any paint. 6.8s and linear: slow, even, machined.
   rotate(0deg) and rotate(360deg) are both exactly the static pose. */
@keyframes em-dv-halo {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.em-dv-halo {
  --em-anim: em-dv-halo;
  animation-duration: 6.8s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The ice glints. One keyframe set, three rules: the twinkle is the same
   optical event everywhere, and what keeps them out of phase is period and
   delay, not bespoke curves. Each glint darkens to almost nothing, pops to
   1.45 with a slight kick of rotation (the four-point sparkle turning is what
   makes it read as light catching an edge rather than a dot fading up), then
   returns to exactly the drawn pose. */
@keyframes em-dv-ice {
  0%, 100% { opacity: 1;   transform: none; }
  22%      { opacity: .12; transform: scale(.35) rotate(-35deg); }
  46%      { opacity: 1;   transform: scale(1.45) rotate(8deg); }
  70%      { opacity: .3;  transform: scale(.6) rotate(-14deg); }
}
.em-dv-ice-a {
  --em-anim: em-dv-ice;
  animation-duration: 2.3s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}
.em-dv-ice-b {
  --em-anim: em-dv-ice;
  animation-duration: 3.4s;
  animation-delay: 0.45s;
  animation-direction: reverse;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}
.em-dv-ice-c {
  --em-anim: em-dv-ice;
  animation-duration: 3.9s;
  animation-delay: 1.1s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------
   LAVA LOOT (em-ll-*) — IT ERUPTS
   ------------------------------------------------------------
   The signature gesture is an eruption, and an eruption is an EVENT: the
   crater pool charges first, the plume punches out of the vent, the ash cloud
   billows a beat later and the lava bombs get flung last, then the whole thing
   subsides to the drawn pose. A volcano that spouts continuously reads as a
   campfire, so every part of that chain holds the static drawing for roughly
   half its cycle and spends the rest firing and recovering. Only the ambient
   heat (the aura breathing) and the moving rock (the flows creeping) run
   continuously, on their own off-beat durations, so the art is never still and
   never in lockstep.

   Both art pieces run the same choreography — the reel symbol / lobby face on a
   3.6s beat, the ceremonial crest on a grander 4.2s one — so a player who
   hovers the tile and later wins the jackpot sees the same mountain.
   ------------------------------------------------------------ */

/* ---- the lobby emblem: VolcanoIcon (48 viewBox) ---- */

/* The vent charges before it blows. The pool swells and brightens to its
   hottest at 50%, which is the cue the plume answers at 56% — cause, then
   effect, instead of two things pulsing at once. */
@keyframes em-ll-pool {
  0%, 100% { transform: none; filter: none; }
  30%      { transform: scale(1.14); filter: brightness(1.2); }
  50%      { transform: scale(1.38); filter: brightness(1.6); }
  70%      { transform: scale(1.16); filter: brightness(1.25); }
}
.em-ll-pool {
  --em-anim: em-ll-pool;
  animation-duration: 3.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE SIGNATURE. Held flat for the first 46%, then a hard surge out of the
   crater and a two-stage slump back. It pivots on its own BASE rather than its
   centre, so growing the jet pushes it upward out of the vent instead of
   stretching it down through the mountain. The overshoot narrows (scaleX .94)
   on the way down, the way a jet necks in as it loses pressure. */
@keyframes em-ll-plume {
  0%, 46%, 100% { transform: none; }
  56%           { transform: translateY(-2px) scaleY(1.34) scaleX(1.16); }
  68%           { transform: translateY(-1px) scaleY(1.2) scaleX(0.94); }
  84%           { transform: scaleY(1.06) scaleX(1.02); }
}
.em-ll-plume {
  --em-anim: em-ll-plume;
  transform-origin: 50% 100%; /* the crater mouth, not the plume's middle */
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.32, 0, .24, 1);
  animation-iteration-count: infinite;
}

/* The ash cloud is the slow part of the blast: it lags the plume (delay), keeps
   rising and swelling past the peak, and drifts downwind (+x) as it goes. */
@keyframes em-ll-ash {
  0%, 100% { transform: none; }
  38%      { transform: translateY(-1.5px) scale(1.08); }
  62%      { transform: translate(1px, -4.5px) scale(1.3); }
  82%      { transform: translate(1.6px, -2px) scale(1.12); }
}
.em-ll-ash {
  --em-anim: em-ll-ash;
  animation-duration: 3.6s;
  animation-delay: 0.18s;
  animation-timing-function: cubic-bezier(.36, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* Lava bombs — thrown clear of the vent just after the plume peaks, each on its
   own delay so the two sides never leave together. The rotation swings the
   trail with the stone so the streak arcs rather than sliding. */
@keyframes em-ll-bomb-a {
  0%, 44%, 100% { transform: none; }
  60%           { transform: translate(-3.2px, -5px) scale(1.16) rotate(-8deg); }
  76%           { transform: translate(-1.4px, -2.2px) scale(1.06) rotate(-3deg); }
}
.em-ll-bomb-a {
  --em-anim: em-ll-bomb-a;
  animation-duration: 3.6s;
  animation-delay: 0.24s;
  animation-timing-function: cubic-bezier(.28, 0, .3, 1);
  animation-iteration-count: infinite;
}
@keyframes em-ll-bomb-b {
  0%, 42%, 100% { transform: none; }
  58%           { transform: translate(3.4px, -5.4px) scale(1.18) rotate(9deg); }
  74%           { transform: translate(1.5px, -2.4px) scale(1.06) rotate(3deg); }
}
.em-ll-bomb-b {
  --em-anim: em-ll-bomb-b;
  animation-duration: 3.6s;
  animation-delay: 0.36s;
  animation-timing-function: cubic-bezier(.28, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* Ambient heat. Deliberately off the eruption beat (2.6s against 3.6s) so the
   mountain always has something breathing even between blasts, and the two
   rhythms drift instead of thumping together. */
@keyframes em-ll-heat {
  0%, 100% { transform: none; opacity: 1; }
  36%      { transform: scale(0.9); opacity: 0.62; }
  64%      { transform: scale(1.2); opacity: 1; }
  84%      { transform: scale(1.04); opacity: 0.82; }
}
.em-ll-heat {
  --em-anim: em-ll-heat;
  animation-duration: 2.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* FX — a molten pulse creeping down the left flow, tracking its two bends by
   translation alone (never d/x/y: those would re-lay-out the path every frame).
   It fades in at the crater lip and dies before the flow reaches the base, so
   the flow reads as rock actually moving downhill. It is opacity 0 at 0%/100%,
   which is what makes the transform snap-back at the end invisible. */
@keyframes em-ll-ooze {
  0%, 100% { transform: none; opacity: 0; }
  14%      { transform: none; opacity: 0.95; }
  52%      { transform: translate(-4px, 9px); opacity: 0.95; }
  86%      { transform: translate(-2px, 19px); opacity: 0; }
}
.em-ll-ooze {
  --em-anim: em-ll-ooze;
  animation-duration: 2.9s;
  animation-delay: 0.45s;
  animation-timing-function: linear; /* rock creeps at a constant crawl */
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: LavaEruptionCrest (96 viewBox) ---- */
/* Same chain, slower and bigger: the crest is held on screen after the jackpot
   capsule lands, so it gets room to build. */

@keyframes em-ll-crest-pool {
  0%, 100% { transform: none; filter: none; }
  30%      { transform: scale(1.16); filter: brightness(1.22); }
  50%      { transform: scale(1.34); filter: brightness(1.6); }
  72%      { transform: scale(1.14); filter: brightness(1.25); }
}
.em-ll-crest-pool {
  --em-anim: em-ll-crest-pool;
  animation-duration: 4.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The crest's signature surge. Same base pivot and same hold-then-fire shape as
   the emblem's plume, with a longer recovery because at 84px the eye has time
   to watch it fall. */
@keyframes em-ll-crest-plume {
  0%, 48%, 100% { transform: none; }
  58%           { transform: translateY(-3px) scaleY(1.3) scaleX(1.18); }
  70%           { transform: translateY(-1.5px) scaleY(1.16) scaleX(0.94); }
  86%           { transform: scaleY(1.05) scaleX(1.02); }
}
.em-ll-crest-plume {
  --em-anim: em-ll-crest-plume;
  transform-origin: 50% 100%; /* the crater mouth */
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.32, 0, .24, 1);
  animation-iteration-count: infinite;
}

/* FX — the ash the crest does not draw at rest. It blooms out from behind the
   plume as the jet peaks, then thins and drifts off the top. Returns to
   opacity 0 at 0%/100% so the landed crest's first frame is the pure drawing. */
@keyframes em-ll-crest-ash {
  0%, 100% { transform: none; opacity: 0; }
  44%      { transform: translateY(-4px) scale(1.22); opacity: 0.95; }
  68%      { transform: translateY(-8px) scale(1.45); opacity: 0.6; }
  88%      { transform: translateY(-12px) scale(1.7); opacity: 0; }
}
.em-ll-crest-ash {
  --em-anim: em-ll-crest-ash;
  animation-duration: 4.2s;
  animation-delay: 0.22s;
  animation-timing-function: cubic-bezier(.36, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* Lava bombs flung clear of the bigger cone — larger throws than the emblem's
   because there are twice as many user units to cross for the same apparent
   distance, staggered so the two sides read as separate events. */
@keyframes em-ll-crest-bomb-a {
  0%, 46%, 100% { transform: none; }
  62%           { transform: translate(-5px, -6px) scale(1.14) rotate(-9deg); }
  78%           { transform: translate(-2.2px, -2.6px) scale(1.05) rotate(-3deg); }
}
.em-ll-crest-bomb-a {
  --em-anim: em-ll-crest-bomb-a;
  animation-duration: 4.2s;
  animation-delay: 0.3s;
  animation-timing-function: cubic-bezier(.28, 0, .3, 1);
  animation-iteration-count: infinite;
}
@keyframes em-ll-crest-bomb-b {
  0%, 44%, 100% { transform: none; }
  60%           { transform: translate(5px, -7.5px) scale(1.16) rotate(10deg); }
  76%           { transform: translate(2.2px, -3.2px) scale(1.05) rotate(4deg); }
}
.em-ll-crest-bomb-b {
  --em-anim: em-ll-crest-bomb-b;
  animation-duration: 4.2s;
  animation-delay: 0.44s;
  animation-timing-function: cubic-bezier(.28, 0, .3, 1);
  animation-iteration-count: infinite;
}

@keyframes em-ll-crest-heat {
  0%, 100% { transform: none; opacity: 1; }
  38%      { transform: scale(0.92); opacity: 0.6; }
  66%      { transform: scale(1.14); opacity: 1; }
  86%      { transform: scale(1.03); opacity: 0.8; }
}
.em-ll-crest-heat {
  --em-anim: em-ll-crest-heat;
  animation-duration: 3.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The flows creep. Pivoting on the TOP of the flow channel means scaling it
   vertically pushes its snout further down the cone — the flow lengthens from
   the crater end, which is the direction lava actually travels — while the
   widening and the brightness swell read as more magma arriving. Slowest part
   on the machine (5.4s) and off the eruption beat, because a lava field keeps
   moving long after the bang. */
@keyframes em-ll-crest-flow {
  0%, 100% { transform: none; opacity: 1; }
  36%      { transform: scaleY(1.03) scaleX(1.08); opacity: 0.78; }
  62%      { transform: scaleY(1.06) scaleX(1.2); opacity: 1; }
  84%      { transform: scaleY(1.02) scaleX(1.06); opacity: 0.86; }
}
.em-ll-crest-flow {
  --em-anim: em-ll-crest-flow;
  transform-origin: 50% 0%; /* the crater end, so the flow grows downhill */
  animation-duration: 5.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------------
   KING OF THE JUNGLE (kj) — THE PANTHER ROARS
   ------------------------------------------------------------------
   Both art pieces hold a dead-still stare and then snap into a single roar:
   ears pinned, gold eyes flaring white-hot, the head lunging, the jaw dropping
   off a lit throat, whiskers shivering in the blast. The lobby symbol is the
   panther itself (slots/king-jungle.skin.js → PantherIcon); the ceremonial
   crest is the same beast carved as the gorilla idol, and it roars on the same
   clock so the two read as one machine.

   THE ROAR IS AN EVENT, NOT A JIGGLE. Every part that takes part in it shares
   one 4.6s cycle and stays at the static pose for roughly two thirds of it —
   the stagger between pieces comes from WHERE in the cycle each fires, not from
   different durations, because durations that differ would drift and the roar
   would come apart into unrelated twitches after a few cycles:

       6%  ears pin back (the tell)  ·  10% eyes flare  ·  13% head lunges and
       the throat lights  ·  16% jaw snaps open  ·  ~30% recover  ·  then still

   The roar deliberately fires EARLY in the cycle. Lobby hovers are often under
   a second, so a gesture held back to the middle of the cycle would never be
   seen; here the cat roars almost the instant the pointer arrives.

   Only the ambient parts run free on their own clocks (halo sweep 5.2s, vine
   arms 6.8s/7.9s, gold glint 2.15s), so nothing pulses in lockstep with the
   roar except the roar. */

/* --- the jackpot sunburst behind the cat --------------------------------
   Two animations on one part: the endless sweep (transform) and the roar flash
   (filter). They never fight over a property. The sweep stops at exactly 30deg
   because the burst has twelve identical wedges 30deg apart — one pitch maps
   each wedge onto its neighbour, so the loop point is seamless AND the 0%/100%
   pose is pixel-identical to the static drawing. */
@keyframes em-kj-halo-sweep {
  0%   { transform: none; }
  100% { transform: rotate(30deg); }
}
@keyframes em-kj-halo-flare {
  0%, 3%, 34%, 100% { filter: none; }
  12%  { filter: brightness(1.85) saturate(1.2) drop-shadow(0 0 3px rgba(250, 204, 21, 0.85)); }
  22%  { filter: brightness(1.3) saturate(1.1); }
}
.em-kj-halo {
  --em-anim: em-kj-halo-sweep, em-kj-halo-flare;
  animation-duration: 5.2s, 4.6s;
  animation-timing-function: linear, cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite, infinite;
}

/* --- the head lunges ----------------------------------------------------
   The whole cat coils a hair, then drives forward and shakes the roar off to one
   side. Scale carries the lunge rather than translation, because scaling pushes
   the ear tips, jaw and fangs outward together and that is what reads as "coming
   at you" on a 56px tile. The rotate+skew pair is deliberately asymmetric — a
   real roar is a head snapping off-axis, not a symmetrical pulse — and it is
   also the only thing that visibly swings the ear tips, which are welded into
   the skull path and cannot move on their own (see the ears below). */
@keyframes em-kj-head-roar {
  0%, 40%, 100% { transform: none; }
  5%   { transform: translateY(0.5px) scale(0.985); }
  13%  { transform: translateY(-1.2px) scale(1.07) rotate(-2.6deg) skewX(-1.6deg); }
  21%  { transform: translateY(-0.9px) scale(1.05) rotate(2deg) skewX(1.2deg); }
  30%  { transform: translateY(-0.3px) scale(1.02) rotate(-0.6deg); }
}
.em-kj-head {
  --em-anim: em-kj-head-roar;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* --- the ears pin back --------------------------------------------------
   The ear spikes are part of the skull silhouette and cannot be hinged without
   redrawing it, so the flick is carried by the dark inner-ear markings turning
   edge-on: they narrow to a sliver and tilt away, exactly what you see when a
   cat lays its ears flat. They peak at 6%, ahead of everything else, because a
   cat's ears go back BEFORE the noise comes out — it is the tell. This is a
   small read by design: it is the only ear motion the silhouette allows without
   redrawing the skull, so the head shake above carries the rest of it. */
@keyframes em-kj-ear-pin-l {
  0%, 1%, 30%, 100% { transform: none; }
  6%   { transform: rotate(-13deg) scaleX(0.38); }
  20%  { transform: rotate(-8deg) scaleX(0.62); }
}
@keyframes em-kj-ear-pin-r {
  0%, 1%, 30%, 100% { transform: none; }
  6%   { transform: rotate(13deg) scaleX(0.38); }
  20%  { transform: rotate(8deg) scaleX(0.62); }
}
.em-kj-ear-l {
  --em-anim: em-kj-ear-pin-l;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.15, .9, .2, 1);
  animation-iteration-count: infinite;
}
.em-kj-ear-r {
  --em-anim: em-kj-ear-pin-r;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.15, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* --- the gold eyes flare ------------------------------------------------
   Scale alone would only make the eyes bigger; the brightness/drop-shadow pass
   is what turns them into lamps, which is the whole promise of a jackpot
   symbol. One class on both eye groups: each pivots on its own bbox centre, so
   they swell in place instead of drifting apart, and they fire together —
   asymmetric eyes read as a broken face, not as life. */
@keyframes em-kj-eye-flare {
  0%, 3%, 34%, 100% { transform: none; filter: none; }
  10%  { transform: scale(1.28); filter: brightness(1.9) saturate(1.15) drop-shadow(0 0 2px rgba(253, 224, 71, 0.95)); }
  24%  { transform: scale(1.16); filter: brightness(1.35); }
}
.em-kj-eye {
  --em-anim: em-kj-eye-flare;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.1, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* --- the muzzle drives forward -----------------------------------------
   The upper jaw is not passive in a roar: the pad compresses on the intake and
   then swells down and out over the dropping lower jaw. Without this the mouth
   opening looks like a hatch rather than an animal. */
@keyframes em-kj-muzzle-swell {
  0%, 40%, 100% { transform: none; }
  6%   { transform: scaleY(0.96); }
  16%  { transform: translateY(0.9px) scale(1.06, 1.16); }
  26%  { transform: translateY(0.5px) scale(1.03, 1.1); }
}
.em-kj-muzzle {
  --em-anim: em-kj-muzzle-swell;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* --- the throat (pure effect) -------------------------------------------
   em-fx, so it is invisible until the mouth is actually open and the art stays
   pixel-identical at rest. It is still fully transparent at 5% and only lands at
   13%, a beat before the jaw bottoms out at 16%, so you read "the mouth opens,
   there is darkness and light inside" instead of a black patch appearing on a
   closed face. The gold ray-light is what makes it read at 56px: a dark hole in
   a dark cat is nothing, a lit throat is a roar. */
@keyframes em-kj-maw-open {
  0%, 5%, 34%, 100% { opacity: 0; transform: none; }
  13%  { opacity: 1; transform: translateY(0.6px) scaleY(1.15); }
  26%  { opacity: 0.85; transform: translateY(0.3px) scaleY(1.05); }
}
.em-kj-maw {
  --em-anim: em-kj-maw-open;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* --- the jaw drops, fangs bared ----------------------------------------
   The signature beat. The lips press together for a frame, then the whole lower
   jaw swings down 3.3 units of a 48 viewBox — around 7% of the symbol — and
   scales up with it, so the fangs travel past the muzzle pad onto the dark chin,
   which is where white reads hardest. This has to survive being drawn at 56px,
   so it is deliberately the largest amplitude in the set. */
@keyframes em-kj-jaw-drop {
  0%, 3%, 36%, 100% { transform: none; }
  7%   { transform: translateY(-0.3px) scaleY(0.9); }
  16%  { transform: translateY(3.3px) scale(1.18); }
  26%  { transform: translateY(2.1px) scale(1.1); }
}
.em-kj-jaw {
  --em-anim: em-kj-jaw-drop;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.15, .85, .2, 1);
  animation-iteration-count: infinite;
}

/* --- the whiskers shiver ------------------------------------------------
   High-frequency, low-amplitude: whiskers are 0.5-wide strokes, so travel would
   not read but a tremble does — the eye catches the flicker. Left and right run
   opposite phases (mirrored keyframes) so they shiver against each other
   instead of sweeping in unison like a moustache. */
@keyframes em-kj-whisk-shiver-l {
  0%, 6%, 36%, 100% { transform: none; }
  12%  { transform: translate(-0.7px, -0.6px) rotate(-8deg); }
  17%  { transform: translate(-0.4px, 0.5px) rotate(6deg); }
  22%  { transform: translate(-0.7px, -0.4px) rotate(-6deg); }
  28%  { transform: translate(-0.2px, 0.3px) rotate(3deg); }
}
@keyframes em-kj-whisk-shiver-r {
  0%, 6%, 36%, 100% { transform: none; }
  12%  { transform: translate(0.7px, 0.5px) rotate(7deg); }
  17%  { transform: translate(0.4px, -0.6px) rotate(-6deg); }
  22%  { transform: translate(0.7px, 0.4px) rotate(6deg); }
  28%  { transform: translate(0.2px, -0.3px) rotate(-3deg); }
}
.em-kj-whisk-l {
  --em-anim: em-kj-whisk-shiver-l;
  animation-duration: 4.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-kj-whisk-r {
  --em-anim: em-kj-whisk-shiver-r;
  animation-duration: 4.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* --- the gold glint twinkles -------------------------------------------
   Ambient, and on purpose NOT a multiple of the roar clock (2.15s against
   4.6s), so the sparkle never lands on the same beat twice and the symbol
   still has something alive in it between roars. */
@keyframes em-kj-glint-twinkle {
  0%, 100% { transform: none; opacity: 1; }
  30%  { transform: scale(0.4) rotate(-30deg); opacity: 0.4; }
  62%  { transform: scale(1.4) rotate(20deg); opacity: 1; }
  82%  { transform: scale(0.9) rotate(5deg); opacity: 0.9; }
}
.em-kj-glint {
  --em-anim: em-kj-glint-twinkle;
  animation-duration: 2.15s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ==== the ceremonial crest: the same roar, carved in stone ============== */

/* --- the idol aura breathes, and lights the roar ------------------------
   One part doing two jobs on the roar clock: a slow settle/exhale across the
   still stretch of the cycle, and a hard bloom at 12% that throws light over
   the whole crest as the mouth opens. The crest is held big on screen after the
   jackpot capsule lands, so this is what keeps it from looking like a decal. */
@keyframes em-kj-aura-breath {
  0%, 100% { transform: none; opacity: 1; filter: none; }
  12%  { transform: scale(1.11); opacity: 1; filter: brightness(1.6) saturate(1.1); }
  30%  { transform: scale(1.05); filter: none; }
  64%  { transform: scale(0.955); opacity: 0.8; }
}
.em-kj-aura {
  --em-anim: em-kj-aura-breath;
  animation-duration: 4.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* --- the vine arms sway -------------------------------------------------
   The only parts that ignore the roar entirely: living jungle keeps moving
   whatever the idol is doing. Two different durations and mirrored directions
   so the crest never breathes symmetrically. Each arm rotates INTO the crest
   (left positive, right negative) because the outermost leaf tips already sit a
   unit from the 96 viewBox edge and this crest's <svg> clips. */
@keyframes em-kj-vine-sway-l {
  0%, 100% { transform: none; }
  30%  { transform: rotate(3deg) translateY(-1.2px); }
  66%  { transform: rotate(-1.4deg) translateY(0.5px); }
}
@keyframes em-kj-vine-sway-r {
  0%, 100% { transform: none; }
  30%  { transform: rotate(-3deg) translateY(-1.2px); }
  66%  { transform: rotate(1.4deg) translateY(0.5px); }
}
.em-kj-vine-l {
  --em-anim: em-kj-vine-sway-l;
  animation-duration: 6.8s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-kj-vine-r {
  --em-anim: em-kj-vine-sway-r;
  animation-duration: 7.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* --- the idol's ears flatten -------------------------------------------
   Here the ears are discrete stone plates, so unlike the panther symbol they
   can genuinely hinge: they rotate back, squash along their long axis and slide
   into the skull, so the head silhouette visibly narrows. They fire on the same
   early beat as the panther's inner ears, so both art pieces pin their ears at
   the same instant of the shared clock and read as one animal. */
@keyframes em-kj-idol-ear-pin-l {
  0%, 1%, 32%, 100% { transform: none; }
  7%   { transform: translate(1.6px, 0.8px) rotate(-27deg) scaleY(0.8); }
  20%  { transform: translate(1px, 0.5px) rotate(-18deg) scaleY(0.88); }
}
@keyframes em-kj-idol-ear-pin-r {
  0%, 1%, 32%, 100% { transform: none; }
  7%   { transform: translate(-1.6px, 0.8px) rotate(27deg) scaleY(0.8); }
  20%  { transform: translate(-1px, 0.5px) rotate(18deg) scaleY(0.88); }
}
.em-kj-idol-ear-l {
  --em-anim: em-kj-idol-ear-pin-l;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.15, .9, .2, 1);
  animation-iteration-count: infinite;
}
.em-kj-idol-ear-r {
  --em-anim: em-kj-idol-ear-pin-r;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.15, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* --- the crown blazes as the brow lifts --------------------------------
   The gold brow-crown, its studs and the apex gem move as one piece of regalia,
   lifting off the stone as the idol raises its brow. The brightness pass is the
   point: on a win ceremony the gold has to actually catch fire, not just shift. */
@keyframes em-kj-idol-crown-blaze {
  0%, 4%, 38%, 100% { transform: none; filter: none; }
  14%  { transform: translateY(-2.4px) scale(1.05); filter: brightness(1.85) saturate(1.15) drop-shadow(0 0 3px rgba(250, 204, 21, 0.9)); }
  26%  { transform: translateY(-1.2px) scale(1.02); filter: brightness(1.3); }
}
.em-kj-idol-crown {
  --em-anim: em-kj-idol-crown-blaze;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}

/* --- the idol's eyes flare ---------------------------------------------
   Same gesture as the panther's eyes, tuned for the 96 viewBox: a wider bloom
   radius because the crest is drawn twice the size. Both sockets share the
   class so the stare comes on as one. */
@keyframes em-kj-idol-eye-flare {
  0%, 3%, 36%, 100% { transform: none; filter: none; }
  11%  { transform: scale(1.3); filter: brightness(1.9) saturate(1.2) drop-shadow(0 0 4px rgba(253, 224, 71, 0.9)); }
  25%  { transform: scale(1.14); filter: brightness(1.35); }
}
.em-kj-idol-eye {
  --em-anim: em-kj-idol-eye-flare;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.1, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* --- the idol's throat (pure effect) -----------------------------------
   em-fx: a void bitten out of the carved stone with the idol's own gold fire
   burning up out of it and two stone teeth. Comes in behind the dropping lip
   strokes, which then read as the rim of the opening. */
@keyframes em-kj-idol-maw-open {
  0%, 6%, 36%, 100% { opacity: 0; transform: none; }
  14%  { opacity: 1; transform: translateY(0.8px) scaleY(1.12); }
  28%  { opacity: 0.8; transform: translateY(0.4px) scaleY(1.04); }
}
.em-kj-idol-maw {
  --em-anim: em-kj-idol-maw-open;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* --- the idol's jaw drops ----------------------------------------------
   The mouth line and its lower-lip highlight are the jaw. They travel down and
   the curve is stretched taller (scaleY) so the closed slit bows into a wide
   open mouth — a stone face has no lips to part, so the bow IS the opening.
   Travel is capped just short of the carved chin so nothing breaks the outline. */
@keyframes em-kj-idol-jaw-drop {
  0%, 3%, 40%, 100% { transform: none; }
  8%   { transform: translateY(-0.4px) scaleY(0.9); }
  17%  { transform: translateY(2.8px) scale(1.07, 1.55); }
  28%  { transform: translateY(1.6px) scale(1.04, 1.3); }
}
.em-kj-idol-jaw {
  --em-anim: em-kj-idol-jaw-drop;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.15, .85, .2, 1);
  animation-iteration-count: infinite;
}

/* ============================================================
   COSMIC DRIFT (em-cd-*) — THE WORMHOLE TURNS
   ------------------------------------------------------------
   Every other machine on the floor fires a one-shot event and recovers — a door
   throwing, a volcano blowing. This one is the exception, and deliberately so: a
   wormhole that spun up and then stopped would read as broken. So the whole art
   is a CONTINUOUS loop, and what carries it is DIFFERENTIAL ROTATION.

   That is the trick the emblem is built on. Four concentric accretion ellipses
   are drawn on top of each other, which on a flat page is just a stack of rings;
   turn each of them at a different rate — outer slow (7.6s), then 5.1s, 3.3s,
   innermost whipping round in 1.9s — and the eye reads the shear between them as
   DEPTH. It becomes a funnel receding toward the throat rather than a target.
   The rates are deliberately non-harmonic (7.6 / 5.1 / 3.3 / 1.9, each roughly
   1.5x its neighbour, none a multiple of another) so the rings never line back up
   into a single pinwheel; the pattern between them keeps changing for minutes.

   On top of the turn: the throat breathes, the white core swells and throws a
   hard flare, and the three infalling streams are drawn down into the core and
   replaced. Only the drawn rings and streams exist at rest — the mote and the
   flare are pure FX and are invisible until the art is alive.

   The ceremonial crest speaks the same language at ceremony scale, since a player
   who hovered the tile must recognise the machine when the jackpot capsule lands:
   nested things turning at different rates around a hot pulsing core. The ray fan
   turns, the ring system opens and closes, the nova core pulses and flares and
   throws a shock front, and debris falls into the planet. The chrome medallion
   and the globe itself stay nailed down as the frame — without something fixed to
   measure against, differential motion just looks like noise.
   ============================================================ */

/* ---- the lobby emblem: WormholeIcon (48 viewBox) ---- */

/* THE SIGNATURE, ring 1 of 4 — the outermost accretion ellipse, the slow layer.
   `rotate(360deg)` at 100% is the geometric identity of the ellipse, so the loop
   seam is invisible and so is the moment the gate switches the animation off;
   `linear` because orbital motion has no ease — an ease-in-out here would read as
   the ring wobbling rather than turning. (180deg would loop just as seamlessly on
   a symmetric ellipse; a full turn is kept for legibility.) */
@keyframes em-cd-ring-1 {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.em-cd-ring-1 {
  --em-anim: em-cd-ring-1;
  animation-duration: 7.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* Ring 2 — 1.5x faster than ring 1. Counter-rotating would look like a gearbox;
   same direction, different rate, is what an accretion disc actually does and it
   is the shear that sells the funnel. */
@keyframes em-cd-ring-2 {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.em-cd-ring-2 {
  --em-anim: em-cd-ring-2;
  animation-duration: 5.1s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* Ring 3 — fast enough that the rotation alone reads, so it also gets a scale
   swell (1.1 out, 0.88 in) which is the ring being stretched and then squeezed as
   it falls inward. The rotation stays even across the keyframes (122deg at 34%,
   245deg at 68%, 360deg at 100% is a constant ~3.6deg per percent) so the swell
   rides on top of the turn instead of stuttering it. */
@keyframes em-cd-ring-3 {
  0%   { transform: rotate(0deg) scale(1); }
  34%  { transform: rotate(122deg) scale(1.1); }
  68%  { transform: rotate(245deg) scale(0.88); }
  100% { transform: rotate(360deg) scale(1); }
}
.em-cd-ring-3 {
  --em-anim: em-cd-ring-3;
  animation-duration: 3.3s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* Ring 4 — the innermost, closest to the hole and therefore the fastest (1.9s, a
   full turn every couple of heartbeats). It is only 4.4 x 3.2 user units, so
   rotation by itself would be nearly invisible at 56px; the wide scale swing
   (1.22 down to 0.82, about 2px of edge travel on a lobby tile) is what makes the
   last ring read at all. Rotation again stays even: 101deg at 28%, 223deg at 62%. */
@keyframes em-cd-ring-4 {
  0%   { transform: rotate(0deg) scale(1); }
  28%  { transform: rotate(101deg) scale(1.22); }
  62%  { transform: rotate(223deg) scale(0.82); }
  100% { transform: rotate(360deg) scale(1); }
}
.em-cd-ring-4 {
  --em-anim: em-cd-ring-4;
  animation-duration: 1.9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The throat glow breathing, on 4.4s — deliberately the longest beat in the
   middle of the art, so the funnel has a slow tide under all the fast turning.
   Off every ring period, so it never lands on the same frame twice running. */
@keyframes em-cd-throat {
  0%, 100% { transform: none; opacity: 1; }
  30%      { transform: scale(1.32); opacity: 0.72; }
  58%      { transform: scale(0.86); opacity: 1; }
  80%      { transform: scale(1.12); opacity: 0.86; }
}
.em-cd-throat {
  --em-anim: em-cd-throat;
  animation-duration: 4.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE CORE PULSE. It draws down first (0.78 at 18%) and then flares hard to 1.85
   — the r2.4 white dot swelling past r4 — which is the one moment the whole icon
   has a bright centre. Ducking BEFORE the flare is what makes it read as a pulse
   with an intake rather than a dot fading up. */
@keyframes em-cd-core {
  0%, 100% { transform: none; }
  18%      { transform: scale(0.78); }
  44%      { transform: scale(1.85); }
  60%      { transform: scale(1.1); }
  80%      { transform: scale(0.92); }
}
.em-cd-core {
  --em-anim: em-cd-core;
  animation-duration: 2.6s;
  animation-timing-function: cubic-bezier(.34, 0, .2, 1);
  animation-iteration-count: infinite;
}

/* FX — the flare the throat throws. Same 2.6s period as the core and a 0.05s
   delay, so it is unmistakably the CONSEQUENCE of the core peaking rather than a
   second thing pulsing nearby; it lies behind the white core in the drawing, so
   the light appears to escape from under it. Hidden for the first quarter of the
   cycle, which keeps it an event even inside a continuous loop. Scale starts and
   ends at 0.3 so the shrink back happens while opacity is already 0 and nothing
   is seen snapping home; the slow rotation stops the four-point star from looking
   stamped on. */
@keyframes em-cd-flare {
  0%, 26%, 100% { opacity: 0;    transform: scale(0.3) rotate(0deg); }
  44%           { opacity: 0.9;  transform: scale(1) rotate(14deg); }
  58%           { opacity: 0.35; transform: scale(1.25) rotate(26deg); }
  72%           { opacity: 0;    transform: scale(1.45) rotate(38deg); }
}
.em-cd-flare {
  --em-anim: em-cd-flare;
  animation-duration: 2.6s;
  animation-delay: 0.05s;
  animation-timing-function: cubic-bezier(.2, .8, .3, 1);
  animation-iteration-count: infinite;
}

/* The three infalling streams get SWALLOWED. Each is a curve reaching from the
   rim to the throat with its leading mote, and it travels inward along its own
   axis while shrinking, so the far end catches up with the near end and both
   arrive at the core — matter falling in, done with transform only (animating the
   path `d` would re-lay-out the geometry every frame).

   The awkward part is the return trip, because unlike a pure-FX part these
   streams ARE the static drawing and cannot sit at opacity 0. The fix is the
   two-step tail: fade to 0 while still out at the core (80%), snap the transform
   home while invisible (90%), then fade back in at the rim (100%). The reset is
   never seen, and 0%/100% are both exactly `transform: none; opacity: 1` — the
   drawn pose — so switching the gate off is invisible. Read as a loop it is even
   better than a reset: fresh debris keeps arriving at the rim.

   Three separate keyframe sets because the three streams come in from three
   different directions, and three different periods (2.7 / 3.3 / 2.1s) plus
   delays because a wormhole does not eat on a metronome. */
@keyframes em-cd-fall-a {
  0%   { transform: none; opacity: 1; }
  22%  { transform: translate(1.6px, 0.8px) scale(0.9); opacity: 1; }
  58%  { transform: translate(4.6px, 2.4px) scale(0.62); opacity: 0.6; }
  80%  { transform: translate(7px, 3.6px) scale(0.32); opacity: 0; }
  90%  { transform: none; opacity: 0; }
  100% { transform: none; opacity: 1; }
}
.em-cd-fall-a {
  --em-anim: em-cd-fall-a;
  animation-duration: 2.7s;
  animation-timing-function: cubic-bezier(.4, 0, .75, .4); /* accelerates in, as gravity does */
  animation-iteration-count: infinite;
}
@keyframes em-cd-fall-b {
  0%   { transform: none; opacity: 1; }
  22%  { transform: translate(-1.6px, -0.8px) scale(0.9); opacity: 1; }
  58%  { transform: translate(-4.6px, -2.4px) scale(0.62); opacity: 0.6; }
  80%  { transform: translate(-7px, -3.6px) scale(0.32); opacity: 0; }
  90%  { transform: none; opacity: 0; }
  100% { transform: none; opacity: 1; }
}
.em-cd-fall-b {
  --em-anim: em-cd-fall-b;
  animation-duration: 3.3s;
  animation-delay: 0.4s;
  animation-timing-function: cubic-bezier(.4, 0, .75, .4);
  animation-iteration-count: infinite;
}
@keyframes em-cd-fall-c {
  0%   { transform: none; opacity: 1; }
  22%  { transform: translate(-1.6px, 1.4px) scale(0.9); opacity: 1; }
  58%  { transform: translate(-4.6px, 4px) scale(0.62); opacity: 0.6; }
  80%  { transform: translate(-7px, 6px) scale(0.34); opacity: 0; }
  90%  { transform: none; opacity: 0; }
  100% { transform: none; opacity: 1; }
}
.em-cd-fall-c {
  --em-anim: em-cd-fall-c;
  animation-duration: 2.1s;
  animation-delay: 0.9s;
  animation-timing-function: cubic-bezier(.4, 0, .75, .4);
  animation-iteration-count: infinite;
}

/* FX — a mote hauled in across the lower-left, the one quadrant the drawn streams
   leave empty, so the funnel is visibly eating something that is not part of the
   picture at rest. Fades in at the rim, accelerates into the throat, gone before
   it reaches the white core; opacity 0 at 0% and 100% as every em-fx part must be,
   which also hides the flight back out to the rim. */
@keyframes em-cd-mote {
  0%, 100% { opacity: 0;    transform: none; }
  14%      { opacity: 0.95; transform: translate(0.6px, -0.5px) scale(0.94); }
  56%      { opacity: 0.9;  transform: translate(4.4px, -4.1px) scale(0.6); }
  84%      { opacity: 0;    transform: translate(7.8px, -7.2px) scale(0.28); }
}
.em-cd-mote {
  --em-anim: em-cd-mote;
  animation-duration: 3.1s;
  animation-delay: 0.55s;
  animation-timing-function: cubic-bezier(.35, 0, .7, .35);
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: CosmicCelebrationCrest (96 viewBox) ---- */

/* THE CREST'S TURN — the eight-ray nova fan, and the slowest thing on the machine
   at 11s. It only ever travels a QUARTER of a turn: the fan is long rays on the
   cardinals and short rays on the diagonals, which makes it exactly 4-fold
   symmetric, so rotate(90deg) is pixel-for-pixel the static drawing. That buys a
   seamless loop AND an invisible off-switch out of a third of the travel — and at
   90deg per 11s the sweep is slow and stately, which is what a crest held on
   screen after a jackpot wants. Linear, like every orbit on this machine. */
@keyframes em-cd-crest-rays {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(90deg); }
}
.em-cd-crest-rays {
  --em-anim: em-cd-crest-rays;
  animation-duration: 11s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The ring system OPENS AND CLOSES. Rotating a planet's rings in the picture plane
   would read as the whole crest tumbling, so instead the ring tilts toward and
   away from us: scaleY swings its ry from 10.5 out to 14 and in to 7.4 — a good
   5px of travel at 84px, unmistakable — with a couple of degrees of roll so the
   tilt reads as a change of viewing angle rather than a squash. */
@keyframes em-cd-crest-ring {
  0%, 100% { transform: none; }
  30%      { transform: scaleY(1.34) rotate(3deg); }
  66%      { transform: scaleY(0.7) rotate(-4deg); }
  86%      { transform: scaleY(1.08) rotate(1deg); }
}
.em-cd-crest-ring {
  --em-anim: em-cd-crest-ring;
  animation-duration: 5.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The front sweep of the ring — the piece that passes in front of the globe. It is
   a separate element only because the planet has to be painted between the two
   halves of the ring, so it is pinned to the back ring's clock: SAME 5.2s period,
   SAME easing, no delay. Any drift at all and the ring would visibly tear where it
   crosses the limb. Its own keyframes differ because its geometry does — it sits a
   touch below the ring's centre, so it takes a small translateY to open by the
   same amount the back of the ring does. */
@keyframes em-cd-crest-sweep {
  0%, 100% { transform: none; }
  30%      { transform: translateY(1.6px) scaleY(1.3); }
  66%      { transform: translateY(-1px) scaleY(0.72); }
  86%      { transform: translateY(0.4px) scaleY(1.06); }
}
.em-cd-crest-sweep {
  --em-anim: em-cd-crest-sweep;
  animation-duration: 5.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE NOVA CORE pulses and flares — the crest's answer to the emblem's core. The
   bloom is already drawn at full opacity, so opacity alone could only ever DIM it;
   a brightness/saturate filter is what carries the flare UPWARD past the drawn
   pose, and `filter: none` at 0%/100% is the exact identity. It dims first (0.86
   at 24%) so the flare has somewhere to come from. */
@keyframes em-cd-crest-nova {
  0%, 100% { transform: none;         filter: none; }
  24%      { transform: scale(0.94);  filter: brightness(0.86); }
  50%      { transform: scale(1.13);  filter: brightness(1.7) saturate(1.15); }
  70%      { transform: scale(1.04);  filter: brightness(1.25); }
}
.em-cd-crest-nova {
  --em-anim: em-cd-crest-nova;
  animation-duration: 4.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* FX — the detonation front. Shares the nova's 4.6s period with a 0.18s delay so
   it leaves the star just after the flare peaks: cause, then effect. It is held
   invisible for the first 38% (an expanding shell that never stops expanding is a
   ripple, not a blast), pops in tight at scale 0.55 and runs out to 1.9 — r22 out
   to r42, right to the edge of the crest — thinning as it goes. Scale returns to
   0.35 at 100% while opacity is already 0, so the recoil is never seen. */
@keyframes em-cd-crest-shock {
  0%, 38%, 100% { opacity: 0;    transform: scale(0.35); }
  46%           { opacity: 0.9;  transform: scale(0.55); }
  66%           { opacity: 0.55; transform: scale(1.2); }
  88%           { opacity: 0;    transform: scale(1.85); }
}
.em-cd-crest-shock {
  --em-anim: em-cd-crest-shock;
  animation-duration: 4.6s;
  animation-delay: 0.18s;
  animation-timing-function: cubic-bezier(.15, .7, .3, 1);
  animation-iteration-count: infinite;
}

/* The gas-giant's cloud bands roll. The slowest secondary on the crest at 6.6s,
   because banded weather on a giant planet drifts rather than pulses. Strictly
   scaleX and translateX: the widest band is rx16 inside an r17 globe, so any
   vertical move or any growth past 1.0 would push a band off the limb and the
   planet would leak. Shrinking and sliding within about a unit keeps every band
   inside the sphere while still shearing the stack visibly. */
@keyframes em-cd-crest-bands {
  0%, 100% { transform: none; }
  26%      { transform: translateX(1.1px) scaleX(0.95); }
  58%      { transform: translateX(-1.4px) scaleX(0.92); }
  82%      { transform: translateX(0.5px) scaleX(0.98); }
}
.em-cd-crest-bands {
  --em-anim: em-cd-crest-bands;
  animation-duration: 6.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The starlight on the lit limb, guttering on 2.3s. Opacity and filter only — no
   transform — because the rim-light arc IS the planet's limb at r17, and scaling
   it by even a percent would lift it off the edge of the globe. `linear` gives the
   hard-edged ramps of light flickering through debris; an ease-in-out would read
   as breathing, which is wrong for a star. */
@keyframes em-cd-crest-spec {
  0%, 100% { opacity: 1;    filter: none; }
  18%      { opacity: 0.42; filter: none; }
  44%      { opacity: 1;    filter: brightness(1.9) saturate(1.2); }
  66%      { opacity: 0.55; filter: none; }
  86%      { opacity: 0.95; filter: brightness(1.4); }
}
.em-cd-crest-spec {
  --em-anim: em-cd-crest-spec;
  animation-duration: 2.3s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* FX — debris dragged into the planet, the crest's echo of the emblem's infalling
   streams, so both art pieces are visibly EATING. It fades in out past the
   medallion, accelerates down the diagonal and shrinks into the globe, and is gone
   before it can pile up on the limb. On its own 3.4s clock with a long delay: it
   should feel like a stray event, not part of the nova's rhythm. */
@keyframes em-cd-crest-infall {
  0%, 100% { opacity: 0;    transform: none; }
  16%      { opacity: 0.9;  transform: translate(1.4px, 1.3px) scale(0.92); }
  58%      { opacity: 0.85; transform: translate(8px, 7.4px) scale(0.58); }
  86%      { opacity: 0;    transform: translate(14px, 13px) scale(0.28); }
}
.em-cd-crest-infall {
  --em-anim: em-cd-crest-infall;
  animation-duration: 3.4s;
  animation-delay: 0.7s;
  animation-timing-function: cubic-bezier(.3, 0, .7, .35);
  animation-iteration-count: infinite;
}

/* The two star glints riding the blast. One keyframe set, two rules — a twinkle is
   the same optical event wherever it happens, and what keeps the pair out of phase
   is period, delay and direction, not bespoke curves. Each glint dims to almost
   nothing, pops to 1.5 with a kick of rotation (a four-point star TURNING is what
   sells light catching an edge, where a dot fading up just looks like a bulb), then
   returns to exactly the drawn pose. */
@keyframes em-cd-crest-glint {
  0%, 100% { opacity: 1;    transform: none; }
  24%      { opacity: 0.15; transform: scale(0.4) rotate(-30deg); }
  50%      { opacity: 1;    transform: scale(1.5) rotate(12deg); }
  74%      { opacity: 0.35; transform: scale(0.66) rotate(-12deg); }
}
.em-cd-crest-glint-a {
  --em-anim: em-cd-crest-glint;
  animation-duration: 2.9s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}
.em-cd-crest-glint-b {
  --em-anim: em-cd-crest-glint;
  animation-duration: 3.7s;
  animation-delay: 0.5s;
  animation-direction: reverse;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* ============================================================
   PENGUIN GOLD (em-pg-*) — THE EMPEROR COMES TO LIFE
   ------------------------------------------------------------
   Every other machine in this file is a mechanism or a monster. This one is the
   friendly one, so its motion is CHARACTER, not engineering: the emperor penguin
   flaps both flippers out of phase, bobs twice and then cocks its head into a
   comic double-take, double-blinks, shuffles its gold feet and kicks up powder,
   and lets a specular run across the gold bib. The ceremonial crest plays the
   same performance in fish language — a whole-body leap answering the head-cock,
   a beating tail answering the flippers, the same blink, the same travelling
   gold specular, frost spray answering the snow — so the lobby tile and the
   landed jackpot crest are unmistakably one machine.

   WHY THE TWO FLIPPERS SHARE ONE 3.2s PERIOD (and the two feet one 1.9s):
   they are limbs of the SAME bird. If they drifted apart on separate clocks they
   would eventually flap in unison, which is the one thing a penguin never does.
   The phase offset therefore lives INSIDE the keyframes (left peaks at 46/60/75%,
   right at 53/67/82%) and in one half-period animation-delay for the far foot,
   which locks them permanently alternating. Everything that is NOT a limb —
   halo 2.7s, bib 2.3s, specular 3.7s, blink 4.0s, head 4.6s, and on the crest
   3.4 / 3.6 / 3.9 / 4.3 / 5.1 / 5.8s — runs on its own off-beat period so the
   art is never still and never pulses as one block.

   The house style here is warm and springy: ease-in-out for anything that
   breathes or flaps, a snappy cubic-bezier for the double-take and the leap
   (they are BEATS and need to land), linear for light travelling across metal.
   ------------------------------------------------------------ */

/* ---- the lobby emblem: EmperorPenguinIcon (48 viewBox) ---------------- */

/* The cold halo breathing. The only genuinely continuous part of the bird, and
   deliberately the slowest-feeling one: it fills the gaps between the flap
   bursts so a hovered tile never looks frozen. Dimming as it expands makes it
   read as light spreading rather than a lamp being turned up. Shared with the
   crest's halo, which runs the same shape at 3.4s. */
@keyframes em-pg-halo {
  0%, 100% { transform: none; opacity: 1; }
  34%      { transform: scale(1.1); opacity: 0.72; }
  68%      { transform: scale(0.93); opacity: 1; }
  86%      { transform: scale(1.03); opacity: 0.88; }
}
.em-pg-halo {
  --em-anim: em-pg-halo;
  animation-duration: 2.7s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE FLAP, left flipper — half of the signature. It is an EVENT, so the flipper
   hangs at the drawn pose for the first third of the cycle, TUCKS against the
   body first (the -7deg wind-up is what makes the eye look at it before anything
   big happens), then beats three times with decaying amplitude — 32deg, 27deg,
   17deg — and settles. Positive rotation is outward on this side: the pivot is
   the shoulder, so the tip swings away from the body instead of the whole flipper
   sliding off it. 32deg at a 13-unit flipper throws the tip about 8 units, which
   is a flap you can read at 56px — the whole point of the exercise. */
@keyframes em-pg-flip-l {
  0%, 34%, 100% { transform: none; }
  38%           { transform: rotate(-7deg); }
  46%           { transform: rotate(32deg); }
  53%           { transform: rotate(3deg); }
  60%           { transform: rotate(27deg); }
  67%           { transform: rotate(1deg); }
  75%           { transform: rotate(17deg); }
  86%           { transform: rotate(2deg); }
}
.em-pg-flip-l {
  --em-anim: em-pg-flip-l;
  /* the shoulder, not the flipper's bbox centre: fill-box would put the origin
     halfway down the blade and the flipper would orbit rather than hinge */
  transform-box: view-box;
  transform-origin: 12px 24.5px;
  animation-duration: 3.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE FLAP, right flipper — mirrored (negative is outward on this side) and
   offset roughly a fifth of a beat, so its peaks fall in the troughs of the left
   one. Same 3.2s period as its partner on purpose; see the banner. */
@keyframes em-pg-flip-r {
  0%, 38%, 100% { transform: none; }
  42%           { transform: rotate(6deg); }
  53%           { transform: rotate(-30deg); }
  60%           { transform: rotate(-2deg); }
  67%           { transform: rotate(-25deg); }
  74%           { transform: none; }
  82%           { transform: rotate(-15deg); }
  92%           { transform: rotate(-2deg); }
}
.em-pg-flip-r {
  --em-anim: em-pg-flip-r;
  transform-box: view-box;
  transform-origin: 36px 24.5px;
  animation-duration: 3.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE COMIC BEAT — the head. Two small bobs first (13% and 38%), which is the
   ambient half of the gesture and keeps the face alive, and then the double-take:
   the whole head cocks 12deg one way, holds a beat, snaps 9deg the other way and
   levels off. The pivot is the NECK, so the hood, the gold ear patches, the eyes
   and the beak lean as one head; pivoting on the group's own centre would slide
   the face off the body. 12deg carries the crown about 3 units sideways — a
   clear, cheeky tilt at 56px that still leaves the head sitting on its shoulders.
   4.6s, the longest cycle on the bird, because a double-take needs the pause
   before it to land. */
@keyframes em-pg-head {
  0%, 100% { transform: none; }
  13%      { transform: translateY(-1.3px); }
  26%      { transform: none; }
  38%      { transform: translateY(-1px); }
  50%      { transform: none; }
  60%      { transform: translateY(-1.2px) rotate(-11deg); }
  70%      { transform: translateY(-1.4px) rotate(-12deg); }
  80%      { transform: translateY(-0.5px) rotate(9deg); }
  90%      { transform: rotate(2.5deg); }
}
.em-pg-head {
  --em-anim: em-pg-head;
  transform-box: view-box;
  transform-origin: 24px 20.5px; /* the neck, where the hood meets the body */
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* THE BLINK. A squash to a sliver, twice, in 0.12s each — collapsing the eye
   group takes the white disc with it, so the eye vanishes into the black hood
   exactly the way a cartoon blink reads, and the pupil needs no separate part.
   Both eyes carry this one class so they always close together. It is the
   FASTEST thing on the machine and the cheapest to notice, which is why it is
   what makes the bird feel awake. 4.0s against the head's 4.6s means the blink
   lands somewhere different in the double-take every cycle. Shared with the
   crest's fish eye at 3.6s. */
@keyframes em-pg-blink {
  0%, 22%, 100% { transform: none; }
  25%           { transform: scaleY(0.1); }
  28%           { transform: none; }
  33%           { transform: scaleY(0.1); }
  36%           { transform: none; }
}
.em-pg-eye {
  --em-anim: em-pg-blink;
  animation-duration: 4s;
  animation-timing-function: linear; /* a blink is a snap, not a swell */
  animation-iteration-count: infinite;
}

/* THE SHUFFLE. Each gold foot lifts on its own ankle — hence the wrapper and the
   pivot up at the heel — rotating a toe clear of the ice and dropping it again.
   Sign is mirrored per side so both feet lift OUTWARD. 1.9s is the shortest full
   gesture on the bird, so the shuffle is what carries the low half of the art
   while the flippers are resting. */
@keyframes em-pg-foot-l {
  0%, 46%, 100% { transform: none; }
  58%           { transform: translateY(-2px) rotate(19deg); }
  70%           { transform: translateY(-0.6px) rotate(7deg); }
}
.em-pg-foot-l {
  --em-anim: em-pg-foot-l;
  transform-box: view-box;
  transform-origin: 18.5px 41.5px; /* the ankle */
  animation-duration: 1.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes em-pg-foot-r {
  0%, 46%, 100% { transform: none; }
  58%           { transform: translateY(-2px) rotate(-19deg); }
  70%           { transform: translateY(-0.6px) rotate(-7deg); }
}
.em-pg-foot-r {
  --em-anim: em-pg-foot-r;
  transform-box: view-box;
  transform-origin: 29.5px 41.5px;
  animation-duration: 1.9s;
  /* exactly half the period: this is what turns two lifting feet into a shuffle
     instead of a hop, and because the periods are identical it can never drift
     into both feet leaving the ice at once */
  animation-delay: 0.95s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The emperor bib swelling and catching the light. Gold is already near-clipped
   in red and green, so brightness ALONE would drive it to a flat acid yellow;
   pulling saturation down at the same time bends the flare toward white-gold,
   which is what polished metal actually does. `filter: none` at 0%/100% is the
   identity, so the off state is pixel-exact. */
@keyframes em-pg-bib {
  0%, 100% { transform: none; filter: none; opacity: 0.9; }
  34%      { transform: scale(1.035); filter: brightness(1.3) saturate(0.95); opacity: 1; }
  62%      { transform: scale(1.05); filter: brightness(1.75) saturate(0.72); opacity: 1; }
  82%      { transform: scale(1.02); filter: brightness(1.2) saturate(0.9); opacity: 0.95; }
}
.em-pg-bib {
  --em-anim: em-pg-bib;
  animation-duration: 2.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* FX — the specular that travels across the bib. `transform` is declared only at
   0% and 100%, so it interpolates as one uninterrupted linear slide from the
   bib's left edge to its right while opacity does its own thing on top: the
   highlight arrives, crosses the gold, and is gone before it can reach the far
   edge and look like it fell off. Opacity 0 at both ends, as every em-fx part
   must be, which is also what hides the slide back to the start. */
@keyframes em-pg-shine {
  0%   { opacity: 0;    transform: translateX(-3.5px); }
  12%  { opacity: 0.9; }
  50%  { opacity: 0.8; }
  80%  { opacity: 0; }
  100% { opacity: 0;    transform: translateX(8px); }
}
.em-pg-shine {
  --em-anim: em-pg-shine;
  animation-duration: 3.7s;
  animation-timing-function: linear; /* light travels at a constant rate */
  animation-iteration-count: infinite;
}

/* FX — powder snow kicked out from under the feet. Scaling the puff group from
   its own centre throws the two clouds apart, so the snow spreads outward from
   between the feet and drifts up as it thins, which is the read we want: the
   shuffle CAUSED this. 3.8s is exactly twice the foot period and the 0.72s delay
   parks the burst on a left-foot step, so it fires on every other step — often
   enough to connect the two, rare enough not to look like a smoke machine. */
@keyframes em-pg-snow {
  0%, 100% { opacity: 0; transform: scale(0.55); }
  10%      { opacity: 0.9; transform: scale(0.85); }
  38%      { opacity: 0.55; transform: translateY(-1px) scale(1.2); }
  62%      { opacity: 0; transform: translateY(-2.4px) scale(1.5); }
}
.em-pg-snow {
  --em-anim: em-pg-snow;
  animation-duration: 3.8s;
  animation-delay: 0.72s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: PgCelebrationCrest (96 viewBox) ------------ */
/* The crest is thrown by the jackpot capsule and then HELD, so its gestures are
   bigger and slower than the reel symbol's and there is time to watch the whole
   arc of the leap. */

/* Same halo shape as the emblem, one beat slower, so a hovered lobby tile and a
   landed crest on screen together are not breathing in step. */
.em-pg-khalo {
  --em-anim: em-pg-halo;
  animation-duration: 3.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE LEAP — the crest's signature, and the fish's answer to the penguin's
   double-take: held at the drawn pose for the first 42%, then a hard launch,
   nose up 9deg, up to the apex nine units clear of the floe, and a pitch
   forward into the dive with a unit of overshoot below the resting pose as it
   re-enters. Nine units of a 96 viewBox is roughly 8px at 84px: a real leap,
   not a hover. The WRAPPER is animated, never the inner group — that group
   carries the rotate(-14) that sets the fish's resting arc, and a CSS transform
   on the same element would silently throw it away. */
@keyframes em-pg-kfish {
  0%, 42%, 100% { transform: none; }
  54%           { transform: translateY(-6px) rotate(9deg); }
  64%           { transform: translateY(-9.5px) rotate(3deg); }
  76%           { transform: translateY(-3px) rotate(-8deg); }
  88%           { transform: translateY(1.2px) rotate(-2deg); }
}
.em-pg-kfish {
  --em-anim: em-pg-kfish;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.33, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* The tail beating. This is the flipper flap transposed onto the crest: the one
   part that runs continuously, on the shortest period of either art piece, so
   the fish is visibly swimming even in the long hold before a leap. Pivoted on
   0% 50% of its own box — the fan's root, exactly where it joins the body — so
   it sweeps like a tail instead of shearing off the tail stock. */
@keyframes em-pg-ktail {
  0%, 100% { transform: none; }
  24%      { transform: rotate(-19deg); }
  52%      { transform: rotate(2deg); }
  78%      { transform: rotate(19deg); }
}
.em-pg-ktail {
  --em-anim: em-pg-ktail;
  transform-origin: 0% 50%; /* the tail root; fill-box, so its own left edge */
  animation-duration: 1.5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The fish blinks too — same keyframes as the penguin's eyes, a little quicker,
   which is most of why the crest reads as the same friendly machine rather than
   a piece of heraldry. */
.em-pg-keye {
  --em-anim: em-pg-blink;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* FX — the same travelling specular as the bib, running head-to-tail along the
   gold. It lives INSIDE the fish group, so it rides the leap and stays on the
   body's own axis instead of sliding across a fish that has pitched out from
   under it. Same one-slide-plus-independent-opacity idiom as em-pg-shine. */
@keyframes em-pg-kshine {
  0%   { opacity: 0;    transform: translateX(-4px); }
  12%  { opacity: 0.9; }
  52%  { opacity: 0.75; }
  84%  { opacity: 0; }
  100% { opacity: 0;    transform: translateX(26px); }
}
.em-pg-kshine {
  --em-anim: em-pg-kshine;
  animation-duration: 3.9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The floe rocks. It is a raft, not a plinth: pivoting on 48px 84px — the middle
   of the slab's waterline rather than the group's centre — means a 3deg roll
   lifts one end and dips the other, carrying the icicles with it, which is why
   the icicles are inside this wrapper instead of being animated separately.
   Slowest part on the machine (5.1s) because swell is slow, and the one thing
   here that must never look urgent. */
@keyframes em-pg-kfloe {
  0%, 100% { transform: none; }
  28%      { transform: translateY(0.8px) rotate(3deg); }
  54%      { transform: translateY(1.4px); }
  78%      { transform: translateY(0.6px) rotate(-2.6deg); }
}
.em-pg-kfloe {
  --em-anim: em-pg-kfloe;
  transform-box: view-box;
  transform-origin: 48px 84px; /* the waterline, centre of the slab */
  animation-duration: 5.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* FX — frost spray off the ice as the fish launches. It shares the leap's exact
   4.4s period with no delay and fires at 56%, a beat AFTER the launch begins at
   42%: cause, then effect. It blooms outward and upward as it thins, so the fish
   appears to rise out of its own spray, and it is back to opacity 0 well before
   the loop seam. This is the crest's answer to the penguin's kicked-up powder. */
@keyframes em-pg-kspray {
  0%, 44%, 100% { opacity: 0; transform: scale(0.5); }
  56%           { opacity: 0.95; transform: translateY(-1px) scale(1); }
  72%           { opacity: 0.5; transform: translateY(-4px) scale(1.35); }
  88%           { opacity: 0; transform: translateY(-7px) scale(1.6); }
}
.em-pg-kspray {
  --em-anim: em-pg-kspray;
  animation-duration: 4.4s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

/* The aurora curtains. Both are pivoted on 50% 100% — their own feet — so
   scaling them vertically makes the curtain climb the sky from the horizon
   instead of growing in both directions from its middle, and the opacity falls
   as it stretches so a taller curtain reads as a thinner one. Two ribbons, two
   clocks (4.3s and 5.8s) with their peaks written at opposite ends of the cycle:
   the green swells while the violet is collapsing, which is what stops the sky
   from pulsing as one flat sheet. These are the crest's ambient layer — they
   never stop, and they hold the frame together during the long hold before the
   fish leaps. */
@keyframes em-pg-kaurora-a {
  0%, 100% { transform: none; opacity: 0.4; }
  28%      { transform: scaleY(1.32) scaleX(1.02); opacity: 0.75; }
  56%      { transform: scaleY(0.86); opacity: 0.26; }
  82%      { transform: scaleY(1.14) scaleX(1.01); opacity: 0.55; }
}
.em-pg-kaurora-a {
  --em-anim: em-pg-kaurora-a;
  transform-origin: 50% 100%;
  animation-duration: 4.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes em-pg-kaurora-b {
  0%, 100% { transform: none; opacity: 0.35; }
  24%      { transform: scaleY(0.84); opacity: 0.6; }
  52%      { transform: scaleY(1.28) scaleX(0.98); opacity: 0.18; }
  78%      { transform: scaleY(1.06); opacity: 0.48; }
}
.em-pg-kaurora-b {
  --em-anim: em-pg-kaurora-b;
  transform-origin: 50% 100%;
  animation-duration: 5.8s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ============================================================
   DEAD MAN'S BOOTY (em-dmb-*) — THE CHEST CREAKS OPEN
   ------------------------------------------------------------
   One event, told in the right order, because a chest that hangs open is just a
   box: the hasp RATTLES itself loose (the telegraph — the player's eye is on the
   lock before anything large moves), the plank lid TIPS BACK on the hinge at its
   foot, gold light FLOODS the gap it opens, the hoard RISES into that light and
   the doubloons turn edge-on and flash, and then the lid falls shut. The lid is
   still for the first 44% of every cycle, so the opening lands as something that
   happens rather than as a lid flapping.

   HOW A LID "TIPS BACK" IN A FLAT DRAWING. The pivot is parked on the seam at
   the lid's foot (transform-box:view-box, 24px 21px — its own bbox centre would
   swing the plank sideways like a signboard), and the gesture is
   rotate + translateY + scaleY: the plank compresses toward that seam as it goes
   edge-on to the viewer, lifts clear of the rim, and leans. That is the standard
   read for out-of-plane rotation with in-plane transforms, and it keeps the lid
   attached to the chest at the hinge the whole way — no part ever floats free.

   THE CREST PLAYS THE SAME GESTURE IN BONE. The Jolly-Roger seal has no chest,
   so its skull is the lid: the cranium tips back on the jaw with the same
   rotate/translateY/scaleY chain about the same kind of bottom-edge pivot, the
   same gold floods out from behind it, conjured doubloons ride the light up, and
   the doubloon clasp at the ring's foot rattles where the hasp did. A player who
   hovers the lobby tile and later wins the jackpot sees one machine open twice.

   WHY THE GEARED PARTS SHARE A PERIOD. Lock, lid, spill and hoard are one
   mechanism: 4.6s on the reel symbol, 5.4s on the crest (the crest is HELD on
   screen after the capsule lands, so it can afford a slower, grander creak). If
   they drifted apart the light would flood before the lid moved and the linkage
   would read as broken. Everything that is NOT geared to the lid runs on its own
   prime-ish clock — 8.3s ray fan, 4.1s blades, 3.3s clasp, 2.9s socket-fire,
   2.8s coin flip — so the art is never in lockstep and never completely still.
   ------------------------------------------------------------ */

/* ---- the lobby emblem: DeadMansChestIcon (48 viewBox) ------------------- */

/* THE SIGNATURE. Dead still to 44%, then one creak: a 3deg crack at 54% (the
   plank unsticking), the full swing at 66%, a held sag while the treasure is on
   show, and a fall back to true. scaleY(0.42) is what does the heavy lifting at
   56px — the 14-unit dome collapses to under 6 units, so the plank visibly goes
   edge-on instead of merely sliding upward. */
@keyframes em-dmb-lid {
  0%, 44%, 100% { transform: none; }
  54%           { transform: rotate(-3deg) translateY(-1.4px) scaleY(0.84); }
  66%           { transform: rotate(-9deg) translateY(-4.6px) scaleY(0.42); }
  82%           { transform: rotate(-8.2deg) translateY(-4.2px) scaleY(0.46); }
  93%           { transform: rotate(-1.5deg) translateY(-0.8px) scaleY(0.9); }
}
.em-dmb-lid {
  --em-anim: em-dmb-lid;
  /* the hinge — the seam where the lid meets the chest rim, NOT the lid's own
     centre, which would swing the plank sideways instead of tipping it back */
  transform-box: view-box;
  transform-origin: 24px 21px;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.36, 0, .22, 1);
  animation-iteration-count: infinite;
}

/* The latch. It goes FIRST — the rattle runs 20%–47%, finishing just before the
   lid cracks at 54%, so the sequence reads as cause and effect rather than two
   things twitching at once. `linear` on purpose: a rattle is a hard mechanical
   chatter, and an ease-in-out would turn it into a wag. The pivot is the plate's
   top pin, so the latch swings from where it actually hangs; ±15deg on a
   5-unit-wide plate is roughly 1.5 units of travel at its lip, which is the
   smallest amplitude that still reads at 56px. It ends dropped a fraction and
   flaring, which is the hasp letting go. */
@keyframes em-dmb-lock {
  0%, 20%, 100% { transform: none; filter: none; }
  26%           { transform: rotate(14deg); }
  31%           { transform: rotate(-15deg); }
  36%           { transform: rotate(11deg); }
  41%           { transform: rotate(-9deg); }
  47%           { transform: rotate(4deg) translateY(0.7px); filter: brightness(1.45) saturate(0.78); }
  62%           { transform: rotate(-2deg) translateY(1.2px); filter: brightness(1.18); }
  82%           { transform: translateY(0.6px); filter: none; }
}
.em-dmb-lock {
  --em-anim: em-dmb-lock;
  transform-box: view-box;
  transform-origin: 24px 33px; /* the plate's top pin, where a hasp hangs */
  animation-duration: 4.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* FX — the gold flooding the gap the swing opens. Dead at opacity 0 until 44%,
   because none of this light exists while the chest is shut. It comes up wide
   and flat first (scaleY 0.3, a slot of light escaping the seam) and only fills
   out once the lid is clear, so the light reads as welling UP out of the chest
   rather than as a lamp being switched on behind it. */
@keyframes em-dmb-spill {
  0%, 44%, 100% { opacity: 0;    transform: scaleX(0.82) scaleY(0.3); }
  54%           { opacity: 0.55; transform: scaleX(0.94) scaleY(0.72); }
  68%           { opacity: 1;    transform: scaleX(1.08) scaleY(1.14); }
  86%           { opacity: 0.75; transform: scaleX(1.02) scaleY(0.96); }
}
.em-dmb-spill {
  --em-anim: em-dmb-spill;
  animation-duration: 4.6s;
  animation-delay: 0.04s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The drawn treasure-glow behind the whole chest. It dips BELOW the static pose
   first (scale .94 / brightness .9) so the surge afterwards has somewhere to
   come from — a glow that only ever brightens reads as a fade-in, one that
   inhales first reads as pressure building. Peaks at 70%, just behind the lid's
   widest point, because it is the consequence of the lid being open. */
@keyframes em-dmb-glow {
  0%, 100% { transform: none;        filter: none; }
  30%      { transform: scale(0.94); filter: brightness(0.9); }
  52%      { transform: scale(1.06); filter: brightness(1.3); }
  70%      { transform: scale(1.24); filter: brightness(1.8) saturate(1.1); }
  88%      { transform: scale(1.1);  filter: brightness(1.3); }
}
.em-dmb-glow {
  --em-anim: em-dmb-glow;
  animation-duration: 4.6s;
  animation-delay: 0.06s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The hoard rising into the light as it is uncovered. Pivoted on the FOOT of the
   pile rather than its middle, so scaling it grows the heap upward out of the
   chest instead of inflating it through the floor — the coins climb and the gems
   bedded at the bottom stay put, which is how a heap behaves. The brightness
   swell is the gems catching the flood; opacity could only ever dim them, since
   the drawing is already fully opaque. */
@keyframes em-dmb-hoard {
  0%, 48%, 100% { transform: none; filter: none; }
  60%           { transform: translateY(-1.4px) scale(1.06); filter: brightness(1.3) saturate(1.12); }
  74%           { transform: translateY(-2.4px) scale(1.1);  filter: brightness(1.55) saturate(1.2); }
  90%           { transform: translateY(-0.8px) scale(1.03); filter: brightness(1.15); }
}
.em-dmb-hoard {
  --em-anim: em-dmb-hoard;
  transform-box: view-box;
  transform-origin: 24px 32px; /* the foot of the pile, so it rises rather than swells */
  animation-duration: 4.6s;
  animation-delay: 0.18s;
  animation-timing-function: cubic-bezier(.3, .75, .25, 1);
  animation-iteration-count: infinite;
}

/* A doubloon turning edge-on. scaleX to 0.18 is the coin passing side-on through
   the light — that is the whole glint, and it is why the flash is carried by
   scaleX rather than by opacity, which would just make the coin vanish. The
   saturate() pull matters: gold is already near-clipped in red and green, so
   brightness alone drives it to a flat acid yellow, while dropping saturation at
   the same time bends the flare toward white, which is what metal does. Runs on
   its own 2.8s clock against the mechanism's 4.6s, and each coin's delay is set
   inline in the art, so the three never turn together. */
@keyframes em-dmb-coin {
  0%, 44%, 100% { transform: none;          filter: none; }
  56%           { transform: scaleX(0.18);  filter: brightness(1.6) saturate(0.68); }
  68%           { transform: scaleX(1.12);  filter: brightness(1.3) saturate(0.85); }
  82%           { transform: scaleX(0.88);  filter: none; }
}
.em-dmb-coin {
  --em-anim: em-dmb-coin;
  animation-duration: 2.8s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
  /* per-coin animation-delay is set inline in the art, where each coin's place
     in the pile is known */
}

/* The ray burst behind the chest, turning without pause. A full 360deg is
   geometrically the static pose, so both the loop seam and the moment the gate
   switches off are invisible, and unlike an oscillation it never unwinds itself.
   8.3s and linear: slow, even, and deliberately sharing no factor with the 4.6s
   mechanism, so the light behind the chest is always drifting even between
   openings. */
@keyframes em-dmb-rays {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.em-dmb-rays {
  --em-anim: em-dmb-rays;
  /* the fan's apex, where all six spokes meet — a bbox centre would put the
     origin off in the middle of the half-plane the fan covers and make the rays
     orbit the chest instead of pivoting */
  transform-box: view-box;
  transform-origin: 24px 20px;
  animation-duration: 8.3s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: DmbCelebrationCrest (96 viewBox) ------------- */
/* The same choreography on a slower 5.4s beat — slower than the reel symbol both
   because the crest is held on screen after the capsule lands and so a lobby
   tile and a landed crest on screen together are not marching in step. */

/* THE SIGNATURE IN BONE. Identical shape to em-dmb-lid — hold, crack, swing,
   sag, fall — and the identical rotate/translateY/scaleY chain about a
   bottom-edge pivot, here the teeth line at y=66 where a jaw hinges. The numbers
   are bigger, not because the gesture is bigger but because 96 user units have
   to cross twice the distance for the same apparent travel. The skull tipping
   its cranium back off the flood is the crest's chest-lid, and it is also
   plainly a dead pirate throwing his head back, which is the point. */
@keyframes em-dmb-kskull {
  0%, 42%, 100% { transform: none; }
  52%           { transform: rotate(-2.5deg) translateY(-2px) scaleY(0.9); }
  66%           { transform: rotate(-9deg) translateY(-6px) scaleY(0.62); }
  82%           { transform: rotate(-8deg) translateY(-5.4px) scaleY(0.66); }
  93%           { transform: rotate(-2deg) translateY(-1.6px) scaleY(0.92); }
}
.em-dmb-kskull {
  --em-anim: em-dmb-kskull;
  transform-box: view-box;
  transform-origin: 48px 66px; /* the teeth line — the jaw is the hinge */
  animation-duration: 5.4s;
  animation-timing-function: cubic-bezier(.36, 0, .22, 1);
  animation-iteration-count: infinite;
}

/* FX — the same flood, behind the skull instead of behind the lid, so it is the
   cranium tipping off it that uncovers the light. Scale alone carries it: a
   flood does not need a shape change to read, it needs to arrive late (nothing
   before 40%) and grow. */
@keyframes em-dmb-kspill {
  0%, 40%, 100% { opacity: 0;   transform: scale(0.55); }
  52%           { opacity: 0.6; transform: scale(0.82); }
  68%           { opacity: 1;   transform: scale(1.08); }
  88%           { opacity: 0.7; transform: scale(0.98); }
}
.em-dmb-kspill {
  --em-anim: em-dmb-kspill;
  animation-duration: 5.4s;
  animation-delay: 0.05s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* FX — the ray burst, cast on the crest as light actually breaking out of the
   seal: it fades up with the skull, sweeps 38deg while it is open, and is gone
   before the head comes down. It turns only DURING the event, unlike the reel
   symbol's endlessly rotating fan, because on the crest the rays are the escaping
   light rather than a backdrop. Since it dies at opacity 0 the unwind back to
   rotate(0) over the last 10% happens invisibly. */
@keyframes em-dmb-krays {
  0%, 40%, 100% { opacity: 0;    transform: rotate(0deg) scale(0.6); }
  52%           { opacity: 0.45; transform: rotate(9deg) scale(0.86); }
  70%           { opacity: 0.85; transform: rotate(22deg) scale(1.05); }
  90%           { opacity: 0;    transform: rotate(38deg) scale(1.1); }
}
.em-dmb-krays {
  --em-anim: em-dmb-krays;
  transform-box: view-box;
  transform-origin: 48px 48px; /* the seal's centre, where all eight spokes meet */
  animation-duration: 5.4s;
  animation-delay: 0.1s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

/* FX — doubloons riding the flood up out of the seal, the crest's answer to the
   reel symbol's hoard lifting. They start dropped and small below the ring, rise
   21 units through the open skull and burn out at the top; the pivot is at their
   foot so growing them pushes them upward, same trick as the hoard. Delayed a
   third of a second behind the skull because the coins are lifted BY the light,
   not with it. */
@keyframes em-dmb-kcoins {
  0%, 44%, 100% { opacity: 0;    transform: translateY(5px) scale(0.65); }
  55%           { opacity: 0.95; transform: translateY(-1px) scale(1); }
  72%           { opacity: 1;    transform: translateY(-12px) scale(1.1); }
  88%           { opacity: 0;    transform: translateY(-21px) scale(1.02); }
}
.em-dmb-kcoins {
  --em-anim: em-dmb-kcoins;
  transform-box: view-box;
  transform-origin: 48px 74px;
  animation-duration: 5.4s;
  animation-delay: 0.3s;
  animation-timing-function: cubic-bezier(.22, .7, .3, 1);
  animation-iteration-count: infinite;
}

/* The doubloon clasp — this crest's lock plate, so it rattles like one: the same
   hard linear chatter about a top pin as em-dmb-lock, finished with the coin
   turning edge-on as it lets go. It runs FREE on 3.3s rather than geared to the
   5.4s seal, unlike the reel symbol's latch: the crest is held on screen for many
   seconds at a time, so the clasp firing two or three times per opening is what
   keeps the seal from ever looking frozen between events. */
@keyframes em-dmb-kclasp {
  0%, 18%, 100% { transform: none; filter: none; }
  24%           { transform: rotate(13deg); }
  29%           { transform: rotate(-14deg); }
  34%           { transform: rotate(10deg); }
  40%           { transform: rotate(-8deg); }
  48%           { transform: rotate(3deg) scaleX(0.2); filter: brightness(1.6) saturate(0.7); }
  62%           { transform: scaleX(1.1); filter: brightness(1.25); }
  80%           { transform: scaleX(0.92); filter: none; }
}
.em-dmb-kclasp {
  --em-anim: em-dmb-kclasp;
  transform-box: view-box;
  transform-origin: 48px 79px; /* where the clasp grips the ring */
  animation-duration: 3.3s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The crossed cutlasses taking the strain — the crest's iron straps. Rocking the
   whole X about the point where the blades cross is what makes it read as two
   swords shifting against each other rather than as the glyph being nudged; at
   28 units out, 5deg swings each tip about 2.4 units, which is visible at 84px
   and nowhere near enough to push a tip through the hemp ring. 4.1s, off every
   other clock in the machine, so the seal always has a slow undercurrent. */
@keyframes em-dmb-kblades {
  0%, 100% { transform: none; }
  32%      { transform: rotate(-5deg) scale(1.02); }
  66%      { transform: rotate(4deg) scale(0.99); }
}
.em-dmb-kblades {
  --em-anim: em-dmb-kblades;
  transform-box: view-box;
  transform-origin: 48px 50px; /* the crossing point of the two blades */
  animation-duration: 4.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The ghost-fire in the eye sockets — the one part with no interest in the lid at
   all, guttering away on 2.9s so the skull is alive even while the seal is shut.
   It gutters DOWN to 0.5 before flaring to 2.7, so the socket goes dark and then
   erupts; at 2.7 the pip slightly overspills its socket onto the bone, which is
   what makes it read as fire escaping rather than a lamp behind a hole. 0%/100%
   sit at opacity 0.9, which is exactly what the drawing paints, so the static
   pose is pixel-identical. The second socket's delay is inline in the art. */
@keyframes em-dmb-keyes {
  0%, 100% { transform: none;         opacity: 0.9; }
  24%      { transform: scale(0.5);   opacity: 0.4; }
  50%      { transform: scale(2.7);   opacity: 1; }
  72%      { transform: scale(1.35);  opacity: 0.72; }
}
.em-dmb-keyes {
  --em-anim: em-dmb-keyes;
  animation-duration: 2.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ============================================================
   HIGH NOON GOLD (em-hn-*) — THE SHERIFF'S BADGE CATCHES THE SUN
   ------------------------------------------------------------
   One optical event, not a jiggle. The badge holds its drawn pose, then the
   sun finds it: a hard white specular RIPS across the star's face in about
   half a second, the centre boss flares a beat behind the light that caused
   it, and the badge itself hauls round one slow proud revolution and settles
   dead true. Underneath all of that the twelve sunburst rays and the engraved
   bezel turn continuously in opposite directions, so the metal is never
   completely still between glints.

   WHY THE GLINT IS THE SIGNATURE: this is a high-noon machine. Light off a
   polished star is brief and violent — it arrives, crosses, and is gone. So
   the glint spends ~48% of its cycle at opacity 0, snaps to full white in
   0.16s, travels, and dies. Anything slower would read as a glowing sticker.

   WHY THE ROTATIONS CAN LOOP INVISIBLY: the badge is built on rotational
   symmetry, and every turning part exploits its own. The 12 rays and the 12
   bezel studs repeat every 30deg, so rotate(30deg) is the drawn pose pixel for
   pixel — they can ratchet round forever and the loop seam never shows. The
   five-pointed star is only self-similar every 72deg, but its `hns-star`
   gradient is top-lit and would visibly tip over with it, so the star ends on
   rotate(360deg) instead: geometry AND shading identical, and unlike `none` it
   does not whip the last segment backwards through a whole turn.

   WHAT IS GEARED TO WHAT: glint → boss flare → (crest) light burst share one
   period and differ only by delay, because they are cause and effect and would
   read as three unrelated loops otherwise. Everything else — the rays, the
   bezel, the star's turn, the guns' heft, the rim glints — runs on its own
   period (2.1 / 2.6 / 2.9 / 4.6 / 5.3 / 5.9 / 6.2 / 7.4s) so nothing ever
   pulses in lockstep.

   Reel symbol / lobby face: em-hn-* (48 viewBox). Ceremonial crest: em-hn-k*
   (96 viewBox) — a separate namespace because the pivots and the specular's
   travel are measured in user units, which are twice as large over there. The
   gesture is deliberately identical so the hovered lobby tile and the landed
   jackpot crest read as the same machine, just grander and slower.
   ------------------------------------------------------------ */

/* ---- the lobby emblem: SheriffBadgeIcon (48 viewBox) ------------------- */

/* The sunburst rays turning and breathing behind the star. The rotation stops
   are exactly proportional to the keyframe percentages (8.4 at 28%, 16.5 at
   55%, 24 at 80% of 30deg) which, with linear timing, gives one dead-steady
   turn while the scale and opacity breathe on top of it at their own pace —
   two rhythms out of one animation. Opacity is restated at 0.3 on both ends
   because that is the group's `opacity="0.3"` presentation attribute, and a
   CSS animation would otherwise reset it to 1 the moment the gate opened. */
@keyframes em-hn-rays {
  0%   { transform: rotate(0deg) scale(1);      opacity: 0.3; }
  28%  { transform: rotate(8.4deg) scale(1.08); opacity: 0.72; }
  55%  { transform: rotate(16.5deg) scale(1);   opacity: 0.34; }
  80%  { transform: rotate(24deg) scale(1.06);  opacity: 0.62; }
  100% { transform: rotate(30deg) scale(1);     opacity: 0.3; }
}
.em-hn-rays {
  --em-anim: em-hn-rays;
  /* the ray fan is symmetric so fill-box would land on the centre anyway, but
     a rotation pivot is worth pinning: view-box units are the badge's own */
  transform-box: view-box;
  transform-origin: 24px 24px;
  animation-duration: 6.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* THE PROUD TURN. Dead still for the first 40% so the move lands as an EVENT,
   then a 13deg wind-back (a telegraph — the eye is on the badge before it
   goes), one long haul round, a 9deg overshoot past true and a settle onto it.
   The star is a shared HnStar call site wrapped in a plain <g>, so this is
   rotating a group with no transform attribute of its own. */
@keyframes em-hn-star {
  0%, 40%   { transform: none; }
  47%       { transform: rotate(-13deg); }
  72%       { transform: rotate(298deg); }
  86%       { transform: rotate(369deg); }
  93%, 100% { transform: rotate(360deg); }
}
.em-hn-star {
  --em-anim: em-hn-star;
  /* a five-pointed star's bbox is NOT centred on the badge — one point up, two
     down — so fill-box would sit ~1.5 units low and the star would orbit
     instead of spinning. Pin it to the badge's true centre. */
  transform-box: view-box;
  transform-origin: 24px 24px;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.42, 0, .16, 1);
  animation-iteration-count: infinite;
}

/* The engraved bezel and its twelve studs, counter-turning against the star.
   Counter, not with: two rings turning the same way read as one flat spinning
   disc, whereas opposed rotation is what makes the badge feel machined. The
   scale is the ring flexing — 8.8 out to 9.4 user units, which stays clear of
   the star's inner vertices at 8.74 so the studs never crawl off the metal. */
@keyframes em-hn-bezel {
  0%   { transform: rotate(0deg) scale(1); }
  34%  { transform: rotate(-10.2deg) scale(1.07); }
  67%  { transform: rotate(-20.1deg) scale(0.96); }
  100% { transform: rotate(-30deg) scale(1); }
}
.em-hn-bezel {
  --em-anim: em-hn-bezel;
  transform-box: view-box;
  transform-origin: 24px 24px;
  animation-duration: 5.3s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* THE SIGNATURE — the specular. Clipped in the art to the star's own outline,
   so all this rule has to do is slide it across x and snap its opacity. It is
   invisible for the first 48%, hits full white 5% later (0.16s: a snap, not a
   fade), crosses the face, and is extinguished by 68% — the way a highlight
   dies as the viewing angle passes rather than sliding off the edge. The
   return to translateX(-10px) happens between 68% and 100%, entirely at
   opacity 0, so nothing is ever seen travelling backwards.
   `linear` on purpose: hard-edged ramps read as light on metal; an ease-in-out
   would read as something breathing. */
@keyframes em-hn-glint {
  0%, 48%, 100% { opacity: 0;    transform: translateX(-10px); }
  53%           { opacity: 1;    transform: translateX(4px); }
  62%           { opacity: 0.95; transform: translateX(22px); }
  68%           { opacity: 0;    transform: translateX(38px); }
}
.em-hn-glint {
  --em-anim: em-hn-glint;
  animation-duration: 3.2s;
  animation-delay: 0.85s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The centre boss flaring. Shared with the crest (scale and filter are
   unit-free), and geared to the glint on the same 3.2s period with 0.07s of
   extra delay: the light crosses the boss at ~60% of the glint's cycle and the
   boss answers at ~66% of its own. That lag is the whole point — the flare is
   a CONSEQUENCE of the specular passing over it, not a second thing pulsing.
   saturate() below 1 is doing real work: gold is already near-clipped in red
   and green, so brightness alone drives it to a flat acid yellow, while
   desaturating at the same time bends the flare toward white — which is what
   hot metal actually does. `filter: none` is the exact identity, so the off
   state is pixel-clean. r3.6 at scale 1.5 stays inside the r8.8 bezel. */
@keyframes em-hn-boss {
  0%, 58%, 100% { transform: none;        filter: none; }
  68%           { transform: scale(1.5);  filter: brightness(1.8) saturate(0.72); }
  80%           { transform: scale(1.14); filter: brightness(1.22) saturate(0.9); }
}
.em-hn-boss {
  --em-anim: em-hn-boss;
  animation-duration: 3.2s;
  animation-delay: 0.92s;
  animation-timing-function: cubic-bezier(.2, .85, .3, 1);
  animation-iteration-count: infinite;
}

/* The rim glints twinkling. One keyframe set, three rules across both art
   pieces: a twinkle is the same optical event everywhere, and what keeps them
   out of phase is period and delay, not bespoke curves. Each dies to almost
   nothing, pops to 1.65 with a kick of rotation — a four-point sparkle TURNING
   is what reads as light catching an edge, where a dot fading up reads as an
   LED — then returns to exactly the drawn pose. Opacity 1 on both ends because
   these are wrappers: the Spark path keeps its own 0.9 / 0.75 / 0.8 underneath. */
@keyframes em-hn-spark {
  0%, 100% { opacity: 1;    transform: none; }
  24%      { opacity: 0.12; transform: scale(0.3) rotate(-38deg); }
  52%      { opacity: 1;    transform: scale(1.65) rotate(12deg); }
  78%      { opacity: 0.28; transform: scale(0.55) rotate(-14deg); }
}
.em-hn-spark-a {
  --em-anim: em-hn-spark;
  animation-duration: 2.1s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}
.em-hn-spark-b {
  --em-anim: em-hn-spark;
  animation-duration: 2.9s;
  animation-delay: 0.6s;
  animation-direction: reverse; /* the two glints never brighten together */
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: HnCelebrationCrest (96 viewBox) ------------- */
/* Same choreography, grander and slower, because the crest is thrown by the
   jackpot capsule and then HELD on screen — it has time to build where a
   56px lobby tile is only glanced at. Every period differs from the emblem's
   so a hovered tile and a landed crest on screen together are not marching. */

/* The six noon rays (drawn as full diameters, so the set is self-similar every
   30deg exactly like the emblem's twelve). Proportional stops again for a
   steady turn under linear timing; opacity restated at the group's own 0.28.
   Peak scale 1.06 pushes the ray tips from 29 to 30.7 units, which keeps them
   inside the r31.5 iron ring — the rays must never cross the rosette rim or
   the crest stops reading as one turned object. */
@keyframes em-hn-krays {
  0%   { transform: rotate(0deg) scale(1);       opacity: 0.28; }
  30%  { transform: rotate(9deg) scale(1.06);    opacity: 0.7; }
  58%  { transform: rotate(17.4deg) scale(0.99); opacity: 0.32; }
  82%  { transform: rotate(24.6deg) scale(1.05); opacity: 0.6; }
  100% { transform: rotate(30deg) scale(1);      opacity: 0.28; }
}
.em-hn-krays {
  --em-anim: em-hn-krays;
  transform-box: view-box;
  transform-origin: 48px 48px;
  animation-duration: 7.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The great star's proud turn — the emblem's keyframes reused verbatim, since
   rotation is unit-free; only the pivot and the tempo change. Sharing the
   gesture is exactly what makes the two art pieces one machine. */
.em-hn-kstar {
  --em-anim: em-hn-star;
  /* the crest's star is crowning the gun crossing, so its centre is high in
     the viewBox — 48,36, not 48,48 */
  transform-box: view-box;
  transform-origin: 48px 36px;
  animation-duration: 5.4s;
  animation-timing-function: cubic-bezier(.42, 0, .16, 1);
  animation-iteration-count: infinite;
}

/* The crest's specular. Same shape of event as the emblem's, but the travel is
   in the crest's own user units (the star is R13.5 at 48,36 and spans x 35 to
   61, so the bar starts 10 units left of it and dies 32 units to the right). */
@keyframes em-hn-kglint {
  0%, 50%, 100% { opacity: 0;    transform: translateX(-10px); }
  55%           { opacity: 1;    transform: translateX(3px); }
  64%           { opacity: 0.95; transform: translateX(20px); }
  70%           { opacity: 0;    transform: translateX(32px); }
}
.em-hn-kglint {
  --em-anim: em-hn-kglint;
  animation-duration: 3.8s;
  animation-delay: 1.1s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The crest's boss flare — the emblem's keyframes, geared to the crest's
   glint (3.8s) with 0.08s of lag so it still answers the light rather than
   accompanying it. */
.em-hn-kboss {
  --em-anim: em-hn-boss;
  animation-duration: 3.8s;
  animation-delay: 1.18s;
  animation-timing-function: cubic-bezier(.2, .85, .3, 1);
  animation-iteration-count: infinite;
}

/* THE PAYOFF, crest only — the burst the specular leaves behind as it crosses
   the boss. Pure FX: 58% of the cycle at opacity 0, one frame-hard pop, then it
   blooms outward and dies. Scale starts and ends at 0.3 so the return trip
   happens while the burst is already invisible. At scale 1.1 its arms reach
   ~14.3 units, a shade past the star's 13.5 points — light spilling off the
   badge's edge, which is what sells the flash as coming FROM the metal. The
   crest gets this and the reel symbol does not: at 56px a burst this size
   would swamp the whole tile, but at 84px, held, it is the reward. */
@keyframes em-hn-kflare {
  0%, 58%, 100% { opacity: 0;   transform: scale(0.3) rotate(-20deg); }
  66%           { opacity: 1;   transform: scale(1.1) rotate(6deg); }
  74%           { opacity: 0.5; transform: scale(1.55) rotate(18deg); }
  82%           { opacity: 0;   transform: scale(1.95) rotate(26deg); }
}
.em-hn-kflare {
  --em-anim: em-hn-kflare;
  animation-duration: 3.8s;
  animation-delay: 1.22s;
  animation-timing-function: cubic-bezier(.15, .85, .3, 1);
  animation-iteration-count: infinite;
}

/* The crossed irons taking their weight. The one continuous, organic gesture
   on the machine — everything else is metal and light — so it gets ease-in-out
   and the longest period of the crest's own parts: the pair settles, hefts up
   through 55% and rocks back to true. Rotation is about the crossing point
   (the wrapper's bbox centre lands on it), so the muzzles rise and fall like a
   held pair of guns instead of the whole X sliding. Amplitudes are ~2.5 units
   at 96, about 2px at 84px display, which is enough to see and small enough
   that the barrels never break the rosette rim. */
@keyframes em-hn-kguns {
  0%, 100% { transform: none; }
  26%      { transform: translateY(2.2px) rotate(-3.2deg) scale(1.02); }
  55%      { transform: translateY(-2.4px) rotate(2.4deg) scale(1.05); }
  80%      { transform: translateY(0.8px) rotate(-1.2deg) scale(1.01); }
}
.em-hn-kguns {
  --em-anim: em-hn-kguns;
  animation-duration: 5.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The crest's big rim glint, on the shared twinkle. */
.em-hn-kspark {
  --em-anim: em-hn-spark;
  animation-duration: 2.6s;
  animation-delay: 0.3s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* ============================================================
   CLOCKWORK CARNIVAL (em-cwk-*) — THE RINGMASTER TIPS HIS HAT
   ------------------------------------------------------------
   One wound-up flourish, played twice in two languages. On the reel symbol the
   Ringmaster does it literally: the baton winds back and twirls a full turn,
   the top hat lifts off the crown of his head and sweeps down across his face,
   the head bows on the collar and the whole tailcoat dips with it, then the
   spring unwinds and everything reseats. On the ceremonial crest the SAME
   gesture is played by the tent — the big top IS this machine's hat, so the
   canopy lifts off its walls and tips, the gear-train under it gets cranked
   hard, and the gaslight the tent was hiding floods out through the gap.

   WHY THE FLOURISH SHARES ONE PERIOD (4.6s on the reel symbol, 5.4s on the
   crest): hat, head, torso and baton are one event in four parts, and the
   canopy, the walls, the crank and the light spill are one event in four parts.
   Split them across four different loop lengths and the hat would come off
   while the head was already straightening — the linkage would read as broken.
   Everything that is NOT geared to the flourish gets its own odd period —
   the bow tie 3.3s, the shoulder boards 2.7s, the steam 2.9s, the pennant
   2.2s, the entrance lamp 3.4s — so the machine is never still between
   flourishes and nothing ever pulses in lockstep. The crest runs slower than
   the reel symbol on purpose: a lobby tile and a landed jackpot crest on
   screen together must not march in step.

   The house easing here is SPRUNG, not organic: overshooting cubic-beziers
   (y > 1) on the hat, the shoulder boards and the canopy so brass parts arrive
   with a bounce off their stop, and hard hold-then-snap curves everywhere else.
   This is an automaton under a wound spring, not something breathing.

   Every keyframe set starts and ends on the drawn pose — `transform: none`, or
   `rotate(360deg)` where a part completes a revolution, which is the same pose
   geometrically but does not whip the part backwards through a whole turn on
   the last segment. So the art is pixel-identical to the static drawing the
   moment the gate closes.
   ============================================================ */

/* ---- the reel symbol / lobby face: RingmasterWildIcon (48 viewBox) ---- */

/* THE SIGNATURE — the hat comes off. It is dead still for the first third of
   the cycle so the doff lands as an EVENT rather than a hat permanently
   wobbling on his head; then a 1.5-unit lift telegraphs it, the sweep hauls
   round to -34deg and HOLDS there through the bow, and the spring returns it
   to true. It pivots on the crown of his head (24,11 — the underside of the
   brim), not on the hat's own bbox centre: rotating about the head is what
   makes the brim's right tip rise 7 units and its left tip drop past his ear,
   which is the whole gesture. Pivoting on the hat's middle instead would swing
   the crown clean off the top of the 48-unit viewBox. */
@keyframes em-cwk-hat {
  0%, 32%, 100% { transform: none; }
  42%           { transform: translateY(-1.5px) rotate(-9deg); }
  56%           { transform: translate(-1px, -2.5px) rotate(-34deg); }
  72%           { transform: translate(-1px, -2.2px) rotate(-31deg); }
  88%           { transform: translateY(-0.6px) rotate(-8deg); }
}
.em-cwk-hat {
  --em-anim: em-cwk-hat;
  transform-box: view-box;
  transform-origin: 24px 11px; /* the crown of his head, under the brim */
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.34, 1.3, .5, 1); /* sprung: it kicks off its stop */
  animation-iteration-count: infinite;
}

/* THE BOW. The head pivots on the collar (24,23) rather than its own centre, so
   15deg of rotation sinks it into the shoulders and cocks the moustache to one
   side — a bow — where a centre pivot would just spin the face like a dial. It
   starts at 40%, a beat AFTER the hat has begun to lift: cause, then courtesy.
   The extra translateY drops it far enough (about 2.5px at 56px) to read at
   lobby-tile size. */
@keyframes em-cwk-head {
  0%, 38%, 100% { transform: none; }
  50%           { transform: rotate(6deg) translateY(0.9px); }
  64%           { transform: rotate(15deg) translateY(2.1px); }
  78%           { transform: rotate(9deg) translateY(1.2px); }
  92%           { transform: rotate(2deg); }
}
.em-cwk-head {
  --em-anim: em-cwk-head;
  transform-box: view-box;
  transform-origin: 24px 23px; /* the collar, not the face */
  animation-duration: 4.6s;
  animation-delay: 0.1s;
  animation-timing-function: cubic-bezier(.4, 0, .25, 1);
  animation-iteration-count: infinite;
}

/* The body dips with the head. Pivoting on the hem (24,44) means scaleY
   compresses the tailcoat toward the floor, so the shoulders drop about two
   units — almost exactly how far the head sinks — and the figure bows as one
   piece instead of a head sliding down a rigid coat. The couple of degrees of
   lean either side is the wound spring letting go, and it is deliberately
   small: the coat tails must stay planted on the floor line. */
@keyframes em-cwk-torso {
  0%, 40%, 100% { transform: none; }
  54%           { transform: scaleY(0.95) rotate(-2.2deg); }
  68%           { transform: scaleY(0.9) rotate(1.4deg); }
  84%           { transform: scaleY(0.97) rotate(0.4deg); }
}
.em-cwk-torso {
  --em-anim: em-cwk-torso;
  transform-box: view-box;
  transform-origin: 24px 44px; /* the hem, so the coat compresses downward */
  animation-duration: 4.6s;
  animation-delay: 0.16s;
  animation-timing-function: cubic-bezier(.36, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* THE BATON TWIRL. Held, wound back 26deg against the wrist, then one full
   revolution about the middle of the stick and dogged home. It ends on
   rotate(360deg) instead of `none` because a full turn IS the drawn pose — so
   the loop seam and the moment the gate closes are both invisible, and the last
   segment does not unwind the whole turn backwards. It leads the hat slightly
   (0.05s) because a showman flourishes the stick to draw the eye BEFORE the
   hat comes off. */
@keyframes em-cwk-baton {
  0%, 28%   { transform: none; }
  36%       { transform: rotate(-26deg); }
  54%       { transform: rotate(178deg); }
  70%       { transform: rotate(332deg); }
  78%, 100% { transform: rotate(360deg); }
}
.em-cwk-baton {
  --em-anim: em-cwk-baton;
  animation-duration: 4.6s;
  animation-delay: 0.05s;
  animation-timing-function: cubic-bezier(.45, 0, .15, 1); /* snap round, then hold */
  animation-iteration-count: infinite;
}

/* The bow tie treated as the figure's escapement wheel — a loose brass cog that
   free-spins whenever it likes. Ends on rotate(360deg) for the same reason as
   the baton (the two triangles are only ALMOST 2-fold symmetric, so a half turn
   would shift the drawing by a fifth of a unit and the seam would show). On
   3.3s against the flourish's 4.6s, so it lands in a different place every
   time round and the figure keeps a tick even between doffs. */
@keyframes em-cwk-tie {
  0%, 42%   { transform: none; }
  54%       { transform: rotate(196deg); }
  68%       { transform: rotate(330deg); }
  76%, 100% { transform: rotate(360deg); }
}
.em-cwk-tie {
  --em-anim: em-cwk-tie;
  animation-duration: 3.3s;
  animation-timing-function: cubic-bezier(.55, 0, .2, 1);
  animation-iteration-count: infinite;
}

/* The brass shoulder boards jolting on their pins. One keyframe set drives both
   because the gesture is mirror-safe by construction — translateY and a scaleX
   squeeze about each board's own centre look identical on the left and right
   shoulder, where a rotation would have to change sign — and what keeps the
   pair from jolting together is the 0.33s delay set inline in the art, where
   which shoulder is which is known. The overshooting curve gives them the
   loose-rivet rattle of a machine that has been running all night. */
@keyframes em-cwk-tab {
  0%, 44%, 100% { transform: none; }
  56%           { transform: translateY(2px) scaleX(0.84); }
  70%           { transform: translateY(1.1px) scaleX(1.08); }
  86%           { transform: translateY(0.3px) scaleX(0.97); }
}
.em-cwk-tab {
  --em-anim: em-cwk-tab;
  animation-duration: 2.7s;
  animation-timing-function: cubic-bezier(.3, 1.5, .5, 1); /* loose rivets rattle */
  animation-iteration-count: infinite;
}

/* FX — the vent puff. This showman is a steam automaton, and nothing sells that
   like exhaust, so a puff slips out from behind his shoulder, swells and drifts
   off. It runs continuously on its own 2.9s clock: the flourish is the event,
   the steam is the idle that proves the machine is under pressure between
   events. Returns to opacity 0 at 0%/100%, as every em-fx part must, which is
   also what hides the transform snapping back at the loop seam. */
@keyframes em-cwk-steam {
  0%, 100% { opacity: 0;    transform: none; }
  20%      { opacity: 0.9;  transform: translateY(-0.4px) scale(1.05); }
  52%      { opacity: 0.72; transform: translate(1px, -3.6px) scale(1.35); }
  84%      { opacity: 0;    transform: translate(2px, -7.5px) scale(1.7); }
}
.em-cwk-steam {
  --em-anim: em-cwk-steam;
  animation-duration: 2.9s;
  animation-delay: 0.4s;
  animation-timing-function: cubic-bezier(.25, .5, .3, 1); /* punches out, then drifts */
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: CwkCelebrationCrest (96 viewBox) ---- */
/* Same flourish, tent language, slower (5.4s) because the crest is thrown by
   the jackpot capsule and then HELD on screen, so the lift has room to build. */

/* THE SIGNATURE, crest edition — the big top doffs itself. The canopy is this
   art's top hat: it lifts about five units clear of the walls and tips 6.5deg,
   which raises the right hem roughly eight units and the left only two, so the
   tent reads as tipping rather than levitating. Those amounts are chosen
   against the drawing: the scalloped hem overlaps the wall tops by about three
   units at rest, so anything less than a four-unit lift would never open a gap
   at all, and much more than five would tear a hole of dark field wide enough
   to look broken instead of lit. It holds the raised pose from 60% to 76% —
   that hold is the bow. Rotating about the canopy's own bbox centre keeps the
   mast and pennant inside the medallion ring at full tip. */
@keyframes em-cwk-crest-canopy {
  0%, 34%, 100% { transform: none; }
  46%           { transform: translateY(-2px) rotate(-2deg); }
  60%           { transform: translate(-1.5px, -5px) rotate(-6.5deg); }
  76%           { transform: translate(-1.2px, -4.4px) rotate(-6deg); }
  90%           { transform: translateY(-1.2px) rotate(-1.5deg); }
}
.em-cwk-crest-canopy {
  --em-anim: em-cwk-crest-canopy;
  animation-duration: 5.4s;
  animation-timing-function: cubic-bezier(.34, 1.22, .5, 1); /* sprung, like the hat */
  animation-iteration-count: infinite;
}

/* The canvas hauls taut as the canopy goes up — the crest's answer to the
   Ringmaster's coat dipping. Pivoting on the platform (48,68) means scaleY
   stretches the walls UPWARD from the show floor, which is the direction a tent
   is actually raised, and it keeps the doorway seated on the boards where it is
   drawn. The slight scaleX pinch is the canvas narrowing as it lifts, and it is
   what stops the stretch reading as a flat vertical smear. */
@keyframes em-cwk-crest-walls {
  0%, 36%, 100% { transform: none; }
  50%           { transform: scaleY(1.05) scaleX(0.99); }
  64%           { transform: scaleY(1.11) scaleX(0.975); }
  80%           { transform: scaleY(1.06) scaleX(0.985); }
}
.em-cwk-crest-walls {
  --em-anim: em-cwk-crest-walls;
  transform-box: view-box;
  transform-origin: 48px 68px; /* the show floor: the tent grows up off it */
  animation-duration: 5.4s;
  animation-delay: 0.06s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* THE PAYOFF — the gaslight inside the tent, which the static drawing never
   shows. It lights at half strength first, while the canopy is only a couple of
   units up: that is the glow escaping the scalloped hem, the tell that
   something is opening. It reaches full only with the tent at its highest, and
   it GROWS as it brightens (scaleY, so the band swells up out of the gap rather
   than switching on flat), then dies before the canopy reseats. Both the
   opacity and the scale are back to their dead pose at 0%/100%, so a crest that
   has just landed shows the pure drawing on its first frame. */
@keyframes em-cwk-crest-spill {
  0%, 32%, 100% { opacity: 0;    transform: scaleY(0.45); }
  46%           { opacity: 0.5;  transform: scaleY(0.8); }
  62%           { opacity: 1;    transform: scaleY(1); }
  80%           { opacity: 0.68; transform: scaleY(0.94); }
}
.em-cwk-crest-spill {
  --em-anim: em-cwk-crest-spill;
  animation-duration: 5.4s;
  animation-delay: 0.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE CRANK. The gear-train is what raises the tent, so it must be seen doing
   work: each cog winds back 14deg against its stop, drives a full revolution
   and dogs home on rotate(360deg) — the drawn pose, since a whole turn is
   pixel-identical while a partial one is not (CwkGear paints a directional
   rim-light across the top of every cog, so the silhouette's rotational
   symmetry is broken and only 360 will do). This rides ON TOP of the constant
   slow SMIL idle inside CwkGear, which is why the cogs read as a running
   machine being leaned on rather than three loops starting from rest. */
@keyframes em-cwk-crest-cog {
  0%, 32%   { transform: none; }
  40%       { transform: rotate(-14deg); }
  56%       { transform: rotate(172deg); }
  72%       { transform: rotate(332deg); }
  80%, 100% { transform: rotate(360deg); }
}
/* The drive gear leads; the outer pair follow it down the train on staggered
   delays, so the crank travels along the linkage instead of all three cogs
   lurching at once. Each pivots on its own axle (view-box), because a 9-tooth
   silhouette's bounding box is not quite centred on its axle and the default
   fill-box pivot would make a cranked cog visibly orbit. */
.em-cwk-crest-crank {
  --em-anim: em-cwk-crest-cog;
  transform-box: view-box;
  transform-origin: 48px 78px; /* the centre drive gear's axle */
  animation-duration: 5.4s;
  animation-timing-function: cubic-bezier(.45, 0, .12, 1);
  animation-iteration-count: infinite;
}
.em-cwk-crest-cog-l {
  --em-anim: em-cwk-crest-cog;
  transform-box: view-box;
  transform-origin: 23px 70px;
  animation-duration: 5.4s;
  animation-delay: 0.12s;
  animation-timing-function: cubic-bezier(.45, 0, .12, 1);
  animation-iteration-count: infinite;
}
.em-cwk-crest-cog-r {
  --em-anim: em-cwk-crest-cog;
  transform-box: view-box;
  transform-origin: 73px 70px;
  animation-duration: 5.4s;
  animation-delay: 0.24s;
  animation-timing-function: cubic-bezier(.45, 0, .12, 1);
  animation-iteration-count: infinite;
}

/* The pennant snapping at the masthead — the fastest thing on the crest (2.2s)
   and the one part that never stops, so the tent is alive even between lifts.
   It pivots at the mast (48,12.5) rather than the flag's centre, so the free
   end whips and the hoist end stays nailed to the pole; the scaleX squeeze is
   the flag turning edge-on into the gust, which is what makes cloth out of a
   rotating triangle. It rides the canopy's lift because it is nested inside
   that group, so the flourish carries it up without any extra keyframes. */
@keyframes em-cwk-crest-pennant {
  0%, 100% { transform: none; }
  20%      { transform: rotate(-12deg) scaleX(0.88); }
  46%      { transform: rotate(8deg) scaleX(1.07); }
  70%      { transform: rotate(-6deg) scaleX(0.94); }
  88%      { transform: rotate(3deg); }
}
.em-cwk-crest-pennant {
  --em-anim: em-cwk-crest-pennant;
  transform-box: view-box;
  transform-origin: 48px 12.5px; /* the hoist, at the mast */
  animation-duration: 2.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The gaslight over the entrance, guttering on its own 3.4s clock. Opacity
   alone could only ever dim it — the lamp is already drawn at full — so the
   flare is carried by brightness, with scale giving it somewhere to bloom;
   `filter: none` at 0%/100% is the identity, so the off state is exact. It
   swells to 1.45 because at four and a half units across it is small enough
   that a subtle pulse would vanish at crest size. */
@keyframes em-cwk-crest-lamp {
  0%, 100% { transform: none; filter: none; }
  28%      { transform: scale(0.86); filter: brightness(0.82); }
  54%      { transform: scale(1.45); filter: brightness(1.7) saturate(0.9); }
  78%      { transform: scale(1.08); filter: brightness(1.15); }
}
.em-cwk-crest-lamp {
  --em-anim: em-cwk-crest-lamp;
  animation-duration: 3.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ============================================================
   HIVE OF MIDAS (em-hm-*) — THE QUEEN HOVERS AND BUZZES
   ------------------------------------------------------------
   The signature is the wingbeat, and a wingbeat is an EVENT, not a tremble: the
   Queen sits with her wings at rest for the first quarter of the cycle, then all
   four of them tear through a dozen half-strokes in about a second — fast enough
   that the eye reads a blur rather than individual flaps — and the beat decays as
   she settles. A hidden fan of wing GHOSTS (em-fx) fades in underneath while that
   burst runs, which is what turns four rotating ellipses into a smear of motion at
   56px. Everything the burst does not carry runs continuously on its own clock so
   she is never completely still between bursts: the whole body drifts up and down,
   the two antennae bob independently, the crown throws a glint, the abdomen pumps
   and flexes its sting, the honey drops stretch under their own weight.
   NOTHING here shares a period with anything it is not physically coupled to —
   the flutter is 2.2s on the reel symbol and 2.6s on the crest, the hover 3.4s /
   4.1s, the feelers 1.9s and 2.3s — so the art never thumps in lockstep.

   The two art pieces are the same insect at two sizes. The reel symbol / lobby
   face is the crowned Queen herself with four grand wings (em-hm-*); the
   ceremonial crest is that Queen enthroned on the comb medallion under the great
   crown (em-hm-k*), and her wings are the same gesture at bigger angles because
   they are physically smaller there — a 10deg beat that sweeps six units on a
   13-unit forewing would be invisible on a 6-unit one.

   WHY THE WINGS PIVOT ON A view-box POINT: each wing must hinge at its ROOT
   (where the membrane meets the thorax), not at its own bbox centre, or it
   paddles about its middle like an oar instead of beating. On the reel symbol the
   wing membrane and its vein are two separate elements that must stay welded
   together, and the only way to guarantee that is to rotate both about one shared
   absolute point — hence transform-box: view-box and an explicit pivot in
   48-unit art coordinates. Inside the crest's bee the art already sits under a
   translate()/scale() of its own, where view-box coordinates would land in the
   wrong place, so those pivots are given as fill-box percentages instead.
   ============================================================ */

/* ---- the lobby emblem / reel symbol: QueenBeeIcon (48 viewBox) ---------- */

/* THE BODY HOVERING. Three units of lift at a 48-unit viewBox is ~3.5px on a
   56px tile — enough to see, small enough that she still reads as holding
   station rather than flying away — and the slight roll on the way through
   keeps it from looking like a lift in a shaft. Deliberately NOT the flutter's
   period: a real bee's body drifts on a slow cycle while the wings do their own
   thing, and the two rhythms sliding past each other is most of why this reads
   as alive. */
@keyframes em-hm-hover {
  0%, 100% { transform: none; }
  22%      { transform: translateY(-1.4px) rotate(-1.5deg); }
  46%      { transform: translateY(-3px) rotate(0.6deg); }
  70%      { transform: translateY(-0.6px) rotate(1.6deg); }
  88%      { transform: translateY(1.2px) rotate(0.4deg); }
}
.em-hm-hover {
  --em-anim: em-hm-hover;
  animation-duration: 3.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE SIGNATURE — the beat itself. Held flat until 24%, then fourteen strokes at
   4% of a 2.2s cycle each (about 90ms a stroke, ~5.7Hz) before it decays back to
   the drawn pose by 82% and rests. The amplitude decays across the burst rather
   than stopping dead, which is how a wing actually spools down. `linear` timing
   is essential: any ease would round the tops off the strokes and turn the blur
   back into visible flapping. The up-stroke (positive on the left wing, negative
   on the right) is the bigger of the two because the recovery stroke of a real
   wingbeat is the shallower one. */
@keyframes em-hm-wing-l {
  0%, 24%, 82%, 100% { transform: none; }
  28% { transform: rotate(10deg); }
  32% { transform: rotate(-7deg); }
  36% { transform: rotate(10deg); }
  40% { transform: rotate(-7deg); }
  44% { transform: rotate(10deg); }
  48% { transform: rotate(-6deg); }
  52% { transform: rotate(9deg); }
  56% { transform: rotate(-6deg); }
  60% { transform: rotate(8deg); }
  64% { transform: rotate(-5deg); }
  68% { transform: rotate(6deg); }
  72% { transform: rotate(-4deg); }
  76% { transform: rotate(4deg); }
  79% { transform: rotate(-2deg); }
}
@keyframes em-hm-wing-r {
  0%, 24%, 82%, 100% { transform: none; }
  28% { transform: rotate(-10deg); }
  32% { transform: rotate(7deg); }
  36% { transform: rotate(-10deg); }
  40% { transform: rotate(7deg); }
  44% { transform: rotate(-10deg); }
  48% { transform: rotate(6deg); }
  52% { transform: rotate(-9deg); }
  56% { transform: rotate(6deg); }
  60% { transform: rotate(-8deg); }
  64% { transform: rotate(5deg); }
  68% { transform: rotate(-6deg); }
  72% { transform: rotate(4deg); }
  76% { transform: rotate(-4deg); }
  79% { transform: rotate(2deg); }
}

/* The forewings. Pivot = the inner tip of the membrane where it meets the
   thorax, in art units. The vein path carries this very same class so membrane
   and vein turn as one rigid body — that is the whole reason the pivot is an
   absolute point instead of a percentage of each element's own box. */
.em-hm-wing-ul {
  --em-anim: em-hm-wing-l;
  transform-box: view-box;
  transform-origin: 23px 14px;
  animation-duration: 2.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-hm-wing-ur {
  --em-anim: em-hm-wing-r;
  transform-box: view-box;
  transform-origin: 25px 14px;
  animation-duration: 2.2s;
  animation-delay: 0.03s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* The hindwings share the forewings' keyframes — a bee's two pairs are coupled,
   so they must beat the same shape — but they hinge lower on the thorax and are
   only 8 units long against 13, so the identical angles give them a visibly
   shorter sweep for free. The few hundredths of a second of delay stop all four
   from being one flat shape and are what makes the pair read as separate
   membranes catching the light. */
.em-hm-wing-ll {
  --em-anim: em-hm-wing-l;
  transform-box: view-box;
  transform-origin: 21.5px 24px;
  animation-duration: 2.2s;
  animation-delay: 0.07s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-hm-wing-lr {
  --em-anim: em-hm-wing-r;
  transform-box: view-box;
  transform-origin: 26.5px 24px;
  animation-duration: 2.2s;
  animation-delay: 0.1s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* THE BLUR. Pure effect: four frozen ghosts of the forewings that only exist
   while the burst is running. They come up hard as the beat starts, hang at a
   low haze through it and are gone before the wings settle, so the Queen never
   has a second pair of wings while she is at rest. Opacity only — the ghosts are
   already drawn at the extremes of the arc, so moving them would fight the
   illusion. Returns to 0 at 0%/100%, as every em-fx part must. */
@keyframes em-hm-haze {
  0%, 24%, 84%, 100% { opacity: 0; }
  30% { opacity: 0.3; }
  50% { opacity: 0.24; }
  70% { opacity: 0.17; }
  78% { opacity: 0.09; }
}
.em-hm-haze {
  --em-anim: em-hm-haze;
  animation-duration: 2.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE ABDOMEN pumping and flexing its sting. Pivot at the waist (24,22 in art
   units) rather than the abdomen's centre, so the swing is a tail wagging from
   the joint and the sting at the tip travels nearly four units — the furthest
   any part of her moves. The scaleY riding along with the rotation is the
   abdomen breathing: a bee at hover visibly pumps its gaster to shift air. */
@keyframes em-hm-sting {
  0%, 42%, 100% { transform: none; }
  52%           { transform: rotate(8deg) scaleY(1.06); }
  64%           { transform: rotate(-9deg) scaleY(0.95); }
  76%           { transform: rotate(5deg) scaleY(1.03); }
  88%           { transform: rotate(-2deg); }
}
.em-hm-sting {
  --em-anim: em-hm-sting;
  transform-box: view-box;
  transform-origin: 24px 22px;
  animation-duration: 2.7s;
  animation-delay: 0.45s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE CROWN GLINTING. At six units wide a crown cannot usefully move, so the
   gesture is carried by light: a hard brightness flare with a small rock and
   swell about its BASE (50% 100%), which grows the spikes upward the way a
   catching highlight does instead of inflating the whole thing off her head.
   saturate() below 1 bends the flare toward white — gold is already near-clipped
   in red and green, so brightness alone would only turn it acid yellow. */
@keyframes em-hm-crown {
  0%, 54%, 100% { transform: none; filter: none; }
  62%           { transform: scale(1.12) rotate(-4deg); filter: brightness(1.6) saturate(0.8); }
  72%           { transform: scale(1.04) rotate(2deg); filter: brightness(1.22) saturate(0.92); }
  84%           { transform: scale(1.01); filter: brightness(1.06); }
}
.em-hm-crown {
  --em-anim: em-hm-crown;
  transform-origin: 50% 100%; /* the band it sits on, so it flares upward */
  animation-duration: 3.1s;
  animation-delay: 0.2s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

/* THE ANTENNAE. Each feeler hinges at its own base on the head, and the two get
   mirrored keyframes and mismatched periods (1.9s against 2.3s) rather than one
   shared animation: antennae that swing together read as a single forked wire
   moving in the wind, where two feelers casting about independently read as an
   animal sensing. Twenty-odd degrees on a six-unit feeler is about two units of
   travel, which is the least this machine can get away with at 56px. */
@keyframes em-hm-ant-l {
  0%, 100% { transform: none; }
  26%      { transform: rotate(-22deg); }
  52%      { transform: rotate(10deg); }
  76%      { transform: rotate(-8deg); }
}
@keyframes em-hm-ant-r {
  0%, 100% { transform: none; }
  26%      { transform: rotate(22deg); }
  52%      { transform: rotate(-10deg); }
  76%      { transform: rotate(8deg); }
}
.em-hm-ant-l {
  --em-anim: em-hm-ant-l;
  transform-box: view-box;
  transform-origin: 21px 11px; /* where the feeler leaves the head */
  animation-duration: 1.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-hm-ant-r {
  --em-anim: em-hm-ant-r;
  transform-box: view-box;
  transform-origin: 27px 11px;
  animation-duration: 2.3s;
  animation-delay: 0.35s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: HiveCelebrationCrest (96 viewBox) ------------ */
/* Same insect, same choreography, every clock slower: the crest is thrown by the
   jackpot capsule and then HELD on screen, so it has time to build, and running
   it off the reel symbol's tempo means a lobby tile and a landed crest on screen
   together are never marching in step. */

@keyframes em-hm-khover {
  0%, 100% { transform: none; }
  22%      { transform: translateY(-2.4px) rotate(-1.8deg); }
  46%      { transform: translateY(-5px) rotate(0.8deg); }
  70%      { transform: translateY(-1.2px) rotate(2deg); }
  88%      { transform: translateY(1.8px) rotate(0.5deg); }
}
.em-hm-khover {
  --em-anim: em-hm-khover;
  animation-duration: 4.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The royal bee's beat. Bigger angles than the reel symbol's Queen for the same
   apparent sweep: her wings are ~6 units long in a 96-unit box where the lobby
   face's are 13 in a 48-unit one, so 18deg here buys what 10deg buys there. */
@keyframes em-hm-kwing-l {
  0%, 26%, 84%, 100% { transform: none; }
  30% { transform: rotate(18deg); }
  34% { transform: rotate(-11deg); }
  38% { transform: rotate(18deg); }
  42% { transform: rotate(-11deg); }
  46% { transform: rotate(17deg); }
  50% { transform: rotate(-10deg); }
  54% { transform: rotate(16deg); }
  58% { transform: rotate(-9deg); }
  62% { transform: rotate(14deg); }
  66% { transform: rotate(-8deg); }
  70% { transform: rotate(11deg); }
  74% { transform: rotate(-6deg); }
  78% { transform: rotate(7deg); }
  81% { transform: rotate(-3deg); }
}
@keyframes em-hm-kwing-r {
  0%, 26%, 84%, 100% { transform: none; }
  30% { transform: rotate(-18deg); }
  34% { transform: rotate(11deg); }
  38% { transform: rotate(-18deg); }
  42% { transform: rotate(11deg); }
  46% { transform: rotate(-17deg); }
  50% { transform: rotate(10deg); }
  54% { transform: rotate(-16deg); }
  58% { transform: rotate(9deg); }
  62% { transform: rotate(-14deg); }
  66% { transform: rotate(8deg); }
  70% { transform: rotate(-11deg); }
  74% { transform: rotate(6deg); }
  78% { transform: rotate(-7deg); }
  81% { transform: rotate(3deg); }
}
/* fill-box percentages, not view-box units: this bee is drawn inside her own
   translate()/scale() group, where the crest's 96-unit coordinates no longer
   mean what they say. 98%/2% across and 18% down IS the wing root — the inner
   tip of the membrane against the thorax — measured off each wrapper's box. */
.em-hm-kwing-l {
  --em-anim: em-hm-kwing-l;
  transform-origin: 98% 18%;
  animation-duration: 2.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-hm-kwing-r {
  --em-anim: em-hm-kwing-r;
  transform-origin: 2% 18%;
  animation-duration: 2.6s;
  animation-delay: 0.04s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* The crest's wing smear, on the burst's own clock so it can never be caught
   glowing while the wings are folded. */
@keyframes em-hm-khaze {
  0%, 26%, 86%, 100% { opacity: 0; }
  32% { opacity: 0.34; }
  52% { opacity: 0.26; }
  72% { opacity: 0.19; }
  80% { opacity: 0.1; }
}
.em-hm-khaze {
  --em-anim: em-hm-khaze;
  animation-duration: 2.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* Her gaster pumping, hinged at the waist (50% 0% of a box whose top edge IS the
   waist) so the sting swings widest. A touch further than the lobby face's,
   because the crest is seen big and can carry it. */
@keyframes em-hm-ksting {
  0%, 44%, 100% { transform: none; }
  54%           { transform: rotate(10deg) scaleY(1.07); }
  66%           { transform: rotate(-11deg) scaleY(0.95); }
  78%           { transform: rotate(6deg) scaleY(1.03); }
  90%           { transform: rotate(-3deg); }
}
.em-hm-ksting {
  --em-anim: em-hm-ksting;
  transform-origin: 50% 0%; /* the waist, where the abdomen meets the head */
  animation-duration: 3.3s;
  animation-delay: 0.5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* POLLEN, pure effect: grains lifted off the comb by the downwash of the beat.
   Same 2.6s period as the wings and a 0.35s delay, which is the point — this is
   a CONSEQUENCE of the burst, not an ambient sparkle, so it must never drift out
   of phase with the thing that caused it. They rise, drift, thin out and are
   gone; the second group's own 1.05s delay keeps the two clouds from puffing in
   unison. */
@keyframes em-hm-kpollen {
  0%, 100% { opacity: 0; transform: none; }
  18%      { opacity: 0.85; transform: translateY(-1.5px); }
  56%      { opacity: 0.6; transform: translate(1.5px, -6px) scale(1.06); }
  84%      { opacity: 0; transform: translate(2.4px, -11px) scale(1.1); }
}
.em-hm-kpollen {
  --em-anim: em-hm-kpollen;
  animation-duration: 2.6s;
  animation-delay: 0.35s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

/* THE HONEY STRETCHING. Pivot at the pinch where the drop hangs off the comb
   (50% 0%), so scaleY lengthens it DOWNWARD as gravity loads it instead of
   growing it back up into the wax. It sags, necks in (scaleX under 1), then
   snaps back a little fatter than it started — the elastic recoil is what reads
   as honey rather than water. Each of the three drops runs on its own inline
   delay, because three drips falling together looks like a leaking pipe. */
@keyframes em-hm-kdrip {
  0%, 46%, 100% { transform: none; }
  60%           { transform: translateY(1.6px) scaleY(1.4) scaleX(0.9); }
  74%           { transform: translateY(2.6px) scaleY(1.15) scaleX(0.96); }
  88%           { transform: translateY(0.6px) scaleY(0.94) scaleX(1.04); }
}
.em-hm-kdrip {
  --em-anim: em-hm-kdrip;
  transform-origin: 50% 0%; /* the pinch against the comb edge */
  animation-duration: 3.9s;
  animation-timing-function: cubic-bezier(.3, 0, .25, 1);
  animation-iteration-count: infinite;
}

/* The great crown gleaming. Restrained on purpose — it is the largest object in
   the crest, so anything more than a percent or two of swell would read as the
   whole piece breathing. The light does the work; the rock about the band gives
   the highlight somewhere to travel. */
@keyframes em-hm-kcrown {
  0%, 46%, 100% { transform: none; filter: none; }
  56%           { transform: scale(1.035) rotate(-1.2deg); filter: brightness(1.3) saturate(0.9); }
  70%           { transform: scale(1.015) rotate(0.8deg); filter: brightness(1.12); }
  84%           { transform: none; filter: brightness(1.04); }
}
.em-hm-kcrown {
  --em-anim: em-hm-kcrown;
  transform-origin: 50% 100%; /* seated on the band, flaring upward */
  animation-duration: 3.7s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The five tip jewels. One keyframe set, one rule, and the per-jewel delay set
   inline in the art where the jewel's place on the crown is known — so the glint
   WALKS across the tips instead of all five popping at once, which is the
   difference between jewellery and a string of fairy lights. */
@keyframes em-hm-kjewel {
  0%, 62%, 100% { transform: none; filter: none; }
  70%           { transform: scale(1.4); filter: brightness(1.5) saturate(0.6); }
  82%           { transform: scale(1.12); filter: brightness(1.15) saturate(0.9); }
}
.em-hm-kjewel {
  --em-anim: em-hm-kjewel;
  animation-duration: 2.9s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

/* THE ATTENDANTS. These two are drawn by the shared <Bee> helper, so their wings
   cannot be tagged without leaking the flutter into every bee in the skin — the
   motion goes on a wrapper instead, and what sells them is FLIGHT: a slow drift
   around their station with the body tilting into each change of direction. The
   longest periods on the machine (4.6s and 5.3s, one of them reversed) because
   they are background life, and background life must not compete with the Queen. */
@keyframes em-hm-kbee {
  0%, 100% { transform: none; }
  24%      { transform: translate(1.6px, -3.4px) rotate(7deg); }
  52%      { transform: translate(-1.2px, -5px) rotate(-5deg); }
  78%      { transform: translate(-2.2px, -1.4px) rotate(3deg); }
}
.em-hm-kbee-a {
  --em-anim: em-hm-kbee;
  animation-duration: 4.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-hm-kbee-b {
  --em-anim: em-hm-kbee;
  animation-duration: 5.3s;
  animation-delay: 0.6s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------
   MAD LAB (em-ml-*) — THE REACTION GOES OFF
   ------------------------------------------------------------
   A reaction is a chain of causes, not a lava lamp, so the parts fire in order
   and most of them spend the first half of their cycle doing nothing at all:
   the burner catches → the brew heaves and the caustic glow blooms white-hot →
   the pressure has to go somewhere, so fumes force their way out of the neck →
   it all subsides to the drawn pose. Each link of that chain holds
   `transform: none` until roughly 45-55% of its cycle, which is what makes the
   surge read as an EVENT a player can point at instead of a constant jiggle.

   Under the surge, the parts that boil the whole time run on deliberately
   off-beat durations — the bubble train at 1.9s and 2.5s, the slosh at 2.6s,
   the burner flicker at 1.05s, against the 4.2s reaction — so the flask is
   never still, never in lockstep, and the surge lands on a different phase of
   the boil every time round.

   THE BUBBLES are the one trick worth explaining. A bubble has to rise, burst
   at the meniscus, and then a NEW one has to appear down at the base — but a
   keyframe set cannot restart halfway, so each bubble bursts to opacity 0,
   teleports back down WHILE INVISIBLE (that is the pair of steps one percent
   apart), and fades in again as it starts the next climb. The snap costs
   nothing because it happens at zero alpha, and 0%/100% are still the drawn
   pose, so switching the animation off is invisible.

   Both art pieces run the same chain — the 48-viewBox lobby face on a 4.2s
   beat, the ceremonial crest on a grander 4.8s one — so hovering the tile and
   winning the jackpot are recognisably the same machine at work.
   ------------------------------------------------------------ */

/* ---- the lobby emblem: ReactionFlaskIcon (48 viewBox) ---- */

/* The caustic glow IS the reaction's brightness, so it carries `filter:
   brightness` as well as scale: the halo is a radial gradient already at full
   alpha, and something at full alpha can only get hotter, not more opaque. It
   breathes shallowly while the mix charges (30%), sags back almost to the drawn
   pose (52%) so the bloom has somewhere to come from, then blows out at 64%. */
@keyframes em-ml-halo {
  0%, 100% { transform: none; filter: none; }
  30%      { transform: scale(1.05); filter: brightness(1.12); }
  52%      { transform: scale(1.02); filter: brightness(1.05); }
  64%      { transform: scale(1.32); filter: brightness(1.95); }
  78%      { transform: scale(1.12); filter: brightness(1.3); }
  90%      { transform: scale(1.03); filter: brightness(1.06); }
}
.em-ml-halo {
  --em-anim: em-ml-halo;
  animation-duration: 4.2s;
  animation-delay: 0.04s;
  animation-timing-function: cubic-bezier(.36, 0, .24, 1);
  animation-iteration-count: infinite;
}

/* The brew sloshes by ROTATING, which is how liquid in a round-bottom flask
   actually behaves — one side climbs the glass while the other drops. The tilt
   is capped at 3.6deg because past that the reagent's own corners leave the
   bulb and the fill visibly escapes its glass. The mid-cycle scaleY is the boil
   heaving the level up rather than a second slosh, so it sits between the two
   tilts instead of on top of one. */
@keyframes em-ml-brew {
  0%, 100% { transform: none; }
  18%      { transform: rotate(3.4deg) translateY(-0.4px); }
  42%      { transform: rotate(-1.2deg) translateY(-1px) scaleY(1.05); }
  66%      { transform: rotate(-3.6deg) translateY(-0.3px); }
  86%      { transform: rotate(1.4deg); }
}
.em-ml-brew {
  --em-anim: em-ml-brew;
  animation-duration: 2.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* Bubble one: the big one. It swells as it rises (pressure drops on the way up),
   bursts wide and vanishes at the meniscus around y=30 in the art, then reforms
   small at the bottom of the bulb. The two steps at 54%/55% are the invisible
   teleport back down described in the header. */
@keyframes em-ml-bub-a {
  0%, 100% { transform: none; opacity: 1; }
  30%      { transform: translateY(-2.4px) scale(1.12); opacity: 1; }
  46%      { transform: translateY(-4.4px) scale(1.34); opacity: 0.9; }
  54%      { transform: translateY(-5.2px) scale(1.75); opacity: 0; }
  55%      { transform: translateY(4.2px) scale(0.45); opacity: 0; }
  72%      { transform: translateY(2.6px) scale(0.7); opacity: 0.9; }
  88%      { transform: translateY(1px) scale(0.9); opacity: 1; }
}
.em-ml-bub-a {
  --em-anim: em-ml-bub-a;
  animation-duration: 1.9s;
  animation-timing-function: cubic-bezier(.4, 0, .6, 1);
  animation-iteration-count: infinite;
}

/* Bubble two: smaller, faster to burst, and on a duration that shares no factor
   with bubble one (2.5s against 1.9s) plus a delay, so the two never leave the
   base together — the single cheapest thing that stops a bubble train reading
   as a pair of blinking dots. It drifts left as it climbs, following the curve
   of the bulb wall. */
@keyframes em-ml-bub-b {
  0%, 100% { transform: none; opacity: 1; }
  26%      { transform: translate(-0.4px, -2.6px) scale(1.15); opacity: 1; }
  44%      { transform: translate(-0.8px, -5.4px) scale(1.4); opacity: 0.85; }
  52%      { transform: translate(-1px, -6.4px) scale(1.85); opacity: 0; }
  53%      { transform: translateY(3px) scale(0.4); opacity: 0; }
  72%      { transform: translateY(1.8px) scale(0.72); opacity: 0.85; }
  88%      { transform: translateY(0.6px) scale(0.92); opacity: 1; }
}
.em-ml-bub-b {
  --em-anim: em-ml-bub-b;
  animation-duration: 2.5s;
  animation-delay: 0.4s;
  animation-timing-function: cubic-bezier(.4, 0, .6, 1);
  animation-iteration-count: infinite;
}

/* The burner. Fire is the one thing here that should never hold a pose, so this
   runs fast and continuously on its own 1.05s beat with the steps at irregular
   percentages (22/48/72) so no lick repeats the last one. It pivots on its BASE,
   not its centre: growing a flame from its middle would push it down through the
   bench, whereas from the base it licks UP and over the bottom of the glass,
   which is the whole reason the flask is boiling. scaleX narrows as scaleY grows
   — flames neck in as they stretch — and the skew is the tongue leaning. */
@keyframes em-ml-flame {
  0%, 100% { transform: none; }
  22%      { transform: scaleY(1.28) scaleX(0.88) skewX(-9deg); }
  48%      { transform: scaleY(0.9) scaleX(1.12) skewX(5deg); }
  72%      { transform: scaleY(1.22) scaleX(0.94) skewX(10deg); }
}
.em-ml-flame {
  --em-anim: em-ml-flame;
  transform-origin: 50% 100%; /* the burner, not the middle of the flame */
  animation-duration: 1.05s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE SIGNATURE. Flat and invisible for the first 44%, then pressure wins and a
   fume jet forces its way out around the cork, timed to land just AFTER the glow
   blooms (delay 0.2s against the halo's 0.04s) so the player reads cause then
   effect. Pivoted at the cork seam so the plume grows out of the flask rather
   than inflating around its own middle, and it drifts right as it climbs so the
   vapour curls away instead of hanging there. em-fx, so opacity 0 at 0%/100%. */
@keyframes em-ml-fume {
  0%, 44%, 100% { transform: none; opacity: 0; }
  52%           { transform: translateY(-0.5px) scale(0.85); opacity: 0.5; }
  62%           { transform: translateY(-2px) scale(1.3); opacity: 1; }
  74%           { transform: translate(0.8px, -4px) scale(1.5); opacity: 0.5; }
  88%           { transform: translate(1.6px, -6px) scale(1.7); opacity: 0; }
}
.em-ml-fume {
  --em-anim: em-ml-fume;
  transform-origin: 50% 100%; /* the cork seam it escapes from */
  animation-duration: 4.2s;
  animation-delay: 0.2s;
  animation-timing-function: cubic-bezier(.3, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* The wisp that was already drawn above the cork gets carried off by that jet,
   one beat later again (0.34s) — the tail of the chain. It is part of the static
   drawing, so unlike the fx jet it starts and ends at opacity 1; it fades out as
   it dissipates, resets while invisible at 76%/77%, and re-forms at the cork. */
@keyframes em-ml-plume {
  0%, 46%, 100% { transform: none; opacity: 1; }
  60%           { transform: translate(0.8px, -3.5px) scale(1.5); opacity: 1; }
  76%           { transform: translate(1.6px, -6.5px) scale(1.9); opacity: 0; }
  77%           { transform: translateY(1.2px) scale(0.6); opacity: 0; }
  90%           { transform: translateY(0.5px) scale(0.85); opacity: 0.55; }
}
.em-ml-plume {
  --em-anim: em-ml-plume;
  animation-duration: 4.2s;
  animation-delay: 0.34s;
  animation-timing-function: cubic-bezier(.3, 0, .35, 1);
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: MadLabCelebrationCrest (96 viewBox) ---- */
/* Same chain, slower and larger: the crest is held on screen after the jackpot
   capsule lands, so the reaction gets room to build — 4.8s, and the burner is
   promoted to a visible cause at the head of it. */

/* The burner ROARS rather than idles here, because the crest flask stands on a
   brass plinth with no drawn flame: the fire is fx, so it may as well be the
   event that starts everything. Gas catches at 40% (a low, mean 0.72 of full
   height), flares, licks twice with the skew flipping side to side, then dies
   back to nothing. It pivots on the plinth line so every bit of that growth
   climbs the outside of the cone. */
@keyframes em-ml-c-burner {
  0%, 34%, 100% { transform: none; opacity: 0; }
  40%           { transform: scaleY(0.72) scaleX(0.94); opacity: 0.5; }
  50%           { transform: scaleY(1.16) scaleX(1.04) skewX(-4deg); opacity: 0.95; }
  58%           { transform: scaleY(0.98) scaleX(0.97) skewX(5deg); opacity: 0.9; }
  66%           { transform: scaleY(1.2) scaleX(1.02) skewX(-3deg); opacity: 0.85; }
  74%           { transform: scaleY(1.02) scaleX(0.96) skewX(4deg); opacity: 0.6; }
  86%           { transform: scaleY(0.66) scaleX(0.9); opacity: 0.18; }
}
.em-ml-c-burner {
  --em-anim: em-ml-c-burner;
  transform-origin: 50% 100%; /* the plinth the flame climbs from */
  animation-duration: 4.8s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* The white-hot core answers the burner (delay 0.1s): it swells nearly half as
   big again and goes brighter still at 62%, which is the peak of the whole
   ceremony. Capped at 1.46 because beyond that the core's edge would pass the
   glass wall of the cone — it must look like something seething INSIDE the
   flask, never something bursting out of it. */
@keyframes em-ml-c-core {
  0%, 100% { transform: none; filter: none; }
  26%      { transform: scale(1.07); filter: brightness(1.1); }
  50%      { transform: scale(1.03) translateY(-0.5px); filter: brightness(1.2); }
  62%      { transform: scale(1.46) translateY(-1.5px); filter: brightness(1.85); }
  74%      { transform: scale(1.2) translateY(-0.6px); filter: brightness(1.35); }
  88%      { transform: scale(1.06); filter: brightness(1.1); }
}
.em-ml-c-core {
  --em-anim: em-ml-c-core;
  animation-duration: 4.8s;
  animation-delay: 0.1s;
  animation-timing-function: cubic-bezier(.34, 0, .2, 1);
  animation-iteration-count: infinite;
}

/* The acid halo takes its cue from the core, a fifth of a second behind it, so
   the light in the room grows out of the light in the flask. */
@keyframes em-ml-c-halo {
  0%, 100% { transform: none; filter: none; }
  32%      { transform: scale(1.05); filter: brightness(1.12); }
  56%      { transform: scale(1.02); filter: brightness(1.04); }
  66%      { transform: scale(1.26); filter: brightness(1.9); }
  80%      { transform: scale(1.11); filter: brightness(1.3); }
  92%      { transform: scale(1.03); filter: brightness(1.07); }
}
.em-ml-c-halo {
  --em-anim: em-ml-c-halo;
  animation-duration: 4.8s;
  animation-delay: 0.22s;
  animation-timing-function: cubic-bezier(.36, 0, .24, 1);
  animation-iteration-count: infinite;
}

/* The crest's slosh is deliberately tighter than the emblem's (3deg, not 3.6)
   and carries no scaleY: an Erlenmeyer cone NARROWS as it rises, so lifting or
   tilting the brew any further pushes its top corners straight through the
   glass wall. The boil shows as a small vertical heave instead, and the loud
   liquid motion is left to the bubbles, which have thirteen units of travel. */
@keyframes em-ml-c-brew {
  0%, 100% { transform: none; }
  20%      { transform: rotate(2.8deg) translateY(-0.5px); }
  44%      { transform: rotate(-1deg) translateY(-1.2px); }
  68%      { transform: rotate(-3deg) translateY(-0.4px); }
  86%      { transform: rotate(1.2deg); }
}
.em-ml-c-brew {
  --em-anim: em-ml-c-brew;
  animation-duration: 3.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* Crest bubble one, climbing the full depth of the cone from the base to the
   meniscus — nearly a sixth of the viewBox, which is what makes it legible at
   84px — swelling the whole way, bursting at the surface and reforming below. */
@keyframes em-ml-c-bub-a {
  0%, 100% { transform: none; opacity: 1; }
  28%      { transform: translate(0.6px, -6px) scale(1.15); opacity: 1; }
  46%      { transform: translate(1.2px, -12.5px) scale(1.45); opacity: 0.9; }
  54%      { transform: translate(1.4px, -15px) scale(1.9); opacity: 0; }
  55%      { transform: translateY(7px) scale(0.4); opacity: 0; }
  74%      { transform: translateY(4px) scale(0.7); opacity: 0.85; }
  90%      { transform: translateY(1.5px) scale(0.9); opacity: 1; }
}
.em-ml-c-bub-a {
  --em-anim: em-ml-c-bub-a;
  animation-duration: 2.2s;
  animation-timing-function: cubic-bezier(.4, 0, .6, 1);
  animation-iteration-count: infinite;
}

/* Crest bubble two starts lower and further right, so it drifts inward (-x) as
   it rises to follow the taper of the cone. Off-beat against bubble one (2.8s
   against 2.2s) and delayed, so the surface is never broken twice at once. */
@keyframes em-ml-c-bub-b {
  0%, 100% { transform: none; opacity: 1; }
  24%      { transform: translate(-0.8px, -5.5px) scale(1.2); opacity: 1; }
  44%      { transform: translate(-2px, -13px) scale(1.5); opacity: 0.85; }
  53%      { transform: translate(-2.6px, -16px) scale(2); opacity: 0; }
  54%      { transform: translateY(5px) scale(0.35); opacity: 0; }
  72%      { transform: translateY(3px) scale(0.65); opacity: 0.8; }
  90%      { transform: translateY(1px) scale(0.88); opacity: 1; }
}
.em-ml-c-bub-b {
  --em-anim: em-ml-c-bub-b;
  animation-duration: 2.8s;
  animation-delay: 0.45s;
  animation-timing-function: cubic-bezier(.4, 0, .6, 1);
  animation-iteration-count: infinite;
}

/* THE SIGNATURE, crest edition: the mouth vents. Last link in the chain (delay
   0.36s, behind burner → core → halo), pivoted at the brass collar so it grows
   up out of the neck, and it climbs to the right as it thins. The rise is kept
   modest while it is bright and only pushes past the top of the viewBox once it
   has faded, because unlike the reel symbol this svg does not draw outside its
   box — a bold plume that got sliced off flat would be worse than a small one. */
@keyframes em-ml-c-fume {
  0%, 46%, 100% { transform: none; opacity: 0; }
  54%           { transform: translateY(-0.4px) scale(0.8); opacity: 0.5; }
  64%           { transform: translateY(-1.2px) scale(1.14); opacity: 1; }
  76%           { transform: translate(1.4px, -3px) scale(1.35); opacity: 0.45; }
  90%           { transform: translate(2.6px, -5.5px) scale(1.6); opacity: 0; }
}
.em-ml-c-fume {
  --em-anim: em-ml-c-fume;
  transform-origin: 50% 100%; /* the collar, so the fume leaves the neck */
  animation-duration: 4.8s;
  animation-delay: 0.36s;
  animation-timing-function: cubic-bezier(.3, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------------
   THE ABYSS (em-ab-*) — THE KRAKEN SWIMS
   ------------------------------------------------------------------
   The signature gesture here is not a bang, it is a stroke: the animal fills
   its mantle, necks it in and jets, and the wave of that push runs out through
   the arms a third of a beat later while the great eye closes over it. Nothing
   snaps. Every curve in this block is either ease-in-out or a symmetrical
   cubic-bezier(.45, 0, .55, 1) — water has mass, so a part accelerates and
   decelerates at both ends of every move, and the hard `cubic-bezier(x, 0, .1, 1)`
   snap the vault and the diamond machines use would read as a servo, not a
   creature. This is the slowest machine in the lobby by design.

   WHY THE SWIM SHARES ONE PERIOD (4.4s reel / 5.2s crest). Mantle, arms and the
   expelled water are one hydraulic event: the mantle contracts, the water leaves,
   the arms trail. Give them separate durations and within three cycles the arms
   would be clamping while the mantle was still filling and the animal would come
   apart into unrelated twitching sticks — exactly the failure the player
   complained about. What separates them is PHASE, not tempo:

       28% arms reach and extend  ·  40% the mantle fills  ·  56% it necks in and
       jets, the arms clamp, the water leaves  ·  ~80% recover  ·  then glide

   The four arms run the same two keyframe sets (one per side, mirrored) and are
   pulled apart purely by animation-delay: 0s / 0.14s for the lower pair, then a
   full third of a cycle later (1.45s / 1.6s) for the side pair. That lag IS the
   travelling wave — the near arms move first and the outer ones follow — and the
   0.14s slop between left and right is deliberate, because a creature that
   flexes in perfect bilateral symmetry reads as a windscreen wiper.

   Everything that is NOT part of the stroke runs free on its own off-beat clock
   so the art is never still and never pulses in lockstep: the bioluminescence
   breathes on 3.1s (reel) / 3.9s (crest), the eye blinks on 5.6s so it lands on
   a different part of the stroke every time, the suckers flicker on 1.9s/2.4s
   and the sparks twinkle on 2.15s/3.3s.

   Reel symbol (KrakenIcon, 48 viewBox) parts are em-ab-*; the ceremonial crest
   (AbyssCelebrationCrest, 96 viewBox) uses em-ab-k* because its pivots live in a
   different coordinate space. The crest is the same beast in ceremonial dress —
   arms cradling the Heart of the Abyss instead of trailing under a mantle — and
   it swims the same stroke a beat slower, because it is held on screen after the
   jackpot capsule lands rather than glanced at on a hovered tile.
   ------------------------------------------------------------------ */

/* THE BIOLUMINESCENCE BREATHES. Shared by the reel symbol's halo and the crest's
   hub glow: the same organ, so the same keyframes, and only the tempo differs.
   The opacity DIPS as the glow swells, which is what makes it read as light
   spreading out and thinning rather than a lamp on a dimmer — a fixed-brightness
   scale looks like a balloon. Ends on `transform: none; opacity: 1`, which is
   exactly the drawn pose, so the moment the pointer leaves is invisible. */
@keyframes em-ab-breathe {
  0%, 100% { transform: none; opacity: 1; }
  34%      { transform: scale(1.16); opacity: 0.66; }
  68%      { transform: scale(0.9);  opacity: 1; }
  86%      { transform: scale(1.03); opacity: 0.88; }
}

/* ---- reel symbol / lobby face (48 viewBox) ---------------------------- */

/* THE MANTLE JETS — the engine of the swim, and the one part of it that reads
   as an EVENT. It holds the drawn pose for the first fifth of the cycle, balloons
   wide and squat as it fills (40%), then necks in HARD and shoots tall (56%)
   before settling. Both halves of that are pure scale about the mantle's own
   base at (24,33), never a translate, and that pivot is doing two jobs: scaleY
   above 1 drives the dome UP by three units instead of stretching it down
   through the arms, so the lurch of propulsion comes for free; and the base
   itself never moves, so the arm roots at (24,26) — which this dome is drawn
   over the top of — stay plugged into the body.
   0.8 on X is a 27% swing in width across the cycle: about 4px of edge travel on
   a 56px lobby tile, which is the whole point of this change. */
@keyframes em-ab-mantle {
  0%, 20%, 100% { transform: none; }
  40%           { transform: scale(1.12, 0.92); }
  56%           { transform: scale(0.8, 1.14); }
  68%           { transform: scale(0.93, 1.06); }
  84%           { transform: scale(1.04, 0.98); }
}
.em-ab-mantle {
  --em-anim: em-ab-mantle;
  /* the mantle's base, not the group's bbox centre: contracting about the centre
     would pull the dome and the arm roots toward each other and the head would
     read as deflating in place instead of pumping */
  transform-box: view-box;
  transform-origin: 24px 33px;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.36, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* THE ARMS UNDULATE. Rotation about the arm's ROOT rather than its own middle is
   the whole trick — the tips sweep five or six units (~7px at 56px, bold enough
   to see) while the base stays welded to the body. The scale riding along with
   it is the curl: 1.05 reaches and straightens the arm out into the water, 0.9
   draws it back in and coils the tip under. Left and right are mirrored, not
   phase-shifted, so the skirt SPREADS at 28% and CLAMPS at 58% with the jet —
   running all four the same way would look like the animal was being blown
   sideways by a current instead of swimming. */
@keyframes em-ab-tent-l {
  0%, 100% { transform: none; }
  28%      { transform: rotate(11deg) scale(1.05); }
  58%      { transform: rotate(-10deg) scale(0.9); }
  80%      { transform: rotate(4deg) scale(1.01); }
}
@keyframes em-ab-tent-r {
  0%, 100% { transform: none; }
  28%      { transform: rotate(-11deg) scale(1.05); }
  58%      { transform: rotate(10deg) scale(0.9); }
  80%      { transform: rotate(-4deg) scale(1.01); }
}
/* All four arms leave the body at the same point, so they all pivot on (24,26).
   Only the delay differs — see the banner: the lower pair leads, the side pair
   follows a third of a beat behind, and that is the travelling wave. */
.em-ab-tent-l1,
.em-ab-tent-r1,
.em-ab-tent-l2,
.em-ab-tent-r2 {
  transform-box: view-box;
  transform-origin: 24px 26px;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.45, 0, .55, 1); /* water at both ends */
  animation-iteration-count: infinite;
}
.em-ab-tent-l1 { --em-anim: em-ab-tent-l; animation-delay: 0s; }
.em-ab-tent-r1 { --em-anim: em-ab-tent-r; animation-delay: 0.14s; }
.em-ab-tent-l2 { --em-anim: em-ab-tent-l; animation-delay: 1.45s; }
.em-ab-tent-r2 { --em-anim: em-ab-tent-r; animation-delay: 1.6s; }

/* THE GREAT EYE BLINKS. A cephalopod blink is slow and heavy, so this one takes
   about a fifth of a long cycle and holds shut for a beat in the middle of it
   (76% → 81%) rather than flickering. scaleY 0.08 collapses an eight-unit eye to
   a 1.3-unit slit — at 56px that is the brightest thing on the tile going out,
   which is why it needs no lid geometry to read. The 1.06 on X as it closes is
   the flesh squeezing sideways, and the 1.08 on Y at 90% is the eye opening past
   true before it settles, so the recovery has some snap-back life in it.
   5.6s is deliberately not a multiple of the 4.4s stroke: the blink lands on a
   different part of the swim every cycle and the machine never looks looped. */
@keyframes em-ab-blink {
  0%, 68%, 100% { transform: none; }
  76%           { transform: scale(1.06, 0.08); }
  81%           { transform: scale(1.07, 0.06); }
  90%           { transform: scale(1, 1.08); }
}
.em-ab-eye {
  --em-anim: em-ab-blink;
  animation-duration: 5.6s;
  animation-delay: 0.8s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* THE WATER LEAVING. Pure effect (em-fx), so it is at opacity 0 for two thirds of
   the cycle and the static drawing is untouched. It fades in only AFTER the
   mantle has started to neck in (54%, against the jet at 56%) — cause, then
   consequence — and pivots on the funnel at (24,27) so growing it pushes the
   plume DOWN between the arms instead of stretching it back up through the head.
   It keeps lengthening and thinning after the mantle has recovered, because
   expelled water carries on going, and it is fully transparent again before the
   transform snaps back so the reset is never seen. Peak opacity is 0.62, not 1:
   this is water, and water in a trench is a suggestion. */
@keyframes em-ab-jet {
  0%, 40%, 100% { opacity: 0;    transform: scale(0.8, 0.25); }
  54%           { opacity: 0.62; transform: scale(1, 0.8); }
  70%           { opacity: 0.4;  transform: scale(1.1, 1.05); }
  88%           { opacity: 0;    transform: scale(1.18, 1.15); }
}
.em-ab-jet {
  --em-anim: em-ab-jet;
  /* the funnel mouth, so the plume grows away from the body */
  transform-box: view-box;
  transform-origin: 24px 27px;
  animation-duration: 4.4s;
  animation-delay: 0.05s;
  animation-timing-function: cubic-bezier(.2, .6, .35, 1);
  animation-iteration-count: infinite;
}

.em-ab-glow {
  --em-anim: em-ab-breathe;
  animation-duration: 3.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest (96 viewBox) ------------------------------------ */
/* Same stroke, 5.2s instead of 4.4s. Slower because the crest is held big on
   screen after the capsule lands and has time to be watched — and slower than
   the reel symbol so that a hovered lobby tile and a landed jackpot crest on
   screen together are visibly two animals, not one animation drawn twice. */

.em-ab-kaura {
  --em-anim: em-ab-breathe;
  animation-duration: 3.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE ARMS CRADLE. The crest's arms are the reel symbol's arms in ceremonial
   pose, so they do the same reach-and-curl — but here the gather (56%, on the
   Heart's beat) closes them IN around the Heart until the tips tuck behind its
   lower edge, which is what turns two decorative arcs into something holding
   something. The sign convention is set by each arm's geometry rather than by
   eye: from a root at the crest's base, a positive rotation swings the left arm
   inward and the right arm outward, hence the mirrored sets. Amplitudes are
   capped at 13deg with 1.06 of reach because this crest's <svg> CLIPS (no
   overflow:visible, unlike the reel symbols) and the outer arm at full stretch
   already sits a couple of units off the viewBox edge. */
@keyframes em-ab-ktent-l {
  0%, 100% { transform: none; }
  26%      { transform: rotate(-7deg) scale(1.06); }
  56%      { transform: rotate(13deg) scale(0.93); }
  80%      { transform: rotate(-3deg) scale(1.02); }
}
@keyframes em-ab-ktent-r {
  0%, 100% { transform: none; }
  26%      { transform: rotate(7deg) scale(1.06); }
  56%      { transform: rotate(-13deg) scale(0.93); }
  80%      { transform: rotate(3deg) scale(1.02); }
}
.em-ab-ktent-l {
  --em-anim: em-ab-ktent-l;
  transform-box: view-box;
  transform-origin: 20px 74px; /* the left arm's root at the base of the crest */
  animation-duration: 5.2s;
  animation-timing-function: cubic-bezier(.45, 0, .55, 1);
  animation-iteration-count: infinite;
}
.em-ab-ktent-r {
  --em-anim: em-ab-ktent-r;
  transform-box: view-box;
  transform-origin: 76px 74px; /* the right arm's root */
  animation-duration: 5.2s;
  /* the same slop that keeps the reel symbol's arms out of bilateral lockstep,
     scaled to this crest's longer beat */
  animation-delay: 0.42s;
  animation-timing-function: cubic-bezier(.45, 0, .55, 1);
  animation-iteration-count: infinite;
}

/* THE HEART BEATS — the crest's answer to the mantle's jet, on the same beat of
   the same clock, because it is the same contraction. It swells full (40%),
   squeezes to 0.87 (56%) and flares as it does: the squeeze alone is 21% of the
   Heart's width across the cycle, ~4px at 84px, but it is the brightness that
   sells it as a pulse of living light rather than a rubber ball, and opacity
   could only ever DIM art that is already drawn at full strength. `filter: none`
   at 0%/100% is the exact identity, so the off state is pixel-clean. */
@keyframes em-ab-kheart {
  0%, 18%, 100% { transform: none;        filter: none; }
  40%           { transform: scale(1.08); filter: brightness(1.12); }
  56%           { transform: scale(0.87); filter: brightness(1.55) saturate(1.1); }
  70%           { transform: scale(1.03); filter: brightness(1.18); }
  86%           { transform: scale(0.98); filter: none; }
}
.em-ab-kheart {
  --em-anim: em-ab-kheart;
  animation-duration: 5.2s;
  animation-timing-function: cubic-bezier(.36, 0, .3, 1);
  animation-iteration-count: infinite;
}

/* THE PRESSURE WAVE. Pure effect: the crest's version of the water the reel
   symbol expels, but read from the front instead of the side, so it is a ring
   travelling outward rather than a plume. It starts inside the Heart at half
   scale and passes out THROUGH it, which is why it is drawn under everything —
   the ring appears to emerge from the pearl rather than to be laid over it. Late
   by 0.1s against the beat so the wave is plainly the consequence of the
   squeeze, and dead before it can reach the viewBox edge and look clipped. */
@keyframes em-ab-kpulse {
  0%, 46%, 100% { opacity: 0;    transform: scale(0.5); }
  58%           { opacity: 0.6;  transform: scale(0.86); }
  74%           { opacity: 0.34; transform: scale(1.14); }
  90%           { opacity: 0;    transform: scale(1.3); }
}
.em-ab-kpulse {
  --em-anim: em-ab-kpulse;
  animation-duration: 5.2s;
  animation-delay: 0.1s;
  animation-timing-function: cubic-bezier(.2, .6, .3, 1);
  animation-iteration-count: infinite;
}

/* THE CAUSTIC WANDERS over the wet pearl. Light on a submerged surface never
   sits still, so this one drifts a couple of units, swells and thins on its own
   2.9s clock while the Heart beats underneath it at 5.2s — the two periods share
   no useful factor, so the highlight lands somewhere new on every beat. 0%/100%
   restate the drawn `opacity: 0.85`, NOT 1: an em-part that is part of the static
   art has to return to the value the art actually draws. */
@keyframes em-ab-kgleam {
  0%, 100% { transform: none; opacity: 0.85; }
  26%      { transform: translate(2.6px, -1.4px) scale(1.28); opacity: 1; }
  54%      { transform: translate(-1.8px, 1.8px) scale(0.78); opacity: 0.45; }
  78%      { transform: translate(1.1px, 0.4px) scale(1.06); opacity: 0.92; }
}
.em-ab-kgleam {
  --em-anim: em-ab-kgleam;
  animation-duration: 2.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE SUCKERS FLICKER. Two-unit dots on a 96 viewBox: travel would be invisible
   at this size, so the read is carried by going out and coming back on — down to
   half size and half brightness, then up to 1.85 and near-white. Riding inside
   the arm groups, they inherit the curl for free, so each light both travels with
   its limb and blinks on its own faster clock. Two rules, one keyframe set: the
   blink is the same organ, and what keeps the pair from firing together is period
   and direction, not bespoke curves. */
@keyframes em-ab-ksucker {
  0%, 100% { transform: none;        filter: none; }
  28%      { transform: scale(0.5);  filter: brightness(0.5); }
  58%      { transform: scale(1.85); filter: brightness(1.9) saturate(0.8); }
  80%      { transform: scale(1.15); filter: brightness(1.25); }
}
.em-ab-ksucker-a {
  --em-anim: em-ab-ksucker;
  animation-duration: 1.9s;
  animation-delay: 0.35s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-ab-ksucker-b {
  --em-anim: em-ab-ksucker;
  animation-duration: 2.4s;
  animation-delay: 0.9s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE MOTES TWINKLE — the two drifting sparks. Ambient only; they take no part in
   the stroke and are the fastest thing in the machine, which is what keeps the
   crest from ever looking frozen in the long glide between beats. The rotation
   is what makes a four-point star read as light catching something turning in
   the current rather than a dot fading up. Applied to a WRAPPER group, so the
   shared Spark primitive is untouched and its own drawn opacity survives
   underneath — hence 0%/100% at opacity 1, the wrapper's own resting value. */
@keyframes em-ab-kspark {
  0%, 100% { transform: none; opacity: 1; }
  22%      { transform: scale(0.3) rotate(-40deg); opacity: 0.25; }
  50%      { transform: scale(1.5) rotate(18deg);  opacity: 1; }
  74%      { transform: scale(0.75) rotate(-8deg); opacity: 0.7; }
}
.em-ab-kspark-a {
  --em-anim: em-ab-kspark;
  animation-duration: 2.15s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-ab-kspark-b {
  --em-anim: em-ab-kspark;
  animation-duration: 3.3s;
  animation-delay: 0.7s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ============================================================
   DRAGON DYNASTY (em-dd-*) — THE DRAGON BLOWS FIRE
   ------------------------------------------------------------
   The player asked for this machine by name, so the breath is built as an EVENT
   and everything else in the art is dressed around it. One cycle, both pieces:

       10%  dead still — the drawn pose, so the gesture has somewhere to start
       18%  the dorsal ridge lights and a wave of gold runs tail→head; the head
            rears BACK off the pearl (the draw of breath, and the telegraph)
       24%  ignition at the muzzle
       30%  the head drives forward, the gold core lights inside the plume
       38%  full extension — the plume is wrapped round the pearl
       50%  the pearl takes the fire and blazes, its flame licks flaring with it
       58%  the jet loses pressure and guts out
       70%  the head recoils, the tail fan lashes
       88%  still again

   The pieces that are CAUSALLY GEARED to the breath (head, plume, core, pearl,
   ridge) all share one period — 3.8s on the reel symbol, 4.6s on the ceremonial
   crest — and are separated by WHERE in that cycle they fire, not by duration.
   Different durations would drift, and after a few cycles the pearl would blaze
   before the fire arrived, which reads as broken rather than alive. The parts
   that are not geared to it — the serpent's ripple (2.9s / 3.5s) and the tail
   flick (4.7s / 5.6s) — run free on their own clocks so the machine is never
   completely still and never pulses in lockstep.

   WHY 3.8s ON THE LOBBY FACE AND 4.6s ON THE CREST: a lobby hover is often only
   a second or two, so the reel symbol has to get to the fire quickly (ignition
   at 0.9s), whereas the crest is thrown by the jackpot capsule and then held big
   on screen, where a slower, grander breath reads better. The crest is also
   deliberately not a multiple of the symbol's period, so a hovered tile and a
   landed crest on screen together are not marching in step.

   MOST KEYFRAMES ARE SHARED BY BOTH ART PIECES. Rotations, scales and opacity
   are unit-free, so the same gesture works in a 48 viewBox and a 96 one; what
   differs is the PIVOT (declared per part below) and the tempo. That is what
   makes the lobby tile and the jackpot crest read as the same dragon. Only the
   two ripples differ, because a shear that undulates an S-curve of body would
   visibly deform a closed ring of it.
   ------------------------------------------------------------ */

/* THE HEAD — rear back, then drive. It pivots on the NECK (where the coil
   disappears behind the skull), not on its own bbox centre: a head is on the end
   of a neck, so 8deg of rotation there swings the muzzle several units across
   the art and the joint itself barely moves — no tearing where the body meets
   the head. The rear-back at 18% is doing real work: it is the anticipation that
   makes the fire land as a blow instead of appearing out of nowhere, and it is
   also the only frame in which the whole silhouette moves, which is what catches
   the eye at 56px. The plume is a CHILD of this part in the art, so it stays
   welded to the mouth all the way through. */
@keyframes em-dd-strike {
  0%, 10%, 88%, 100% { transform: none; }
  18%  { transform: rotate(8deg); }
  28%  { transform: rotate(-3deg); }
  38%  { transform: rotate(-8.5deg); }
  54%  { transform: rotate(-6.5deg); }
  70%  { transform: rotate(3.5deg); }
}

/* THE PLUME — the gesture the player named, so it is the loudest thing here.
   Pure effect (em-fx): dead at opacity 0 until it ignites, so the art is
   pixel-identical to the drawing at rest. It grows about the MOUTH rather than
   its own centre (see the pivots below), which is what turns a scale into a JET:
   the flame's root stays in the muzzle while the tip travels the whole length of
   the curve and wraps the pearl. It overshoots to 1.06 and settles back to 1.0
   as the pressure drops, then necks in and dies. The rotation is small and runs
   one way through the cycle — fire lashes, it does not oscillate. */
@keyframes em-dd-breath {
  0%, 24%, 100% { opacity: 0;    transform: scale(0.12) rotate(-7deg); }
  30%           { opacity: 1;    transform: scale(0.68) rotate(-3deg); }
  38%           { opacity: 1;    transform: scale(1.06); }
  48%           { opacity: 0.95; transform: scale(1) rotate(3deg); }
  58%           { opacity: 0.45; transform: scale(0.86) rotate(5deg); }
  66%           { opacity: 0;    transform: scale(0.5) rotate(6deg); }
}

/* THE GOLD CORE AND THE WHITE-HOT TIP, on the same clock and the same mouth
   pivot as the plume around them but with their own shape: they light a beat
   later (ignition needs the gas out first) and then FLICKER, darting past the
   envelope to 1.12 and shrinking back inside it twice. That mismatch between the
   two layers is the whole difference between fire and an orange triangle. */
@keyframes em-dd-lick {
  0%, 26%, 100% { opacity: 0;   transform: scale(0.1) rotate(-5deg); }
  32%           { opacity: 1;   transform: scale(0.82) rotate(-2deg); }
  38%           { opacity: 0.8; transform: scale(0.96) rotate(2deg); }
  44%           { opacity: 1;   transform: scale(1.12) rotate(-1deg); }
  52%           { opacity: 0.8; transform: scale(0.98) rotate(4deg); }
  60%           { opacity: 0.4; transform: scale(1.06) rotate(2deg); }
  68%           { opacity: 0;   transform: scale(0.6) rotate(6deg); }
}

/* THE PEARL TAKES THE FIRE. It peaks at 50%, after the plume's own peak at 38%,
   so the eye reads cause and then effect rather than two things pulsing at once.
   Opacity could only ever DIM it — the drawing is already fully opaque — so the
   flare is carried by a brightness filter; saturate is pulled DOWN at the same
   time because gold is already near-clipped in red and green and brightness
   alone drives it to flat acid yellow, where desaturating bends it toward white,
   which is what a hot object actually does. `filter: none` is the identity, so
   the off state is exact. */
@keyframes em-dd-blaze {
  0%, 30%, 100% { transform: none;         filter: none; }
  40%           { transform: scale(1.1);   filter: brightness(1.5) saturate(0.92); }
  50%           { transform: scale(1.22);  filter: brightness(1.95) saturate(0.75) drop-shadow(0 0 2px rgba(254, 243, 199, 0.95)); }
  64%           { transform: scale(1.12);  filter: brightness(1.45); }
  80%           { transform: scale(1.04);  filter: brightness(1.12); }
}

/* THE DORSAL RIDGE — gold spikes and scale plates, one keyframe set for both
   because a plate standing up and a spike flaring are the same event: the ridge
   erecting as the dragon charges the blast. It fires FIRST (peak 18%), and each
   piece's animation-delay is indexed off its position along the body in the art,
   so what you see is a wave of gold running from the tail to the head that
   arrives exactly as the fire erupts. Uniform scale, not scaleY: the spikes sit
   at every angle round the coil, and a vertical stretch would shear the ones
   near the horizontal instead of standing them up. The flare is carried mostly
   by BRIGHTNESS with scale only along for the ride: a plate is about as wide as
   the body it sits on, so scale alone has to go past 1.7 before it reads, and by
   then the ridge looks inflated rather than raised. Saturation comes down as
   brightness goes up, because gold is already near-clipped in red and green and
   brightness alone would drive it to flat acid yellow instead of hot white. */
@keyframes em-dd-ridge {
  0%, 8%, 44%, 100% { transform: none;        filter: none; }
  18%               { transform: scale(1.5);  filter: brightness(1.75) saturate(0.8); }
  30%               { transform: scale(1.18); filter: brightness(1.25); }
}

/* THE SERPENT RIPPLES. A shear, not a wobble: skewY slides the two ends of the
   body past each other vertically while skewX does the same horizontally, which
   on an S-curve of stroked path is exactly an undulation travelling its length —
   and it costs one composited transform instead of animating `d` every frame.
   The scale breath on top of it swells the body as the wave passes. The ridge
   plates and spikes, the tail and the head are all children of this part in the
   art, so they are carried by the ripple and the joints cannot come apart. */
@keyframes em-dd-ripple {
  0%, 100% { transform: none; }
  24%      { transform: skewY(-8deg) skewX(5deg) scale(1.03); }
  52%      { transform: skewY(3deg) skewX(-2deg) scale(0.99); }
  78%      { transform: skewY(6deg) skewX(-4deg) scale(1.02); }
}

/* The crest's coil is a closed RING of the same body, and a ring shows shear as
   distortion far more readily than an open curve does, so the same undulation is
   pitched at less than half the amplitude. The travelling ridge wave carries the
   read there instead. */
@keyframes em-dd-kripple {
  0%, 100% { transform: none; }
  26%      { transform: skewY(-3.6deg) skewX(2.4deg) scale(1.02); }
  56%      { transform: skewY(1.6deg) skewX(-1.2deg) scale(0.99); }
  80%      { transform: skewY(2.8deg) skewX(-2deg) scale(1.01); }
}

/* THE TAIL FAN LASHES — the after-beat of the strike, and the one gesture on its
   own clock, because a tail keeps moving long after the mouth has closed. It
   pivots where the fan joins the body, so it swings like a fan on a wrist rather
   than spinning about its middle: 16deg one way, 12deg back, settle. */
@keyframes em-dd-flick {
  0%, 60%, 100% { transform: none; }
  68%           { transform: rotate(-16deg) scale(1.06); }
  76%           { transform: rotate(12deg) scale(1.02); }
  86%           { transform: rotate(-5deg); }
}

/* ---- reel symbol / lobby face: EternalDragonIcon (48 viewBox) ------------ */

.em-dd-head {
  --em-anim: em-dd-strike;
  /* the neck: where the body path's last point (30 16) meets the skull */
  transform-box: view-box;
  transform-origin: 30.5px 16.5px;
  animation-duration: 3.8s;
  animation-timing-function: cubic-bezier(.3, 0, .2, 1);
  animation-iteration-count: infinite;
}
.em-dd-fire {
  --em-anim: em-dd-breath;
  /* the mouth. fill-box would pivot on the plume's own middle and the flame
     would balloon symmetrically out of the dragon's cheek instead of jetting */
  transform-box: view-box;
  transform-origin: 17.9px 8.4px;
  animation-duration: 3.8s;
  animation-timing-function: cubic-bezier(.16, .84, .3, 1);
  animation-iteration-count: infinite;
}
.em-dd-firecore {
  --em-anim: em-dd-lick;
  transform-box: view-box;
  transform-origin: 17.9px 8.4px;
  animation-duration: 3.8s;
  animation-timing-function: linear; /* hard-edged ramps gutter; ease breathes */
  animation-iteration-count: infinite;
}
.em-dd-pearl {
  --em-anim: em-dd-blaze;
  animation-duration: 3.8s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}
.em-dd-ridge {
  --em-anim: em-dd-ridge;
  animation-duration: 3.8s;
  animation-timing-function: cubic-bezier(.2, .9, .25, 1);
  animation-iteration-count: infinite;
  /* per-piece animation-delay is set inline in the art, where each plate's and
     spike's position along the body — and so its place in the wave — is known */
}
.em-dd-body {
  --em-anim: em-dd-ripple;
  animation-duration: 2.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-dd-tail {
  --em-anim: em-dd-flick;
  /* the joint, not the fan's centre: the last point of the body path */
  transform-box: view-box;
  transform-origin: 13px 42px;
  animation-duration: 4.7s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest: DdCelebrationCrest (96 viewBox) ------------------- */
/* Same choreography, same shared keyframes, 4.6s and its own pivots. */

.em-dd-khead {
  --em-anim: em-dd-strike;
  /* the neck, just behind the eye, where the coil ends at (62.5 22.9). Two
     elements carry this class — the head itself and the plume that hangs off its
     mouth — and they stay in phase because they share keyframes, period, delay
     and pivot exactly; the plume is a separate element only because it has to
     paint on top of the pearl it is washing. */
  transform-box: view-box;
  transform-origin: 66.5px 24px;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.3, 0, .2, 1);
  animation-iteration-count: infinite;
}
.em-dd-kfire {
  --em-anim: em-dd-breath;
  transform-box: view-box;
  transform-origin: 46.8px 19.4px; /* the crest head's mouth */
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.16, .84, .3, 1);
  animation-iteration-count: infinite;
}
.em-dd-kcore {
  --em-anim: em-dd-lick;
  transform-box: view-box;
  transform-origin: 46.8px 19.4px;
  animation-duration: 4.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-dd-kpearl {
  --em-anim: em-dd-blaze;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}
.em-dd-kridge {
  --em-anim: em-dd-ridge;
  animation-duration: 4.6s;
  animation-timing-function: cubic-bezier(.2, .9, .25, 1);
  animation-iteration-count: infinite;
  /* delay per plate/spike is set inline in the art, from its angle round the
     coil, so the wave travels the body at a constant rate */
}
.em-dd-kcoil {
  --em-anim: em-dd-kripple;
  animation-duration: 3.5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-dd-ktail {
  --em-anim: em-dd-flick;
  transform-box: view-box;
  transform-origin: 36px 22px; /* where the tail curl leaves the coil */
  animation-duration: 5.6s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* ============================================================
   PHARAOH'S RIFT (em-ph-*) — THE PHARAOH AWAKENS
   ============================================================
   An eternity opening its eyes. The order of the beats is the whole gesture, so
   read it as a sentence rather than a set of loops: the brow cobra stirs first
   (a snake moves before a king does — it is the tell), the eyes ignite, the sun
   on the crown throws its rays, and only then do the crook and flail scissor
   closed over the chest in salute. Then it all settles back to stone.

   The ceremonial crest is the same awakening told in the winged-sun-disk's own
   vocabulary — the twin uraei coil to strike, the falcon wings spread, the disk
   ignites and its ray crown lengthens — so a player who hovered the lobby tile
   recognises the machine when the jackpot capsule throws the crest.

   WHY THE AWAKENING SHARES ONE CLOCK. Every part that takes part in the event
   runs on 5.6s (emblem) / 6.4s (crest) and is staggered by WHERE in that cycle
   it fires, not by having its own duration. Durations that differed would drift
   apart within a few cycles and the ceremony would decay into unrelated
   twitches — the eyes lighting after the regalia had already saluted. What does
   get its own tempo is everything ambient: the sun-glow breath (3.4s / 3.9s),
   the crown orb's pulse (2.6s), the gleam running down the nemes (4.1s) and the
   specular crossing the crest's disk (2.9s). None of those share a factor with
   the ceremony or with each other, so the art is never still between
   awakenings and never pulses in lockstep.

   The two art pieces deliberately run DIFFERENT ceremony clocks (5.6 vs 6.4) so
   a hovered lobby tile and a landed jackpot crest on screen together are not
   marching in step.

   Regal and slow throughout: long ease-in-out holds, no snap. The one exception
   is the eyes, which use a hard cubic-bezier — an ignition is instantaneous
   even when everything around it is ceremonial.
   ------------------------------------------------------------ */

/* THE SUN NEVER STOPS. One breath shared by the emblem's halo and the crest's
   radiance, because it is literally the same thing drawn at two scales; the
   opacity dips as it expands so the light spreads rather than throbs. Both host
   circles are painted with gradients that carry their own alpha, so element
   opacity is 1 in the drawing and 1 is the correct value at 0%/100%. */
@keyframes em-ph-glow {
  0%, 100% { transform: none; opacity: 1; }
  38%      { transform: scale(1.09); opacity: 0.72; }
  70%      { transform: scale(0.94); opacity: 1; }
}

/* ---- the lobby emblem / reel symbol: Pharaoh Eternal (48 viewBox) ------- */

.em-ph-halo {
  --em-anim: em-ph-glow;
  animation-duration: 3.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE SUN ON THE CROWN. The orb is r1.7 in a 48 viewBox — about 2px at a 56px
   lobby tile — so a polite 5% swell would be literally invisible and the pulse
   has to be carried by a big scale AND brightness together. It runs on its own
   short clock rather than the ceremony's: the sun is the one thing in this art
   that is awake even while the king is not. */
@keyframes em-ph-disc {
  0%, 100% { transform: none; filter: none; }
  32%      { transform: scale(1.18); filter: brightness(1.22); }
  58%      { transform: scale(1.5);  filter: brightness(1.7) saturate(0.9); }
  80%      { transform: scale(1.15); filter: brightness(1.18); }
}
.em-ph-disc {
  --em-anim: em-ph-disc;
  animation-duration: 2.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE RAYS. Pure effect: the crown is drawn with a bare orb, and rays only exist
   while the king is awake, which is exactly what em-fx is for. They wind back to
   scale .5 and -14deg while invisible so the burst comes from nothing, then fan
   out and keep turning as they die — light leaving, not a shape being scaled.
   Fires at 40%, a beat after the eyes at 34%: the crown answers the face. */
@keyframes em-ph-rays {
  0%, 40%, 100% { opacity: 0;    transform: scale(0.5) rotate(-14deg); }
  52%           { opacity: 0.95; transform: scale(1) rotate(0deg); }
  70%           { opacity: 1;    transform: scale(1.12) rotate(9deg); }
  88%           { opacity: 0;    transform: scale(1.2) rotate(17deg); }
}
.em-ph-rays {
  --em-anim: em-ph-rays;
  animation-duration: 5.6s;
  animation-timing-function: cubic-bezier(.25, .8, .3, 1);
  animation-iteration-count: infinite;
}

/* THE EYES IGNITE — the signature, and the one beat that must be unmistakable at
   56px. It is carried by opacity rather than brightness on purpose: brightening
   the drawn kohl lines would only lift near-black toward brown, whereas fading
   painted gold lamps up over them turns two dark slits into two lit eyes, which
   is the largest possible contrast change this face can make. Dead for the first
   40% of the cycle, one hard snap on, held lit through the whole salute so the
   player has time to meet the stare, then out.
   0%/100% are opacity 0, so the pose the art holds when nothing is alive is the
   original drawing, pixel for pixel. */
@keyframes em-ph-eyes {
  0%, 40%, 100% { opacity: 0;    transform: scale(0.55); }
  48%           { opacity: 1;    transform: scale(1.18); }
  60%           { opacity: 1;    transform: scale(1); }
  78%           { opacity: 0.85; transform: scale(1.06); }
  92%           { opacity: 0;    transform: scale(0.8); }
}
.em-ph-eyes {
  --em-anim: em-ph-eyes;
  animation-duration: 5.6s;
  animation-timing-function: cubic-bezier(.1, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* THE URAEUS REARS — the tell, so it moves FIRST (32%, ahead of the eyes at 40%).
   The cobra is a 2x3.2 unit hook, far too small for travel to read, so the rear
   is a near-doubling in length: scale(1.5,2) about the point where it is fixed to
   the headband lifts its head about 3.8 units almost straight up over the brow,
   and because a CSS transform scales the rendered stroke too, the body thickens
   as it rises — a hood flaring, for free, with no second element to draw.
   The 20deg roll is what turns a stretch into a rear: without it the scale alone
   drags the head up and to the LEFT, following the hook's lean. */
@keyframes em-ph-uraeus {
  0%, 32%, 100% { transform: none; }
  44%           { transform: rotate(20deg) scale(1.5, 2); }
  62%           { transform: rotate(16deg) scale(1.4, 1.85); }
  82%           { transform: rotate(7deg) scale(1.15, 1.3); }
}
.em-ph-uraeus {
  --em-anim: em-ph-uraeus;
  /* pivot where the cobra is pinned to the headband, not on its own bbox centre:
     fill-box would put the origin mid-body and the snake would swim in place */
  transform-box: view-box;
  transform-origin: 24px 13.4px;
  animation-duration: 5.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}

/* THE CROOK AND FLAIL CROSS. Two mirrored halves of one ceremonial gesture, and
   the last beat of the awakening (44%, after the face is lit): the arms open a
   little (10deg apart — the breath before the salute), then scissor through
   neutral into a tight cross at 14deg the other way, then relax.
   Each is hinged at its GRIP, where the hand would be, so the crossing point
   stays put and only the tips travel. That is worth about 3.5 units of swing on
   a 14-unit shaft — clearly visible on a 56px tile — and it keeps both tips
   inside the symbol instead of throwing them off the bottom edge. */
@keyframes em-ph-crook {
  0%, 44%, 100% { transform: none; }
  54%           { transform: rotate(10deg); }
  68%           { transform: rotate(-14deg); }
  84%           { transform: rotate(-5deg); }
}
@keyframes em-ph-flail {
  0%, 44%, 100% { transform: none; }
  54%           { transform: rotate(-10deg); }
  68%           { transform: rotate(14deg); }
  84%           { transform: rotate(5deg); }
}
.em-ph-crook {
  --em-anim: em-ph-crook;
  transform-box: view-box;
  transform-origin: 20px 33px; /* the left grip */
  animation-duration: 5.6s;
  animation-timing-function: cubic-bezier(.3, .8, .25, 1);
  animation-iteration-count: infinite;
}
.em-ph-flail {
  --em-anim: em-ph-flail;
  transform-box: view-box;
  transform-origin: 28px 33px; /* the right grip */
  animation-duration: 5.6s;
  animation-timing-function: cubic-bezier(.3, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* THE NEMES GLEAM. A soft-edged sheen bar starting off the left of the headdress
   and driven all the way past its right edge, clipped in the art to the nemes
   silhouette so it only ever lights the striped cloth. `linear`, because light
   crossing a surface travels at a constant rate — an ease would read as the
   headdress rocking. It is faded out by 72% and only then slides back to its
   start, so the reset happens at opacity 0 and is never seen. On its own 4.1s
   clock: the cloth catches the light whether or not the king is awake. */
@keyframes em-ph-gleam {
  0%, 100% { opacity: 0;   transform: none; }
  16%      { opacity: 0.9; transform: translateX(5px); }
  48%      { opacity: 1;   transform: translateX(15px); }
  72%      { opacity: 0;   transform: translateX(26px); }
}
.em-ph-gleam {
  --em-anim: em-ph-gleam;
  animation-duration: 4.1s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* ---- the ceremonial crest: winged sun-disk of Horus (96 viewBox) -------- */

.em-ph-kglow {
  --em-anim: em-ph-glow;
  animation-duration: 3.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* THE DISK IGNITES. The crest's answer to the emblem's eyes — a 30-unit disk held
   big on screen cannot snap on from nothing without looking like a flashbulb, so
   here the ignition is a swell plus a brightness/saturate lift on the gold, and
   the hard-edged part of the flash is delegated to em-ph-kblaze behind it.
   filter: none at 0%/100% is the exact identity, so the off state is the drawing. */
@keyframes em-ph-kdisc {
  0%, 38%, 100% { transform: none;         filter: none; }
  52%           { transform: scale(1.1);   filter: brightness(1.45) saturate(1.08); }
  68%           { transform: scale(1.05);  filter: brightness(1.25); }
  86%           { transform: scale(1.01);  filter: brightness(1.08); }
}
.em-ph-kdisc {
  --em-anim: em-ph-kdisc;
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.3, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* THE RAY CROWN LENGTHENS. Growing the fan from the DISK CENTRE (not from its own
   bbox centre, which sits ~18 units too high because these rays only cover the
   upper arc) is what pushes every ray outward along its own axis, so the crown
   extends like light rather than sliding up the medallion. The small counter-roll
   between 54% and 70% keeps the fan from reading as a rigid stamp being zoomed. */
@keyframes em-ph-krays {
  0%, 40%, 100% { transform: none;                      filter: none; }
  54%           { transform: scale(1.2) rotate(3deg);   filter: brightness(1.5) saturate(1.1); }
  70%           { transform: scale(1.28) rotate(-3deg); filter: brightness(1.3); }
  88%           { transform: scale(1.08);               filter: brightness(1.1); }
}
.em-ph-krays {
  --em-anim: em-ph-krays;
  transform-box: view-box;
  transform-origin: 48px 44px; /* the disk's centre, where the rays originate */
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.25, .8, .3, 1);
  animation-iteration-count: infinite;
}

/* THE BLAZE. Pure effect, and the crest's equivalent of the eyes igniting: two
   nested soft radial glows welling out from behind the disk to wash the wings and
   the uraei in gold. It leads the disk (peaks at 50%, the disk at 52%) because
   the glare escaping around a thing is what tells you the thing is about to come
   alive. It keeps expanding as it dies, so the light leaves outward instead of
   dimming in place, and it is back to opacity 0 — and therefore invisible — long
   before the transform resets. */
@keyframes em-ph-kblaze {
  0%, 40%, 100% { opacity: 0;   transform: scale(0.6); }
  50%           { opacity: 1;   transform: scale(1.06); }
  66%           { opacity: 0.9; transform: scale(1.16); }
  86%           { opacity: 0;   transform: scale(1.3); }
}
.em-ph-kblaze {
  --em-anim: em-ph-kblaze;
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.15, .85, .3, 1);
  animation-iteration-count: infinite;
}

/* THE WINGS SPREAD. Mirrored keyframes rather than one shared set, because each
   wing is hinged at its own shoulder in the 96 viewBox and the two rotate in
   opposite senses to open together. They GATHER first (4deg down, scale .97) —
   a bird drops its wings a hair before it lifts them, and that dip is what makes
   the spread read as an intake of breath rather than an inflation. The scale to
   1.05 extends the primaries as they rise; it is capped there because the outer
   feather already sits two units from the crest's clipping viewBox edge. */
@keyframes em-ph-kwing-r {
  0%, 36%, 100% { transform: none; }
  44%           { transform: rotate(4deg) scale(0.97); }
  58%           { transform: rotate(-12deg) scale(1.05); }
  74%           { transform: rotate(-9deg) scale(1.035); }
  90%           { transform: rotate(-3deg) scale(1.01); }
}
@keyframes em-ph-kwing-l {
  0%, 36%, 100% { transform: none; }
  44%           { transform: rotate(-4deg) scale(0.97); }
  58%           { transform: rotate(12deg) scale(1.05); }
  74%           { transform: rotate(9deg) scale(1.035); }
  90%           { transform: rotate(3deg) scale(1.01); }
}
.em-ph-kwing-r {
  --em-anim: em-ph-kwing-r;
  transform-box: view-box;
  transform-origin: 57px 46px; /* the right wing root, against the disk */
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.3, .8, .2, 1);
  animation-iteration-count: infinite;
}
.em-ph-kwing-l {
  --em-anim: em-ph-kwing-l;
  transform-box: view-box;
  transform-origin: 39px 46px; /* mirror of the right root, 96 - 57 */
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.3, .8, .2, 1);
  animation-iteration-count: infinite;
}

/* THE TWIN URAEI COIL TO STRIKE — the crest's tell, first to move at 30%, the
   same beat the emblem's brow cobra takes. These two hang DOWN from the disk, so
   rotation alone would only swing them sideways; the rear is scaleY(0.6) about
   the point where each is fixed to the disk, which shortens the body and hauls
   the head about 5 units up under its own anchor, with scaleX(1.22) flaring the
   hood outward as it comes. The roll then throws each one clear of the collar so
   they arc away from the centre instead of coiling into it. */
@keyframes em-ph-kuraeus-l {
  0%, 30%, 100% { transform: none; }
  42%           { transform: rotate(24deg) scale(1.22, 0.6); }
  60%           { transform: rotate(19deg) scale(1.18, 0.66); }
  82%           { transform: rotate(8deg) scale(1.07, 0.86); }
}
@keyframes em-ph-kuraeus-r {
  0%, 30%, 100% { transform: none; }
  42%           { transform: rotate(-24deg) scale(1.22, 0.6); }
  60%           { transform: rotate(-19deg) scale(1.18, 0.66); }
  82%           { transform: rotate(-8deg) scale(1.07, 0.86); }
}
.em-ph-kuraeus-l {
  --em-anim: em-ph-kuraeus-l;
  transform-box: view-box;
  transform-origin: 40px 56px; /* where the left uraeus meets the disk */
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.2, .9, .25, 1);
  animation-iteration-count: infinite;
}
.em-ph-kuraeus-r {
  --em-anim: em-ph-kuraeus-r;
  transform-box: view-box;
  transform-origin: 56px 56px; /* where the right uraeus meets the disk */
  animation-duration: 6.4s;
  animation-timing-function: cubic-bezier(.2, .9, .25, 1);
  animation-iteration-count: infinite;
}

/* THE SPECULAR CROSSES THE DISK. The crest's echo of the emblem's nemes gleam,
   and the part that keeps a landed crest from ever looking like a decal between
   awakenings: the white highlight slides right across the polished gold and
   shrinks as it goes, the way a reflection foreshortens toward the curve of a
   dome. Travel is capped at 7 units because beyond that the highlight would run
   off the disk's edge onto the medallion, and 0.7 is the opacity the art draws it
   at — so 0% and 100% restate exactly that. Its own 2.9s clock, deliberately
   sharing no factor with the 6.4s ceremony. */
@keyframes em-ph-kspec {
  0%, 100% { transform: none;                                opacity: 0.7; }
  22%      { transform: translate(2.5px, -0.6px) scale(1.1);  opacity: 0.9; }
  50%      { transform: translate(7px, 0.8px) scale(0.85);    opacity: 0.5; }
  76%      { transform: translate(3px, -0.4px) scale(1.05);   opacity: 0.8; }
}
.em-ph-kspec {
  --em-anim: em-ph-kspec;
  animation-duration: 2.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ============================================================
   YETI SUMMIT (em-ys-*) — THE YETI ROARS
   ------------------------------------------------------------
   One event, played twice. On the reel symbol / lobby face you see the beast do
   it: the pelt draws in, the ice-blue eyes narrow and then flare like lamps, the
   head drives forward off-axis, the maw tears open under bared fangs, a plume of
   frozen breath and torn snow bursts over the chin, and the whole shaggy
   silhouette bristles and shivers through the bellow before it settles back to a
   dead-still stare. On the ceremonial crest the beast is off-frame, so the crest
   shows what the roar DOES: the rope-lashed claws cock and then wrench the
   Frozen Star, the ice medallion heaves like a chest, the star flares exactly as
   the eyes flare, and the same breath and snow blast out past it.

   THE BEAT MAP (percentages of one cycle, shared by both art pieces so the two
   read as the same animal):

       0–22%   dead still — the stare. This is most of the loop on purpose: a
               yeti that roars continuously is a yapping dog.
       ~29%    THE INTAKE — pelt draws in, lips press shut, eyes narrow, head
               pulls back. The tell, so the eye is already on the face.
       ~36%    THE ROAR — head drives forward, maw wide, eyes white-hot.
       ~40%    the breath lands, a beat AFTER the mouth opens. Cause, effect.
       40–55%  the bellow sustains and the fur shivers in the blast.
       ~62%    recovered; still again through 100%.

   WHY THE ROAR PARTS SHARE ONE DURATION (4.4s reel symbol / 5.1s crest). These
   parts are one anatomy, not one rhythm section: the fangs must be inside the
   maw while the maw is open, the plume must arrive after the jaw and not before
   it. Durations that differed by even a tenth would drift, and after a dozen
   cycles the beast would be breathing with its mouth shut. The stagger you see
   therefore comes from WHERE in the cycle each part fires (the beat map above),
   with a few hundredths of animation-delay on the consequences — the fangs, the
   breath, the snow — so they trail their cause instead of landing on it.

   The only parts that ignore the roar are the cold auras (2.9s on the symbol,
   3.6s on the crest): they breathe on their own clock, deliberately sharing no
   factor with the roar, so the art is alive between roars and the two rhythms
   drift past each other instead of thumping together. Each aura ALSO carries the
   roar's flare as a second animation on the roar clock — the two never fight,
   because one animates transform/opacity and the other only filter.

   The crest runs slower than the symbol (5.1s against 4.4s) because it is thrown
   by the jackpot capsule and then held big on screen, and because a lobby tile
   and a landed crest on screen together must not march in step.
   ------------------------------------------------------------ */

/* ---- reel symbol / lobby face: YetiIcon (48 viewBox) ------------------- */

/* THE FUR MANTLE. The spiky silhouette and its fur streaks move as one pelt.
   It draws IN first (0.94) and only then bristles out (1.075): the eye reads the
   DELTA, so winding the pelt in first turns a 7% bristle into a 14% swing for
   free. The alternating rotations on top are the shiver — a 40-unit-wide spiky
   shape rocking three degrees flicks every spike tip at once, which is the one
   thing that makes fur read as fur rather than as a scaling sticker. Peak scale
   is capped at 1.075 because past that the outer spikes leave the 48 box. */
@keyframes em-ys-mane {
  0%, 22%, 66%, 100% { transform: none; }
  29%  { transform: scale(0.94); }
  35%  { transform: scale(1.075) rotate(-3deg); }
  40%  { transform: scale(1.06) rotate(2.8deg); }
  45%  { transform: scale(1.07) rotate(-2.2deg); }
  50%  { transform: scale(1.05) rotate(1.8deg); }
  57%  { transform: scale(1.025) rotate(-0.8deg); }
}
.em-ys-mane {
  --em-anim: em-ys-mane;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* THE HEAD DRIVES FORWARD. Scale carries the lunge rather than translation,
   because scaling pushes the brow, the eyes, the maw and the fangs outward
   together and that is what reads as "coming at you" on a 56px tile. The head
   sinks a unit on the intake (translateY +1) and comes up and out of it; the
   rotate flips sign through the bellow because a roar is a head snapping
   off-axis, not a symmetrical pulse. Everything in the face is nested inside
   this group, so each sub-part's own beat rides on top of the lunge. */
@keyframes em-ys-head {
  0%, 22%, 68%, 100% { transform: none; }
  29%  { transform: translateY(1px) scale(0.97); }
  36%  { transform: translateY(-1.4px) scale(1.09) rotate(-2.8deg); }
  44%  { transform: translateY(-1.1px) scale(1.07) rotate(2.2deg); }
  52%  { transform: translateY(-0.5px) scale(1.04) rotate(-1deg); }
  60%  { transform: scale(1.015); }
}
.em-ys-head {
  --em-anim: em-ys-head;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* THE ICE-BLUE EYES FLARE. Scale alone would only make the eyes bigger; the
   brightness/drop-shadow pass is what turns them into lamps, which is the whole
   promise of a WILD symbol. They SQUINT first (0.8 and darker) — a beast winds
   its face up before it lets go, and the dip also buys contrast for the flare
   that follows. The flare peaks at 33%, three points AHEAD of the maw opening:
   the light is the warning, the mouth is the payload. One class on both eyes and
   no delay between them, because asymmetric eyes read as a broken face. */
@keyframes em-ys-eye {
  0%, 20%, 64%, 100% { transform: none; filter: none; }
  27%  { transform: scale(0.8); filter: brightness(0.75); }
  33%  { transform: scale(1.5); filter: brightness(2) saturate(1.2) drop-shadow(0 0 2.4px rgba(165, 243, 252, 0.95)); }
  46%  { transform: scale(1.34); filter: brightness(1.6) drop-shadow(0 0 1.6px rgba(165, 243, 252, 0.8)); }
  56%  { transform: scale(1.12); filter: brightness(1.2); }
}
.em-ys-eye {
  --em-anim: em-ys-eye;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.1, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* THE MAW TEARS OPEN — the signature beat, and the largest amplitude on the
   machine because it has to survive being drawn at 56px. The closed mouth is a
   2.5-unit-tall lens — nothing short of a near-3x stretch registers at that size
   — so it goes to 6.9 units, a black hole that swallows the whole lower muzzle
   and drops the jaw line a couple of units past the chin, which is exactly how a
   bellow looks. The pivot is moved to the TOP of the shape (50% 0%, the upper
   lip) so the opening hinges downward off a fixed lip instead of ballooning
   symmetrically up through the nose. scaleX widens it to the full width of the
   face at the same time, because a bellow is wide as well as deep. */
@keyframes em-ys-maw {
  0%, 24%, 68%, 100% { transform: none; }
  30%  { transform: scale(0.8, 0.55); }
  37%  { transform: scale(1.36, 2.75); }
  46%  { transform: scale(1.3, 2.45); }
  56%  { transform: scale(1.14, 1.6); }
}
.em-ys-maw {
  --em-anim: em-ys-maw;
  /* the upper lip: the jaw hinges off it, so the opening grows downward only */
  transform-origin: 50% 0%;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.15, .85, .2, 1);
  animation-iteration-count: infinite;
}

/* THE FANGS BARE. They are NOT scaled with the maw — stretched teeth look
   rubbery — so they get their own beat: a 1.5 swell that also spreads the pair
   apart (they share one path, so scaling from its centre pushes left and right
   canine outward) plus two units of drop, which hangs them into the new darkness
   instead of leaving them stranded on the lip. White on near-black is the
   highest-contrast pair in the drawing, so this is what actually carries "fangs
   showing" at 56px. The delay lands them a hair after the maw: teeth are
   revealed BY the mouth opening, they do not open it. */
@keyframes em-ys-fang {
  0%, 26%, 68%, 100% { transform: none; }
  32%  { transform: translateY(-0.5px) scale(0.88); }
  39%  { transform: translateY(1.8px) scale(1.5); }
  48%  { transform: translateY(1.5px) scale(1.4); }
  58%  { transform: translateY(0.6px) scale(1.14); }
}
.em-ys-fang {
  --em-anim: em-ys-fang;
  animation-duration: 4.4s;
  animation-delay: 0.05s;
  animation-timing-function: cubic-bezier(.15, .85, .2, 1);
  animation-iteration-count: infinite;
}

/* THE FROZEN BREATH (pure effect, so the static drawing is untouched). Still at
   opacity 0 through the intake and only fully in at 40%, three points behind the
   maw, so you read "the mouth opens, and THEN something comes out of it". It
   pivots on its own top edge (50% 0%, level with the mouth) so it grows away
   from the face down over the chin rather than swelling through the head, and it
   keeps expanding as it dies — breath disperses, it does not retract. The 0%/100%
   pose is opacity 0 at a collapsed scale, which is also what makes the snap back
   to nothing at the end of the loop invisible. */
@keyframes em-ys-breath {
  0%, 30%, 100% { opacity: 0; transform: scale(0.4, 0.22); }
  40%  { opacity: 0.9; transform: scale(1, 1); }
  50%  { opacity: 0.62; transform: translateY(0.6px) scale(1.06, 1.08); }
  62%  { opacity: 0; transform: translateY(1.4px) scale(1.12, 1.18); }
}
.em-ys-breath {
  --em-anim: em-ys-breath;
  /* the mouth, not the cloud's middle: the plume blooms out of the maw */
  transform-origin: 50% 0%;
  animation-duration: 4.4s;
  animation-delay: 0.1s;
  animation-timing-function: cubic-bezier(.2, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* THE COLD AURA — two jobs on two clocks. `breath` is the ambient one at 2.9s:
   the halo swells and dims on a cycle that shares no factor with the 4.4s roar,
   so the symbol is never completely still and the two rhythms slide past each
   other forever. `flare` is on the roar clock and only touches filter, so the
   two animations can never fight over a property: it throws cold light across
   the whole glyph at the moment the mouth opens, which is what stops the roar
   from being a local event happening inside a static badge. */
@keyframes em-ys-aura-breath {
  0%, 100% { transform: none; opacity: 1; }
  34%  { transform: scale(0.93); opacity: 0.72; }
  68%  { transform: scale(1.08); opacity: 1; }
}
@keyframes em-ys-aura-flare {
  0%, 24%, 66%, 100% { filter: none; }
  36%  { filter: brightness(1.9) saturate(1.15) drop-shadow(0 0 4px rgba(125, 211, 252, 0.8)); }
  50%  { filter: brightness(1.35); }
}
.em-ys-aura {
  --em-anim: em-ys-aura-breath, em-ys-aura-flare;
  animation-duration: 2.9s, 4.4s;
  animation-timing-function: ease-in-out, cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite, infinite;
}

/* ---- ceremonial crest: YsCelebrationCrest (96 viewBox) ---------------- */

/* THE MEDALLION HEAVES. The crest's stand-in for the chest: it draws in on the
   intake and swells taller than it is wide (1.05 / 1.075) at the bellow, because
   ribs expand upward and outward, not evenly. Small numbers by design — this is
   the crest's ground, and if the ground pumped hard the claws and the star would
   look like they were floating on it. */
@keyframes em-ys-kmantle {
  0%, 22%, 70%, 100% { transform: none; }
  30%  { transform: scale(0.975, 0.955); }
  38%  { transform: scale(1.05, 1.075); }
  48%  { transform: scale(1.035, 1.05); }
  60%  { transform: scale(1.012, 1.018); }
}
.em-ys-kmantle {
  --em-anim: em-ys-kmantle;
  animation-duration: 5.1s;
  animation-timing-function: cubic-bezier(.2, .8, .25, 1);
  animation-iteration-count: infinite;
}

/* THE STAR FLARES — the crest's eyes. Identical shape to em-ys-eye (dim and
   wind back, then a hard bright flare that decays in two stages) so the two art
   pieces are unmistakably the same animal, and it fires on the same beat. It
   OSCILLATES back to `transform: none` instead of ratcheting on to the next of
   its eight-fold-symmetric points: the helper also paints a single directional
   facet highlight on the upper spike, so a 45deg landing would move that
   highlight to a different spike and the pose the gate switches off to would no
   longer match the drawing. */
@keyframes em-ys-kstar {
  0%, 22%, 68%, 100% { transform: none; filter: none; }
  30%  { transform: scale(0.95) rotate(-5deg); filter: brightness(0.85); }
  37%  { transform: scale(1.2) rotate(7deg); filter: brightness(1.6) saturate(1.35) drop-shadow(0 0 6px rgba(165, 243, 252, 0.9)); }
  46%  { transform: scale(1.12) rotate(-3.5deg); filter: brightness(1.35) saturate(1.2) drop-shadow(0 0 3px rgba(125, 211, 252, 0.7)); }
  56%  { transform: scale(1.04) rotate(1.4deg); filter: brightness(1.18); }
}
.em-ys-kstar {
  --em-anim: em-ys-kstar;
  animation-duration: 5.1s;
  animation-timing-function: cubic-bezier(.1, .9, .2, 1);
  animation-iteration-count: infinite;
}

/* THE GRIP WRENCHES. Each claw pivots on its own forearm, so a rotation swings
   the talons in an arc across the star's flank — roughly five units of travel
   between the cocked pose and the peak, which is a clearly visible slip of the
   grip at 84px. The claw COCKS first (rotate away, and a unit of lift: the beast
   hauls the prize up as it fills its lungs — the lift also keeps the arm's round
   cap inside the crest's clipping 96 box, which the rotation alone would push
   past), then the bellow drives the talons back INTO the star and the arm
   shudders itself still. The two claws are mirror images of each other in the
   art, so they get mirrored keyframes rather than one shared set: the right-hand
   claw is drawn inside a scale(-1 1), and rotating its wrapper turns it in SCREEN
   space, where positive is the wrong way round. */
@keyframes em-ys-kclaw-l {
  0%, 22%, 72%, 100% { transform: none; }
  30%  { transform: rotate(-8deg) translateY(-1px); }
  38%  { transform: rotate(6deg) translateX(1.2px); }
  46%  { transform: rotate(-3.5deg) translateX(-0.5px); }
  54%  { transform: rotate(2.2deg) translateX(0.4px); }
  62%  { transform: rotate(-0.8deg); }
}
@keyframes em-ys-kclaw-r {
  0%, 22%, 72%, 100% { transform: none; }
  30%  { transform: rotate(8deg) translateY(-1px); }
  38%  { transform: rotate(-6deg) translateX(-1.2px); }
  46%  { transform: rotate(3.5deg) translateX(0.5px); }
  54%  { transform: rotate(-2.2deg) translateX(-0.4px); }
  62%  { transform: rotate(0.8deg); }
}
.em-ys-kclaw-l {
  --em-anim: em-ys-kclaw-l;
  animation-duration: 5.1s;
  animation-timing-function: cubic-bezier(.18, .85, .22, 1);
  animation-iteration-count: infinite;
}
.em-ys-kclaw-r {
  --em-anim: em-ys-kclaw-r;
  animation-duration: 5.1s;
  /* a hair behind its partner: two arms that seize at the exact same instant
     read as one mechanical clamp, a fortieth of a second apart reads as muscle */
  animation-delay: 0.04s;
  animation-timing-function: cubic-bezier(.18, .85, .22, 1);
  animation-iteration-count: infinite;
}

/* THE CREST'S FROZEN BREATH — the same exhale as the reel symbol, vented up out
   from behind the star. It pivots on its BOTTOM edge (50% 100%, sitting on the
   star's top spike), so growing it drives the plume up and off the medallion
   instead of stretching it through the prize, and it keeps climbing as it fades.
   Pure effect: opacity 0 at 0%/100%, so the crest the capsule throws is the
   exact static drawing until it lands and the gate opens. */
@keyframes em-ys-kbreath {
  0%, 30%, 100% { opacity: 0; transform: scale(0.5, 0.15); }
  41%  { opacity: 0.95; transform: scale(1, 1); }
  50%  { opacity: 0.6; transform: translateY(-2px) scale(1.1, 1.16); }
  60%  { opacity: 0; transform: translateY(-4.5px) scale(1.2, 1.3); }
}
.em-ys-kbreath {
  --em-anim: em-ys-kbreath;
  /* the vent: the plume grows upward out of the star, not out of its own middle */
  transform-origin: 50% 100%;
  animation-duration: 5.1s;
  animation-delay: 0.08s;
  animation-timing-function: cubic-bezier(.2, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* SNOW TORN OFF THE SUMMIT. Crystals scattered all round the star, blown outward
   as one body: the pivot is forced onto the star's centre in view-box space
   (48,52) rather than the group's own bbox, because the crystals are placed
   asymmetrically and fill-box would put the origin off the star and make the
   spray slide sideways instead of radiating. The drift of rotation as it expands
   is the wind curling the spray. It fires last of everything (42%) — snow is the
   final consequence of the bellow, and giving the eye something still arriving
   after the roar peak is what makes the gesture feel heavy and mountain-sized
   rather than instantaneous. */
@keyframes em-ys-ksnow {
  0%, 32%, 100% { opacity: 0; transform: scale(0.55); }
  42%  { opacity: 1; transform: scale(1); }
  54%  { opacity: 0.55; transform: scale(1.18) rotate(3deg); }
  68%  { opacity: 0; transform: scale(1.32) rotate(6deg); }
}
.em-ys-ksnow {
  --em-anim: em-ys-ksnow;
  transform-box: view-box;
  transform-origin: 48px 52px;
  animation-duration: 5.1s;
  animation-delay: 0.12s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}

/* THE CREST'S COLD AURA — the reel symbol's aura at crest scale and crest tempo:
   3.6s of ambient breathing (again sharing no factor with the 5.1s roar) with the
   roar's flash laid over it on the roar clock as a filter-only second animation.
   On the crest this matters more than on the tile: the capsule leaves the crest
   held big and motionless on screen, and a 40-unit glow slowly breathing under
   everything else is what keeps the whole medallion from reading as a decal
   between roars. */
@keyframes em-ys-kglow-breath {
  0%, 100% { transform: none; opacity: 1; }
  36%  { transform: scale(0.94); opacity: 0.75; }
  70%  { transform: scale(1.07); opacity: 1; }
}
@keyframes em-ys-kglow-flare {
  0%, 24%, 70%, 100% { filter: none; }
  38%  { filter: brightness(1.85) saturate(1.15) drop-shadow(0 0 7px rgba(125, 211, 252, 0.75)); }
  52%  { filter: brightness(1.3); }
}
.em-ys-kglow {
  --em-anim: em-ys-kglow-breath, em-ys-kglow-flare;
  animation-duration: 3.6s, 5.1s;
  animation-timing-function: ease-in-out, cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite, infinite;
}

/* ------------------------------------------------------------------
   NEON NIGHTS (em-nn-*) — THE NEON FLICKERS AND THE PHANTOM DRIFTS
   ------------------------------------------------------------------
   One fault, two bodies. This machine's world is a synthwave arcade after dark,
   and the signature is not a motion at all — it is a FAILING SIGN. The tube
   tracing the phantom's hood (reel symbol) and the marquee's magenta border
   tube (ceremonial crest) run the identical strobe: a long steady burn, then a
   fit of hard off-flickers with two false strikes, then a hot relight that
   settles back to normal. That is what makes a hovered lobby tile and a landed
   jackpot crest read as the same machine even though one is a ghost and the
   other is a signboard.

   WHY THE FLICKER IS `linear` WITH KEYFRAMES 1.5% APART, NOT AN EASE. A gas
   tube does not fade — it strikes or it does not. At 2.9s a 1.5% step is ~43ms,
   two or three frames, which the eye reads as an instantaneous cut. Any easing
   in here turns an electrical fault into something breathing, which is the one
   thing it must not look like. The false strikes at 14.5% and 24% overshoot to
   brightness(1.75) and the real relight at 33% to 2.05 — magenta is already
   near-clipped in red and blue, so pulling saturate() DOWN at the same time
   bends the overshoot toward white-hot instead of to a flat lilac, which is
   what a tube actually does when the gas catches.

   WHY THE FIT FIRES AT 12% OF THE CYCLE. Lobby hovers are frequently under a
   second. A gesture held back to the middle of the cycle would simply never be
   seen on a tile, so the fault starts ~350ms in on the reel symbol and the
   relight peak lands at ~950ms. The rest of the cycle is the static drawing,
   burning steady — which is also exactly what the art must look like when the
   gate switches motion off.

   WHAT IS GEARED AND WHAT IS NOT. On the crest the tube, its glow bloom, the
   starburst and the body's buzz all share one 3.6s period: they are one
   electrical event, and if they drifted apart the sign would shiver while the
   tube was burning happily. Everything ambient runs on its own prime-ish clock
   and is deliberately never a multiple of the fault — phantom drift 4.6s,
   halo/aura 3.7s and 4.3s, cloak 3.3s, tatters 2.15s, visor 2.6s, chase bulbs
   1.55s, sparks 1.9 / 2.05 / 2.45 / 2.8 / 3.1s — so the art is alive in the
   gaps between fits and nothing ever pulses in lockstep.
   ------------------------------------------------------------------ */

/* THE SIGNATURE — the tube strobing. Shared by the phantom's hood tube and the
   marquee's border tube; only the period differs, so the two are recognisably
   the same broken transformer at two different sizes. Both live on a wrapper
   <g>, never on the stroke itself: the marquee tube is drawn at opacity 0.9 and
   the phantom's tube is three layered strokes from the shared NeonStroke
   helper, and a wrapper lets 0%/100% be a clean opacity:1 identity so the
   moment the gate drops the animation is pixel-invisible.
   Read it as: burn · cut · false strike · cut · dying glow · dead beat · false
   strike · cut · half-life · cut · RELIGHT · settle · burn. */
@keyframes em-nn-flicker {
  0%, 12%   { opacity: 1;    filter: none; }
  13%       { opacity: 0.05; filter: none; }
  14.5%     { opacity: 1;    filter: brightness(1.55) saturate(0.9); }
  16%       { opacity: 0.08; filter: none; }
  18%       { opacity: 0.62; filter: none; }
  20%       { opacity: 0.04; filter: none; }
  23%       { opacity: 0.04; filter: none; }
  24%       { opacity: 1;    filter: brightness(1.75) saturate(0.88); }
  26%       { opacity: 0.1;  filter: none; }
  28%       { opacity: 0.5;  filter: none; }
  30%       { opacity: 0.06; filter: none; }
  33%       { opacity: 1;    filter: brightness(2.05) saturate(0.82); }
  40%       { opacity: 1;    filter: brightness(1.3); }
  50%, 100% { opacity: 1;    filter: none; }
}

/* The SECOND tube's fault — the crest's inner cyan liner. Same electrical
   language, deliberately a different pattern in a different half of the cycle:
   a slow sag around 30% where the gas can no longer hold the arc, then its own
   stutter fit from 58%. Two tubes blinking on the same frames would read as one
   animated layer switching off; two tubes failing independently reads as a sign
   that nobody has maintained in years, which is the whole fiction here. */
@keyframes em-nn-flicker-b {
  0%        { opacity: 1;    filter: none; }
  30%       { opacity: 0.42; filter: none; }
  31.5%     { opacity: 1;    filter: brightness(1.45); }
  58%       { opacity: 1;    filter: none; }
  59%       { opacity: 0.06; filter: none; }
  60.5%     { opacity: 0.95; filter: none; }
  62%       { opacity: 0.05; filter: none; }
  65%       { opacity: 0.05; filter: none; }
  66%       { opacity: 1;    filter: brightness(1.6); }
  68%       { opacity: 0.28; filter: none; }
  70%       { opacity: 0.06; filter: none; }
  73%       { opacity: 1;    filter: brightness(1.85); }
  80%       { opacity: 1;    filter: brightness(1.2); }
  88%, 100% { opacity: 1;    filter: none; }
}

/* The haze a tube throws as it strikes. Pure effect (em-fx), dead at opacity 0
   until the fault begins, and it tracks the flicker's exact percentages: a weak
   halo on each false strike, full bloom on the relight at 33%, then gone. This
   is what carries the flicker at 56px — a 1.1-wide stroke going dark is a
   subtraction the eye can miss, but light spilling onto the cloak around it is
   an addition it cannot. Scale starts and ends at 0.85 so the return trip
   happens while the bloom is already invisible. */
@keyframes em-nn-bloom {
  0%, 11%, 100% { opacity: 0;    transform: scale(0.85); }
  14.5%         { opacity: 0.45; transform: scale(1); }
  17%           { opacity: 0.08; transform: scale(0.92); }
  24%           { opacity: 0.7;  transform: scale(1.06); }
  27%           { opacity: 0.12; transform: scale(0.95); }
  33%           { opacity: 1;    transform: scale(1.22); }
  44%           { opacity: 0.5;  transform: scale(1.08); }
  56%           { opacity: 0;    transform: scale(0.9); }
}

/* The dead electrode arcing across the gap at the bottom of the phantom's tube.
   Fires on 13%, 20% and 30% — the frames where the tube is DARK — because that
   is the physics: the arc jumps the gap precisely when the tube cannot strike.
   Three two-frame snaps, no fade in or out; a fading spark is a firefly, an
   instantaneous one is electricity. */
@keyframes em-nn-arc {
  0%, 12%, 100% { opacity: 0; transform: scale(0.5); }
  13%           { opacity: 1; transform: scale(1.2) rotate(-10deg); }
  15%           { opacity: 0; transform: scale(0.8); }
  19.5%         { opacity: 0; transform: scale(0.6); }
  20%           { opacity: 1; transform: scale(1.35) rotate(8deg); }
  22%           { opacity: 0; transform: scale(0.9); }
  29.5%         { opacity: 0; transform: scale(0.6); }
  30%           { opacity: 1; transform: scale(1.5) rotate(-6deg); }
  33%           { opacity: 0; transform: scale(1); }
}

/* THE PHANTOM DRIFTS. The whole specter — cloak, face, tube, arc — rides this,
   so the flicker travels with the body instead of sitting still while the ghost
   moves out from under it. 2.4 units of a 48 viewBox is 5% of the symbol, about
   3px at 56px: enough to read as floating, small enough that the hem never
   leaves the tile. The faint scale is the ghost coming a little nearer on the
   rise, which is what keeps it from looking like a sprite on a slider. */
@keyframes em-nn-drift {
  0%, 100% { transform: none; }
  30%      { transform: translateY(-2.4px) scale(1.015); }
  62%      { transform: translateY(1px) scale(0.99); }
  84%      { transform: translateY(-1.2px) scale(1.005); }
}

/* The cloak sways under a face that holds still. skewX from a pivot at the TOP
   of the hood is doing the real work: the shear grows with distance below the
   pivot, so the head is pinned and the scalloped hem 37 units down swings ~3
   units — drapery hanging off a floating body, not a glyph rocking. A rotate
   would have swung the head too and read as a nodding sticker. */
@keyframes em-nn-hem {
  0%, 100% { transform: none; }
  26%      { transform: skewX(-4.5deg) scaleY(1.035); }
  58%      { transform: skewX(1.5deg) scaleY(0.985); }
  80%      { transform: skewX(4deg) scaleY(1.02); }
}

/* The trailing tatters ripple. Same shear trick, hung from the shoulders and
   pushed harder (8deg, and they stretch 9% longer at the extremes) because they
   are the loosest, lightest cloth on the figure and the fastest thing on it —
   2.15s against the cloak's 3.3s, so the rags visibly lag and lead the body
   they hang from instead of moving as one slab with it. */
@keyframes em-nn-tatter {
  0%, 100% { transform: none; }
  22%      { transform: skewX(7deg) scaleY(1.09); }
  48%      { transform: skewX(-2deg) scaleY(0.94); }
  72%      { transform: skewX(-8deg) scaleY(1.06); }
}

/* The cyan visor pulsing. Scale alone would only make the eyes bigger; the
   brightness and the cyan drop-shadow are what turn them into lamps burning
   inside a dark hood, which is the whole promise of a WILD symbol. The dip to
   0.84 at 40% matters as much as the swell — an eye that only ever grows reads
   as a throb, an eye that guts and then flares reads as something looking. */
@keyframes em-nn-eye {
  0%, 100% { transform: none; filter: none; }
  18%      { transform: scale(1.32); filter: brightness(1.7) drop-shadow(0 0 2px rgba(34, 211, 238, 0.95)); }
  40%      { transform: scale(0.84); filter: brightness(0.78); }
  62%      { transform: scale(1.44); filter: brightness(2.05) drop-shadow(0 0 2.5px rgba(103, 232, 249, 1)); }
  82%      { transform: scale(1.04); filter: brightness(1.12); }
}

/* The spectral field / the marquee's tube halo breathing. One keyframe set for
   both art pieces (the emblem's core glow and the crest's hubglow are the same
   optical object at two scales), and the opacity falls as it expands so it
   scans outward rather than throbbing in place. */
@keyframes em-nn-aura {
  0%, 100% { transform: none; opacity: 1; }
  34%      { transform: scale(1.14); opacity: 0.55; }
  68%      { transform: scale(0.9); opacity: 1; }
  86%      { transform: scale(1.03); opacity: 0.8; }
}

/* The sparks twinkle. One keyframe set, five rules across the two art pieces:
   a twinkle is the same optical event everywhere, and what keeps them out of
   phase is period and delay, not bespoke curves. Each collapses to almost
   nothing and then overshoots to 1.6 with a kick of rotation — turning the
   four-point star is what makes it read as light catching an edge rather than a
   dot fading up. Back to exactly the drawn pose at 0%/100%. */
@keyframes em-nn-spark {
  0%, 100% { transform: none; opacity: 1; }
  20%      { transform: scale(0.25) rotate(-40deg); opacity: 0.2; }
  46%      { transform: scale(1.6) rotate(18deg); opacity: 1; }
  70%      { transform: scale(0.6) rotate(-10deg); opacity: 0.45; }
}

/* CREST — the sign body floats, then SHIVERS. This is the crest's answer to the
   phantom's drift, and it is geared to the tube (same 3.6s): a slow 1.1-unit
   rise across the steady burn, a hard ±1.2-unit tremble with a fraction of a
   degree of roll for exactly as long as the fault lasts, then the float again.
   The struts are outside this group on purpose — a body trembling against legs
   that hold station is what reads as electrical buzz; moving both would just be
   the whole sticker being shaken, which is the failure mode this whole feature
   exists to get away from. `linear` for the same reason as the flicker: a
   mains-frequency buzz has no ease in it. */
@keyframes em-nn-kbuzz {
  0%, 100% { transform: none; }
  12%      { transform: translateY(-1.1px); }
  13%      { transform: translate(1.2px, -1.3px) rotate(0.4deg); }
  15%      { transform: translate(-1.2px, -0.8px) rotate(-0.4deg); }
  18%      { transform: translate(0.9px, -1.2px) rotate(0.3deg); }
  21%      { transform: translate(-1.1px, -0.9px) rotate(-0.35deg); }
  25%      { transform: translate(1px, -1.3px) rotate(0.3deg); }
  29%      { transform: translate(-0.8px, -1px) rotate(-0.25deg); }
  33%      { transform: translate(0.6px, -1.4px) rotate(0.2deg); }
  40%      { transform: translateY(-1px); }
  62%      { transform: translateY(0.8px); }
  84%      { transform: translateY(-0.5px); }
}

/* CREST — the starburst on the sign face. It is wired to the same supply as the
   border tube, so it guts to brightness(0.42) on every cut and blazes on the
   relight at 33%, growing a quarter again and rolling 10deg as it goes: the
   rotation is what stops the flare reading as a zoom. Brightness alone carries
   the dim, with no saturate() alongside it: dropping saturation as well turned
   the magenta star a dead grey, which read as a colour bug rather than as a
   sign browning out. This is the crest's payoff beat, the reason the gesture
   still lands when the capsule holds the crest big on screen. */
@keyframes em-nn-kstar {
  0%, 12%, 100% { transform: none; filter: none; }
  16%           { transform: scale(0.92) rotate(-5deg); filter: brightness(0.42); }
  24%           { transform: scale(1.08) rotate(4deg); filter: brightness(1.45); }
  28%           { transform: scale(0.9) rotate(-4deg); filter: brightness(0.45); }
  33%           { transform: scale(1.26) rotate(10deg); filter: brightness(2) saturate(1.15) drop-shadow(0 0 4px rgba(232, 121, 249, 0.9)); }
  44%           { transform: scale(1.09) rotate(3deg); filter: brightness(1.35); }
  62%           { transform: scale(1.01); filter: none; }
}

/* CREST — one marquee chase bulb. The bulbs are drawn full-on, so the light
   that travels has to be carried by a bloom (scale 1.75 plus a white
   drop-shadow) and by the DIP behind it at 38% — a bright crest with a dark
   trough is a wave; brightness alone would just make the whole row hum. The
   travelling part is per-bulb animation-delay, set inline in the art where each
   bulb's place in the row is known, which runs the light along the top and back
   along the bottom. Shortest clock on the machine at 1.55s, so the chase keeps
   circling all through the tube's long steady burn. */
@keyframes em-nn-kbulb {
  0%, 100% { transform: none; filter: none; opacity: 1; }
  16%      { transform: scale(1.75); filter: brightness(1.95) drop-shadow(0 0 3px rgba(255, 255, 255, 0.9)); opacity: 1; }
  38%      { transform: scale(0.8); filter: none; opacity: 0.22; }
  68%      { transform: scale(1.02); filter: brightness(1.1); opacity: 0.85; }
}

/* ---- reel symbol / lobby face: NeonPhantomIcon (48 viewBox) ------------ */
/* The tube, its bloom and the electrode arc share 2.9s and zero delay because
   they are ONE electrical event described three ways — they would fall apart
   into unrelated blinking at three different periods. */
.em-nn-tube {
  --em-anim: em-nn-flicker;
  animation-duration: 2.9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-bloom {
  --em-anim: em-nn-bloom;
  animation-duration: 2.9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-arc {
  --em-anim: em-nn-arc;
  animation-duration: 2.9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-drift {
  --em-anim: em-nn-drift;
  animation-duration: 4.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-hem {
  --em-anim: em-nn-hem;
  /* pivot at the crown of the hood, not the cloak's bbox centre: fill-box would
     put the origin halfway down the garment and the head would swing as far as
     the hem, which is a rocking sticker rather than hanging cloth */
  transform-box: view-box;
  transform-origin: 24px 6px;
  animation-duration: 3.3s;
  animation-delay: 0.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-tatter {
  --em-anim: em-nn-tatter;
  /* the shoulders, where the rags are actually attached */
  transform-box: view-box;
  transform-origin: 24px 25px;
  animation-duration: 2.15s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-eye {
  --em-anim: em-nn-eye;
  animation-duration: 2.6s;
  animation-timing-function: cubic-bezier(.3, 0, .2, 1);
  animation-iteration-count: infinite;
}
.em-nn-aura {
  --em-anim: em-nn-aura;
  animation-duration: 3.7s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-spark-a {
  --em-anim: em-nn-spark;
  animation-duration: 1.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-spark-b {
  --em-anim: em-nn-spark;
  animation-duration: 2.45s;
  animation-delay: 0.4s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-spark-c {
  --em-anim: em-nn-spark;
  animation-duration: 3.1s;
  animation-delay: 0.95s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest: NeonMarqueeCrest (96 viewBox) ------------------- */
/* Same fault, slower (3.6s against the reel symbol's 2.9s) because the crest is
   held on screen rather than glanced at — and slower ON PURPOSE so a lobby tile
   and a landed jackpot crest sharing the screen are never marching in step. */
.em-nn-ktube {
  --em-anim: em-nn-flicker;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-kbloom {
  --em-anim: em-nn-bloom;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-kcyan {
  --em-anim: em-nn-flicker-b;
  animation-duration: 2.3s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-kstar {
  --em-anim: em-nn-kstar;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-nn-kbuzz {
  --em-anim: em-nn-kbuzz;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-nn-khalo {
  --em-anim: em-nn-aura;
  animation-duration: 4.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-kbulb {
  --em-anim: em-nn-kbulb;
  animation-duration: 1.55s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  /* no animation-delay here: it is set per bulb inline in the art, which is the
     only place the bulb's position along the frame is known */
}
.em-nn-kspark-a {
  --em-anim: em-nn-spark;
  animation-duration: 2.05s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-nn-kspark-b {
  --em-anim: em-nn-spark;
  animation-duration: 2.8s;
  animation-delay: 0.55s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------------
   VALHALLA THUNDER (em-vt-*) — THE SKY PICKS A TARGET
   ------------------------------------------------------------------
   One strike, two bodies. The reel symbol is the hammer and the ceremonial
   crest is the mead-hall, and both play the SAME electrical event: a charge,
   a discharge, and everything under it taking the hit. On the hammer the
   sigil cut into the head is what the bolt earths through; on the crest it is
   the fork behind the gable, and the hall's windows flare because the light
   just came through the roof. That shared beat is what makes a hovered lobby
   tile and a landed jackpot crest read as the same machine, even though one
   is a tool and the other is a building.

   WHY THE STRIKE IS THE FIRST THING THAT HAPPENS. Lobby hovers are often
   under a second, so the discharge lands ~480ms into the hammer's 3.4s cycle
   and ~500ms into the crest's 4.2s. The rest of both cycles is the static
   drawing sitting still under a slow storm glow — which is also exactly what
   the art has to look like the instant the gate switches motion off.

   WHY THE FLASH FRAMES ARE 1–2% APART AND `linear`. Lightning does not fade
   in. At 3.4s a 1.5% step is ~50ms, three frames, which the eye reads as an
   instantaneous cut. The double-strike (a false strike, a dead beat, then the
   real one) is what stops it reading as a lamp being switched on: real forked
   lightning almost always fires more than once down the same channel.

   WHAT IS GEARED AND WHAT IS NOT. Halo, fork, head, sigil, haft and rivets all
   share the strike period on each body — they are one event, and any drift
   between them would make the hammer shiver while the sky was quiet. Everything
   ambient is deliberately off that clock and off each other: struck sparks at
   2.3s and 2.9s, the crest's ground-line at 3.1s, so the art keeps breathing in
   the long gaps and nothing ever pulses in lockstep.
   ------------------------------------------------------------------ */

/* THE SIGNATURE — the discharge. Shared shape on both bodies: charge · false
   strike · dead beat · REAL STRIKE · afterglow · out. The fork lives on an
   em-fx element so it is dead at opacity 0 when the gate is off; the crest's
   fork is part of the static drawing, so its keyframes return to opacity 0.35
   rather than to zero. */
@keyframes em-vt-fork {
  0%, 11%   { opacity: 0; transform: scaleY(0.25); }
  12%       { opacity: 0.9; transform: scaleY(1); }
  13.5%     { opacity: 0.12; transform: scaleY(1); }
  15%       { opacity: 1; transform: scaleY(1); }
  18%       { opacity: 0.3; transform: scaleY(1); }
  22%       { opacity: 0; transform: scaleY(1); }
  /* back to the 0% pose, or the loop snaps the (invisible) bolt back to full height */
  100%      { opacity: 0; transform: scaleY(0.25); }
}
/* The storm field charging: it DIMS and tightens first — the sky drawing
   breath — then blows out on the strike and rolls off. Starting with a dim is
   what telegraphs the hit, so the eye is already on the symbol when it lands. */
@keyframes em-vt-halo {
  0%, 7%    { opacity: 0.72; transform: none; }
  11%       { opacity: 0.4;  transform: scale(0.95); }
  12.5%     { opacity: 1;    transform: scale(1.16); }
  16%       { opacity: 0.6;  transform: scale(1.02); }
  17.5%     { opacity: 0.95; transform: scale(1.1); }
  30%       { opacity: 0.78; transform: scale(1.01); }
  46%, 100% { opacity: 0.72; transform: none; }
}
/* THE RECOIL. The head is driven DOWN by the hit and rebounds through a
   decaying wobble — down hard, up past true, down a little, home. It is the
   whole reason the strike reads as force rather than as a light turning on. */
@keyframes em-vt-head {
  0%, 11%   { transform: none; }
  13%       { transform: translateY(2.1px) rotate(-3.6deg); }
  17%       { transform: translateY(-1px) rotate(1.7deg); }
  22%       { transform: translateY(0.5px) rotate(-0.7deg); }
  27%       { transform: translateY(-0.2px) rotate(0.3deg); }
  34%, 100% { transform: none; }
}
/* The haft swings the other way under the head, pivoting where it enters the
   iron — a hammer struck from above kicks its handle out, it does not slide. */
@keyframes em-vt-haft {
  0%, 11%   { transform: none; }
  14%       { transform: rotate(3.4deg); }
  19%       { transform: rotate(-2.1deg); }
  25%       { transform: rotate(0.9deg); }
  31%       { transform: rotate(-0.4deg); }
  38%, 100% { transform: none; }
}
/* The cut sigil earthing the bolt. Saturation is pulled DOWN as brightness goes
   up so the overshoot bends toward white-hot instead of to a flat pale blue —
   which is what metal actually does when something that hot goes through it. */
@keyframes em-vt-bolt {
  0%, 11%   { filter: none; }
  12%       { filter: brightness(2.1) saturate(0.55); }
  13.5%     { filter: brightness(1.05); }
  15%       { filter: brightness(2.9) saturate(0.4) drop-shadow(0 0 2.5px #ffffff); }
  20%       { filter: brightness(1.6) saturate(0.8); }
  30%, 100% { filter: none; }
}
/* Domed rivets catching the flash — a hard specular, no movement. */
@keyframes em-vt-rivet {
  0%, 12%   { filter: none; }
  15%       { filter: brightness(2.4); }
  24%, 100% { filter: none; }
}
/* Struck iron thrown clear of the head. Pure effect: dead until the hit. */
@keyframes em-vt-chip-l {
  0%, 12%   { opacity: 0; transform: none; }
  15%       { opacity: 1; transform: translate(-3px, -3px) scale(1.2); }
  30%       { opacity: 0; transform: translate(-7px, -9px) scale(0.5); }
  100%      { opacity: 0; transform: none; }
}
/* The mirror of the above, written out rather than done with `scale: -1 1`: the
   standalone scale property composes BEFORE transform, so mirroring that way
   left both chips travelling the same direction. */
@keyframes em-vt-chip-r {
  0%, 12%   { opacity: 0; transform: none; }
  15%       { opacity: 1; transform: translate(3px, -3px) scale(1.2); }
  30%       { opacity: 0; transform: translate(7px, -9px) scale(0.5); }
  100%      { opacity: 0; transform: none; }
}
/* Ambient: the two sparks already in the drawing, twinkling off the beat. */
/* NB: this class sits on a WRAPPER <g>, and the Spark helper paints its own
   opacity="0.85" on the path inside it. The wrapper's neutral value is therefore
   1, NOT 0.85 — starting at 0.85 multiplies the two and visibly dims the spark
   the moment the gate switches on. */
@keyframes em-vt-spark {
  0%, 100%  { opacity: 1;    transform: scale(1); }
  50%       { opacity: 0.42; transform: scale(0.72); }
}

/* ---- reel symbol / lobby face: MjolnirIcon (48 viewBox) ---------------- */
/* Halo, fork, head, haft, sigil and rivets share 3.4s and zero delay because
   they are ONE event described six ways; split their periods and the hammer
   twitches on its own while the sky is quiet. */
.em-vt-halo {
  --em-anim: em-vt-halo;
  animation-duration: 3.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-fork {
  --em-anim: em-vt-fork;
  /* the bolt grows DOWN out of the sky, so it scales from its top end, not
     from the middle of its own bounding box */
  transform-box: view-box;
  transform-origin: 24px -6px;
  animation-duration: 3.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-head {
  --em-anim: em-vt-head;
  animation-duration: 3.4s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-vt-haft {
  --em-anim: em-vt-haft;
  /* where the haft enters the iron — a handle pivots at its socket */
  transform-box: view-box;
  transform-origin: 24px 33px;
  animation-duration: 3.4s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-vt-bolt {
  --em-anim: em-vt-bolt;
  animation-duration: 3.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-rivet {
  --em-anim: em-vt-rivet;
  animation-duration: 3.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-chip-a {
  --em-anim: em-vt-chip-l;
  animation-duration: 3.4s;
  animation-timing-function: cubic-bezier(.1, .8, .3, 1);
  animation-iteration-count: infinite;
}
.em-vt-chip-b {
  --em-anim: em-vt-chip-r;
  animation-duration: 3.4s;
  animation-timing-function: cubic-bezier(.1, .8, .3, 1);
  animation-iteration-count: infinite;
}
.em-vt-spark-a {
  --em-anim: em-vt-spark;
  animation-duration: 2.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-vt-spark-b {
  --em-anim: em-vt-spark;
  animation-duration: 2.9s;
  animation-delay: 0.5s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest: VtHallCrest (96 viewBox) ------------------------ */
/* Slower (4.2s) because the crest is HELD on screen after a jackpot rather
   than glimpsed under a pointer, so the strike has room to be an event with a
   before and an after instead of a blink. */
/* NB: the crest's fork is DRAWN with filter="url(#vt-soft)" — a Gaussian blur
   that is part of the static art, not something this animation adds. Every
   frame therefore has to carry it, including 0% and 100%: writing `filter: none`
   here strips the blur the instant the gate opens and the soft bolt snaps to a
   hard edge, which is precisely the pixel-visible switch-on the contract forbids. */
@keyframes em-vt-kbolt {
  0%, 10%   { opacity: 0.35; filter: url(#vt-soft); }
  11%       { opacity: 1;    filter: url(#vt-soft) brightness(1.9); }
  12.5%     { opacity: 0.2;  filter: url(#vt-soft); }
  14%       { opacity: 1;    filter: url(#vt-soft) brightness(2.3); }
  17%       { opacity: 0.42; filter: url(#vt-soft); }
  24%, 100% { opacity: 0.35; filter: url(#vt-soft); }
}
/* The white-out the strike throws across the whole crest. Kept low (0.5 peak)
   and 60ms long: any brighter or longer and the art it is lighting disappears
   into it, which is a camera flash rather than lightning hitting a building. */
@keyframes em-vt-kflash {
  0%, 10.5% { opacity: 0; }
  11.5%     { opacity: 0.5; }
  13%       { opacity: 0.04; }
  14.5%     { opacity: 0.34; }
  19%, 100% { opacity: 0; }
}
/* The hall taking the hit — compressed into its own foundations and springing
   back. Pivoted at the ground line so the roof travels and the base does not,
   which is how a building absorbs a shock. */
@keyframes em-vt-khall {
  0%, 10.5% { transform: none; }
  13%       { transform: translateY(1.5px) scaleY(0.982); }
  17%       { transform: translateY(-0.6px) scaleY(1.008); }
  22%       { transform: translateY(0.4px) scaleY(0.996); }
  28%, 100% { transform: none; }
}
/* Doorway and windows flaring — the light that just came through the roof.
   The doorway is a bigger opening, so it takes more of it and holds longer. */
@keyframes em-vt-kdoor {
  0%, 10.5% { opacity: 0.92; filter: none; }
  12.5%     { opacity: 1; filter: brightness(2.2) saturate(0.7); }
  18%       { opacity: 1; filter: brightness(1.35); }
  34%, 100% { opacity: 0.92; filter: none; }
}
@keyframes em-vt-kwindow {
  0%, 11%   { opacity: 0.8; filter: none; }
  13.5%     { opacity: 1; filter: brightness(2.4) saturate(0.65); }
  19%       { opacity: 0.92; filter: brightness(1.2); }
  32%, 100% { opacity: 0.8; filter: none; }
}
/* The carved prows ringing on the gable after the hit — the long decay that
   tells you something heavy just landed on the roof. */
@keyframes em-vt-kprow {
  0%, 11%   { transform: none; }
  13.5%     { transform: rotate(2.6deg); }
  18%       { transform: rotate(-1.9deg); }
  23%       { transform: rotate(1.1deg); }
  29%       { transform: rotate(-0.5deg); }
  36%, 100% { transform: none; }
}
@keyframes em-vt-khalo {
  0%, 7%    { opacity: 0.55; transform: none; }
  10%       { opacity: 0.3;  transform: scale(0.96); }
  12%       { opacity: 0.95; transform: scale(1.12); }
  16%       { opacity: 0.5;  transform: scale(1.02); }
  18%       { opacity: 0.8;  transform: scale(1.07); }
  34%, 100% { opacity: 0.55; transform: none; }
}
/* The storm line under the hall, on its own slow clock — the one part that is
   never on the strike, so the crest is alive in the gaps between them. */
@keyframes em-vt-kground {
  0%, 100%  { opacity: 0.5; }
  50%       { opacity: 0.9; }
}
.em-vt-kbolt {
  --em-anim: em-vt-kbolt;
  animation-duration: 4.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-kflash {
  --em-anim: em-vt-kflash;
  animation-duration: 4.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-khall {
  --em-anim: em-vt-khall;
  /* the ground line the hall stands on */
  transform-box: view-box;
  transform-origin: 48px 82px;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-vt-kdoor {
  --em-anim: em-vt-kdoor;
  animation-duration: 4.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-kwindow {
  --em-anim: em-vt-kwindow;
  animation-duration: 4.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-kprow {
  --em-anim: em-vt-kprow;
  /* the apex the prows are pegged to */
  transform-box: view-box;
  transform-origin: 48px 24px;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-vt-krivet {
  --em-anim: em-vt-rivet;
  animation-duration: 4.2s;
  animation-iteration-count: infinite;
  animation-timing-function: linear;
}
.em-vt-khalo {
  --em-anim: em-vt-khalo;
  animation-duration: 4.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-vt-kground {
  --em-anim: em-vt-kground;
  animation-duration: 3.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------------
   GILDED SPEAKEASY (em-gs-*) — SOMEBODY IS ON THE OTHER SIDE
   ------------------------------------------------------------------
   One beat, two bodies, and they are the two halves of getting in. The reel
   symbol is the door: you knock, an eye appears in the hatch, tracks left,
   tracks right, blinks, and the brass cover slams across while whoever it
   belongs to makes up their mind — then slides back open. The ceremonial crest
   is what is behind that door once it does: the marquee bulbs run their chase,
   the board lights, the horns lift and blare. Door then room. That is why a
   hovered lobby tile and a landed jackpot crest belong to the same night.

   WHY THE EYE IS THE SLOW PART AND THE COVER IS THE FAST ONE. A person looking
   you over takes their time — the tracking runs on soft cubic easing across
   nearly half the cycle. The verdict does not: the cover slams in ~130ms of
   the 4.4s cycle, on a hard ease-in, because a hatch that eases shut reads as a
   lift door rather than as somebody deciding they do not like the look of you.

   HOW THE COVER AND THE LID HIDE WHEN THE GATE IS OFF. Neither is em-fx. Both
   are drawn OUTSIDE the hatch hole and clipped to it (see gs-s-peep in the
   skin), so at rest they are simply not in the hole — no opacity trick, and the
   static drawing is exactly the open hatch with an eye in it that makes this
   read as a speakeasy door at 16px.

   WHAT IS GEARED AND WHAT IS NOT. On the door the knock, the tracking, the
   blink and the cover are one performance on one 4.4s clock. On the crest the
   board, letters, horns and chevron share 3.8s — one downbeat — while the bulb
   chase runs its own brisk 1.55s lap and the deco rays turn once every 22s and
   never stop. Nothing ambient is a multiple of anything else.
   ------------------------------------------------------------------ */

/* THE KNOCK — two fast raps that start the whole thing, so the eye arriving has
   a cause. Tiny (0.7px) on purpose: the door is 32 units wide in a 48 viewBox
   and anything bigger reads as the drawing sliding rather than timber shaking. */
@keyframes em-gs-door {
  0%       { transform: none; }
  2%       { transform: translateX(-0.7px); }
  4%       { transform: translateX(0.6px); }
  6%       { transform: translateX(-0.4px); }
  9%, 100% { transform: none; }
}
/* THE LOOK. The eye — white and pupil together — shifts with the head behind
   it; the pupil shifts further on top of that, which is what stops it reading
   as one flat sticker being slid about. */
@keyframes em-gs-eye {
  0%, 10%   { transform: none; }
  20%       { transform: translateX(-1.1px); }
  34%       { transform: translateX(1.3px); }
  44%, 100% { transform: none; }
}
@keyframes em-gs-pupil {
  0%, 10%   { transform: none; }
  20%       { transform: translateX(-1.5px); }
  34%       { transform: translateX(1.7px); }
  44%, 100% { transform: none; }
}
/* THE BLINK — the lid is parked above the hole and drops through it. Two frames
   down, two back: any slower and it is a wink, which is a different character. */
@keyframes em-gs-lid {
  0%, 46%   { transform: none; }
  48.5%     { transform: translateY(9.5px); }
  51%       { transform: none; }
  100%      { transform: none; }
}
/* THE VERDICT — the cover slams across, holds while they think about it, then
   slides back. Parked left of the hole and clipped, so 0%/100% is invisible. */
@keyframes em-gs-cover {
  0%, 55%   { transform: none; }
  58%       { transform: translateX(20.4px); }
  72%       { transform: translateX(20.4px); }
  80%       { transform: translateX(0.6px); }
  84%, 100% { transform: none; }
}
/* Brass catching the room's light, on the cover's beat. */
@keyframes em-gs-knob {
  0%, 52%   { filter: none; }
  58%       { filter: brightness(1.8); }
  70%, 100% { filter: none; }
}
@keyframes em-gs-glow {
  0%, 100%  { opacity: 0.45; }
  50%       { opacity: 0.72; }
}
/* Same wrapper/inner-opacity trap as em-vt-spark: the Spark helper already
   carries opacity="0.85", so the wrapper's neutral value is 1. */
@keyframes em-gs-spark {
  0%, 100%  { opacity: 1;   transform: scale(1); }
  50%       { opacity: 0.36; transform: scale(0.7); }
}

/* ---- reel symbol / lobby face: PeepholeDoorIcon (48 viewBox) ----------- */
/* Knock, look, blink and cover all share 4.4s and zero delay: they are one
   person's behaviour, and split clocks would have the eye tracking while the
   hatch was shut. */
.em-gs-door {
  --em-anim: em-gs-door;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.3, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-gs-eye {
  --em-anim: em-gs-eye;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}
.em-gs-pupil {
  --em-anim: em-gs-pupil;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.4, 0, .3, 1);
  animation-iteration-count: infinite;
}
.em-gs-lid {
  --em-anim: em-gs-lid;
  animation-duration: 4.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-gs-cover {
  --em-anim: em-gs-cover;
  /* ease-IN on the way across: a slammed hatch accelerates into the stop */
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.7, 0, .9, .4);
  animation-iteration-count: infinite;
}
.em-gs-knob {
  --em-anim: em-gs-knob;
  animation-duration: 4.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-gs-glow {
  --em-anim: em-gs-glow;
  animation-duration: 3.5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-gs-spark {
  --em-anim: em-gs-spark;
  animation-duration: 2.6s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest: GsMarqueeCrest (96 viewBox) --------------------- */
/* THE CHASE. Each bulb runs the identical 1.55s pulse and the art hands it a
   0.11s stagger inline, so the light travels the marquee instead of the whole
   row breathing together. 7 bulbs x 0.11s = 0.77s, comfortably inside the lap,
   which is what makes it read as one light moving rather than a queue. */
@keyframes em-gs-kbulb {
  0%, 100%  { filter: none; transform: none; }
  12%       { filter: brightness(2.1); transform: scale(1.16); }
  40%       { filter: none; transform: none; }
}
/* The board lighting on the downbeat, and the two rules of type with it. */
@keyframes em-gs-kboard {
  0%, 8%    { filter: none; transform: none; }
  16%       { filter: brightness(1.28); transform: translateY(-0.7px); }
  30%       { filter: brightness(1.08); transform: none; }
  52%, 100% { filter: none; transform: none; }
}
@keyframes em-gs-kletter {
  0%, 8%    { opacity: 0.75; }
  16%       { opacity: 1; }
  46%, 100% { opacity: 0.75; }
}
/* The horns lifting to blare — outward and up, each away from the centre, so
   the pair opens like a fanfare instead of both sliding the same way. */
@keyframes em-gs-khorn-l {
  0%, 10%   { transform: none; }
  20%       { transform: translate(-1.4px, -1.6px) rotate(-5deg); }
  34%       { transform: translate(-0.4px, -0.4px) rotate(-1.4deg); }
  48%, 100% { transform: none; }
}
@keyframes em-gs-khorn-r {
  0%, 10%   { transform: none; }
  20%       { transform: translate(1.4px, -1.6px) rotate(5deg); }
  34%       { transform: translate(0.4px, -0.4px) rotate(1.4deg); }
  48%, 100% { transform: none; }
}
/* GsChevron paints opacity="0.9" on its own inner <g>, so this wrapper is
   neutral at 1 — see the note on em-vt-spark. */
@keyframes em-gs-kchev {
  0%, 100%  { opacity: 1;    transform: none; }
  18%       { opacity: 1;    transform: translateY(-1.1px); }
  46%       { opacity: 0.88; transform: none; }
}
@keyframes em-gs-kglow {
  0%, 100%  { opacity: 0.5; }
  50%       { opacity: 0.8; }
}
/* The deco sunburst behind everything — one full turn, never stopping, never
   on anybody else's clock. 360deg is the same drawing as 0deg, so switching
   the gate off can only ever land on the static pose. */
@keyframes em-gs-kray {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.em-gs-kbulb {
  --em-anim: em-gs-kbulb;
  animation-duration: 1.55s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-gs-kboard {
  --em-anim: em-gs-kboard;
  animation-duration: 3.8s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-gs-kletter {
  --em-anim: em-gs-kletter;
  animation-duration: 3.8s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-gs-khorn-l {
  --em-anim: em-gs-khorn-l;
  animation-duration: 3.8s;
  animation-timing-function: cubic-bezier(.3, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-gs-khorn-r {
  --em-anim: em-gs-khorn-r;
  animation-duration: 3.8s;
  animation-timing-function: cubic-bezier(.3, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-gs-kchev {
  --em-anim: em-gs-kchev;
  animation-duration: 3.8s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-gs-kglow {
  --em-anim: em-gs-kglow;
  animation-duration: 4.7s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-gs-kray {
  --em-anim: em-gs-kray;
  /* the crest's own centre, not the ray fan's bounding box */
  transform-box: view-box;
  transform-origin: 48px 48px;
  animation-duration: 22s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}


/* ------------------------------------------------------------------
   SULTAN'S BAZAAR (em-sb-*) — THE LAMP IS RUBBED
   ------------------------------------------------------------------
   One wake-up, two bodies. The reel symbol is the brass lamp and the
   ceremonial crest is the palace gate, and both play the SAME event: something
   inside draws breath, wakes, and pushes light out through the brass. On the
   lamp the plume is sucked back into the spout and then swells out of it, the
   whole thing rocks back off its foot, the lid is unseated, and the tile star on
   its belly flares violet. On the gate the identical beat arrives as light
   coming UP THROUGH IT — the star at the heart flares, a shaft rises out of the
   gate mouth, and the wash runs outward through the three nested arches. That
   shared beat is what makes a hovered lobby tile and a landed jackpot crest read
   as one machine even though one is an object you can pick up and the other is a
   building you walk through.

   WHY THE ARCHES LIGHT FROM THE INSIDE OUT. The source is the star at the heart
   of the gate, so the wash has to travel AWAY from it: inner ring at 10.5% of
   the cycle, mid at +90ms, outer at +180ms. Lighting outside-in would say the
   frame was switched on by something off-crest, which is the opposite of the
   fiction — the light is already inside, it is getting out. 90ms per ring is
   under the ~120ms at which two flashes stop reading as one travelling front, so
   three rings read as one wash rather than as three lamps taking turns.

   WHY THE GESTURE OPENS WITH A DRAW-IN. Both bodies dim and tighten around 8-9%
   before anything blazes — the glow field contracts, the plume is pulled back
   toward the spout, the stars shrink and darken. A gesture that only ever grows
   reads as a lamp being switched on; a gesture that gathers first reads as
   something inhaling. It also telegraphs, so the eye is already on the art when
   the wake lands.

   WHY THE WAKE FIRES AT ~11% AND NOT IN THE MIDDLE. Lobby hovers are frequently
   under a second. The lamp's flare peaks ~414ms into its 3.6s cycle and the
   gate's ~462ms into its 4.4s, with the outermost arch closing the wash at
   ~640ms; a gesture held to the middle of the cycle would simply never be seen
   on a tile. The flare itself is spent by ~46%, but the plume is the last thing
   to stop moving — smoke keeps drifting after the light that stirred it has gone
   — so em-sb-smoke settles at 55% and em-sb-curl at 60%. Everything after 60% is
   the static drawing sitting still, which is also exactly what the art must look
   like the instant the gate drops motion.

   WHY TWO FLARE KEYFRAME SETS FOR ONE OPTICAL EVENT (em-sb-wake vs em-sb-kwake).
   Same skeleton, different multipliers, because the two stars are drawn at
   opposite ends of the value scale: the lamp's belly star is SB.royal (#3b0764)
   at opacity 0.6 and needs brightness(3.6) before it lifts off the brass at all,
   while the gate's heart star is SB.brass (#f59e0b) at 0.9 and turns into a
   featureless white blob past about 2.4. One shared curve would either fail to
   light the dark one or destroy the bright one. Same reason the two glow fields
   get their own sets: the art draws them at 0.6 and 0.55, and the contract says
   0%/100% is the pose the ART draws, not a convenient shared number.

   WHAT IS GEARED AND WHAT IS NOT. On the lamp: glow, body rock, lid, sheen,
   plume, curl, puff, belly star and flare all share 3.6s at zero delay — they
   are one event described nine ways, and any drift between them would have the
   brass twitching while the smoke sat still. On the gate: halo, three arches,
   three tracers, shaft, both stars and the bosses share 4.4s, the arches and
   tracers offset only by the 0.09s/0.18s that IS the wash. 4.4 is deliberately
   not a multiple of 3.6, so a lobby tile and a jackpot crest on screen together
   never march in step. Everything ambient is off both clocks and off each other:
   the market glint 2.35s, the star field 2.9 / 3.35 / 5.2s, the ground line
   3.7s, the crescent 6.1s — so the art keeps breathing in the long gaps and
   nothing ever pulses in lockstep.

   THOSE NUMBERS ARE PICKED FOR THEIR COMMON PERIOD, not for feel. Two cycles
   sharing a factor visibly re-align, and the shorter that period, the more the
   pair reads as one pulse with a stutter in it. Across all eight durations here
   the tightest pairing is the two clocks themselves, 3.6 against 4.4, at 39.6s;
   every ambient runs longer than that against both. Two values were moved to get
   there — the ground line off 3.3s, which is exactly 3/4 of the crest's 4.4 and
   re-locked every 13.2s, and a star off 2.7s, which sat 10.8s from the lamp's
   3.6. Both were short enough to see.
   ------------------------------------------------------------------ */

/* THE SIGNATURE, part one — the plume. The wisp is SUCKED BACK toward the spout
   (scale 0.82) before it blows out to 1.34, which is the whole reason it reads
   as a lamp exhaling rather than a stroke being scaled up. It never returns
   straight home: the second, smaller swell at 26% is the breath still coming,
   and only by 55% is the plume the drawing again. Rotation rides along because a
   plume that only scales is a rubber band; a plume that leans as it grows is
   smoke. */
@keyframes em-sb-smoke {
  0%, 7%    { transform: none; opacity: 1; filter: none; }
  9.5%      { transform: scale(0.82) rotate(-5deg); opacity: 0.72; filter: none; }
  12%       { transform: scale(1.34) rotate(7deg); opacity: 1; filter: brightness(1.45) saturate(1.15); }
  18%       { transform: scale(1.18) rotate(-3deg); opacity: 1; filter: brightness(1.1); }
  26%       { transform: scale(1.27) rotate(5deg); opacity: 0.92; filter: brightness(1.2); }
  38%       { transform: scale(1.08) rotate(-1.5deg); opacity: 1; filter: none; }
  55%, 100% { transform: none; opacity: 1; filter: none; }
}
/* The outer wisp, nested inside the inner one so the joint holds. It runs the
   same shape a beat LATER and further — smoke arrives at the far end of a plume
   after it arrives at the near end, and pushing the tip harder (1.45 against
   1.34) is what makes the plume unroll instead of inflate. */
@keyframes em-sb-curl {
  0%, 8%    { transform: none; opacity: 1; }
  11%       { transform: scale(0.78) rotate(6deg); opacity: 0.6; }
  14.5%     { transform: scale(1.45) rotate(-9deg); opacity: 1; }
  22%       { transform: scale(1.2) rotate(4deg); opacity: 0.85; }
  31%       { transform: scale(1.32) rotate(-6deg); opacity: 1; }
  44%       { transform: scale(1.1) rotate(2deg); opacity: 0.95; }
  60%, 100% { transform: none; opacity: 1; }
}
/* The puff forced out of the spout mouth. Pure effect (em-fx), dead at opacity 0
   until the rub. It leaves fast and then coasts up-left along the plume, growing
   as it thins — which is what smoke does and what a fading dot does not. The
   return to scale(0.35) between 36% and 100% happens entirely at opacity 0, so
   the trip back is invisible. */
@keyframes em-sb-puff {
  0%, 7%    { opacity: 0; transform: translate(0, 0) scale(0.35); }
  12%       { opacity: 0.9; transform: translate(0.4px, -1.4px) scale(1); }
  22%       { opacity: 0.5; transform: translate(-0.8px, -5px) scale(1.7); }
  36%       { opacity: 0; transform: translate(-2.2px, -10px) scale(2.4); }
  100%      { opacity: 0; transform: translate(0, 0) scale(0.35); }
}

/* THE SIGNATURE, part two — THE ROCK. The lamp settles forward a hair first (the
   hand pressing down on the brass), then rocks BACK: rotate is negative, which
   about the foot pivot lifts the spout and drops the handle, so the plume is
   thrown up and out exactly as it swells. Then a decaying wobble — 5.2, 2.6,
   1.6, 0.7, 0.25 degrees — because a struck object rings down, it does not
   glide home. Under 6 degrees on purpose: the lamp is 36 units wide in a 48
   viewBox and anything larger stops reading as brass on a table and starts
   reading as the whole sticker being tilted. */
@keyframes em-sb-lamp {
  0%, 7%    { transform: none; }
  9%        { transform: rotate(1.4deg); }
  11.5%     { transform: rotate(-5.2deg) translateY(-0.5px); }
  16%       { transform: rotate(2.6deg); }
  21%       { transform: rotate(-1.6deg); }
  27%       { transform: rotate(0.7deg); }
  33%       { transform: rotate(-0.25deg); }
  42%, 100% { transform: none; }
}
/* The lid unseated by the pressure under it, rocking on the rim it sits on. It
   lifts barely 1.5 units — the point is that the seal breaks, not that the lid
   flies off, and a lid that leaves the lamp is a different (and much sillier)
   story than a lid that will not quite stay shut. */
@keyframes em-sb-lid {
  0%, 8%    { transform: none; }
  11.5%     { transform: translateY(-1.5px) rotate(-2.6deg); }
  15%       { transform: translateY(-0.3px) rotate(1.2deg); }
  19%       { transform: translateY(-0.8px) rotate(-1.1deg); }
  25%       { transform: translateY(-0.1px) rotate(0.4deg); }
  32%, 100% { transform: none; }
}

/* THE SIGNATURE, part three — the belly star flaring. This is the payoff beat:
   the star is drawn in SB.royal at opacity 0.6, near-black against the brass, so
   the flare has to be carried almost entirely by brightness — 3.6 lifts #3b0764
   to a hot violet — with the drop-shadow spilling that light onto the metal
   around it, which is what the eye actually reads at 56px. Saturation goes UP,
   not down (the opposite of a white-hot metal flare): this is enchantment coming
   through brass, and it should get MORE violet, not bleach out. The rotation
   matters as much as the scale, and the star's own geometry sets its ceiling:
   SbStar walks 16 vertices alternating r and 0.44r, so the shape maps onto
   itself every 45deg (2π/8) — not every 22.5deg, which lands the outer points
   where the notches were and reads as a second, re-registered star rather than
   the same one turned. 16deg keeps the turn well inside that first ambiguity, so
   the eye reads one star rotating. Second, softer pulse at 22%: the wish is
   still coming up. */
@keyframes em-sb-wake {
  0%, 7%    { transform: none; filter: none; }
  9%        { transform: scale(0.86) rotate(-8deg); filter: brightness(0.7); }
  11.5%     { transform: scale(1.55) rotate(16deg); filter: brightness(3.6) saturate(1.5) drop-shadow(0 0 3px rgba(168, 85, 247, 0.95)); }
  16%       { transform: scale(1.12) rotate(6deg); filter: brightness(1.8) saturate(1.2); }
  22%       { transform: scale(1.26) rotate(11deg); filter: brightness(2.5) saturate(1.35) drop-shadow(0 0 2px rgba(196, 181, 253, 0.8)); }
  32%       { transform: scale(1.04) rotate(2deg); filter: brightness(1.3); }
  46%, 100% { transform: none; filter: none; }
}
/* The lamp lighting from within, behind the star. Pure effect: it is the light
   the star is a hole in, so it fires one frame BEFORE the star's peak and guts
   between the two pulses. Starts and ends at scale(0.4) so the collapse back
   happens while it is already invisible. */
@keyframes em-sb-flare {
  0%, 8%    { opacity: 0; transform: scale(0.4); }
  11.5%     { opacity: 0.95; transform: scale(1.15); }
  17%       { opacity: 0.28; transform: scale(0.95); }
  22%       { opacity: 0.62; transform: scale(1.32); }
  40%, 100% { opacity: 0; transform: scale(0.4); }
}
/* The shoulder sheen. No movement at all — a specular on curved brass does not
   travel, it appears. Drawn at opacity 0.6, so 0%/100% is 0.6. */
@keyframes em-sb-sheen {
  0%, 8%    { opacity: 0.6; filter: none; }
  11.5%     { opacity: 1;   filter: brightness(1.9); }
  20%       { opacity: 0.78; filter: brightness(1.2); }
  34%, 100% { opacity: 0.6; filter: none; }
}
/* The violet field around the lamp. It CONTRACTS and dims at 9% before it blows
   out at 11.5% — the telegraph that puts the eye on the symbol before the flare
   lands. Drawn at opacity 0.6, so 0%/100% is 0.6. */
@keyframes em-sb-glow {
  0%, 5%    { opacity: 0.6;  transform: none; }
  9%        { opacity: 0.34; transform: scale(0.93); }
  11.5%     { opacity: 1;    transform: scale(1.2); }
  17%       { opacity: 0.7;  transform: scale(1.05); }
  23%       { opacity: 0.9;  transform: scale(1.12); }
  40%, 100% { opacity: 0.6;  transform: none; }
}

/* AMBIENT — the twinkle. One keyframe set, four rules across the two art pieces:
   a glint is the same optical event on a market spark and on a star over the
   city, and what keeps them out of phase is period and direction, not bespoke
   curves. Each collapses to almost nothing and overshoots to 1.5 with a kick of
   rotation, because turning the point is what makes it read as light catching an
   edge rather than a dot fading up. Back to exactly the drawn pose at 0%/100%. */
@keyframes em-sb-twinkle {
  0%, 100% { transform: none; opacity: 1; filter: none; }
  24%      { transform: scale(1.5) rotate(20deg); opacity: 1; filter: brightness(1.55); }
  56%      { transform: scale(0.42) rotate(-14deg); opacity: 0.32; filter: none; }
  78%      { transform: scale(0.9) rotate(6deg); opacity: 0.82; filter: none; }
}

/* ---- reel symbol / lobby face: BrassLampIcon (48 viewBox) ---- */
/* Glow, rock, lid, sheen, plume, curl, puff, star and flare all share 3.6s at
   zero delay: they are ONE rub described nine ways, and split periods would have
   the brass rocking while the smoke sat still. */
.em-sb-glow {
  --em-anim: em-sb-glow;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-sb-lamp {
  --em-anim: em-sb-lamp;
  /* the underside of the copper foot, which is the line the lamp actually stands
     on. fill-box would pivot it around the middle of its own body and the lamp
     would spin in the air like a compass needle instead of tipping on a table. */
  transform-box: view-box;
  transform-origin: 24px 39.4px;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-sb-lid {
  --em-anim: em-sb-lid;
  /* the rim of the bowl the lid seats into — a lid that will not stay shut rocks
     on its seat, and pivoting at the lid's own centre would rock the knob and
     the skirt equally, which reads as a hat being waved */
  transform-box: view-box;
  transform-origin: 24px 19.4px;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-sb-sheen {
  --em-anim: em-sb-sheen;
  animation-duration: 3.6s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-sb-smoke {
  --em-anim: em-sb-smoke;
  /* the spout mouth, where the plume is actually attached to the lamp. The
     wisp's own bounding-box centre is up around (43, 6), so scaling from there
     would tear the smoke loose from the brass and float it in mid-air. */
  transform-box: view-box;
  transform-origin: 46.4px 12px;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-sb-curl {
  --em-anim: em-sb-curl;
  /* the joint — the exact point the inner wisp ends and this one begins. Same
     reason as above, one link further out along the plume. */
  transform-box: view-box;
  transform-origin: 39.4px 1.6px;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-sb-puff {
  --em-anim: em-sb-puff;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.15, .75, .3, 1);
  animation-iteration-count: infinite;
}
.em-sb-flare {
  --em-anim: em-sb-flare;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-sb-star {
  --em-anim: em-sb-wake;
  animation-duration: 3.6s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-sb-spark {
  --em-anim: em-sb-twinkle;
  animation-duration: 2.35s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest: SbGateCrest (96 viewBox) ---- */
/* Slower (4.4s against the lamp's 3.6s) because the crest is HELD on screen
   after a jackpot rather than glanced at under a pointer, so the wake has room
   to be an event with a before and an after — and because 4.4 is not a multiple
   of 3.6, a lobby tile and a landed crest sharing the screen never sync up. */

/* The heart star. Same skeleton as the lamp's belly star, gentler multipliers:
   this one is already SB.brass at opacity 0.9, so brightness(2.3) is as far as
   it can go before the eight points fuse into one white lozenge. The violet
   drop-shadow does the work brightness cannot — it is the light LEAVING the
   star, which is the whole subject of the crest. */
@keyframes em-sb-kwake {
  0%, 6%    { transform: none; filter: none; }
  8.5%      { transform: scale(0.88) rotate(-9deg); filter: brightness(0.72); }
  10.5%     { transform: scale(1.4) rotate(14deg); filter: brightness(2.3) saturate(1.25) drop-shadow(0 0 5px rgba(168, 85, 247, 0.9)); }
  16%       { transform: scale(1.1) rotate(5deg); filter: brightness(1.4); }
  22%       { transform: scale(1.2) rotate(9deg); filter: brightness(1.8) saturate(1.2) drop-shadow(0 0 3px rgba(196, 181, 253, 0.7)); }
  32%       { transform: scale(1.03) rotate(2deg); filter: brightness(1.15); }
  48%, 100% { transform: none; filter: none; }
}
/* The sand core inside it, turning the OTHER way. Two stars flaring in the same
   direction are one star with a thick outline; counter-rotation is what makes
   the middle read as a separate light source turning inside a fixed frame. It
   peaks half a frame earlier because it is the source — the outer star is
   reacting to it. Saturation goes DOWN here while brightness goes up, so #fde68a
   bends toward white instead of to a flat yellow: this is the one place on the
   machine that should look like raw light rather than like violet. */
@keyframes em-sb-kcore {
  0%, 6%    { transform: none; filter: none; }
  8%        { transform: scale(0.8) rotate(7deg); filter: brightness(0.75); }
  10%       { transform: scale(1.5) rotate(-16deg); filter: brightness(2.8) saturate(0.8) drop-shadow(0 0 4px #ffffff); }
  15%       { transform: scale(1.06) rotate(-4deg); filter: brightness(1.3); }
  21%       { transform: scale(1.28) rotate(-10deg); filter: brightness(2) saturate(0.88); }
  32%       { transform: scale(1.02) rotate(-2deg); filter: brightness(1.1); }
  46%, 100% { transform: none; filter: none; }
}
/* THE WASH, on the arches themselves. Deliberately filter-ONLY, with no opacity
   in it: the three arches are drawn at opacity 1, 0.9 and 0.65, so an opacity
   keyframe could not be shared, while `filter: none` at 0%/100% is identical at
   rest whatever the element's own alpha. That is what lets one keyframe set plus
   two animation-delays sequence all three rings. */
@keyframes em-sb-karch {
  0%, 6%    { filter: none; }
  10.5%     { filter: brightness(1.85) saturate(1.15) drop-shadow(0 0 3px rgba(168, 85, 247, 0.75)); }
  16%       { filter: brightness(1.28); }
  22%       { filter: brightness(1.5) drop-shadow(0 0 2px rgba(45, 212, 191, 0.6)); }
  34%, 100% { filter: none; }
}
/* THE WASH, as light. Pure effect (em-fx): a stroke tracing each arch's outline,
   dead at opacity 0 at rest. This is the part that actually carries the gesture
   — the innermost arch is #050210 at opacity 0.65 and no brightness() will ever
   lift near-black off near-black, so the light has to be an ADDITION drawn over
   it rather than a multiplication of it. The swell is tiny (0.985 to 1.03)
   because these strokes sit exactly on top of the drawn arch edges and anything
   larger would visibly slide off them. */
@keyframes em-sb-klight {
  0%, 6%    { opacity: 0; transform: scale(0.985); }
  11%       { opacity: 0.9; transform: scale(1.03); }
  19%       { opacity: 0.22; transform: scale(1.01); }
  25%       { opacity: 0.55; transform: scale(1.02); }
  40%, 100% { opacity: 0; transform: scale(0.985); }
}
/* The shaft rising out of the gate mouth — the literal "light coming up through
   the gate". Pure effect. It grows upward and keeps going as it dies; the return
   to translateY(6px) between 34% and 100% is entirely at opacity 0, so the shaft
   is never seen sinking back down, which would read as it being sucked in. */
@keyframes em-sb-kspill {
  0%, 6%    { opacity: 0;    transform: translateY(6px) scaleY(0.55); }
  12%       { opacity: 0.85; transform: translateY(-1px) scaleY(1); }
  22%       { opacity: 0.45; transform: translateY(-8px) scaleY(1.18); }
  34%       { opacity: 0;    transform: translateY(-16px) scaleY(1.3); }
  100%      { opacity: 0;    transform: translateY(6px) scaleY(0.55); }
}
/* The halo behind the whole crest. Same contract-and-shape as the lamp's field,
   its own keyframe set only because the art draws this one at 0.55. */
@keyframes em-sb-kglow {
  0%, 5%    { opacity: 0.55; transform: none; }
  8%        { opacity: 0.3;  transform: scale(0.94); }
  10.5%     { opacity: 1;    transform: scale(1.16); }
  16%       { opacity: 0.66; transform: scale(1.03); }
  21%       { opacity: 0.88; transform: scale(1.1); }
  38%, 100% { opacity: 0.55; transform: none; }
}
/* Domed brass catching the wash as it goes past — a hard specular, no movement. */
@keyframes em-sb-kboss {
  0%, 7%    { filter: none; }
  11%       { filter: brightness(2.2); }
  20%, 100% { filter: none; }
}
/* Ambient: the crescent, on the slowest clock on the machine. It is the one
   thing in the frame that is not part of the market and should not react to it,
   so it only ever breathes — and it dips below its drawn 0.85 on the way back,
   because a moon that only brightens reads as a lamp. */
@keyframes em-sb-kmoon {
  0%, 100% { opacity: 0.85; transform: none; filter: none; }
  36%      { opacity: 1;    transform: scale(1.055); filter: brightness(1.25) drop-shadow(0 0 3px rgba(254, 243, 199, 0.55)); }
  68%      { opacity: 0.76; transform: scale(0.985); filter: none; }
}
/* Ambient: the brass line the gate stands on, drawn at opacity 0.7 — the one
   part deliberately never on the gate's clock, so the crest is alive in the long
   gaps between wakes. */
@keyframes em-sb-kground {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}

.em-sb-khalo {
  --em-anim: em-sb-kglow;
  animation-duration: 4.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* The three arches, one keyframe set, inside-out by delay. 0 / 0.09s / 0.18s is
   the wash travelling; it is NOT the parts falling out of gear, since all three
   share the 4.4s period and come home to the same static pose. */
.em-sb-kinner {
  --em-anim: em-sb-karch;
  animation-duration: 4.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-sb-kmid {
  --em-anim: em-sb-karch;
  animation-duration: 4.4s;
  animation-delay: 0.09s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-sb-kouter {
  --em-anim: em-sb-karch;
  animation-duration: 4.4s;
  animation-delay: 0.18s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* The three light tracers ride the same 0 / 0.09s / 0.18s stagger as the arches
   they sit on, so the addition and the multiplication arrive together. */
.em-sb-klight-a,
.em-sb-klight-b,
.em-sb-klight-c {
  --em-anim: em-sb-klight;
  /* the gate's foot line, not each tracer's own centre: these arches spring from
     the ground, so a swelling light has to grow UP out of the base. Scaling from
     a bbox centre would lift the arch feet off the ground line and the whole
     gate would look like it was hovering for a quarter of a second. */
  transform-box: view-box;
  transform-origin: 48px 86px;
  animation-duration: 4.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-sb-klight-b { animation-delay: 0.09s; }
.em-sb-klight-c { animation-delay: 0.18s; }
.em-sb-kspill {
  --em-anim: em-sb-kspill;
  /* the ground line under the gate — the shaft is light coming UP out of the
     threshold, so it has to grow from the threshold. A fill-box pivot would
     stretch it from its own middle and it would grow downward into the base as
     hard as it grew upward, which is a lens flare, not a doorway. */
  transform-box: view-box;
  transform-origin: 48px 88px;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.15, .75, .3, 1);
  animation-iteration-count: infinite;
}
.em-sb-kstar {
  --em-anim: em-sb-kwake;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-sb-kcore {
  --em-anim: em-sb-kcore;
  animation-duration: 4.4s;
  animation-timing-function: cubic-bezier(.2, .85, .25, 1);
  animation-iteration-count: infinite;
}
.em-sb-kboss {
  --em-anim: em-sb-kboss;
  animation-duration: 4.4s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-sb-kmoon {
  --em-anim: em-sb-kmoon;
  animation-duration: 6.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-sb-kfleck-a {
  --em-anim: em-sb-twinkle;
  animation-duration: 2.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-sb-kfleck-b {
  --em-anim: em-sb-twinkle;
  animation-duration: 3.35s;
  animation-delay: 0.6s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-sb-kfleck-c {
  --em-anim: em-sb-twinkle;
  animation-duration: 5.2s;
  animation-delay: 1.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-sb-kground {
  --em-anim: em-sb-kground;
  /* 3.7s, NOT 3.3s. The crest's clock is 4.4s, and 3.3 is exactly 3/4 of it —
     the two would re-align every 13.2s, so the "ambient" part would visibly beat
     with the wake it is supposed to be independent of. 3.7 shares no factor with
     4.4 (37/44), pushing the common period out to 162.8s, which is longer than
     anyone looks at a lobby tile. Same reasoning as the flecks above. */
  animation-duration: 3.7s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ------------------------------------------------------------------
   SUGAR SPIRES (em-ss-*) — THE CRANK TURNS AND ONE DROPS
   ------------------------------------------------------------------
   One mechanism, two bodies. The reel symbol is the gumball machine and you
   are the hand on it: the crank takes a full revolution, the wheel BITES
   part-way round, the machine rocks on its foot as it catches, the gumballs
   jostle bottom-up in the globe, the chute flap kicks out and one ball falls
   through and bounces on the counter. The ceremonial crest is the other end
   of that same thump — something lands on the cake, the squash travels down
   the stack tier by tier, the piped rosettes pop along each row as the wave
   reaches them and the crown's three gems wink in turn. A hovered lobby tile
   and a landed jackpot crest are the same machine catching, once seen from
   the front of the cabinet and once from under the icing.

   WHY THE CATCH IS AT 288ms AND NOT AT THE TOP OF THE TURN. Lobby hovers are
   often under a second, so the crank starts moving at 64ms and hits the bite
   at ~288ms of its 3.2s cycle — everything else in the symbol is a reaction
   to that one moment. The full revolution finishes at ~610ms and the dropped
   ball hits the counter at ~670ms, so even a very short hover gets turn ·
   catch · drop. The back two thirds of the cycle is the static drawing
   sitting still, which is also exactly what the art must look like the
   instant the gate takes the animation-name away. The crest lands at ~504ms
   of its 4.2s cycle for the same reason; it runs slower because a crest is
   HELD on screen after a jackpot rather than glimpsed under a pointer, so
   the settle has room to be three separate tiers rather than one shudder.

   WHY THE FALLING BALL IS `linear` AND ITS FRAMES ARE UNEVENLY SPACED. Gravity
   is in the frame POSITIONS, not in the easing: 0.4 / 1.6 / 3.6 / 6.4-style
   spacing over equal time steps is what an accelerating body actually does,
   and an ease curve on top of that would fight it. The bounces lose a little
   over half their height each time and the squash/stretch pair on contact
   (1.22/0.78 then 0.92/1.08) is what makes it read as rubbery candy rather
   than a dot sliding down a track.

   WHY THE CRANK ROTATES A FULL 360deg. A half-turn would have to unwind to
   get home, and a rewinding crank reads as a mistake. 360deg is the same
   drawing as 0deg, so the last frame IS the static pose and switching the
   gate off can only ever land on it.

   WHAT IS GEARED AND WHAT IS NOT. On the symbol the crank, the body rock, the
   ring flare, the gumballs, the gloss, the chute, the puff and the dropped
   ball all share 3.2s and zero base delay: they are ONE mechanical event
   described eight ways, and split clocks would have the flap opening while
   the crank was asleep. The gumballs' only offset is a sub-frame per-ball
   delay computed from HEIGHT in the skin, so the jostle ripples up the globe
   off a single clock. On the crest the crown, all three tiers, the rosettes,
   the gems, the icing balls and the puff share 4.2s the same way, with the
   wave written into the frame positions rather than into animation-delay so
   the three tiers can never drift apart. Everything ambient is deliberately
   off both clocks and off each other: the symbol's sugar-light field at 4.3s
   and its glint at 2.3s, the crest's halo at 5.3s and its glints at 2.9s and
   3.7s.

   THE NUMBERS ARE PICKED FOR THEIR COMMON PERIOD, not for feel. Two cycles that
   share a factor visibly re-align, and the shorter that period is the more the
   pair reads as one pulse with a stutter in it. 4.2 against the symbol's 3.2
   goes 67.2s before it repeats; the earlier 4.0 went 16s, and the glint's
   earlier 2.5 sat 20s from the crest — both short enough to see. At the values
   above the tightest pairing left is 66.7s, between the two shortest ambients
   (2.3 and 2.9), which are coprime — that is simply the floor you get from
   durations that small, and both are faint glints rather than the gesture. Every
   pairing that involves a clock runs past a minute, so the art keeps breathing
   in the gaps and never pulses in lockstep.
   ------------------------------------------------------------------ */

/* THE SIGNATURE — the crank. A free quarter-turn, THE BITE (the turn stalls
   and even kicks back a couple of degrees as the wheel takes the load), then
   the mechanism lets go and the handle runs the rest of the lap, overshoots
   the stop by 4deg and settles onto it. `linear`, because the frames already
   carry the acceleration and an ease would smear the stall flat. */
@keyframes em-ss-crank {
  0%, 2%    { transform: rotate(0deg); }
  6%        { transform: rotate(64deg); }
  9%        { transform: rotate(96deg); }
  10.5%     { transform: rotate(88deg); }
  14%       { transform: rotate(232deg); }
  17%       { transform: rotate(348deg); }
  19%       { transform: rotate(364deg); }
  21%, 100% { transform: rotate(360deg); }
}
/* THE MACHINE taking the load. It leans INTO the bite and rebounds through a
   decaying wobble — over, back past true, over a little, home. This is the
   whole reason the catch reads as force going through a mechanism instead of
   a handle spinning freely in the air. */
@keyframes em-ss-body {
  0%, 8%    { transform: none; }
  10.5%     { transform: rotate(-1.5deg) translateY(0.35px); }
  14%       { transform: rotate(0.95deg) translateY(-0.15px); }
  18%       { transform: rotate(-0.45deg); }
  23%       { transform: rotate(0.18deg); }
  29%, 100% { transform: none; }
}
/* The gold ring round the glass catching the light on the bite — a hard
   specular with no movement, so the globe does not appear to swell. */
@keyframes em-ss-ring {
  0%, 9%    { filter: none; }
  12%       { filter: brightness(1.55) drop-shadow(0 0 1.6px #fbbf24); }
  20%       { filter: brightness(1.15); }
  30%, 100% { filter: none; }
}
/* The gumballs settling. Two mirrored variants, alternated along the stack in
   the skin: a handful of loose candy shoved from below scatters, it does not
   translate as a block. Sub-pixel on purpose — the globe is 29 units across in
   a 48 viewBox and anything bigger reads as the balls escaping the glass. */
@keyframes em-ss-ball-a {
  0%, 9%    { transform: none; }
  12%       { transform: translate(-0.35px, -0.95px); }
  16%       { transform: translate(0.3px, 0.5px); }
  21%       { transform: translate(-0.2px, -0.25px); }
  26%       { transform: translate(0.08px, 0.1px); }
  33%, 100% { transform: none; }
}
@keyframes em-ss-ball-b {
  0%, 9%    { transform: none; }
  12%       { transform: translate(0.4px, -0.8px); }
  16%       { transform: translate(-0.32px, 0.55px); }
  21%       { transform: translate(0.22px, -0.2px); }
  26%       { transform: translate(-0.1px, 0.12px); }
  33%, 100% { transform: none; }
}
/* The specular sliding on curved glass as the machine rocks. It travels the
   OPPOSITE way to the lean, because a highlight is the room reflected in the
   glass and the room is not what moved. */
@keyframes em-ss-gloss {
  0%, 8%    { transform: none; }
  11%       { transform: translate(0.85px, 0.2px); }
  16%       { transform: translate(-0.5px, -0.1px); }
  22%       { transform: translate(0.2px, 0.05px); }
  30%, 100% { transform: none; }
}
/* THE FLAP. Hinged along its top edge (pivot below), so a ball shoving through
   foreshortens it toward the hinge instead of shrinking it into the base. It
   swings wide, falls most of the way back, knocks once, and closes. */
@keyframes em-ss-chute {
  0%, 11%   { transform: none; }
  13.5%     { transform: scaleY(0.42); }
  18%       { transform: scaleY(0.9); }
  21%       { transform: scaleY(0.62); }
  25%       { transform: scaleY(0.97); }
  30%, 100% { transform: none; }
}
/* THE ONE THAT DROPS. Pure effect — dead at opacity 0 until the flap lets it
   out. Frames bunch at the top of the fall and spread toward the counter: that
   uneven spacing IS the gravity. Three bounces, each keeping a bit under half
   the height of the last, then it rolls clear and fades. */
/* THE FLOOR IS 3.2, and it is not a taste call. The ball is drawn cy=39.4 r=2.4,
   so its underside rests at y=41.8; the machine's base is `M13 34 h22 l3 11
   h-28 Z`, whose bottom edge — the counter both objects stand on — is y=45.
   45 − 41.8 = 3.2. Every frame that says "on the counter" reads 3.2px, and the
   bounce apexes are measured up from there. Push the floor past 3.2 and the
   gumball sinks through the counter the machine is standing on. */
@keyframes em-ss-drop {
  0%, 12%   { opacity: 0; transform: none; }
  13.5%     { opacity: 1; transform: translate(0.15px, 0.17px); }
  15.5%     { opacity: 1; transform: translate(0.5px, 0.6px); }
  17.5%     { opacity: 1; transform: translate(0.9px, 1.4px); }
  19.5%     { opacity: 1; transform: translate(1.4px, 2.4px); }
  21%       { opacity: 1; transform: translate(1.8px, 3.2px) scale(1.22, 0.78); }
  22.5%     { opacity: 1; transform: translate(2.2px, 2.87px) scale(0.92, 1.08); }
  25%       { opacity: 1; transform: translate(2.9px, 1.73px); }
  28%       { opacity: 1; transform: translate(3.5px, 3.2px) scale(1.12, 0.88); }
  30%       { opacity: 1; transform: translate(3.9px, 2.6px); }
  32%       { opacity: 1; transform: translate(4.4px, 3.2px) scale(1.05, 0.95); }
  34%       { opacity: 1; transform: translate(4.7px, 3.07px); }
  36%       { opacity: 1; transform: translate(5px, 3.2px); }
  46%       { opacity: 0; transform: translate(5.4px, 3.2px); }
  100%      { opacity: 0; transform: none; }
}
/* Sugar dust off the counter on the first bounce only — the second and third
   bounces do not kick any more up, which is what makes the first one land. */
@keyframes em-ss-puff {
  0%, 20%   { opacity: 0; transform: none; }
  22%       { opacity: 0.9; transform: scale(0.85); }
  28%       { opacity: 0.45; transform: scale(1.4) translateY(-0.6px); }
  34%       { opacity: 0; transform: scale(1.7) translateY(-1.1px); }
  100%      { opacity: 0; transform: none; }
}
/* Ambient: the sugar-light field the machine stands in. */
@keyframes em-ss-glow {
  0%, 100%  { opacity: 0.55; transform: none; }
  50%       { opacity: 0.8;  transform: scale(1.05); }
}
/* Ambient: the drawn glint. 0%/100% is opacity 1 on the WRAPPER — the glint's
   own 0.85 is painted into the child, so the wrapper's rest value is a clean
   1 and toggling the gate cannot shift it. */
@keyframes em-ss-spark {
  0%, 100%  { opacity: 1;    transform: scale(1); }
  50%       { opacity: 0.35; transform: scale(0.72); }
}

/* ---- reel symbol / lobby face: GumballMachineIcon (48 viewBox) ---- */
/* Crank, body, ring, balls, gloss, chute, puff and drop all share 3.2s and
   zero base delay: one mechanism, eight descriptions of it. */
.em-ss-crank {
  --em-anim: em-ss-crank;
  /* the shaft the handle is keyed to. The knob+handle bounding box centres at
     about x=40.6, so the default fill-box pivot would swing the bar round a
     point 1.2 units off the shaft and the knob would orbit its own hole. */
  transform-box: view-box;
  transform-origin: 39.4px 31px;
  animation-duration: 3.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-ss-body {
  --em-anim: em-ss-body;
  /* the foot — where the pedestal meets the counter. A machine bolted to a
     counter rocks about that line; about its own centre it would hover. */
  transform-box: view-box;
  transform-origin: 24px 45px;
  animation-duration: 3.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-ring {
  --em-anim: em-ss-ring;
  animation-duration: 3.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* The per-ball animation-delay is set in the skin, where each ball's HEIGHT in
   the globe is known, so the jostle ripples upward from the wheel. */
.em-ss-ball-a {
  --em-anim: em-ss-ball-a;
  animation-duration: 3.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-ball-b {
  --em-anim: em-ss-ball-b;
  animation-duration: 3.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-gloss {
  --em-anim: em-ss-gloss;
  animation-duration: 3.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-ss-chute {
  --em-anim: em-ss-chute;
  /* the top edge of the flap — its hinge. Squashing about the flap's own
     middle would shrink it into the base instead of swinging it open. */
  transform-box: view-box;
  transform-origin: 24px 37px;
  animation-duration: 3.2s;
  animation-timing-function: cubic-bezier(.3, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-drop {
  --em-anim: em-ss-drop;
  /* the ball's CONTACT point, not its middle: candy flattens against the
     surface it hits, so the squash has to happen at the bottom of the ball. */
  transform-box: view-box;
  transform-origin: 24px 41.8px;
  animation-duration: 3.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.em-ss-puff {
  --em-anim: em-ss-puff;
  /* where the ball actually hit — the dust blows out from the impact, not
     from the middle of the three specks that draw it. */
  transform-box: view-box;
  transform-origin: 25.8px 46.6px;
  animation-duration: 3.2s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-ss-glow {
  --em-anim: em-ss-glow;
  animation-duration: 4.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-ss-spark {
  --em-anim: em-ss-spark;
  animation-duration: 2.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* ---- ceremonial crest: SsCrownCrest (96 viewBox) ---- */
/* THE LANDING. The crown hops first — 3.4px of anticipation over ~200ms — and
   then comes down at 12% (~480ms). Without the hop the crown just twitches;
   with it the eye is already following the crown when it hits, which is the
   whole trick that makes a still cake read as struck. */
@keyframes em-ss-kcrown {
  0%, 3%    { transform: none; }
  8%        { transform: translateY(-3.4px); }
  12%       { transform: translateY(0.9px) scaleY(0.93); }
  16.5%     { transform: translateY(-1px)  scaleY(1.035); }
  21%       { transform: translateY(0.4px) scaleY(0.985); }
  27%       { transform: translateY(-0.15px); }
  34%, 100% { transform: none; }
}
/* THE WAVE DOWN THE STACK. Three keyframe sets rather than one shared set with
   three delays, so the sequence lives in the frame positions and all three
   tiers stay on one clock with zero delay — they physically cannot drift.
   Top tier takes the crown's whole weight (10% squash), the middle has spent
   some of it (6.5%), the bottom barely gives (3.5%) — and each widens as it
   shortens, because icing has to go somewhere. */
@keyframes em-ss-ktier-a {
  0%, 11%   { transform: none; }
  13.5%     { transform: scaleY(0.9) scaleX(1.05); }
  17.5%     { transform: scaleY(1.04) scaleX(0.98); }
  22%       { transform: scaleY(0.98) scaleX(1.01); }
  27%       { transform: scaleY(1.008); }
  33%, 100% { transform: none; }
}
@keyframes em-ss-ktier-b {
  0%, 14%   { transform: none; }
  16.5%     { transform: scaleY(0.935) scaleX(1.032); }
  20.5%     { transform: scaleY(1.026) scaleX(0.988); }
  25%       { transform: scaleY(0.988); }
  30%       { transform: scaleY(1.005); }
  36%, 100% { transform: none; }
}
@keyframes em-ss-ktier-c {
  0%, 17%   { transform: none; }
  19.5%     { transform: scaleY(0.965) scaleX(1.018); }
  23.5%     { transform: scaleY(1.014) scaleX(0.994); }
  28%       { transform: scaleY(0.994); }
  38%, 100% { transform: none; }
}
/* The piped rosettes popping as the wave passes under them. Overshoot then
   undershoot then home: piped icing is soft and takes two goes to sit down. */
@keyframes em-ss-krose {
  0%, 15%   { transform: none; }
  18%       { transform: scale(1.28); }
  22%       { transform: scale(0.94); }
  26%       { transform: scale(1.06); }
  32%, 100% { transform: none; }
}
/* The gems winking. Brightness and a white-hot drop-shadow together, because a
   candied gem catching light goes glassy, not just paler. */
@keyframes em-ss-kgem {
  0%, 11%   { transform: none; filter: none; }
  14.5%     { transform: scale(1.2);  filter: brightness(1.85) drop-shadow(0 0 2px #ffffff); }
  18%       { transform: scale(0.98); filter: brightness(1.12); }
  23%       { transform: scale(1.02); filter: none; }
  30%, 100% { transform: none; filter: none; }
}
/* Icing balls on the crown's points, knocked loose and settling. */
@keyframes em-ss-kicing {
  0%, 11%   { transform: none; }
  14%       { transform: translateY(-1.6px) scale(1.1); }
  18.5%     { transform: translateY(0.5px)  scale(0.96); }
  23%       { transform: translateY(-0.2px); }
  30%, 100% { transform: none; }
}
/* Icing sugar off the seat. Pure effect: dead until the crown comes down. It
   spreads outward AND drifts up, because that is what a light powder does when
   something heavy pushes the air out from under it. */
@keyframes em-ss-kpuff {
  0%, 11%   { opacity: 0; transform: none; }
  13%       { opacity: 0.9; transform: scale(0.85); }
  20%       { opacity: 0.4; transform: scale(1.45) translateY(-1.6px); }
  27%       { opacity: 0; transform: scale(1.8) translateY(-2.6px); }
  100%      { opacity: 0; transform: none; }
}
/* Ambient: the crest's sugar-light halo, and its two drawn glints. All three
   are off the landing's clock, so the crest is alive in the long gaps. */
@keyframes em-ss-khalo {
  0%, 100%  { opacity: 0.55; transform: none; }
  50%       { opacity: 0.82; transform: scale(1.03); }
}
@keyframes em-ss-kspark {
  0%, 100%  { opacity: 1;    transform: scale(1)    rotate(0deg); }
  50%       { opacity: 0.32; transform: scale(0.72) rotate(18deg); }
}
.em-ss-kcrown {
  --em-anim: em-ss-kcrown;
  /* the gold band — where the crown actually sits on the cake. Squashing about
     the crown's own centre would drive its points down into the icing. */
  transform-box: view-box;
  transform-origin: 48px 50px;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-ktier-a {
  --em-anim: em-ss-ktier-a;
  /* the top tier's own base line, where it meets the middle tier */
  transform-box: view-box;
  transform-origin: 48px 61px;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-ktier-b {
  --em-anim: em-ss-ktier-b;
  /* the middle tier's base line, where it meets the bottom tier */
  transform-box: view-box;
  transform-origin: 48px 74px;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-ktier-c {
  --em-anim: em-ss-ktier-c;
  /* the board the cake stands on — the bottom tier compresses into the table */
  transform-box: view-box;
  transform-origin: 48px 88px;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
/* Per-rosette animation-delay is set in the skin, where the row and the place
   along it are known, so the pop travels along each row and down the cake. */
.em-ss-krose {
  --em-anim: em-ss-krose;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.3, .9, .3, 1);
  animation-iteration-count: infinite;
}
/* Three gems, one keyframe set, equal periods, split phase. Equal periods are
   the point: a delay offsets the wink, a different duration would let the
   three of them slide out of the crown's beat over a long jackpot hold. */
.em-ss-kgem-a {
  --em-anim: em-ss-kgem;
  animation-duration: 4.2s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-ss-kgem-b {
  --em-anim: em-ss-kgem;
  animation-duration: 4.2s;
  animation-delay: 0.14s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-ss-kgem-c {
  --em-anim: em-ss-kgem;
  animation-duration: 4.2s;
  animation-delay: 0.28s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
/* The two shoulder balls carry their +0.07s in the skin, next to the geometry
   that explains it; the centre one has none, because it is the one that was
   directly under the crown when it came down. */
.em-ss-kicing {
  --em-anim: em-ss-kicing;
  animation-duration: 4.2s;
  animation-timing-function: cubic-bezier(.25, .9, .3, 1);
  animation-iteration-count: infinite;
}
.em-ss-kpuff {
  --em-anim: em-ss-kpuff;
  /* the crown's seat — the powder blows out from where the weight landed */
  transform-box: view-box;
  transform-origin: 48px 50px;
  animation-duration: 4.2s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}
.em-ss-khalo {
  --em-anim: em-ss-khalo;
  animation-duration: 5.3s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-ss-kspark-a {
  --em-anim: em-ss-kspark;
  animation-duration: 2.9s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.em-ss-kspark-b {
  --em-anim: em-ss-kspark;
  animation-duration: 3.7s;
  animation-delay: 0.6s;
  animation-direction: reverse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
