/**
 * Eruda Toolkit - text animations
 *
 * Nothing here hides text on its own. Every rule that makes something
 * invisible is scoped to [data-eanm-ready], and only the script sets that.
 * If the script never runs, this file has no visible effect at all.
 *
 * The split mode per preset is declared here rather than in the script, so the
 * mapping lives in one place. The script reads it back with getComputedStyle().
 */

.eanm-preset-fade-in       { --eanm-split: none; }
.eanm-preset-words-up      { --eanm-split: words; }
.eanm-preset-words-fade    { --eanm-split: words; }
.eanm-preset-words-build  { --eanm-split: words; }

/* The one preset tied to scroll position rather than played on a trigger.
   --eanm-mode tells the script to scrub it. */
.eanm-preset-scroll-highlight { --eanm-split: words; --eanm-mode: scrub; }
.eanm-preset-chars-cascade { --eanm-split: chars; }
.eanm-preset-chars-flip    { --eanm-split: chars; }
.eanm-preset-lines-mask    { --eanm-split: lines; }
.eanm-preset-blur-in       { --eanm-split: none; }
.eanm-preset-scale-pop     { --eanm-split: words; }
.eanm-preset-slide-left    { --eanm-split: words; }

/* Direction. Named for where the text comes FROM, which is the only phrasing
   that is not ambiguous. Presets read --eanm-dx / --eanm-dy rather than
   hard-coding an axis, so one set of rules serves every direction. */
.eanm-dir-up    { --eanm-dx: 0; --eanm-dy: var(--eanm-distance, 24px); }
.eanm-dir-down  { --eanm-dx: 0; --eanm-dy: calc(-1 * var(--eanm-distance, 24px)); }
.eanm-dir-left  { --eanm-dx: calc(-1 * var(--eanm-distance, 24px)); --eanm-dy: 0; }
.eanm-dir-right { --eanm-dx: var(--eanm-distance, 24px); --eanm-dy: 0; }
.eanm-dir-none  { --eanm-dx: 0; --eanm-dy: 0; }

/* Easing. The control stores a name; the curve lives here, so it can be
   retuned later without touching a single saved page. */
/* GSAP's power2.out, which is easeOutCubic. The usual choice for text. */
.eanm-ease-natural   { --eanm-ease: cubic-bezier(0.215, 0.61, 0.355, 1); }
.eanm-ease-out-expo  { --eanm-ease: cubic-bezier(0.16, 1, 0.3, 1); }
.eanm-ease-out-quart { --eanm-ease: cubic-bezier(0.25, 1, 0.5, 1); }
.eanm-ease-out-back  { --eanm-ease: cubic-bezier(0.34, 1.56, 0.64, 1); }
.eanm-ease-in-out    { --eanm-ease: cubic-bezier(0.65, 0, 0.35, 1); }
.eanm-ease-linear    { --eanm-ease: linear; }

/* Defaults, so a widget whose sliders were never touched still animates. */
[class*="eanm-preset-"] {
	--eanm-duration: 500ms;
	--eanm-stagger: 60ms;
	--eanm-delay: 0ms;
	--eanm-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
	--eanm-threshold: 20;
	--eanm-distance: 24px;
	--eanm-dx: 0;
	--eanm-dy: var(--eanm-distance, 24px);
}

/* The split spans. Layout, not state: these apply whether or not the
   animation has run. */
.eanm-u {
	display: inline-block;
}

.eanm-i {
	display: inline-block;
}

/*
 * Transitions switch on only once the element is armed, which the script does
 * a forced reflow after it applies the start state.
 *
 * Declaring the transition alongside the start state is what broke Fade In and
 * Blur In: those presets animate the target element itself, which is already
 * on the page and already visible, so "become invisible" was itself a
 * transition. Above the fold it reversed a frame later and nothing appeared to
 * happen; below the fold the heading visibly faded out before it ever faded in.
 * The split presets hid the bug, because their spans are created already
 * hidden and a freshly inserted element does not transition its first style.
 */
[data-eanm-armed] .eanm-i {
	transition-property: transform, opacity, filter;
	transition-duration: var(--eanm-duration, 500ms);
	transition-timing-function: var(--eanm-ease, cubic-bezier(0.16, 1, 0.3, 1));
	transition-delay: calc(var(--eanm-delay, 0ms) + var(--eanm-stagger, 60ms) * var(--eanm-i, 0));
}

/* Only while there is something still to animate. Left on permanently, a
   will-change per word is a real cost on a long page. */
[data-eanm-ready]:not([data-eanm-in]) .eanm-i {
	will-change: transform, opacity;
}

/* Masked presets: the outer span clips, the inner one slides out from behind
   it. overflow:hidden on an inline-block cuts descenders off, so pad the box
   and pull the padding back out of the layout. */
.eanm-preset-words-up .eanm-u,
.eanm-preset-slide-left .eanm-u,
.eanm-preset-chars-cascade .eanm-u,
.eanm-preset-lines-mask .eanm-u {
	overflow: hidden;
	padding-bottom: 0.18em;
	margin-bottom: -0.18em;
	vertical-align: bottom;
}

