/**
 * MCN Animations
 * A token-driven animation library for Oxygen 6 builds.
 *
 * Everything is composable: stack a base class with modifiers.
 *   <div class="anim-reveal anim-up anim-slow" data-anim>
 *
 * Accessibility contract for every effect in this file:
 *   - Hover effects have a :focus-visible twin (WCAG 2.1.1 / 2.4.7).
 *   - Nothing that appears on hover disappears before you can reach it (1.4.13).
 *   - Nothing flashes more than 3x/sec (2.3.1).
 *   - Nothing animates at all under prefers-reduced-motion (2.3.3, AAA).
 *
 * @package MCN_Animations
 */

/* --------------------------------------------------------------------------
 * 1. Tokens
 * Override these per client in the Oxygen global stylesheet, not here.
 * ----------------------------------------------------------------------- */

:root {
	--anim-duration-fast: 150ms;
	--anim-duration: 320ms;
	--anim-duration-slow: 620ms;

	/* Default easing. Decelerating — feels like the element is settling. */
	--anim-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
	--anim-ease-in: cubic-bezier(0.55, 0.06, 0.68, 0.19);
	--anim-ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1);

	/* How far a reveal travels. Keep it small; big travel reads as jank. */
	--anim-distance: 24px;

	/* Stagger step for sequenced children. */
	--anim-stagger: 90ms;

	/* Accent used by underlines, shines, loaders. Inherits text colour by
	   default so effects stay on-brand without per-site config. */
	--anim-accent: currentColor;

	/* Focus ring. 3:1 against adjacent colours is the AA floor (1.4.11). */
	--anim-focus-color: currentColor;
	--anim-focus-width: 2px;
	--anim-focus-offset: 3px;
}

/* --------------------------------------------------------------------------
 * 2. Global guards
 * ----------------------------------------------------------------------- */

/**
 * Reduced motion.
 *
 * We do not merely shorten durations — we remove motion and land every
 * element in its final state. `animation: none` on a reveal would leave it
 * stuck at opacity:0, so the reveal rules below are explicitly re-asserted.
 */
