/* ============================================================================
   FOOTDICE — Keyframes (keyframes.css)
   Single-shot punctuation for meaningful events. No infinite decorative loops
   on meaningful content (diceWiggle is the one permitted loop, only while rolling).
   All collapse under reduced motion (block at the bottom).
   Source: GRIMOIRE/ILLUMINATIONS/Design Tokens.md §Motion.
   ============================================================================ */

/* slamIn — offset (-12,-12) → seat, opacity gated. Commentary rows, confirmations. */
@keyframes slamIn {
  0%   { transform: translate(-12px, -12px); opacity: 0; }
  100% { transform: translate(0, 0);         opacity: 1; }
}

/* popIn — transform-only (-10,-10) → seat (never hides content). Path / draw items. */
@keyframes popIn {
  0%   { transform: translate(-10px, -10px); }
  100% { transform: translate(0, 0); }
}

/* fadeUp — translateY(10px) + fade. Commentary lines, dice prompts. */
@keyframes fadeUp {
  0%   { transform: translateY(10px); opacity: 0; }
  100% { transform: translateY(0);    opacity: 1; }
}

/* draftReveal — staggered card reveal at the draft: drop in + slight overshoot, fade gated. */
@keyframes draftReveal {
  0%   { transform: translateY(-8px) scale(0.96); opacity: 0; }
  60%  { transform: translateY(2px)  scale(1.01); opacity: 1; }
  100% { transform: translateY(0)    scale(1);    opacity: 1; }
}

/* stampIn — scale(2.4) rotate(-14deg) → settle at rotate(-6deg). Tier stamps. */
@keyframes stampIn {
  0%   { transform: scale(2.4) rotate(-14deg); opacity: 0; }
  60%  { opacity: 1; }
  100% { transform: scale(1) rotate(-6deg);    opacity: 1; }
}

/* numSlam — scale(2.0) → 0.85 → 1. D100 center readout on land. */
@keyframes numSlam {
  0%   { transform: scale(2.0); }
  70%  { transform: scale(0.85); }
  100% { transform: scale(1); }
}

/* resultPunch — scale(1.7) + fade-in punch. Full-time score, RUN OVER / CHAMPIONS. */
@keyframes resultPunch {
  0%   { transform: scale(1.7); opacity: 0; }
  100% { transform: scale(1);   opacity: 1; }
}

/* scoreSlam — scale(2.2) starting yellow → settles ink. Live score digit on goal. */
@keyframes scoreSlam {
  0%   { transform: scale(2.2); color: var(--color-accent-yellow); }
  100% { transform: scale(1);   color: currentColor; }
}

/* zoneLand — brightness(2.2) → 1 flash on the hit tier zone. D100 tier wheel. */
@keyframes zoneLand {
  0%   { filter: brightness(2.2); }
  100% { filter: brightness(1); }
}

/* zoneDim — non-hit tier zones dim to --opacity-dim over 240ms. */
@keyframes zoneDim {
  0%   { opacity: 1; }
  100% { opacity: var(--opacity-dim); }
}

/* edgeFlash — 16px inset frame flash, decaying once (no strobe). NatFlash layer. */
@keyframes edgeFlash {
  0%   { box-shadow: inset 0 0 0 16px currentColor; opacity: 1; }
  100% { box-shadow: inset 0 0 0 16px currentColor; opacity: 0; }
}

/* natStamp — scale(3) rotate(-10deg) → seat at -7deg, holds, self-fades. NAT stamp. */
@keyframes natStamp {
  0%   { transform: scale(3) rotate(-10deg); opacity: 0; }
  20%  { transform: scale(1) rotate(-7deg);  opacity: 1; }
  80%  { transform: scale(1) rotate(-7deg);  opacity: 1; }
  100% { transform: scale(1) rotate(-7deg);  opacity: 0; }
}

/* diceShake — angular jitter. D20 crit/fumble flash. */
@keyframes diceShake {
  0%   { transform: rotate(0deg); }
  25%  { transform: rotate(-6deg); }
  50%  { transform: rotate(5deg); }
  75%  { transform: rotate(-3deg); }
  100% { transform: rotate(0deg); }
}

/* diceWiggle — ±2px bob. The ONLY permitted loop, only while rolling. */
@keyframes diceWiggle {
  0%   { transform: translateY(-2px); }
  50%  { transform: translateY(2px); }
  100% { transform: translateY(-2px); }
}

/* d100Pop — scale(1.16) → 0.965 → 1 body pop. D100 on settle. */
@keyframes d100Pop {
  0%   { transform: scale(1.16); }
  60%  { transform: scale(0.965); }
  100% { transform: scale(1); }
}

/* d20Tumble — fast rotate+scale churn while a D20 is mid-roll (composes with diceWiggle). */
@keyframes d20Tumble {
  0%   { transform: rotate(0deg)  scale(1); }
  25%  { transform: rotate(9deg)  scale(1.05); }
  50%  { transform: rotate(-7deg) scale(0.97); }
  75%  { transform: rotate(6deg)  scale(1.04); }
  100% { transform: rotate(0deg)  scale(1); }
}

