/* ---------------------------------------------------------------- Motion
   Scroll-driven reveals, tuned to feel hand-made rather than machine-made.

   Four rules this file obeys:
   1. Content is visible by default. The reveal only exists inside @supports + no-preference,
      so a browser without scroll timelines, or a reader who asked for less motion, sees the page.
   2. Only transform and opacity animate. Nothing that triggers layout.
   3. Irregular on purpose. Travel, tilt and start point differ per item, taken from coprime
      nth-child sets (5, 7, 11) so the cycle needs 385 items before it repeats, and from
      sibling-index() where the browser has it. A grid that arrives as one block is the
      mechanical look this file exists to avoid.
   4. Curves are modern CSS: easeOutExpo for arrival, a damped `linear()` for the settle,
      and a spring `linear()` for feedback. No animation library.
*/

:root {
	/* easeOutExpo-ish: the arrival curve */
	--zd-ease-reveal: cubic-bezier(0.16, 1, 0.3, 1);
	/* damped settle: overshoots 6% once and returns. Reads as weight, not as a bounce. */
	--zd-ease-settle: linear(
		0, 0.176 5%, 0.514 11%, 0.784 18%, 0.94 25%, 1.036 33%, 1.06 41%,
		1.048 50%, 1.024 60%, 1.007 71%, 0.999 82%, 1
	);
	/* spring with slight overshoot, for hover and press feedback */
	--zd-ease-spring: linear(
		0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%, 1.017,
		1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%,
		1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%,
		0.997 69.8%, 1.003 76.9%, 1.004 83.8%, 1
	);
}

@keyframes zd-reveal {
	from {
		opacity: 0;
		transform: translate3d(0, var(--zd-rise, 1.5rem), 0) rotate(var(--zd-tilt, 0deg));
	}
	to {
		opacity: 1;
		transform: translate3d(0, 0, 0) rotate(0deg);
	}
}

/* A logo settles into scale instead of sliding: a slide reads as a card, a settle as a mark. */
@keyframes zd-reveal-soft {
	from {
		opacity: 0;
		transform: scale(var(--zd-shrink, 0.988)) rotate(var(--zd-tilt, 0deg));
	}
	to {
		opacity: 1;
		transform: scale(1) rotate(0deg);
	}
}

/* A photograph that settles into its frame. */
@keyframes zd-settle {
	from {
		opacity: 0;
		transform: scale(1.045) translate3d(0, var(--zd-drift, -0.9rem), 0);
	}
	to {
		opacity: 1;
		transform: scale(1) translate3d(0, 0, 0);
	}
}

/* ------------------------------------------------------------------ Variance
   Declared outside the media query so the JavaScript fallback inherits the same character. */

.project-card,
.post-card,
.logo-grid__item {
	--zd-rise: 1.5rem;
	--zd-tilt: 0deg;
}

.logo-grid__item { --zd-shrink: 0.985; }

/* Coprime sets: 5 x 7 x 11 = 385, so a 24-card page never shows the pattern again. */
.project-card:nth-child(5n + 1),
.post-card:nth-child(5n + 1),
.logo-grid__item:nth-child(5n + 1) {
	--zd-rise: 2.05rem;
	--zd-tilt: 0.28deg;
	--zd-shrink: 0.978;
}

.project-card:nth-child(5n + 3),
.post-card:nth-child(5n + 3),
.logo-grid__item:nth-child(5n + 3) {
	--zd-rise: 1.2rem;
	--zd-tilt: -0.2deg;
	--zd-shrink: 0.992;
}

.project-card:nth-child(7n),
.post-card:nth-child(7n),
.logo-grid__item:nth-child(7n) {
	--zd-rise: 1.78rem;
	--zd-tilt: 0.13deg;
}

.project-card:nth-child(11n),
.post-card:nth-child(11n),
.logo-grid__item:nth-child(11n) {
	--zd-rise: 2.3rem;
	--zd-tilt: -0.26deg;
	--zd-shrink: 0.981;
}

/* ------------------------------------------------------------------ Ranges
   Every range finishes at `entry 100%` - the moment the item is fully inside the viewport - and never
   in the `cover` phase. A cover-based end cannot be reached by the last row of a page: at maximum
   scroll such a row only reaches elementH / (viewportH + elementH) of the range, about 27% for a
   300px card, so the footer row would sit half-faded forever. Found in a real browser, not by reading. */

