/* ==========================================================================
   animations.css — every @keyframes in the app plus the utility classes and
   component hooks that apply them.

   WHY the "from" state lives only inside @keyframes and never on the base
   class: the motion kill-switch in tokens.css resolves to
   `animation: none !important`. If a class carried `opacity: 0` of its own,
   killing the animation would strand the element invisible. Keeping the
   start state inside the keyframes means "no animation" degrades to "already
   in its final state" — the whole file disappears cleanly.

   Every utility uses `both` fill mode so an element is correct before the
   first frame and after the last one.

   Component hooks at the bottom only ever set `animation` (never colour or
   layout) and animations.css loads BEFORE components.css, so a component can
   always override them.
   ========================================================================== */

/* ==========================================================================
   Keyframes
   ========================================================================== */

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes fade-in-up {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: none; }
}

@keyframes scale-in {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: none; }
}

@keyframes slide-in-left {
  from { opacity: 0; transform: translate3d(-18px, 0, 0); }
  to   { opacity: 1; transform: none; }
}

/* Travels its own full height, so it works for a dock/drawer of any size. */
@keyframes slide-in-up {
  from { opacity: 0; transform: translate3d(0, 100%, 0); }
  to   { opacity: 1; transform: none; }
}

/* Skeletons: a sweep of light across a background-size:220% gradient. */
@keyframes shimmer {
  from { background-position: 140% 0; }
  to   { background-position: -60% 0; }
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

@keyframes dot-pulse {
  0%, 80%, 100% { opacity: 0.22; transform: scale(0.62); }
  40%           { opacity: 1;    transform: scale(1); }
}

@keyframes ripple {
  from { opacity: 0.32; transform: scale(0); }
  to   { opacity: 0;    transform: scale(2.6); }
}

/* Expanding halo in whatever colour the element currently is — one keyframe
   serves the accent, the online dot and the error dot alike. */
@keyframes glow-pulse {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 55%, transparent);
  }
  70% {
    box-shadow: 0 0 0 9px color-mix(in srgb, currentColor 0%, transparent);
  }
  100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 0%, transparent);
  }
}

@keyframes toast-in {
  from { opacity: 0; transform: translate3d(0, 16px, 0) scale(0.96); }
  to   { opacity: 1; transform: none; }
}

@keyframes toast-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(0, -6px, 0) scale(0.96); }
}

@keyframes modal-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0) scale(0.97); }
  to   { opacity: 1; transform: none; }
}

@keyframes modal-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(0, 6px, 0) scale(0.98); }
}

/* Decoded images resolve out of a soft blur instead of popping in — the one
   detail that makes an image app feel native rather than web. */
@keyframes image-reveal {
  from {
    opacity: 0;
    transform: scale(1.025);
    filter: blur(14px) saturate(0.65);
  }
  60% {
    opacity: 1;
  }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0) saturate(1);
  }
}

/* ==========================================================================
   Utility classes
   ========================================================================== */

.anim-fade-in       { animation: fade-in var(--dur) var(--ease) both; }
.anim-fade-in-up    { animation: fade-in-up var(--dur-slow) var(--ease-out) both; }
.anim-scale-in      { animation: scale-in var(--dur) var(--ease-spring) both; }
.anim-slide-in-left { animation: slide-in-left var(--dur-slow) var(--ease-out) both; }
.anim-slide-in-up   { animation: slide-in-up var(--dur-slow) var(--ease-out) both; }
.anim-image-reveal  { animation: image-reveal 520ms var(--ease-out) both; }

.anim-toast-in  { animation: toast-in var(--dur) var(--ease-spring) both; }
.anim-toast-out { animation: toast-out var(--dur-fast) var(--ease) both; }
.anim-modal-in  { animation: modal-in var(--dur) var(--ease-spring) both; }
.anim-modal-out { animation: modal-out var(--dur-fast) var(--ease) both; }

.anim-spin      { animation: spin 800ms linear infinite; }
.anim-shimmer   { animation: shimmer 1.5s linear infinite; }
.anim-dot-pulse { animation: dot-pulse 1.2s var(--ease) infinite; }
.anim-glow      { animation: glow-pulse 2.2s var(--ease-out) infinite; }
.anim-ripple    { animation: ripple var(--dur-slow) var(--ease-out) forwards; }

/* Delay steps for hand-placed sequences. */
.anim-delay-1 { animation-delay: 40ms; }
.anim-delay-2 { animation-delay: 80ms; }
.anim-delay-3 { animation-delay: 120ms; }
.anim-delay-4 { animation-delay: 160ms; }
.anim-delay-5 { animation-delay: 220ms; }