/* fdNudge — DL-02 idle tap affordance: a scale-snap pulse on a clickable, waiting die.
   Loops only while the die is idle AND clickable; the class drops the instant a roll starts. */
@keyframes fdNudge {
  0%, 74%, 100% { transform: scale(1); }
  78%           { transform: scale(1.05); }
  86%           { transform: scale(1); }
  90%           { transform: scale(1.03); }
  94%           { transform: scale(1); }
}

/* fdBlink — DL-02 TAP TO ROLL chip blink (opacity snap, no fade). Shipped WITH fdNudge. */
@keyframes fdBlink {
  0%, 55%, 100% { opacity: 1; }
  60%, 72%      { opacity: .25; }
  76%           { opacity: 1; }
}

/* fdClack — DL-01 ±3° needle clack as the D100 sweep arrives on the rolled value. */
@keyframes fdClack {
  0%   { transform: rotate(3deg); }
  100% { transform: rotate(0deg); }
}

/* goPulse — core-loop CTA shout: the hard shadow snaps out then reseats (binary, no glow/blur).
   Finite (×5 via .fd-btn--go), so it settles on its own — not an infinite decorative loop. */
@keyframes goPulse {
  0%   { transform: translate(0, 0); box-shadow: var(--shadow-hard); }
  50%  { transform: translate(-3px, -3px);
         box-shadow: calc(var(--shadow-offset) + 3px) calc(var(--shadow-offset) + 3px) 0 0 var(--color-shadow); }
  100% { transform: translate(0, 0); box-shadow: var(--shadow-hard); }
}

/* chancePulse — slam in, hold a breath, self-fade. GF-10's ephemeral near-miss strip. */
@keyframes chancePulse {
  0%   { transform: translate(-12px, -12px); opacity: 0; }
  10%  { transform: translate(0, 0);         opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: translate(0, 0);         opacity: 0; }
}

/* ----------------------------------------------------------------------------
   Reusable animation utility classes (durations/eases from tokens).
   Components add these classes at beat boundaries; the server toggles state only.
   -------------------------------------------------------------------------- */
.anim-slam-in     { animation: slamIn 210ms steps(1, end) both; }
.anim-pop-in      { animation: popIn 240ms steps(1, end) both; }
.anim-fade-up     { animation: fadeUp 280ms ease-out both; }
.anim-stamp-in    { animation: stampIn 420ms var(--ease-punch) both; }
.anim-num-slam    { animation: numSlam 420ms var(--ease-punch) both; }
.anim-result-punch{ animation: resultPunch 460ms var(--ease-punch) both; }
.anim-score-slam  { animation: scoreSlam 300ms ease-out both; }
.anim-zone-land   { animation: zoneLand 700ms ease-out both; }
.anim-zone-dim    { animation: zoneDim 240ms ease-out both; }
.anim-edge-flash  { animation: edgeFlash 950ms ease-out both; }
.anim-nat-stamp   { animation: natStamp 1250ms var(--ease-punch) both; }
.anim-dice-shake  { animation: diceShake 600ms steps(8, end) both; }
.anim-dice-wiggle { animation: diceWiggle 220ms linear infinite; }
.anim-d100-pop    { animation: d100Pop 460ms var(--ease-punch) both; }
.anim-chance-pulse{ animation: chancePulse 1300ms ease-out both; }

/* ----------------------------------------------------------------------------
   Reduced motion — OS media query OR the Settings-mirrored .reduced-motion class
   on <html>. Either one on = reduced. Animations collapse to instant; the one
   permitted loop is killed. No information is motion-only (every roll also lands
   as number + TierStamp), so collapsing is safe.
   html.motion-full = the Settings REDUCED MOTION: OFF override — it defeats the
   OS media query (every raw @media block below must carry the :not() guard).
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-full) *,
  html:not(.motion-full) *::before,
  html:not(.motion-full) *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

html.reduced-motion *,
html.reduced-motion *::before,
html.reduced-motion *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

/* EXCEPTION — DL-01 reduced motion: the churn/sweep/tumble are SKIPPED (dice.js/dice3d.js check the
   same flags), the value lands instantly, and only the readout slam plays as the single land beat.
   It is brief (420ms), center-anchored (low vestibular risk), and never the sole carrier of
   information (every roll also lands as a number + TierStamp). */
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-full) .anim-num-slam { animation-duration: 420ms !important; }
}
html.reduced-motion .anim-num-slam { animation-duration: 420ms !important; }

/* GF-10 under reduced motion: chancePulse ENDS at opacity 0, so the instant-collapse would erase the
   strip entirely. Hold it statically visible instead — the screen clears it on the next event. */
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-full) .fd-match__chance { animation: none !important; opacity: 1; }
}
html.reduced-motion .fd-match__chance { animation: none !important; opacity: 1; }

/* Tutorial tour — single-shot attention pop when a highlight target is marked. The steady yellow
   outline carries the information (never motion-only); the pop is punctuation and collapses under
   the reduced-motion block above like every other one-shot. */
@keyframes tourPop {
  0%   { outline-offset: 12px; }
  100% { outline-offset: 4px; }
}
.fd-tour-target { animation: tourPop 240ms ease-out; }
