/* ==========================================================================
   Goblin Miners — animations.css
   Loaded last, so it may override anything in components.css.

   Everything here animates transform and opacity only (CONTRACT §8). Nothing
   animates width, top or left; the two bars use scaleX and are declared in
   components.css.

   HOW THE POOLED EFFECTS REPLAY
   Floating numbers, ore bits and the golden goblin come from fixed-size pools
   and are shown and hidden with the `hidden` attribute. `[hidden]` resolves to
   display:none, and taking an element out of display:none restarts its CSS
   animations from frame zero. So the coder replays an effect by toggling
   `hidden` off — no class juggling, no forced reflow, no animation API.
   ========================================================================== */

/* ---------------------------------------------------------------- keyframes */

/* POSITION INDEPENDENCE
   Every keyframe below is pure relative motion: each starts from the identity
   transform and displaces from there. None of them names a coordinate on the
   page, so an effect animates the same way wherever the game's left/top puts
   it. Centring on the spawn point is done by the static `translate` longhand
   in components.css, which composes with `transform` instead of being
   overwritten by it — that is why no keyframe has to carry a -50% any more. */

/* TRAVEL AND DISSOLVE
   Cookie Clicker's model, replacing the punch-and-bloom that came before. The
   dominant impression should be a number rising a real distance and fading out
   gently, not something detonating at the point of impact.

   Three things changed. Travel roughly doubled, so the eye follows the number
   away from the rock instead of watching it swell in place. Scale drama is
   nearly gone — the numbers now hold their size for the whole rise, so growth
   never competes with movement. And the fade is genuinely slow: opacity holds
   near full for the first third and then eases away across the remaining
   two-thirds, which is what makes it read as dissolving rather than as being
   switched off.

   What is deliberately kept: a small entry so a number never simply appears,
   and a distinct one for crits. Everything sits inside the 1600ms hold. */

/* --gm-drift is set per spawn by the game, so no two numbers take the same path
   up. Without it a fast tapper stacks the whole pool on one pixel and reads a
   smear instead of a number. */
@keyframes gm-float-hit {
  0%   { transform: translate(0, 0) scale(.92); opacity: 0; }
  9%   { transform: translate(calc(var(--gm-drift, 0px) * .12), -7px) scale(1); opacity: 1; }
  38%  { opacity: .95; }
  100% { transform: translate(var(--gm-drift, 0px), -88px) scale(1); opacity: 0; }
}

/* A crit still has to announce itself, but through a brief entry pop and a
   longer, further drift rather than through size. It rises higher than a normal
   hit and outlives it, so it is the last thing on screen. */
@keyframes gm-float-crit {
  0%   { transform: translateY(0)      scale(.7)  rotate(-5deg); opacity: 0; }
  7%   { transform: translateY(-7px)   scale(1.3) rotate(2deg);  opacity: 1; }
  17%  { transform: translateY(-15px)  scale(1.1) rotate(0deg);  opacity: 1; }
  45%  { opacity: .95; }
  100% { transform: translateY(-116px) scale(1.1) rotate(0deg);  opacity: 0; }
}

/* Gold drifts furthest and slowest — it is a reward, and it should still be
   climbing after the damage numbers around it have gone. */
@keyframes gm-float-gold {
  0%   { transform: translateY(0)      scale(.92); opacity: 0; }
  10%  { transform: translateY(-8px)   scale(1.06); opacity: 1; }
  40%  { opacity: .95; }
  100% { transform: translateY(-100px) scale(1.06); opacity: 0; }
}

/* Eight fixed trajectories, handed out around the pool by :nth-child so the
   spray looks scattered without any per-element inline style. Each starts from
   the identity transform, so like the numbers they displace from wherever the
   game placed them.

   Chips now arc instead of sliding: they burst outward and upward, then gravity
   takes them below the impact point. The midpoint keyframe is the apex. They
   also stay full-size and opaque through the fast part and only shrink on the
   way down, so the eye reads a spray of debris rather than eight dots
   dissolving. All inside the game's 800ms hold for bits. */
