/* ============================================================
   animations.css — keyframes + the reveal system
   Preston Thoman · Palm Springs portfolio

   Entrances are transitions, not animations. A transition that
   gets interrupted lands somewhere sensible; an animation that
   never gets its frames leaves content stranded at opacity 0.
   ============================================================ */

/* ============================================================
   1. THE REVEAL SYSTEM
   ------------------------------------------------------------
   Markup:  <p data-reveal="up" data-reveal-delay=".15">
   js/scroll.js adds .is-revealed via IntersectionObserver and
   writes the delay into --rd.
   ============================================================ */
[data-reveal] {
  opacity: 0;
  transition:
    opacity   .9s var(--ease) var(--rd, 0s),
    transform .9s var(--ease) var(--rd, 0s),
    filter    .9s var(--ease) var(--rd, 0s);
}

/* "fade" needs no extra declarations — opacity alone */
[data-reveal="up"]    { transform: translateY(30px); }
[data-reveal="scale"] { transform: scale(.965) translateY(18px); }
/* "wipe" pre-clipped itself with clip-path: inset(0 0 100% 0). Chrome folds
   an element's own clip-path into the rect IntersectionObserver reports, so a
   fully collapsed element has an intersection ratio of exactly 0 — the
   observer can never see it, it never gets .is-revealed, and the clip never
   opens. A reveal that could not fire. Measured across a full scroll of the
   page: up 31/31, rule 4/4, scale 1/1, wipe 0/6. The 6 were every project
   card, which is why none of them ever appeared.

   Rule for this system: an observed element must never clip itself. The
   uncovering is done by a panel sliding off the card instead (see
   .project::after), which clips nothing and runs on the compositor. */
[data-reveal="wipe"]  { transform: translateY(22px) scale(.99); }
[data-reveal="card"]  { transform: translateY(30px) scale(.985); }
[data-reveal="rule"]  { opacity: 1; transform: scaleX(0); transform-origin: left; }

[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  filter: none;
}

/* the rule keeps its own end state — declared after the shared one */
[data-reveal="rule"].is-revealed { transform: scaleX(1); }

/* ============================================================
   2. PALM MOTION
   ============================================================ */

/* ------------------------------------------------------------
   The sway keyframes are deliberately two-stop. An oscillation
   with intermediate stops has to accelerate in and out of each
   one, which reads as ticking; a single extreme-to-extreme pair
   eased with a sine curve (--sine below) is continuous motion
   with no seam at the loop point. Periods are long and mutually
   prime across trunk, crown and frond, so the composite never
   visibly repeats.
   ------------------------------------------------------------ */

/* whole-tree sway, pivoting at the base of the trunk */
@keyframes palmSway {
  from { transform: rotate(calc(var(--sway, 1.2deg) * -1)) translateX(calc(var(--drift, 2px) * -1)); }
  to   { transform: rotate(var(--sway, 1.2deg)) translateX(var(--drift, 2px)); }
}

/* crown counter-sway: gives the trunk a whip/bend feel */
@keyframes crownSway {
  from { transform: rotate(calc(var(--camp, 2.5deg) * -1)); }
  to   { transform: rotate(var(--camp, 2.5deg)); }
}

/* every frond breathes on its own amplitude, period and offset */
@keyframes frondSway {
  from { transform: rotate(calc(var(--amp, 5deg) * -1)); }
  to   { transform: rotate(var(--amp, 5deg)); }
}

/* A gust rolling through the grove — the only motion out here larger than
   the sway, which makes its *shape* matter more than its size. The old
   curve took a tree from upright to full lean inside 40% of a 2s cycle and
   then kicked it back past centre, which moved a crown roughly fifteen
   times faster than the sway does: read as a flinch, not as weather.
   Air does not change direction like that.

   So: the lean now builds and lets go across the whole cycle, with the
   peak sitting just past the middle, and there is no recoil past upright
   at the end. A tree that snaps the other way on the way back is a tree
   being yanked. The stops are spaced to keep the per-segment velocity
   roughly even, so no single stretch of the curve stands out. */