@media (prefers-reduced-motion: no-preference) {
	@supports (animation-timeline: view()) {
		.page__title,
		.project__title,
		.project-filter,
		.project-card,
		.post-card,
		.logo-grid__item {
			animation: zd-reveal 1ms var(--zd-ease-settle) both;
			animation-timeline: view();
			animation-range: entry 12% entry 100%;
		}

		/* Headings lead. They use the plain arrival curve and never overshoot far. */
		.page__title,
		.project__title,
		.project-filter {
			animation-timing-function: var(--zd-ease-reveal);
			animation-range: entry 16% entry 100%;
		}

		.project__content > * {
			animation: zd-reveal 1ms var(--zd-ease-reveal) both;
			animation-timeline: view();
			animation-range: entry 18% entry 100%;
		}

		.project__thumb {
			animation: zd-settle 1ms var(--zd-ease-reveal) both;
			animation-timeline: view();
			animation-range: entry 4% entry 100%;
		}

		.logo-grid__item {
			animation-name: zd-reveal-soft;
			animation-range: entry 10% entry 100%;
		}

		/* Start points are coprime too: neighbours never leave together. */
		.project-card:nth-child(3n) { animation-range: entry 8% entry 100%; }
		.project-card:nth-child(5n + 2) { animation-range: entry 18% entry 100%; }
		.project-card:nth-child(7n) { animation-range: entry 3% entry 100%; }

		.logo-grid__item:nth-child(4n + 2) { animation-range: entry 15% entry 100%; }
		.logo-grid__item:nth-child(9n) { animation-range: entry 6% entry 100%; }

		/* Where sibling-index() exists the offset comes from the item's real position, not a guess.
		   The nth-child ranges above stay as the fallback for browsers with view() but without it. */
		@supports (animation-range: entry calc(1% * sibling-index())) {
			.project-card:nth-child(n),
			.post-card:nth-child(n),
			.logo-grid__item:nth-child(n) {
				animation-range: entry clamp(3%, 1.2% * sibling-index(), 16%) entry 100%;
			}
		}

		/* Feedback: a small lift with a hint of growth, damped by the spring curve. */
		.project-card__link,
		.logo-grid__item {
			transition: transform 260ms var(--zd-ease-spring);
		}

		.project-card__link:hover,
		.project-card__link:focus-visible {
			transform: translate3d(0, -3px, 0) scale(1.008);
		}

		.logo-grid__item:hover {
			transform: translate3d(0, -3px, 0);
		}

		/* The photograph answers the pointer slowly, so the hover feels weighted, not twitchy. */
		.project-card__media img {
			transition: transform 620ms var(--zd-ease-reveal);
		}

		.project-card:hover .project-card__media img {
			transform: scale(1.035);
		}
	}

	/* Fallback path for browsers without scroll timelines: motion.js adds .zd-motion. */
	.zd-motion .zd-reveal-pending {
		opacity: 0;
		transform: translate3d(0, var(--zd-rise, 1.5rem), 0) rotate(var(--zd-tilt, 0deg));
		transition: opacity 460ms var(--zd-ease-reveal), transform 620ms var(--zd-ease-settle);
	}

	.zd-motion .zd-reveal-pending.is-revealed {
		opacity: 1;
		transform: translate3d(0, 0, 0) rotate(0deg);
	}

	/* Stagger survives the fallback: coprime delays instead of one shared start. */
	.zd-motion .project-card:nth-child(3n).zd-reveal-pending { transition-delay: 45ms, 45ms; }
	.zd-motion .project-card:nth-child(5n + 2).zd-reveal-pending { transition-delay: 90ms, 90ms; }
	.zd-motion .logo-grid__item:nth-child(4n + 2).zd-reveal-pending { transition-delay: 60ms, 60ms; }
}

/* Reduced motion always wins, including over the fallback classes. */
@media (prefers-reduced-motion: reduce) {
	.zd-motion .zd-reveal-pending,
	.zd-motion .zd-reveal-pending.is-revealed {
		opacity: 1;
		transform: none;
		transition: none;
	}

	.project-card__link,
	.project-card__media img,
	.logo-grid__item {
		transition: none;
	}

	.project-card__link:hover,
	.project-card__link:focus-visible,
	.logo-grid__item:hover,
	.project-card:hover .project-card__media img {
		transform: none;
	}
}
