/**
 * Crisp Motion — base runtime styles.
 *
 * Every reveal is a transition from a hidden "before" state to the natural
 * state, driven by three per-instance custom properties the PHP/blocks emit:
 *   --cm-duration, --cm-delay, --cm-easing
 * with sensible fallbacks to the global --crisp-motion-* design tokens.
 *
 * The hidden state only applies while JS has marked the element ready but not
 * yet in view (`.crisp-motion-ready:not(.is-inview)`), so that if JS never
 * runs the content is always visible — motion is a progressive enhancement.
 */

.crisp-motion {
	--cm-duration: var( --crisp-motion-duration-base, 600ms );
	--cm-delay: 0ms;
	--cm-easing: var( --crisp-motion-ease-out, cubic-bezier( 0.22, 1, 0.36, 1 ) );
	--cm-distance: var( --crisp-motion-distance, 2rem );
	--cm-scale-from: var( --crisp-motion-scale-from, 0.92 );
	--cm-blur-from: var( --crisp-motion-blur-from, 12px );

	transition-property: opacity, transform, filter;
	transition-duration: var( --cm-duration );
	transition-timing-function: var( --cm-easing );
	transition-delay: var( --cm-delay );
}

/* Hidden "before" state — only once JS has taken ownership of the element. */
.crisp-motion.crisp-motion-ready:not( .is-inview ) {
	opacity: 0;
	will-change: opacity, transform;
}

/* Per-animation "before" transforms. */
.crisp-motion.crisp-motion-ready.is-animation-fade-up:not( .is-inview ) {
	transform: translateY( var( --cm-distance ) );
}

.crisp-motion.crisp-motion-ready.is-animation-fade-down:not( .is-inview ) {
	transform: translateY( calc( -1 * var( --cm-distance ) ) );
}

.crisp-motion.crisp-motion-ready.is-animation-fade-left:not( .is-inview ) {
	transform: translateX( var( --cm-distance ) );
}

.crisp-motion.crisp-motion-ready.is-animation-fade-right:not( .is-inview ) {
	transform: translateX( calc( -1 * var( --cm-distance ) ) );
}

.crisp-motion.crisp-motion-ready.is-animation-slide-up:not( .is-inview ) {
	transform: translateY( calc( var( --cm-distance ) * 2.5 ) );
}

.crisp-motion.crisp-motion-ready.is-animation-zoom-in:not( .is-inview ) {
	transform: scale( var( --cm-scale-from ) );
}

.crisp-motion.crisp-motion-ready.is-animation-zoom-out:not( .is-inview ) {
	transform: scale( calc( 2 - var( --cm-scale-from ) ) );
}

.crisp-motion.crisp-motion-ready.is-animation-blur-in:not( .is-inview ) {
	filter: blur( var( --cm-blur-from ) );
}

.crisp-motion.crisp-motion-ready.is-animation-flip-up:not( .is-inview ) {
	transform: perspective( 1000px ) rotateX( 40deg );
	transform-origin: bottom center;
}

.crisp-motion.crisp-motion-ready.is-animation-flip-down:not( .is-inview ) {
	transform: perspective( 1000px ) rotateX( -40deg );
	transform-origin: top center;
}

.crisp-motion.crisp-motion-ready.is-animation-rotate-in:not( .is-inview ) {
	transform: rotate( -8deg ) scale( var( --cm-scale-from ) );
}

/* The natural / revealed state. */
.crisp-motion.is-inview {
	opacity: 1;
	transform: none;
	filter: none;
}

/*
 * Continuous & interaction motion.
 *
 * A second family, distinct from the reveal transitions above: the element is
 * always visible and plays a CSS keyframe animation. The runtime deliberately
 * does NOT mark these elements ready or observe them (see crisp-motion.js), so
 * the reveal hidden-state never applies and no JS is needed for `loop`/`hover`.
 * `click` needs a script (interaction.js toggles `.is-playing`), and so does a
 * loop with a pause between iterations (loop.js — see `.cm-loop-paused` below).
 *
 * Timing is curated per preset rather than driven by --cm-duration: these read
 * as texture, not entrances, and the editor hides the timing controls for them.
 * The easing token is the one knob that can be overridden, via --cm-loop-easing
 * (spin reads as linear, the rest ease-in-out); the extension emits it inline
 * when the author sets a pause.
 */