@media (prefers-reduced-motion: reduce) {

	.anim-reveal,
	.anim-reveal-children > * {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}

	.anim-rotator__list {
		animation: none !important;
	}

	.anim-loader__spinner {
		/* A spinner with no motion communicates nothing, so keep it — but
		   slow it well below any flash threshold and drop the scale pulse. */
		animation-duration: 2.4s !important;
	}

	*,
	*::before,
	*::after {
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}

/**
 * Focus ring applied by every interactive effect.
 * :focus-visible only, so mouse users never see it, keyboard users always do.
 */
.anim-link:focus-visible,
.anim-btn:focus-visible,
.anim-card:focus-visible,
.anim-card a:focus-visible,
.anim-rotator__pause:focus-visible {
	outline: var(--anim-focus-width) solid var(--anim-focus-color);
	outline-offset: var(--anim-focus-offset);
	border-radius: 2px;
}

/* --------------------------------------------------------------------------
 * 3. Shared modifiers
 * ----------------------------------------------------------------------- */

.anim-fast { --anim-duration: var(--anim-duration-fast); }
.anim-slow { --anim-duration: var(--anim-duration-slow); }

.anim-delay-1 { transition-delay: 80ms;  animation-delay: 80ms; }
.anim-delay-2 { transition-delay: 160ms; animation-delay: 160ms; }
.anim-delay-3 { transition-delay: 240ms; animation-delay: 240ms; }
.anim-delay-4 { transition-delay: 320ms; animation-delay: 320ms; }

/* --------------------------------------------------------------------------
 * 4. Effect: scroll reveal
 *
 * Opt-in via the data-anim attribute — a bare class never triggers, so
 * dropping .anim-reveal on something by accident can't hide it.
 *
 * The hidden state is scoped to .js-anim (set on <html> by our JS the moment
 * it loads). No JS, no hiding — content is always readable. This is the
 * single most important rule in the file.
 * ----------------------------------------------------------------------- */

.js-anim .anim-reveal[data-anim] {
	opacity: 0;
	transition:
		opacity var(--anim-duration) var(--anim-ease),
		transform var(--anim-duration) var(--anim-ease);
	will-change: opacity, transform;
}

.js-anim .anim-reveal[data-anim].anim-up    { transform: translate3d(0, var(--anim-distance), 0); }
.js-anim .anim-reveal[data-anim].anim-down  { transform: translate3d(0, calc(var(--anim-distance) * -1), 0); }
.js-anim .anim-reveal[data-anim].anim-left  { transform: translate3d(var(--anim-distance), 0, 0); }
.js-anim .anim-reveal[data-anim].anim-right { transform: translate3d(calc(var(--anim-distance) * -1), 0, 0); }
.js-anim .anim-reveal[data-anim].anim-zoom  { transform: scale(0.94); }

.js-anim .anim-reveal[data-anim].is-inview {
	opacity: 1;
	transform: none;
}

/* Once it has played we drop the compositor hint — leaving will-change on
   hundreds of elements is its own performance problem. */
.js-anim .anim-reveal[data-anim].is-done {
	will-change: auto;
}

/* Staggered children: one wrapper, no per-child delay classes. */
.js-anim .anim-reveal-children[data-anim] > * {
	opacity: 0;
	transform: translate3d(0, var(--anim-distance), 0);
	transition:
		opacity var(--anim-duration) var(--anim-ease),
		transform var(--anim-duration) var(--anim-ease);
}

.js-anim .anim-reveal-children[data-anim].is-inview > * {
	opacity: 1;
	transform: none;
}

.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(1) { transition-delay: calc(var(--anim-stagger) * 0); }
.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(2) { transition-delay: calc(var(--anim-stagger) * 1); }
.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(3) { transition-delay: calc(var(--anim-stagger) * 2); }
.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(4) { transition-delay: calc(var(--anim-stagger) * 3); }
.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(5) { transition-delay: calc(var(--anim-stagger) * 4); }
.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(6) { transition-delay: calc(var(--anim-stagger) * 5); }
.js-anim .anim-reveal-children[data-anim].is-inview > *:nth-child(n+7) { transition-delay: calc(var(--anim-stagger) * 6); }

/* --------------------------------------------------------------------------
 * 5. Effect: animated link underline
 *
 * Uses a pseudo-element rather than text-decoration so the wipe direction is
 * controllable. transform-origin flip means it grows in from the left and
 * retracts to the right — reads as intentional rather than a rubber band.
 * ----------------------------------------------------------------------- */

.anim-link {
	position: relative;
	text-decoration: none;
	/* Keeps a visible affordance for users who can't perceive the hover
	   animation — never rely on motion alone to signal a link. */
	text-decoration-skip-ink: auto;
}

.anim-link::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: -0.15em;
	width: 100%;
	height: 2px;
	background: var(--anim-accent);
	transform: scaleX(0);
	transform-origin: right center;
	transition: transform var(--anim-duration) var(--anim-ease);
}

.anim-link:hover::after,
.anim-link:focus-visible::after {
	transform: scaleX(1);
	transform-origin: left center;
}

/* --------------------------------------------------------------------------
 * 6. Effect: button
 *
 * Lift + shadow on hover, press-down on active. Deliberately restrained:
 * a CTA that does gymnastics reads as a scam.
 * ----------------------------------------------------------------------- */

.anim-btn {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 0.5em;
	/* WCAG 2.5.8 (AA, 2.2): minimum 24x24 CSS px target. */
	min-height: 44px;
	min-width: 44px;
	transition:
		transform var(--anim-duration-fast) var(--anim-ease),
		box-shadow var(--anim-duration-fast) var(--anim-ease),
		background-color var(--anim-duration-fast) var(--anim-ease);
}

.anim-btn:hover,
.anim-btn:focus-visible {
	transform: translate3d(0, -2px, 0);
	box-shadow: 0 6px 18px -6px rgb(0 0 0 / 0.35);
}