@keyframes gustBend {
  0%   { transform: rotate(0deg); }
  20%  { transform: rotate(calc(var(--gust, 1.8deg) * .30)); }
  38%  { transform: rotate(calc(var(--gust, 1.8deg) * .72)); }
  52%  { transform: rotate(var(--gust, 1.8deg)); }
  70%  { transform: rotate(calc(var(--gust, 1.8deg) * .74)); }
  86%  { transform: rotate(calc(var(--gust, 1.8deg) * .32)); }
  100% { transform: rotate(0deg); }
}

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

/* ============================================================
   3. SKY
   ============================================================ */
@keyframes twinkle {
  0%, 100% { opacity: var(--o, .2); transform: scale(.7); }
  50%      { opacity: 1; transform: scale(1.25); }
}

@keyframes bandSlide {
  0%, 100% { transform: translateX(0) scaleY(1); opacity: .9; }
  50%      { transform: translateX(-6px) scaleY(1.15); opacity: .7; }
}

@keyframes cloudDrift {
  from { transform: translateX(-20vw); }
  to   { transform: translateX(120vw); }
}

@keyframes birdFly {
  0%   { transform: translate(-10vw, 0) scale(var(--s, 1)); opacity: 0; }
  8%   { opacity: .85; }
  50%  { transform: translate(50vw, -6vh) scale(var(--s, 1)); }
  92%  { opacity: .85; }
  100% { transform: translate(115vw, -2vh) scale(var(--s, 1)); opacity: 0; }
}

@keyframes wingFlap {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(.35); }
}

@keyframes heatHaze {
  0%, 100% { transform: translateY(0) scaleX(1); opacity: .32; }
  50%      { transform: translateY(-6px) scaleX(1.04); opacity: .5; }
}

/* ============================================================
   4. TYPE + UI
   ============================================================ */
@keyframes caretBlink {
  0%, 45%  { opacity: 1; }
  50%, 95% { opacity: 0; }
  100%     { opacity: 1; }
}

@keyframes pinPing {
  0%        { transform: scale(.6); opacity: .9; }
  70%, 100% { transform: scale(2.4); opacity: 0; }
}

@keyframes statusPulse {
  0%   { box-shadow: 0 0 0 0 rgba(232, 86, 44, .5); }
  70%  { box-shadow: 0 0 0 12px rgba(232, 86, 44, 0); }
  100% { box-shadow: 0 0 0 0 rgba(232, 86, 44, 0); }
}

@keyframes scrollNudge {
  0%   { transform: scaleY(0); transform-origin: top; }
  45%  { transform: scaleY(1); transform-origin: top; }
  55%  { transform: scaleY(1); transform-origin: bottom; }
  100% { transform: scaleY(0); transform-origin: bottom; }
}

/* the portrait breathes — small enough to register as alive, not as motion */
@keyframes portraitBreathe {
  from { transform: translateY(0) scale(1); }
  to   { transform: translateY(-6px) scale(1.012); }
}

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

@keyframes glowBreathe {
  0%, 100% { opacity: .78; }
  50%      { opacity: 1; }
}

/* a soft band of light travelling across him, left to right */
@keyframes flashSweep {
  0%   { opacity: 0;   -webkit-mask-position: 130% 0; mask-position: 130% 0; }
  20%  { opacity: .85; }
  80%  { opacity: .85; }
  100% { opacity: 0;   -webkit-mask-position: -30% 0; mask-position: -30% 0; }
}

@keyframes shineSweep {
  0%   { transform: translateX(-120%) rotate(18deg); }
  100% { transform: translateX(220%) rotate(18deg); }
}

/* ============================================================
   5. REDUCED MOTION
   ------------------------------------------------------------
   Kill the ambient loops and the scroll-linked scene, but keep
   every element visible and every hover affordance intact.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal] { opacity: 1 !important; transform: none !important; clip-path: none !important; }
  [data-reveal="rule"] { transform: scaleX(1) !important; }
  .hero__title .char { opacity: 1 !important; transform: none !important; filter: none !important; }
  .statement .word { opacity: 1 !important; }
  .portrait__ring circle, .portrait__stage::before { animation: none !important; }
  .portrait__sun { transform: none !important; }
  .palm-wind { transform: none !important; }
}