.crisp-motion.cm-trigger-loop {
	will-change: transform;
	--cm-loop-easing: ease-in-out;
}

.crisp-motion.cm-trigger-loop.is-animation-spin {
	--cm-loop-easing: linear;
}

@keyframes cm-float {

	0%,
	100% {
		transform: translateY( 0 );
	}

	50% {
		transform: translateY( -10px );
	}
}

@keyframes cm-pulse {

	0%,
	100% {
		transform: scale( 1 );
	}

	50% {
		transform: scale( 1.05 );
	}
}

@keyframes cm-bounce {

	0%,
	20%,
	53%,
	100% {
		transform: translateY( 0 );
	}

	40%,
	43% {
		transform: translateY( -16px );
	}

	70% {
		transform: translateY( -8px );
	}

	90% {
		transform: translateY( -2px );
	}
}

@keyframes cm-wiggle {

	0%,
	100% {
		transform: rotate( 0deg );
	}

	25% {
		transform: rotate( -4deg );
	}

	75% {
		transform: rotate( 4deg );
	}
}

@keyframes cm-spin {

	from {
		transform: rotate( 0deg );
	}

	to {
		transform: rotate( 360deg );
	}
}

@keyframes cm-shake {

	0%,
	100% {
		transform: translateX( 0 );
	}

	20%,
	60% {
		transform: translateX( -6px );
	}

	40%,
	80% {
		transform: translateX( 6px );
	}
}

/* Loop (seamless): runs forever, no gap. The default when pause = 0. */
.crisp-motion.cm-trigger-loop:not( .cm-loop-paused ).is-animation-float {
	animation: cm-float 3s var( --cm-loop-easing ) infinite;
}

.crisp-motion.cm-trigger-loop:not( .cm-loop-paused ).is-animation-pulse {
	animation: cm-pulse 2s var( --cm-loop-easing ) infinite;
}

.crisp-motion.cm-trigger-loop:not( .cm-loop-paused ).is-animation-bounce {
	animation: cm-bounce 1.8s var( --cm-loop-easing ) infinite;
}

.crisp-motion.cm-trigger-loop:not( .cm-loop-paused ).is-animation-wiggle {
	animation: cm-wiggle 1.2s var( --cm-loop-easing ) infinite;
}

.crisp-motion.cm-trigger-loop:not( .cm-loop-paused ).is-animation-spin {
	animation: cm-spin 6s var( --cm-loop-easing ) infinite;
}

.crisp-motion.cm-trigger-loop:not( .cm-loop-paused ).is-animation-shake {
	animation: cm-shake 1.1s var( --cm-loop-easing ) infinite;
}

/* Loop (paused): plays one cycle each time loop.js adds `.is-playing`; the
   script then waits the author's pause and replays. Same curated per-preset
   durations as the seamless loop, so only the gap between cycles differs. */
.crisp-motion.cm-trigger-loop.cm-loop-paused.is-animation-float.is-playing {
	animation: cm-float 3s var( --cm-loop-easing );
}

.crisp-motion.cm-trigger-loop.cm-loop-paused.is-animation-pulse.is-playing {
	animation: cm-pulse 2s var( --cm-loop-easing );
}

.crisp-motion.cm-trigger-loop.cm-loop-paused.is-animation-bounce.is-playing {
	animation: cm-bounce 1.8s var( --cm-loop-easing );
}

.crisp-motion.cm-trigger-loop.cm-loop-paused.is-animation-wiggle.is-playing {
	animation: cm-wiggle 1.2s var( --cm-loop-easing );
}

.crisp-motion.cm-trigger-loop.cm-loop-paused.is-animation-spin.is-playing {
	animation: cm-spin 6s var( --cm-loop-easing );
}

.crisp-motion.cm-trigger-loop.cm-loop-paused.is-animation-shake.is-playing {
	animation: cm-shake 1.1s var( --cm-loop-easing );
}

/* Hover / click: plays once. `:hover` needs no JS; `.is-playing` is toggled by
   interaction.js on click and removed on animationend so each click replays. */