.anim-btn:active {
	transform: translate3d(0, 0, 0);
	box-shadow: 0 2px 6px -4px rgb(0 0 0 / 0.35);
	transition-duration: 60ms;
}

/* Sheen sweep — opt in with .anim-btn--sheen. */
.anim-btn--sheen {
	overflow: hidden;
	isolation: isolate;
}

.anim-btn--sheen::before {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(
		100deg,
		transparent 30%,
		rgb(255 255 255 / 0.28) 50%,
		transparent 70%
	);
	transform: translateX(-100%);
	transition: transform var(--anim-duration-slow) var(--anim-ease);
	pointer-events: none;
}

.anim-btn--sheen:hover::before,
.anim-btn--sheen:focus-visible::before {
	transform: translateX(100%);
}

/* --------------------------------------------------------------------------
 * 7. Effect: card
 *
 * Lift, plus an optional detail panel that slides up.
 *
 * WCAG 1.4.13 (AA): the panel is part of the card's own box, so moving the
 * pointer onto the revealed content keeps it open (hoverable), it stays until
 * the pointer leaves (persistent), and it never overlays anything outside the
 * card (nothing to dismiss). Do not reimplement this with a detached
 * absolutely-positioned tooltip — that is where 1.4.13 gets failed.
 * ----------------------------------------------------------------------- */

.anim-card {
	position: relative;
	overflow: hidden;
	transition:
		transform var(--anim-duration) var(--anim-ease),
		box-shadow var(--anim-duration) var(--anim-ease);
}

.anim-card:hover,
.anim-card:focus-within {
	transform: translate3d(0, -6px, 0);
	box-shadow: 0 18px 40px -20px rgb(0 0 0 / 0.45);
}

/* Media zoom inside a card. */
.anim-card__media {
	overflow: hidden;
}

.anim-card__media img {
	display: block;
	width: 100%;
	transition: transform var(--anim-duration-slow) var(--anim-ease);
}

.anim-card:hover .anim-card__media img,
.anim-card:focus-within .anim-card__media img {
	transform: scale(1.06);
}

/* Slide-up detail panel. */
.anim-card__panel {
	transform: translate3d(0, 100%, 0);
	opacity: 0;
	transition:
		transform var(--anim-duration) var(--anim-ease),
		opacity var(--anim-duration) var(--anim-ease);
}

.anim-card:hover .anim-card__panel,
.anim-card:focus-within .anim-card__panel {
	transform: none;
	opacity: 1;
}

/* Shine sweep across the card face. */
.anim-card--shine::after {
	content: "";
	position: absolute;
	inset: 0;
	background: radial-gradient(
		circle at var(--anim-shine-x, 50%) var(--anim-shine-y, 50%),
		rgb(255 255 255 / 0.16),
		transparent 45%
	);
	opacity: 0;
	transition: opacity var(--anim-duration) var(--anim-ease);
	pointer-events: none;
}

.anim-card--shine:hover::after,
.anim-card--shine:focus-within::after {
	opacity: 1;
}

/* --------------------------------------------------------------------------
 * 7b. Effect: dotfield
 *
 * A grid of dots that blooms around the pointer. Reinterprets Vincent
 * Durand's "Full CSS growing dot effect" (codepen.io/onediv/pen/zYEZXdz, MIT).
 *
 * The original renders a 10x10 grid of <a> tiles, each projecting ~100
 * box-shadow copies of a single dot. Beautiful, but it puts 100 empty links
 * in the tab order (fails 2.4.4 and 4.1.2, both Level A) and costs ~10,000
 * shadow paints.
 *
 * This version paints the dots as two background layers on the host's OWN
 * pseudo-elements and reveals the bright layer through a pointer-tracked mask.
 * No extra nodes, no links, no tab stops, nothing in the accessibility tree.
 *
 * WHY PSEUDO-ELEMENTS AND NOT A CHILD SPAN
 * The first version used a child <span> with position:absolute; inset:0. In a
 * page builder that breaks: Oxygen wraps an HTML-code element in its own div
 * and the section's inner .section-container is position:relative, so the span
 * sized itself to that container instead of the section — the field covered
 * only part of the hero, and the pointer mask was offset by the container's
 * top edge. Pseudo-elements are always positioned by the host, so no wrapper
 * anyone inserts between can hijack them.
 *
 * Markup is now just:
 *   <section class="anim-dotfield">
 *     <div class="anim-dotfield__content"> ...real content... </div>
 *   </section>
 * ----------------------------------------------------------------------- */

