/**
 * MCN Animations — Text
 *
 * Rebuilt from, in order:
 *   Mamun Khandaker, "Pure CSS Text animation"  (codepen.io/kh-mamun/pen/NdwZdW, MIT)
 *   David East,      "CSS Party"                (codepen.io/davideast/pen/vYPZdWV, MIT)
 *   Jhey,            "Sparkly Shiny Text"       (codepen.io/jh3y/pen/oNqdmbW, MIT)
 *   Nooray Yemon,    "Simple CSS text animation"(codepen.io/yemon/pen/pWoROm, MIT)
 *
 * ACCESSIBILITY — READ BEFORE USING PER-LETTER EFFECTS
 * Splitting a word into one <span> per letter makes most screen readers spell
 * it out: "M. C. N." instead of "MCN". Any split text MUST be marked up as:
 *
 *   <span class="anim-split" aria-label="Innovation">
 *     <span aria-hidden="true" style="--i:0">I</span>
 *     <span aria-hidden="true" style="--i:1">n</span> ...
 *   </span>
 *
 * The aria-label carries the real word; the spans are decoration. Our JS
 * splitter does this automatically — hand-rolled markup must not skip it.
 *
 * @package MCN_Animations
 */

/* --------------------------------------------------------------------------
 * Split-text scaffold
 * ----------------------------------------------------------------------- */

.anim-split {
	display: inline-block;
}

.anim-split > span {
	display: inline-block;
	/* Preserves the space between words once they become inline-blocks. */
	white-space: pre;
	/* Per-letter stagger. --i is set on each span by the splitter. */
	--anim-index: var(--i, 0);
}

/* --------------------------------------------------------------------------
 * Entrance animations
 *
 * Opt in with data-anim so they fire on scroll via the shared observer,
 * exactly like .anim-reveal. Without JS the text simply sits there, visible.
 * ----------------------------------------------------------------------- */

.js-anim [class*="anim-text-in"][data-anim] > span,
.js-anim [class*="anim-text-in"][data-anim]:not(.anim-split) {
	opacity: 0;
}

.js-anim [class*="anim-text-in"][data-anim].is-inview > span,
.js-anim [class*="anim-text-in"][data-anim].is-inview:not(.anim-split) {
	opacity: 1;
	animation-duration: var(--anim-duration-slow);
	animation-timing-function: var(--anim-ease);
	animation-fill-mode: both;
	animation-delay: calc(var(--anim-index, 0) * var(--anim-stagger, 45ms));
}

.js-anim .anim-text-in--revolve[data-anim].is-inview > span { animation-name: anim-text-revolve; }
.js-anim .anim-text-in--drop[data-anim].is-inview > span    { animation-name: anim-text-drop; }
.js-anim .anim-text-in--slide[data-anim].is-inview > span   { animation-name: anim-text-slide; }
.js-anim .anim-text-in--twist[data-anim].is-inview > span   { animation-name: anim-text-twist; }
.js-anim .anim-text-in--rise[data-anim].is-inview > span    { animation-name: anim-text-rise; }

@keyframes anim-text-revolve {
	0%   { opacity: 0; transform: translate(18px, 18px) rotate(30deg) scale(0.3); }
	100% { opacity: 1; transform: none; }
}

@keyframes anim-text-drop {
	0%   { opacity: 0; transform: translateY(-42px) rotate(180deg); }
	60%  { opacity: 1; transform: translateY(10px) rotate(0deg) scale(0.9); }
	100% { opacity: 1; transform: none; }
}

@keyframes anim-text-slide {
	0%   { opacity: 0; transform: translateX(28px) scale(0.75); }
	100% { opacity: 1; transform: none; }
}

@keyframes anim-text-twist {
	0%   { opacity: 0; transform: rotate(-140deg) translateY(20px); }
	100% { opacity: 1; transform: none; }
}

/* Clipped rise — the workhorse. Needs overflow:hidden on the parent, which
   .anim-text-in--rise supplies via its own line wrapper. */
.anim-text-in--rise {
	overflow: hidden;
	/* A descender-safe gutter, otherwise overflow:hidden shears the g/y/p. */
	padding-bottom: 0.14em;
}

