/* ==========================================================================
   ANIMATIONS
   ========================================================================== */

/* ---------- Loader ---------- */

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

@keyframes loaderLabelIn {
  to { transform: translateY(0); }
}

@keyframes loaderFill {
  0%   { width: 0%; }
  60%  { width: 70%; }
  100% { width: 100%; }
}

/* ---------- Mobile menu ---------- */

@keyframes mobileLinkIn {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ---------- Hero atmosphere — aurora ----------
   Three independent, slow drifts (different periods so they never
   sync into a visible loop) — this is the Hero's ambient light source,
   not decoration layered on top of one. */

@keyframes auroraDriftA {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(3%, 4%) scale(1.06); }
}

@keyframes auroraDriftB {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(-4%, 3%) scale(1.04); }
}

@keyframes auroraDriftC {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(2%, -3%) scale(1.08); }
}

/* ---------- Hero status badge — live pulse ----------
   Signals "current/active," tied to interaction/credibility, not
   ambient — it is the only fast-cycling motion in the Hero and stays
   small and contained on purpose. */

@keyframes badgePulse {
  0%   { box-shadow: 0 0 0 0 rgba(45, 212, 191, 0.55); }
  70%  { box-shadow: 0 0 0 7px rgba(45, 212, 191, 0); }
  100% { box-shadow: 0 0 0 0 rgba(45, 212, 191, 0); }
}

/* ---------- Hero composition — floating card + orb + chips ----------
   Depth cue: card, orb and each chip float on their own period/phase
   so the composition never reads as flat or synchronized — this is
   what sells "layered" rather than "pasted on." */

@keyframes cardFloat {
  0%, 100% { transform: translate(-50%, -50%) rotateY(-8deg) rotateX(4deg) translateY(0); }
  50%      { transform: translate(-50%, -50%) rotateY(-8deg) rotateX(4deg) translateY(-10px); }
}

@keyframes orbPulse {
  0%, 100% { opacity: 0.85; transform: scale(1); }
  50%      { opacity: 1; transform: scale(1.06); }
}

@keyframes chipFloatA {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(-12px) rotate(-1deg); }
}

@keyframes chipFloatB {
  0%, 100% { transform: translateY(0) rotate(2deg); }
  50%      { transform: translateY(10px) rotate(3deg); }
}

@keyframes chipFloatC {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-8px) rotate(-2deg); }
}

/* ---------- Scroll indicator ---------- */

@keyframes scrollWheel {
  0%   { transform: translateY(0); opacity: 0.9; }
  60%  { transform: translateY(6px); opacity: 0.15; }
  61%  { transform: translateY(0); opacity: 0; }
  100% { transform: translateY(0); opacity: 0.9; }
}

/* ---------- Hero cinematic entrance ----------
   Motion language: opacity + blur only (never transform) so this can
   run alongside the mouse-parallax system, which owns `transform` on
   the same elements without the two fighting for the same channel. */

.hero-reveal {
  opacity: 0;
  filter: blur(6px);
  animation: heroFadeReveal 1000ms var(--ease-out) forwards;
  animation-delay: calc(var(--stagger, 0) * 130ms + 520ms);
}

@keyframes heroFadeReveal {
  to {
    opacity: 1;
    filter: blur(0);
  }
}

/* ---------- Hero title mask reveal ----------
   Classic editorial technique: the line is clipped by an overflow-
   hidden mask and slides up from beneath it, like a curtain lifting. */

.hero__title-mask {
  display: block;
  overflow: hidden;
}

.hero-reveal-line {
  display: inline-block;
  transform: translateY(112%);
  animation: titleLineReveal 1150ms var(--ease-out) forwards;
  animation-delay: calc(var(--stagger, 0) * 150ms + 420ms);
  will-change: transform;
}

@keyframes titleLineReveal {
  to { transform: translateY(0); }
}

/* ---------- Stats — infinity pulse ----------
   The one counter that isn't a number: a slow glow pulse stands in
   for "counting forever" rather than looping digits pointlessly. */

@keyframes statsInfinitePulse {
  0%, 100% { opacity: 0.75; filter: drop-shadow(0 0 0 rgba(76, 130, 255, 0)); }
  50%      { opacity: 1; filter: drop-shadow(0 0 14px rgba(139, 92, 246, 0.45)); }
}

/* ---------- Ambient field (sitewide) ----------
   Four blobs, four periods (34s/41s/47s/39s) chosen to be mutually
   prime-ish so the composition practically never repeats the same
   relative arrangement twice — this is what keeps the background
   reading as "alive" rather than as an obvious loop. */

@keyframes fieldDriftA {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.5; }
  50%      { transform: translate(4%, 5%) scale(1.08); opacity: 0.62; }
}

@keyframes fieldDriftB {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.46; }
  50%      { transform: translate(-5%, 4%) scale(1.05); opacity: 0.58; }
}

@keyframes fieldDriftC {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.4; }
  50%      { transform: translate(3%, -4%) scale(1.1); opacity: 0.52; }
}

@keyframes fieldDriftD {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.42; }
  50%      { transform: translate(-4%, -3%) scale(1.06); opacity: 0.55; }
}

@keyframes fieldGrainDrift {
  0%   { transform: translate(0, 0); }
  100% { transform: translate(-120px, -80px); }
}

/* ---------- Footer atmosphere ----------
   The page's closing beat — two soft glows drifting behind the sign-off,
   slower and lower-contrast than the Hero's aurora so it reads as an
   echo of the opening rather than a repeat of it. */

@keyframes footerGlowDriftA {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.55; }
  50%      { transform: translate(4%, -6%) scale(1.1); opacity: 0.7; }
}

@keyframes footerGlowDriftB {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.4; }
  50%      { transform: translate(-4%, 5%) scale(1.08); opacity: 0.55; }
}

/* ---------- Idle card float ----------
   Applied to .project / .education__card / .about__biodata /
   .about__portrait-frame via the .js-floaty class (added by
   js/interactions.js only once the element has revealed). Paused via
   `animation-play-state` on pointer-enter and resumed on pointer-leave
   so it never fights the JS-driven 3D tilt for the same `transform`
   channel — see js/interactions.js for the handshake. */

@keyframes idleFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}

.js-floaty {
  animation: idleFloat 7s var(--ease-in-out) infinite;
  animation-delay: var(--float-delay, 0s);
}

/* While the pointer is actively tilting a card, the idle-float animation
   is fully removed (not just paused) — a paused CSS animation still wins
   the cascade over an inline `transform`, so "pause" alone would silently
   block js/interactions.js's tilt output. `animation: none` hands the
   `transform` channel to JS cleanly; removing the .is-tilting class hands
   it back once the pointer leaves. */
.js-floaty.is-tilting {
  animation: none;
}

/* ---------- Premium CTA idle glow ----------
   A very slow breathing shadow on the primary button so the strongest
   call-to-action never sits completely still, even before any hover. */

@keyframes ctaBreathe {
  0%, 100% { box-shadow: 0 18px 40px -20px rgba(76, 130, 255, 0.35); }
  50%      { box-shadow: 0 22px 54px -18px rgba(139, 92, 246, 0.55); }
}

.btn--primary {
  animation: ctaBreathe 5.5s var(--ease-in-out) infinite;
}
