/* =========================================================================
   Scroll animations

   Two separate mechanisms, kept apart on purpose:

   1. Reveals — an element animates once, when it first enters the viewport.
      Driven by IntersectionObserver, so there is no per-frame work at all.
      Everything here is opt-in through `data-reveal` in the markup.

   2. Scroll-linked — a value that follows the scroll position continuously
      (the hero parallax). Driven by a single rAF loop in
      js/scroll-animations.js, which writes `--scroll-progress` and nothing
      else. CSS decides what that number means.

   Only `transform` and `opacity` are animated. Both are composited, so a
   phone never recalculates layout or repaints while scrolling; animating
   `top`/`height`/`margin` instead is what makes scroll effects stutter.

   The whole file is gated behind `.js-anim` on <html>, which the inline
   script in index.html adds before the first paint. With JS disabled or
   broken, nothing is ever hidden and the page reads exactly as before.
   ========================================================================= */

:root {
	/* One easing for everything, so the whole page moves with one hand.
	   A gentle ease-out: fast at the start, settling at the end — motion
	   that decelerates reads as physical rather than mechanical. */
	--reveal-ease: cubic-bezier(0.16, 0.68, 0.28, 1);
	--reveal-duration: 780ms;
	--reveal-shift: 32px;
	--reveal-stagger: 90ms;
}

/* ---- 1. Reveals --------------------------------------------------------- */

.js-anim [data-reveal] {
	opacity: 0;
	transition:
		opacity var(--reveal-duration) var(--reveal-ease) var(--reveal-delay, 0ms),
		transform var(--reveal-duration) var(--reveal-ease) var(--reveal-delay, 0ms);
	/* Promotes the element to its own layer for the duration of the
	   animation only — see .is-revealed below, which hands the layer back. */
	will-change: opacity, transform;
}

/* The default and the named variants. `translate3d` rather than
   `translateY` so the transform is composited on older mobile Safari. */
.js-anim [data-reveal],
.js-anim [data-reveal="up"] {
	transform: translate3d(0, var(--reveal-shift), 0);
}

.js-anim [data-reveal="left"] {
	transform: translate3d(calc(var(--reveal-shift) * -1.4), 0, 0);
}

.js-anim [data-reveal="right"] {
	transform: translate3d(calc(var(--reveal-shift) * 1.4), 0, 0);
}

/* For artwork and cards: rises while growing back to full size, the way a
   product shot settles into place. Kept subtle — a large scale delta looks
   like a zoom, not an entrance. */
.js-anim [data-reveal="rise"] {
	transform: translate3d(0, var(--reveal-shift), 0) scale(0.94);
}

/* Text that should not move, only resolve — used where a shift would fight
   with the layout around it. */
.js-anim [data-reveal="fade"] {
	transform: none;
}

.js-anim [data-reveal].is-revealed {
	opacity: 1;
	transform: none;
	/* Dropped once the element has arrived: a permanent `will-change` keeps
	   a compositor layer alive for every revealed element on the page, which
	   is real memory on a phone. */
	will-change: auto;
}

/* ---- 2. Hero parallax --------------------------------------------------- */

/* `--scroll-progress` is 0 while the hero fills the screen and reaches 1
   once it has scrolled a full viewport away. The JS only writes that number;
   every distance below is this file's decision, so the feel can be tuned
   here without touching the loop. */
.js-anim .hero {
	--scroll-progress: 0;
}

.js-anim .hero-backdrop-lower {
	transform: translate3d(0, calc(var(--scroll-progress) * 90px), 0);
}

/* The upper shape travels further than the lower one. That difference in
   speed is the entire parallax illusion — matching speeds would just look
   like the whole hero sliding. */
.js-anim .hero-backdrop-upper {
	transform: translate3d(0, calc(var(--scroll-progress) * 150px), 0);
}

/* The copy drifts up slightly faster than the page and fades as it leaves,
   so the hero dissolves instead of being cut off by the viewport edge. */
.js-anim .hero-copy {
	transform: translate3d(0, calc(var(--scroll-progress) * -60px), 0);
	opacity: calc(1 - var(--scroll-progress) * 1.35);
}

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

/* Same choreography, shorter distances. A 32px rise on a 1440px screen is a
   nudge; on a 390px phone it is a third of a card, and the stagger of a
   three-item row turns into a queue you have to wait for. */
/* Sideways entrances become vertical ones. left/right mirrors the demo
   cards' two columns; once they stack they are full-width, and a horizontal
   offset on a full-width block reaches past the viewport edge.

   900px is not a round number picked by eye — it is the breakpoint where
   responsive.css collapses `.demo-card-providers` to one column. The two
   have to move together, or between 700 and 900px the cards would be
   stacked while still sliding in sideways. */
@media (max-width: 900px) {
	.js-anim [data-reveal="left"],
	.js-anim [data-reveal="right"] {
		transform: translate3d(0, var(--reveal-shift), 0);
	}
}

@media (max-width: 700px) {
	:root {
		--reveal-duration: 620ms;
		--reveal-shift: 20px;
		--reveal-stagger: 70ms;
	}

	.js-anim .hero-backdrop-lower {
		transform: translate3d(0, calc(var(--scroll-progress) * 40px), 0);
	}

	.js-anim .hero-backdrop-upper {
		transform: translate3d(0, calc(var(--scroll-progress) * 70px), 0);
	}

	.js-anim .hero-copy {
		transform: translate3d(0, calc(var(--scroll-progress) * -30px), 0);
	}
}

/* ---- Reduced motion ----------------------------------------------------- */

/* Everything arrives, nothing moves. The JS stops writing
   `--scroll-progress` under this preference, but the rules are neutralised
   here as well so a mid-session change of the setting cannot leave an
   element stranded mid-transform. */
@media (prefers-reduced-motion: reduce) {
	.js-anim [data-reveal],
	.js-anim [data-reveal].is-revealed {
		opacity: 1;
		transform: none;
		transition: none;
	}

	.js-anim .hero-backdrop-lower,
	.js-anim .hero-backdrop-upper,
	.js-anim .hero-copy {
		transform: none;
		opacity: 1;
	}
}