@keyframes gm-bit-1 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(-24px, -14px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(-38px, 30px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-2 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(22px, -18px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(34px, 34px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-3 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(-34px, -22px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(-50px, 18px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-4 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(32px, -10px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(48px, 26px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-5 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(-16px, -32px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(-26px, 16px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-6 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(14px, -36px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(24px, 12px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-7 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(-40px, -26px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(-56px, 10px) scale(.5); opacity: 0; }
}
@keyframes gm-bit-8 {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; animation-timing-function: ease-out; }
  40%  { transform: translate(38px, -28px) scale(1); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(54px, 14px) scale(.5); opacity: 0; }
}

/* A recoil, not a jitter. The old five-step shake read as the rock vibrating,
   which is what a phone does, not what a struck rock does. This drives it down
   and squashes it on the frame of impact, lets it rebound past its rest height,
   then settles — the squash-and-stretch that makes a hit read as force. Kept to
   one axis pair so it stays a rigid body move and the pixel grid holds. */
@keyframes gm-rock-hit {
  0%   { transform: translate(0, 0)    scale(1, 1); }
  14%  { transform: translate(0, 4px)  scale(1.07, .92); }
  38%  { transform: translate(0, -3px) scale(.965, 1.04); }
  64%  { transform: translate(0, 1px)  scale(1.015, .988); }
  100% { transform: translate(0, 0)    scale(1, 1); }
}

/* THE SWING IS FRAMES, NOT A TWEEN.

   This used to be `rotate(-7deg)` to `rotate(7deg)`, and a rotation is the one
   transform pixel art cannot survive: every edge in the sprite lands off the
   device grid and the browser antialiases the whole goblin into a smudge. A
   16-bit goblin swings the way a 16-bit goblin has always swung — in whole
   pixels, on hard frames, with steps(1) doing the cutting.

   Displacement is in SOURCE pixels via --gm-gp (the goblin's display size over
   its 24px grid), so a 2px chop is two sprite pixels at 2x on a desktop and two
   sprite pixels at 1x on a phone, rather than a twitch at one size and a lurch
   at the other.

   +x is the direction the goblin faces. The right-hand half of the crew carries
   `scale: -1 1`, and because `scale` is applied before `transform`, their +x
   resolves the other way — both sides swing INTO the rock off one keyframe. */
@keyframes gm-mine-swing {
  0%   { transform: translate(calc(var(--gm-gp) * -1), calc(var(--gm-gp) * -1)); }
  25%  { transform: translate(calc(var(--gm-gp) * -2), calc(var(--gm-gp) * -2)); }
  50%  { transform: translate(calc(var(--gm-gp) *  2), calc(var(--gm-gp) *  1)); }
  75%  { transform: translate(calc(var(--gm-gp) *  1), var(--gm-gp)); }
  100% { transform: translate(calc(var(--gm-gp) * -1), calc(var(--gm-gp) * -1)); }
}

/* Two frames, same reasoning: a goblin leaning on its pick, breathing. */
@keyframes gm-idle-bob {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(calc(var(--gm-gp) * -1)); }
  100% { transform: translateY(0); }
}

/* -40px enters from just off the left edge; 100% is the full mine width, so the
   sprite exits just past the right. See the .gm-golden note in components.css
   for why the animated element is full width. */
@keyframes gm-scurry {
  from { transform: translateX(-40px); }
  to   { transform: translateX(100%); }
}

@keyframes gm-banner-in {
  from { transform: translateY(-10px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

/* Slides in from the left edge now that the stack is bottom-left anchored. */
@keyframes gm-toast-in {
  from { transform: translateX(-16px); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

@keyframes gm-dot-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.5); opacity: .65; }
}

@keyframes gm-perk-glow {
  0%, 100% { opacity: 1; }
  50%      { opacity: .62; }
}

@keyframes gm-prestige-flash {
  0%   { opacity: 0; }
  8%   { opacity: 1; }
  100% { opacity: 1; }
}

@keyframes gm-sigil-throb {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}

/* ------------------------------------------------------------- dig feedback */

.gm-dig {
  transition: transform var(--gm-dur) ease, background-color var(--gm-dur) ease;
}

.gm-dig[data-pressed="true"] {
  transform: translateY(3px) scale(.985);
  border-bottom-width: 1px;
}

.gm-dig__icon { transition: transform var(--gm-dur) ease; }
.gm-dig[data-pressed="true"] .gm-dig__icon { transform: rotate(-18deg) scale(1.1); }

/* --------------------------------------------------------------- rock shake */

/* Damage from any source — tap, goblin DPS, dynamite — shakes the rock, via
   data-hit. Toggle it off and on again to replay; re-adding the attribute
   restarts the animation.

   The press feedback is deliberately on the button and the shake on the sprite
   inside it. Two animations competing for `transform` on one element would
   cancel each other, and a tap fires both at once. */
.gm-rock[data-hit="true"] .gm-rock__sprite {
  animation: gm-rock-hit 190ms cubic-bezier(.2, .9, .3, 1);
  transform-origin: 50% 82%;   /* pivot near the base, so it sits on the floor */
}

/* data-hit fires for damage from any source, so goblin chewing shakes the rock
   too. The press is what separates *your* tap from the background work: it is
   on the button rather than the sprite, so the two never fight over transform,
   and it bites harder than the recoil for the instant the finger is down. */
.gm-rock { transition: transform 60ms ease-out; }
.gm-rock:active { transform: scale(.94); }

/* ------------------------------------------------- floating damage numbers */

.gm-fx__num {
  /* ease-out so the rise decelerates as it climbs — a number that keeps a
     constant speed reads as mechanical. Durations use the 1600ms ceiling
     rather than crowding it: 1300/1500/1400 leaves the game headroom to hide
     the element without ever clipping the fade. */
  animation: gm-float-hit 1300ms ease-out forwards;
}

.gm-fx__num[data-kind="crit"] { animation: gm-float-crit 1500ms ease-out forwards; }
.gm-fx__num[data-kind="gold"] { animation: gm-float-gold 1400ms ease-out forwards; }

/* ------------------------------------------------------------- ore particles */

.gm-fx__bit {
  animation: gm-bit-1 620ms ease-out forwards;
}

.gm-fx__bit:nth-child(8n + 2) { animation-name: gm-bit-2; animation-duration: 680ms; }
.gm-fx__bit:nth-child(8n + 3) { animation-name: gm-bit-3; animation-duration: 560ms; }
.gm-fx__bit:nth-child(8n + 4) { animation-name: gm-bit-4; animation-duration: 700ms; }
.gm-fx__bit:nth-child(8n + 5) { animation-name: gm-bit-5; animation-duration: 640ms; }
.gm-fx__bit:nth-child(8n + 6) { animation-name: gm-bit-6; animation-duration: 720ms; }
.gm-fx__bit:nth-child(8n + 7) { animation-name: gm-bit-7; animation-duration: 600ms; }
.gm-fx__bit:nth-child(8n + 8) { animation-name: gm-bit-8; animation-duration: 660ms; }

/* data-particles="off" removes the bits from the render tree in
   components.css; belt and braces, make sure they cost no animation either. */
.gm-app[data-particles="off"] .gm-fx__bit { animation: none; }

/* ------------------------------------------------------------------ goblins */

/* One source pixel, in display pixels. Everything the crew moves is a multiple
   of this, so the swing is the same swing at 1x and at 2x. */
.gm-goblin { --gm-gp: calc(var(--gm-goblin-size) / 24); }

.gm-goblin svg { transform-origin: 50% 100%; }

.gm-goblin[data-state="mine"] svg {
  animation: gm-mine-swing 420ms steps(1, end) infinite;
}

.gm-goblin[data-state="idle"] svg {
  animation: gm-idle-bob 1900ms steps(1, end) infinite;
}

/* STAGGER.

   The old stagger was three `nth-child(4n + k)` delays plus four per-variant
   durations, and those two are the SAME partition: the pool is built with
   `data-variant = i % 4`, so nth-child(4n+2) is always variant 1. Delay and
   tempo were therefore locked together and slots 1, 5 and 9 swung in perfect
   unison — three goblins moving as one, three times over, which is exactly the
   block the stagger existed to break up.

   Twelve explicit delays against four durations gives twelve distinct phases
   that also drift apart, because the periods do not divide each other. Negative
   delays start every goblin mid-swing, so nothing snaps into line when the crew
   grows by one. */
.gm-goblin:nth-child(1)  svg { animation-delay: 0ms; }
.gm-goblin:nth-child(2)  svg { animation-delay: -137ms; }
.gm-goblin:nth-child(3)  svg { animation-delay: -61ms; }
.gm-goblin:nth-child(4)  svg { animation-delay: -211ms; }
.gm-goblin:nth-child(5)  svg { animation-delay: -89ms; }
.gm-goblin:nth-child(6)  svg { animation-delay: -173ms; }
.gm-goblin:nth-child(7)  svg { animation-delay: -29ms; }
.gm-goblin:nth-child(8)  svg { animation-delay: -251ms; }
.gm-goblin:nth-child(9)  svg { animation-delay: -113ms; }
.gm-goblin:nth-child(10) svg { animation-delay: -47ms; }
.gm-goblin:nth-child(11) svg { animation-delay: -191ms; }
.gm-goblin:nth-child(12) svg { animation-delay: -71ms; }

.gm-goblin[data-variant="0"] svg { animation-duration: 400ms; }
.gm-goblin[data-variant="1"] svg { animation-duration: 468ms; }
.gm-goblin[data-variant="2"] svg { animation-duration: 356ms; }
.gm-goblin[data-variant="3"] svg { animation-duration: 524ms; }

.gm-goblin[data-state="idle"][data-variant="0"] svg { animation-duration: 1800ms; }
.gm-goblin[data-state="idle"][data-variant="1"] svg { animation-duration: 2100ms; }
.gm-goblin[data-state="idle"][data-variant="2"] svg { animation-duration: 1650ms; }
.gm-goblin[data-state="idle"][data-variant="3"] svg { animation-duration: 2350ms; }

/* ------------------------------------------------------------ golden goblin */

.gm-golden {
  animation: gm-scurry 12s linear forwards;
}

/* Scale the sprite, never the strip — a transform on .gm-golden would replace
   the scurry transform and snap it back to the start. */
.gm-golden svg:hover { transform: scale(1.12); }

/* --------------------------------------------------------- banners & toasts */

.gm-event { animation: gm-banner-in 220ms ease-out; }
.gm-toast { animation: gm-toast-in 220ms ease-out; }

/* --------------------------------------------------------------- indicators */

.gm-tabs__dot { animation: gm-dot-pulse 1400ms ease-in-out infinite; }

.gm-perk[data-state="available"] { animation: gm-perk-glow 2200ms ease-in-out infinite; }

/* --------------------------------------------------------------- prestige */

/* No dedicated full-screen flash element exists in the contract, so the
   prestige modal's own backdrop carries the flash as it opens. */
.gm-modal[data-modal="prestige"] {
  animation: gm-prestige-flash 900ms ease-out;
}

.gm-prestige__sigil { animation: gm-sigil-throb 2600ms ease-in-out infinite; }

/* The one gem you can actually light breathes. Everything else in the sky is
   still, so the eye goes to it without an arrow or a label saying "here". */
.gm-ascend__gem[data-state="ready"] {
  animation: gm-perk-glow 2200ms ease-in-out infinite;
}

/* ------------------------------------------------------- hover transitions */

.gm-item,
.gm-btn,
.gm-perk,
.gm-tabs__tab,
.gm-buybar__opt {
  transition: background-color var(--gm-dur) ease,
              border-color var(--gm-dur) ease,
              color var(--gm-dur) ease,
              transform var(--gm-dur) ease;
}

.gm-item[data-affordable="true"]:active { transform: translateY(1px); }

/* ========================================================================== */
/* REDUCED MOTION                                                             */
/* Minimal motion, not zero feedback: a player who asks for less movement      */
/* still needs to see that a number appeared and a toast arrived. Loops stop,  */
/* travel stops, cross-fades stay.                                            */
/* ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  @keyframes gm-fade-only {
    from { opacity: 1; }
    to   { opacity: 0; }
  }

  /* The travel is what reduced motion objects to, not the dwell — so the
     numbers still linger and fade gently, they just do it in place. */
  .gm-fx__num,
  .gm-fx__num[data-kind="crit"],
  .gm-fx__num[data-kind="gold"] {
    animation: gm-fade-only 1200ms linear forwards;
    transform: translateY(-14px);
  }

  .gm-fx__bit {
    animation: gm-fade-only 400ms linear forwards;
  }

  .gm-goblin[data-state="mine"] svg,
  .gm-goblin[data-state="idle"] svg,
  .gm-tabs__dot,
  .gm-perk[data-state="available"],
  .gm-ascend__gem[data-state="ready"],
  .gm-prestige__sigil,
  .gm-modal[data-modal="prestige"] {
    animation: none;
  }

  .gm-rock[data-hit="true"] .gm-rock__sprite { animation: none; }
  .gm-rock:active { transform: none; }

  /* The golden goblin has to cross the mine or it cannot be tapped, but it can
     do it calmly and without the pop-in. */
  .gm-golden { animation: gm-scurry 12s linear forwards; }

  .gm-event,
  .gm-toast {
    animation: gm-fade-only 200ms linear reverse;
  }

  .gm-dig[data-pressed="true"] { transform: none; }
  .gm-dig[data-pressed="true"] .gm-dig__icon { transform: none; }

  .gm-item,
  .gm-btn,
  .gm-perk,
  .gm-tabs__tab,
  .gm-buybar__opt {
    transition-duration: 1ms;
  }

  .gm-item[data-affordable="true"]:active { transform: none; }
  .gm-golden svg:hover { transform: none; }
}