.anim-dotfield {
	--dotfield-gap: 28px;         /* distance between dot centres */
	--dotfield-dot: 1.5px;        /* resting dot radius */
	--dotfield-dot-lit: 4px;      /* radius inside the spotlight */
	--dotfield-radius: 120px;     /* spotlight reach */

	/* Resting dot colour. Falls back to the text colour only when no accent
	   has been set — on most sites you want the brand colour here, not ink. */
	--dotfield-color: var(--anim-accent, currentColor);

	/* Colour of the lit dots and their bloom. Defaults to the dot colour, but
	   set it separately when you want the glow hotter than the resting grid. */
	--dotfield-glow: var(--dotfield-color);

	--dotfield-dim: 0.22;         /* resting opacity of the base layer */

	/* Pointer position. Parked off-canvas so nothing is lit before the first
	   pointermove — otherwise the field boots with a glow stuck at 0,0. */
	--dotfield-x: -100%;
	--dotfield-y: -100%;

	position: relative;
	isolation: isolate;
}

/**
 * Legacy child layer. Earlier markup shipped a <span class="anim-dotfield__layer">;
 * it is no longer used and is hidden so old markup does not double-paint.
 */
.anim-dotfield__layer {
	display: none;
}

/* Both layers share geometry — identical grid, different dot radius. */
.anim-dotfield::before,
.anim-dotfield::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	border-radius: inherit;
	background-size: var(--dotfield-gap) var(--dotfield-gap);
	background-position: center;
}

/* Base layer: the resting dot grid. Always painted, no JS required. */
.anim-dotfield::before {
	background-image: radial-gradient(
		circle at center,
		var(--dotfield-color) var(--dotfield-dot),
		transparent calc(var(--dotfield-dot) + 0.5px)
	);
	opacity: var(--dotfield-dim);
}

/**
 * Lit layer: identical grid at a larger dot radius, revealed only inside the
 * spotlight. Because both layers share background-size and -position the dots
 * register exactly, so a dot appears to grow rather than a second dot fading
 * in on top of the first.
 */
.anim-dotfield::after {
	background-image: radial-gradient(
		circle at center,
		var(--dotfield-glow) var(--dotfield-dot-lit),
		transparent calc(var(--dotfield-dot-lit) + 1px)
	);
	opacity: 0;
	transition: opacity var(--anim-duration) var(--anim-ease);

	-webkit-mask-image: radial-gradient(
		circle var(--dotfield-radius) at var(--dotfield-x) var(--dotfield-y),
		#000 0%,
		rgb(0 0 0 / 0.55) 45%,
		transparent 100%
	);
	mask-image: radial-gradient(
		circle var(--dotfield-radius) at var(--dotfield-x) var(--dotfield-y),
		#000 0%,
		rgb(0 0 0 / 0.55) 45%,
		transparent 100%
	);
}

/* JS confirms pointer support and sets .is-live; that is the only thing that
   switches the lit layer on. No JS, no pointer, no half-broken glow. */
.anim-dotfield.is-live::after {
	opacity: 1;
}

.anim-dotfield__content {
	position: relative;
	z-index: 1;
}

/**
 * Glow variant — a soft bloom behind the lit dots.
 * drop-shadow uses --dotfield-glow explicitly rather than currentColor: inside
 * a pseudo-element currentColor resolves to the host's TEXT colour, which on a
 * light hero is near-black and makes the glow read as a smudge.
 */
.anim-dotfield--glow::after {
	filter:
		drop-shadow(0 0 4px var(--dotfield-glow))
		drop-shadow(0 0 12px var(--dotfield-glow));
}