.crisp-motion.cm-trigger-hover.is-animation-float:hover,
.crisp-motion.cm-trigger-click.is-animation-float.is-playing {
	animation: cm-float 0.8s ease-in-out;
}

.crisp-motion.cm-trigger-hover.is-animation-pulse:hover,
.crisp-motion.cm-trigger-click.is-animation-pulse.is-playing {
	animation: cm-pulse 0.5s ease-in-out;
}

.crisp-motion.cm-trigger-hover.is-animation-bounce:hover,
.crisp-motion.cm-trigger-click.is-animation-bounce.is-playing {
	animation: cm-bounce 0.9s ease-in-out;
}

.crisp-motion.cm-trigger-hover.is-animation-wiggle:hover,
.crisp-motion.cm-trigger-click.is-animation-wiggle.is-playing {
	animation: cm-wiggle 0.5s ease-in-out;
}

.crisp-motion.cm-trigger-hover.is-animation-spin:hover,
.crisp-motion.cm-trigger-click.is-animation-spin.is-playing {
	animation: cm-spin 0.7s ease-in-out;
}

.crisp-motion.cm-trigger-hover.is-animation-shake:hover,
.crisp-motion.cm-trigger-click.is-animation-shake.is-playing {
	animation: cm-shake 0.6s ease-in-out;
}

/*
 * Scratch emphasis.
 *
 * A third concern alongside reveal and continuous motion, applied by the Motion
 * extension to *any* block: a hand-drawn mark (underline, circle, strike,
 * highlight…) over the block's text box. It's a decorative `::after` overlay —
 * it never touches the host's transform/opacity, so it composes with a box
 * reveal on the same element.
 *
 * Each shape supplies its rough stroke as an SVG `mask` on the pseudo-element,
 * tinted by `--cm-scratch-color` (falling back to the text colour). The draw-on
 * is a `clip-path` wipe: undrawn by default, revealed to full width when the
 * runtime adds `.is-inview` (scroll/load), on `:hover`, or by an infinite
 * keyframe (loop). Because the resting state is undrawn, a scroll/load mark
 * needs JS to draw — unlike a reveal it guards no content, so this is an
 * acceptable enhancement; hover/loop marks are pure CSS and need none.
 */
.cm-scratch {
	position: relative;
}

.cm-scratch::after {
	content: "";
	position: absolute;
	z-index: 1;
	pointer-events: none;
	background-color: var( --cm-scratch-color, currentColor );
	-webkit-mask: var( --cm-scratch-mask ) no-repeat center / 100% 100%;
	mask: var( --cm-scratch-mask ) no-repeat center / 100% 100%;

	/* Undrawn: clipped away from the right. `.is-scratch-dir-center` starts from
	   the middle instead; `.is-inview`/`:hover`/the loop keyframe fill it in. */
	clip-path: inset( 0 100% 0 0 );
	transition:
		clip-path var( --cm-scratch-duration, 700ms )
		var( --cm-scratch-easing, ease ) var( --cm-scratch-delay, 0ms );
}

.cm-scratch.is-scratch-dir-center::after {
	clip-path: inset( 0 50% 0 50% );
}

/* --- shape geometry ------------------------------------------------------- */

/* Line marks ride just under the text baseline; their weight is the element
   height driven by --cm-scratch-size. */
.is-scratch-underline::after,
.is-scratch-double::after,
.is-scratch-wavy::after {
	left: 0;
	right: 0;
	bottom: -0.16em;
	height: var( --cm-scratch-size, 8px );
}

/* Box-spanning marks cover the whole text box (bled out a little each side). */
.is-scratch-circle::after,
.is-scratch-cross::after,
.is-scratch-highlight::after {
	inset: -0.12em -0.18em;
}

/* --- shape strokes (SVG masks) -------------------------------------------- */

.is-scratch-underline::after {
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 24' preserveAspectRatio='none'%3E%3Cpath d='M6 15 C 70 7, 130 20, 190 12 C 240 6, 290 16, 314 11' fill='none' stroke='%23000' stroke-width='9' stroke-linecap='round'/%3E%3C/svg%3E" );
}

