/* ==========================================================================
   Keyframes + entrance / feedback animation utilities

   Motion rules used across the template:
   - Only transform and opacity are animated.
   - The default curve (--ease-out) never overshoots.
   - State changes use transitions, so they can be reversed mid-flight.
   - Nothing loops forever unless it reports a live status (spinner, ping).
   ========================================================================== */

@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

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

@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.15); opacity: 0.7; }
}

@keyframes shimmer {
  0%   { background-position: -400px 0; }
  100% { background-position: 400px 0; }
}

@keyframes shake {
  20%, 60% { transform: translateX(-4px); }
  40%, 80% { transform: translateX(4px); }
}

@keyframes ping {
  0%   { transform: scale(1); opacity: 0.6; }
  100% { transform: scale(2.4); opacity: 0; }
}

@keyframes stripeMove {
  to { background-position: 40px 0; }
}

/* --- Entrance: fade + rise, staggered by nth-child on the parent --- */
.anim-in {
  animation: fadeInUp var(--duration-slow) var(--ease-out) both;
}

.stagger > * {
  animation: fadeInUp var(--duration-slow) var(--ease-out) both;
}

.stagger > *:nth-child(1) { animation-delay: 0ms; }
.stagger > *:nth-child(2) { animation-delay: 40ms; }
.stagger > *:nth-child(3) { animation-delay: 80ms; }
.stagger > *:nth-child(4) { animation-delay: 120ms; }
.stagger > *:nth-child(5) { animation-delay: 160ms; }
.stagger > *:nth-child(6) { animation-delay: 200ms; }
.stagger > *:nth-child(7) { animation-delay: 240ms; }
.stagger > *:nth-child(8) { animation-delay: 280ms; }

/* --- Hover lift, for clickable cards. Pointer devices only. --- */
.hover-lift {
  transition: transform var(--transition-base), box-shadow var(--transition-base);
}

@media (hover: hover) {
  .hover-lift:hover {
    transform: translateY(-3px);
  }
}

.hover-lift:active {
  transform: scale(var(--press-scale));
  transition-duration: var(--duration-fast);
}

/* --- Skeleton loading shimmer --- */
.skeleton {
  background: linear-gradient(90deg, var(--bg) 25%, var(--bg-elevated) 37%, var(--bg) 63%);
  background-size: 800px 100%;
  animation: shimmer 1.6s linear infinite;
  border-radius: var(--radius-sm);
  box-shadow: var(--neu-pressed);
  color: transparent;
  user-select: none;
}

.skeleton-text { height: 0.9em; margin-block: 0.45em; }
.skeleton-title { height: 1.4em; width: 55%; margin-bottom: var(--space-3); }
.skeleton-avatar { width: 52px; height: 52px; border-radius: 50%; flex-shrink: 0; }
.skeleton-block { height: 120px; border-radius: var(--radius-md); }

/* --- Scroll-triggered reveal: paired with an IntersectionObserver in
   js/main.js. Elements start hidden/offset and animate in the first time
   they enter the viewport. --- */
.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--transition-slow), transform var(--transition-slow);
}

.reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

.reveal.stagger > * {
  opacity: 0;
  animation: none;
}

.reveal.stagger.in-view > * {
  animation: fadeInUp var(--duration-slow) var(--ease-out) both;
}

/* --- Error feedback: a short horizontal shake, used on invalid input --- */
.shake {
  animation: shake 320ms var(--ease-in-out) both;
}

/* --- Static ambient background blobs for hero sections. They do not move:
   large drifting backgrounds are a vestibular trigger. --- */
.bg-orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.28;
  pointer-events: none;
  z-index: -1;
}

.bg-orb-accent { background: var(--accent); }
.bg-orb-warm { background: var(--accent-warm); }

/* --------------------------------------------------------------------------
   Reduced motion: not "no feedback", but a gentler equivalent. Movement
   (transform) snaps, while opacity and color still cross-fade briefly.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, color, background-color, box-shadow, visibility !important;
    transition-duration: 200ms !important;
    transition-timing-function: ease !important;
    scroll-behavior: auto !important;
  }

  /* A loading indicator must keep reporting status. */
  .neu-spinner::before,
  .neu-btn.loading::after {
    animation-duration: 900ms !important;
    animation-iteration-count: infinite !important;
  }
}