/* Pre-animation states. Every one of these is gated on [data-eanm-ready]. */
.eanm-preset-words-up [data-eanm-ready] .eanm-i,
.eanm-preset-chars-cascade [data-eanm-ready] .eanm-i,
.eanm-preset-lines-mask [data-eanm-ready] .eanm-i {
	transform: translateY(110%);
}

.eanm-preset-slide-left [data-eanm-ready] .eanm-i {
	transform: translateX(-110%);
}

.eanm-preset-words-fade [data-eanm-ready] .eanm-i {
	opacity: 0;
	transform: translate3d(var(--eanm-dx, 0), var(--eanm-dy, 0.35em), 0);
}

.eanm-preset-chars-flip [data-eanm-ready] {
	perspective: 600px;
}

.eanm-preset-chars-flip [data-eanm-ready] .eanm-i {
	opacity: 0;
	transform: rotateX(-90deg);
	transform-origin: 50% 100%;
}

.eanm-preset-scale-pop [data-eanm-ready] .eanm-i {
	opacity: 0;
	transform: scale(0.8);
}

/*
 * Words Build is the standard word reveal, matching the GSAP recipe it is
 * named after:
 *
 *   gsap.from( split.words, {
 *     opacity: 0, y: 15, stagger: 0.06, duration: 0.5, ease: 'power2.out'
 *   } );
 *
 * A fixed 15px lift rather than an em-relative one, so it reads the same on a
 * 20px paragraph as on a 60px headline. The timing comes straight from the
 * sliders, whose defaults are that recipe's 500ms and 60ms.
 */
.eanm-preset-words-build [data-eanm-ready] .eanm-i {
	opacity: 0;
	transform: translate3d(0, 15px, 0);
}

/* fade-in and blur-in split nothing: the target element is itself the moving
   part, so it carries the transition as well as the state attributes. */
.eanm-preset-fade-in [data-eanm-ready] {
	opacity: 0;
	transform: translate3d(var(--eanm-dx, 0), var(--eanm-dy, 24px), 0);
	will-change: opacity, transform;
}

.eanm-preset-fade-in [data-eanm-armed] {
	transition-property: opacity, transform;
	transition-duration: var(--eanm-duration, 500ms);
	transition-timing-function: var(--eanm-ease, cubic-bezier(0.16, 1, 0.3, 1));
	transition-delay: var(--eanm-delay, 0ms);
}

.eanm-preset-fade-in [data-eanm-ready][data-eanm-in] {
	opacity: 1;
	transform: none;
}

/*
 * Scroll Highlight: the words start dim and light up as the paragraph rises
 * to the middle of the screen, finishing there.
 *
 * The script writes --eanm-p per word, from 0 to 1, every frame. That is the
 * whole of the scrubbing; the sum below turns it into an opacity between the
 * dim level and full. Continuous rather than a switch, so a word is visibly
 * mid-way when the scroll stops mid-way.
 *
 * The transition has to be switched off explicitly. Every split word carries
 * the reveal transition once armed -- 500ms with a per-word stagger delay --
 * and left in place it eases towards each scrubbed value instead of tracking
 * the scrollbar. Measured: --eanm-p read 1.000 on every word while the
 * opacities were still crawling through 0.92, 0.86, 0.75, 0.59, 0.35.
 */
.eanm-preset-scroll-highlight [data-eanm-ready] .eanm-i {
	opacity: calc(var(--eanm-dim, 0.3) + (1 - var(--eanm-dim, 0.3)) * var(--eanm-p, 0));
}

.eanm-preset-scroll-highlight [data-eanm-armed] .eanm-i {
	transition: none;
}

/* blur-in splits nothing: the target element is itself the moving part. */
.eanm-preset-blur-in [data-eanm-ready] {
	opacity: 0;
	filter: blur(12px);
	will-change: opacity, filter;
}

.eanm-preset-blur-in [data-eanm-armed] {
	transition-property: opacity, filter;
	transition-duration: var(--eanm-duration, 500ms);
	transition-timing-function: var(--eanm-ease, cubic-bezier(0.16, 1, 0.3, 1));
	transition-delay: var(--eanm-delay, 0ms);
}

/* The animated state. Specificity has to beat every pre-state above, which is
   why the preset class is matched by attribute here. */
[class*="eanm-preset-"] [data-eanm-ready][data-eanm-in] .eanm-i {
	transform: none;
	opacity: 1;
}

.eanm-preset-blur-in [data-eanm-ready][data-eanm-in] {
	opacity: 1;
	filter: blur(0);
}

/*
 * The script declines to split at all when Reduce Motion is set, so this is
 * only for the case where the setting changes after a split has happened.
 */
@media (prefers-reduced-motion: reduce) {
	[data-eanm-armed],
	[data-eanm-armed] .eanm-i {
		transition: none;
	}
}