@media (prefers-reduced-motion: reduce) {
	/* The dots themselves never animate, but the spotlight chasing the cursor
	   is motion. Hold the resting grid and drop the tracking layer entirely. */
	.anim-dotfield::after {
		display: none;
	}
}

/* Coarse pointers get no spotlight — there is no hover to track, and a glow
   frozen wherever the last tap landed just looks like a rendering bug. */
@media (hover: none), (pointer: coarse) {
	.anim-dotfield::after {
		display: none;
	}
}

/* --------------------------------------------------------------------------
 * 8. Effect: loader
 *
 * Rotation only — no opacity flashing, so 2.3.1 (three flashes) is a
 * non-issue by construction. Markup should carry role="status" and an
 * accessible label so screen readers announce the wait.
 * ----------------------------------------------------------------------- */

.anim-loader {
	display: inline-flex;
	align-items: center;
	gap: 0.6em;
}

.anim-loader__spinner {
	width: 1.25em;
	height: 1.25em;
	border-radius: 50%;
	border: 2px solid;
	border-color: var(--anim-accent) var(--anim-accent) var(--anim-accent) transparent;
	animation: anim-spin 800ms linear infinite;
	flex: none;
}

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

/* Three-dot variant. Opacity cycles at ~1.4Hz — well under the 3/sec limit. */
.anim-loader__dots {
	display: inline-flex;
	gap: 0.3em;
}

.anim-loader__dots span {
	width: 0.45em;
	height: 0.45em;
	border-radius: 50%;
	background: var(--anim-accent);
	animation: anim-dot 1.4s var(--anim-ease) infinite;
}

.anim-loader__dots span:nth-child(2) { animation-delay: 200ms; }
.anim-loader__dots span:nth-child(3) { animation-delay: 400ms; }

@keyframes anim-dot {
	0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
	30%           { opacity: 1;   transform: translateY(-0.3em); }
}

/* --------------------------------------------------------------------------
 * 9. Effect: headline word rotator
 *
 * WCAG 2.2.2 (Level A) — this moves automatically, loops past five seconds,
 * and sits alongside other content, so it MUST ship with a pause control.
 * The .anim-rotator__pause button is not optional decoration; without it the
 * component fails a Level A criterion. It also pauses on hover and focus.
 * ----------------------------------------------------------------------- */

.anim-rotator {
	display: inline-flex;
	align-items: baseline;
	gap: 0.4em;
}

.anim-rotator__viewport {
	display: inline-block;
	overflow: hidden;
	height: 1.2em;
	vertical-align: bottom;
}

.anim-rotator__list {
	display: block;
	margin: 0;
	padding: 0;
	list-style: none;
	/* translate3d, not margin-top — margin animation forces a layout pass on
	   every frame. This is the fix applied to the classic CodePen version. */
	animation: anim-rotate-words 9s var(--anim-ease) infinite;
}

.anim-rotator__list > li {
	display: block;
	height: 1.2em;
	line-height: 1.2em;
}

.anim-rotator:hover .anim-rotator__list,
.anim-rotator:focus-within .anim-rotator__list,
.anim-rotator.is-paused .anim-rotator__list {
	animation-play-state: paused;
}

/* Three words, held then advanced. Extend the keyframe if you add words. */
@keyframes anim-rotate-words {
	0%, 26%   { transform: translate3d(0, 0, 0); }
	33%, 59%  { transform: translate3d(0, -1.2em, 0); }
	66%, 92%  { transform: translate3d(0, -2.4em, 0); }
	100%      { transform: translate3d(0, -3.6em, 0); }
}

.anim-rotator__pause {
	flex: none;
	display: inline-grid;
	place-items: center;
	/* 2.5.8 target size. */
	min-width: 24px;
	min-height: 24px;
	padding: 0;
	border: 0;
	background: none;
	color: inherit;
	cursor: pointer;
	opacity: 0.6;
	transition: opacity var(--anim-duration-fast) var(--anim-ease);
}

.anim-rotator__pause:hover,
.anim-rotator__pause:focus-visible {
	opacity: 1;
}