.is-scratch-double::after {
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 30' preserveAspectRatio='none'%3E%3Cpath d='M6 9 C 90 3, 200 15, 314 7 M8 23 C 110 17, 210 27, 312 20' fill='none' stroke='%23000' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E" );
}

.is-scratch-wavy::after {
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 24' preserveAspectRatio='none'%3E%3Cpath d='M4 12 Q 24 2 44 12 T 84 12 T 124 12 T 164 12 T 204 12 T 244 12 T 284 12 T 320 12' fill='none' stroke='%23000' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E" );
}

/* Strike sits on the text's vertical centre. */
.is-scratch-strike::after {
	left: 0;
	right: 0;
	top: 50%;
	height: var( --cm-scratch-size, 8px );
	transform: translateY( -50% );
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 20' preserveAspectRatio='none'%3E%3Cpath d='M6 11 C 90 7, 220 14, 314 9' fill='none' stroke='%23000' stroke-width='8' stroke-linecap='round'/%3E%3C/svg%3E" );
}

.is-scratch-circle::after {
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 160' preserveAspectRatio='none'%3E%3Cpath d='M250 24 C 120 4, 20 40, 28 84 C 34 132, 150 156, 250 140 C 330 128, 330 46, 236 22' fill='none' stroke='%23000' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E" );
}

.is-scratch-cross::after {
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 160' preserveAspectRatio='none'%3E%3Cpath d='M20 20 C 120 70, 200 90, 300 140 M300 24 C 200 74, 120 92, 22 138' fill='none' stroke='%23000' stroke-width='6' stroke-linecap='round'/%3E%3C/svg%3E" );
}

/* Highlight is a marker sweep *behind* the text, so it stays readable. */
.is-scratch-highlight::after {
	z-index: -1;
	--cm-scratch-mask: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 160' preserveAspectRatio='none'%3E%3Cpath d='M8 30 C 60 18, 260 14, 312 26 C 318 60, 316 104, 310 132 C 250 144, 70 146, 12 134 C 6 100, 4 62, 8 30 Z' fill='%23000'/%3E%3C/svg%3E" );
}

/* --- draw triggers -------------------------------------------------------- */

/* Scroll / load / chain: `.is-inview` is the draw signal — the runtime
   toggles it for scroll/load; chain.js sets it on the source's phase. */
.cm-scratch.cm-scratch-scroll.is-inview::after,
.cm-scratch.cm-scratch-load.is-inview::after,
.cm-scratch.cm-scratch-chain.is-inview::after {
	clip-path: inset( 0 0 0 0 );
}

/* Hover: pure CSS. */
.cm-scratch.cm-scratch-hover:hover::after {
	clip-path: inset( 0 0 0 0 );
}

/* Loop: pure CSS — draw from the left, hold, then wipe off to the right. */
.cm-scratch.cm-scratch-loop::after {
	animation: cm-scratch-loop 2.8s var( --cm-scratch-easing, ease-in-out ) infinite;
}

/* Center out: draw from the middle, hold, then retract back to the middle so the
   loop honours the same direction the resting clip-path uses. */
.cm-scratch.cm-scratch-loop.is-scratch-dir-center::after {
	animation-name: cm-scratch-loop-center;
}

@keyframes cm-scratch-loop {

	0% {
		clip-path: inset( 0 100% 0 0 );
	}

	35%,
	65% {
		clip-path: inset( 0 0 0 0 );
	}

	100% {
		clip-path: inset( 0 0 0 100% );
	}
}

@keyframes cm-scratch-loop-center {

	0% {
		clip-path: inset( 0 50% 0 50% );
	}

	35%,
	65% {
		clip-path: inset( 0 0 0 0 );
	}

	100% {
		clip-path: inset( 0 50% 0 50% );
	}
}

/* --- typewriter ----------------------------------------------------------- *
 * The typewriter effect is driven entirely by typewriter.js, which clears
 * the element and retypes its text; the only styling here is the caret it
 * appends. The caret is a thin bar sized to the text, blinking on a steps()
 * loop so it snaps on and off rather than fading. `currentcolor` keeps it on
 * the text's colour without any per-instance property. */
.cm-typewriter__caret {
	display: inline-block;
	width: 0.08em;
	height: 1em;
	margin-left: 0.06em;
	vertical-align: text-bottom;
	background-color: currentcolor;
	animation: cm-typewriter-blink 1s steps( 1, end ) infinite;
}