/* Cascade for freshly rendered lists/grids. JS may set `--i` on each child
   for unlimited depth; the nth-child fallback covers the first screenful so
   nothing has to be instrumented to look right. */
.anim-stagger > * {
  animation: fade-in-up var(--dur-slow) var(--ease-out) both;
  animation-delay: calc(var(--stagger-step, 34ms) * var(--i, 0));
}

.anim-stagger > *:nth-child(1)  { --i: 0; }
.anim-stagger > *:nth-child(2)  { --i: 1; }
.anim-stagger > *:nth-child(3)  { --i: 2; }
.anim-stagger > *:nth-child(4)  { --i: 3; }
.anim-stagger > *:nth-child(5)  { --i: 4; }
.anim-stagger > *:nth-child(6)  { --i: 5; }
.anim-stagger > *:nth-child(7)  { --i: 6; }
.anim-stagger > *:nth-child(8)  { --i: 7; }
.anim-stagger > *:nth-child(9)  { --i: 8; }
.anim-stagger > *:nth-child(10) { --i: 9; }
.anim-stagger > *:nth-child(11) { --i: 10; }
.anim-stagger > *:nth-child(12) { --i: 11; }
/* Anything past the fold arrives together rather than trailing off. */
.anim-stagger > *:nth-child(n + 13) { --i: 12; }

/* ==========================================================================
   Component hooks
   Only `animation` (plus the geometry a purely-decorative element needs).
   components.css loads afterwards and wins on every other property.
   ========================================================================== */

/* Skeletons: background-image only, so the component keeps ownership of the
   base background-color and the gradient composites on top of it. */
.skeleton {
  background-image: linear-gradient(
    100deg,
    transparent 18%,
    color-mix(in srgb, var(--text-0) 9%, transparent) 46%,
    transparent 74%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: shimmer 1.5s linear infinite;
}

.spinner {
  animation: spin 800ms linear infinite;
}

/* Three children, any tag — staggered so it reads as a wave, not a blink. */
.dot-pulse > * {
  animation: dot-pulse 1.2s var(--ease) infinite;
}

.dot-pulse > *:nth-child(2) { animation-delay: 160ms; }
.dot-pulse > *:nth-child(3) { animation-delay: 320ms; }

/* Ripple span injected on click. Purely decorative: it must never intercept
   the pointer or affect layout. */
.btn__ripple {
  position: absolute;
  border-radius: var(--radius-full);
  pointer-events: none;
  background: currentColor;
  transform: scale(0);
  animation: ripple var(--dur-slow) var(--ease-out) forwards;
}

/* Views and overlays get their entrance for free. */
.view--active {
  animation: fade-in var(--dur) var(--ease) both;
}

.modal-backdrop {
  animation: fade-in var(--dur-fast) var(--ease) both;
}

.modal {
  animation: modal-in var(--dur) var(--ease-spring) both;
}

.menu {
  animation: scale-in var(--dur-fast) var(--ease-out) both;
  transform-origin: var(--menu-origin, top left);
}

.toast {
  animation: toast-in var(--dur) var(--ease-spring) both;
}

/* Exit states — set the class, listen for `animationend`, then remove. */
.modal.is-closing        { animation: modal-out var(--dur-fast) var(--ease) both; }
.modal-backdrop.is-closing,
.menu.is-closing         { animation: fade-in var(--dur-fast) var(--ease) reverse both; }
.toast.is-closing        { animation: toast-out var(--dur-fast) var(--ease) both; }

/* ==========================================================================
   Reduced motion — belt and braces.

   The global kill-switch in tokens.css already removes every animation. The
   rules below only clean up the *decorative leftovers* that would otherwise
   freeze mid-effect: a shimmer gradient parked at 140%, a ripple stuck at
   scale(0), a spinner locked at an arbitrary angle.
   ========================================================================== */

:root[data-anim="off"] .skeleton {
  background-image: none;
}

:root[data-anim="off"] .btn__ripple {
  display: none;
}

:root[data-anim="off"] .dot-pulse > * {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  :root:not([data-anim="on"]) .skeleton {
    background-image: none;
  }

  :root:not([data-anim="on"]) .btn__ripple {
    display: none;
  }

  :root:not([data-anim="on"]) .dot-pulse > * {
    opacity: 1;
    transform: none;
  }
}