@keyframes anim-text-rise {
	0%   { opacity: 0; transform: translateY(100%); }
	100% { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
 * Breathing / glow text
 *
 * WCAG 2.2.2 (Level A): this loops indefinitely. Only use it on a short
 * decorative headline, and give the user a way out — .anim-text-breath stops
 * on hover/focus-within, and the whole thing is disabled under reduced motion.
 * Never wrap body copy in this.
 * ----------------------------------------------------------------------- */

.anim-text-breath > span {
	animation: anim-text-breath var(--anim-breath-speed, 1600ms) ease-in-out infinite alternate;
	animation-delay: calc(var(--anim-index, 0) * 90ms);
}

.anim-text-breath:hover > span,
.anim-text-breath:focus-within > span,
.anim-text-breath.is-paused > span {
	animation-play-state: paused;
}

@keyframes anim-text-breath {
	from {
		transform: none;
		text-shadow: none;
	}
	to {
		transform: scale(1.18) translateY(-4px);
		text-shadow: 0 0 18px var(--anim-accent);
	}
}

/* --------------------------------------------------------------------------
 * Gradient sweep text
 *
 * background-clip:text on an oversized gradient, panned with background-position.
 * Cheaper than animating colour and it composites cleanly.
 * ----------------------------------------------------------------------- */

.anim-text-sweep {
	background-image: linear-gradient(
		100deg,
		var(--anim-sweep-from, currentColor) 0%,
		var(--anim-sweep-to, var(--anim-accent)) 45%,
		var(--anim-sweep-from, currentColor) 90%
	);
	background-size: 220% 100%;
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	animation: anim-text-sweep 4.5s linear infinite;
}

.anim-text-sweep:hover,
.anim-text-sweep:focus-within {
	animation-play-state: paused;
}

@keyframes anim-text-sweep {
	to { background-position: -220% 0; }
}

@media (prefers-reduced-motion: reduce) {
	.anim-text-sweep {
		animation: none;
		/* Fall back to a solid colour — a frozen clipped gradient can drop
		   text contrast below the 4.5:1 floor (1.4.3, AA). */
		background-image: none;
		-webkit-background-clip: border-box;
		background-clip: border-box;
		color: inherit;
	}
}

/* --------------------------------------------------------------------------
 * Sparkle on hover
 *
 * Sparkles are decorative SVGs inside the link; they carry aria-hidden and
 * pointer-events:none so they never intercept the click target.
 * ----------------------------------------------------------------------- */

.anim-text-sparkle {
	position: relative;
	display: inline-block;
	isolation: isolate;
}

.anim-text-sparkle__spark {
	position: absolute;
	top: var(--y, 50%);
	left: var(--x, 50%);
	width: var(--size, 14px);
	height: var(--size, 14px);
	transform: translate(-50%, -50%) scale(0);
	opacity: 0;
	pointer-events: none;
	z-index: -1;
	color: var(--anim-accent);
	transition: opacity var(--anim-duration-fast) var(--anim-ease);
}

.anim-text-sparkle:hover .anim-text-sparkle__spark,
.anim-text-sparkle:focus-visible .anim-text-sparkle__spark {
	opacity: 1;
	animation: anim-sparkle var(--speed, 900ms) var(--anim-ease) infinite;
	animation-delay: calc(var(--anim-index, 0) * 140ms);
}

@keyframes anim-sparkle {
	0%, 100% { transform: translate(-50%, -50%) scale(0) rotate(0deg); }
	50%      { transform: translate(-50%, -50%) scale(var(--s, 1)) rotate(90deg); }
}

/* --------------------------------------------------------------------------
 * Layered header hover reveal
 *
 * Reinterprets Olivia Ng's header hover. The original stacks nine absolutely
 * positioned .hover divs; we get the same depth from two pseudo-elements and
 * a background zoom, which is nine fewer nodes per heading.
 * ----------------------------------------------------------------------- */

.anim-heading-reveal {
	position: relative;
	display: grid;
	place-items: center;
	overflow: hidden;
	isolation: isolate;
	min-height: var(--anim-heading-height, 320px);
}

.anim-heading-reveal__bg {
	position: absolute;
	inset: 0;
	z-index: -2;
	background-size: cover;
	background-position: center;
	transform: scale(1.02);
	transition: transform var(--anim-duration-slow) var(--anim-ease);
}

/* Scrim. Keeps heading text above the 4.5:1 contrast floor over photography
   (1.4.3, AA) — without it, contrast is whatever the image happens to be. */
.anim-heading-reveal::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: rgb(0 0 0 / var(--anim-scrim, 0.45));
	transition: background-color var(--anim-duration) var(--anim-ease);
}

.anim-heading-reveal:hover .anim-heading-reveal__bg,
.anim-heading-reveal:focus-within .anim-heading-reveal__bg {
	transform: scale(1.12);
}

.anim-heading-reveal:hover::before,
.anim-heading-reveal:focus-within::before {
	background: rgb(0 0 0 / calc(var(--anim-scrim, 0.45) - 0.12));
}

.anim-heading-reveal__title {
	position: relative;
	letter-spacing: 0.02em;
	transition:
		letter-spacing var(--anim-duration-slow) var(--anim-ease),
		transform var(--anim-duration-slow) var(--anim-ease);
}

.anim-heading-reveal:hover .anim-heading-reveal__title,
.anim-heading-reveal:focus-within .anim-heading-reveal__title {
	letter-spacing: 0.14em;
	transform: translateY(-4px);
}

@media (prefers-reduced-motion: reduce) {

	.anim-text-breath > span,
	.anim-text-sparkle .anim-text-sparkle__spark {
		animation: none !important;
	}

	.anim-text-sparkle .anim-text-sparkle__spark {
		display: none;
	}

	.js-anim [class*="anim-text-in"][data-anim] > span,
	.js-anim [class*="anim-text-in"][data-anim] {
		opacity: 1 !important;
		animation: none !important;
		transform: none !important;
	}
}