@keyframes cm-typewriter-blink {

	0%,
	49% {
		opacity: 1;
	}

	50%,
	100% {
		opacity: 0;
	}
}

/* Editor-only static marker. The extension-editor HOC adds
   `has-crisp-typewriter` in the canvas so an author can see the effect is on;
   the front end never emits this class (it uses `cm-typewriter`), so this rule
   stays out of the way there. A quiet trailing caret glyph reads as "this text
   types on" without animating — animating there fights the RichText caret. */
.has-crisp-typewriter::after {
	content: "▏";
	margin-left: 0.1em;
	opacity: 0.45;
}

/* --- written sweep -------------------------------------------------------- *
 * The written sweep is driven by sweep.js, which splits the heading into one
 * `.sweep__g` span per character and reveals them left to right on a hand-paced
 * cadence. The typeface is untouched: only a per-glyph `clip-path` sweep
 * animates, so existing heading styles are unaffected. Each glyph strokes on
 * over a duration the script sets per letter (`--sweep-dur`), so no two letters
 * take quite the same time.
 *
 * The clip is a four-point polygon, not an inset: its moving right edge is
 * slanted like an italic pen nib (top leading the bottom), so the ink front
 * sweeping across each letter reads as a stroke being drawn, not a vertical
 * curtain opening. Both right-edge points travel the same horizontal distance,
 * keeping the nib angle constant for the whole stroke. Every resting
 * coordinate sits outside the glyph box (−22%…134%) so overhanging
 * letterforms — italic tails, swashes, descenders — are never clipped at
 * either end of the sweep. */
.sweep__g {
	display: inline-block;
	white-space: pre;
	clip-path: polygon( -22% -22%, -10% -22%, -34% 122%, -22% 122% );
	transition: clip-path var( --sweep-dur, 85ms ) linear;
}

.sweep__g.is-lit {
	clip-path: polygon( -22% -22%, 134% -22%, 110% 122%, -22% 122% );
}

/* Pre-split hold: when JS is present (the wp_head flag adds `cm-sweep-js`
   to the document element before first paint), hold a sweep heading from
   raw text until the runtime has split it into glyphs and added
   `is-sweep-ready`. That prevents a flash of the full string ahead of the
   reveal. It is gated on the flag, so a no-JS visitor — who never gets the flag
   — always sees the heading as plain, readable text. `visibility` keeps the
   heading's box in layout, so nothing reflows when the glyphs take over. */
.cm-sweep-js [data-sweep]:not( .is-sweep-ready ) {
	visibility: hidden;
}

/* Editor-only static marker: the extension-editor HOC adds `has-crisp-sweep` in
   the canvas so an author can see the effect is on. A quiet inset accent reads
   as "this heading sweeps on" without animating — the editor runs no runtime,
   and animating the text there would fight the RichText caret. `inset`
   box-shadow draws without displacing the block's layout. */
.has-crisp-sweep {
	box-shadow: inset 2px 0 0 rgba( 120, 130, 255, 0.55 );
}

/* Respect the user's reduced-motion preference: no movement, always visible.
   `animation` is killed too, so continuous/interaction presets fall silent. */
@media ( prefers-reduced-motion: reduce ) {

	.crisp-motion,
	.crisp-motion.crisp-motion-ready:not( .is-inview ) {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		transition: none !important;
		animation: none !important;
	}

	/* Show the emphasis mark fully drawn and static, never mid-wipe. */
	.cm-scratch::after {
		clip-path: none !important;
		transition: none !important;
		animation: none !important;
	}

	/* The text is presented finished; hold the caret steady rather than blink. */
	.cm-typewriter__caret {
		opacity: 1 !important;
		animation: none !important;
	}

	/* Present the swept heading whole and static: release the pre-split hold and
	   unclip every glyph so the letters are shown at once, never mid-sweep. The
	   runtime also skips its timers, so this is a belt-and-braces stop. */
	.cm-sweep-js [data-sweep] {
		visibility: visible !important;
	}

	.sweep__g {
		clip-path: none !important;
		transition: none !important;
	}
}
