/* Animation presets (public/fx.js): a preset is the class fx-<id> on an element of a signature design.
   Every cycle divides the GIF's 4.8 s loop and ends where it starts, and its first frame shows the complete element
   (Outlook on Windows shows only a GIF's first frame). A preset's animation wins over the design's own (!important):
   putting it on is an explicit choice. A custom design (lib/custom.js) has these classes with the c- prefix and
   carries this file with it, prefixed.
   The knobs are custom properties, set by modifier classes next to the preset's class:
   --fx-d   one cycle: a preset that plays once per loop has 4.8 s / max(1, s), never slower; one that plays twice has
            2.4 s / s, so 4.8 s (slow) or 1.2 s (fast); s is --fx-s, the speed (fx-slow / fx-fast)
   --fx-i   intensity, a factor on the distances, angles and sizes (fx-subtle / fx-strong)
   --fx-x / --fx-y   direction, the way the motion travels (fx-right, fx-left, fx-up, fx-down)
   --fx-lag the step in a choreography (fx-step1 / fx-step2): its motion comes 6 % / 12 % of its cycle later
   --fx-at  the delay every animation starts with, negative: a lag or a stagger never leaves a gap in the loop; a preset
            that moves all the time (rhythm loop) has no start to delay and ignores the step, so its first frame is at rest
   --fx-k / --fx-gap  a group's part number, and the share of the cycle between two parts (the stagger)
   A preset that plays once per loop rests from its 0 % keyframe until its motion starts and is back at rest by 88 % of
   its cycle (less its stagger), so the first frame is at rest at any step. Curves: sine (.37,0,.63,1), out (.22,1,.36,1),
   in (.55,0,.75,.3), in-out (.65,0,.35,1), back (.34,1.56,.64,1) – the overshoot of a pop.
   Registry v3 (public/fx.js): the knobs may be written inline instead of a modifier class – --fx-s a loop-safe speed
   stop, --fx-i a size, --fx-c the preset's own colour (every var(--acc) here reads it first), --fx-lag a start within its
   cycle that keeps frame 0 at rest. Each element of an animation starts from the defaults (the rule below, of no
   weight: a modifier class or an inline value wins), so a wrapper's knobs never reach the part inside it. */
:where([class*="fx-"]:not(.fx-ring)) { --fx-s: 1; --fx-i: 1; --fx-lag: 0; --fx-c: var(--acc); }
.fx-slow { --fx-s: .5; }
.fx-fast { --fx-s: 2; }
.fx-subtle { --fx-i: .6; }
.fx-strong { --fx-i: 1.6; }
.fx-right { --fx-x: 1; --fx-y: 0; }
.fx-left { --fx-x: -1; --fx-y: 0; }
.fx-up { --fx-x: 0; --fx-y: -1; }
.fx-down { --fx-x: 0; --fx-y: 1; }
.fx-step1 { --fx-lag: .06; }
.fx-step2 { --fx-lag: .12; }
[class*="fx-"] { --fx-at: calc((var(--fx-lag, 0) - 1) * var(--fx-d, 4.8s)); }
/* a group's parts in order (direction left reverses a row of icons) */
:is(.fx-fade-up, .fx-social-pop, .fx-slide-in, .fx-icon-wave) > :nth-child(2) { --fx-k: 1; }
:is(.fx-fade-up, .fx-social-pop, .fx-slide-in, .fx-icon-wave) > :nth-child(3) { --fx-k: 2; }
:is(.fx-fade-up, .fx-social-pop, .fx-slide-in, .fx-icon-wave) > :nth-child(4) { --fx-k: 3; }
:is(.fx-fade-up, .fx-social-pop, .fx-slide-in, .fx-icon-wave) > :nth-child(5) { --fx-k: 4; }
:is(.fx-fade-up, .fx-social-pop, .fx-slide-in, .fx-icon-wave) > :nth-child(n+6) { --fx-k: 5; }
.fx-icon-wave.fx-left > :nth-last-child(1) { --fx-k: 0; }
.fx-icon-wave.fx-left > :nth-last-child(2) { --fx-k: 1; }
.fx-icon-wave.fx-left > :nth-last-child(3) { --fx-k: 2; }
.fx-icon-wave.fx-left > :nth-last-child(4) { --fx-k: 3; }
.fx-icon-wave.fx-left > :nth-last-child(5) { --fx-k: 4; }
.fx-icon-wave.fx-left > :nth-last-child(n+6) { --fx-k: 5; }

/* an image cannot draw around itself: applyFx frames it, and the frame takes the preset */
.fx-frame { position: relative; display: grid; width: fit-content; flex: none; border-radius: var(--fx-r, 50%); }
.fx-frame > * { grid-area: 1 / 1; }
/* a part's second animation sits on a wrapper (fx-frame fx-wrap) around it: inline, so it takes the part's place in any
   container (a flex row, a centred block) without a line of its own; the part's first animation stays on the part */
.fx-frame.fx-wrap { display: inline-grid; vertical-align: top; max-width: 100%; }

/* ring-draw: a ring drawn around the photo (its <svg class="fx-ring"> comes with applyFx) */
.fx-ring-draw { position: relative; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); }
.fx-ring-draw > .fx-ring { position: absolute; z-index: 2; inset: -7px; width: calc(100% + 14px); height: calc(100% + 14px); overflow: visible; pointer-events: none; transform: rotate(-90deg); }
.fx-ring-draw > .fx-ring circle { fill: none; stroke: var(--fx-c, var(--acc)); stroke-width: 2.6; stroke-linecap: round; stroke-dasharray: 100 100; animation: fx-ring-draw var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-ring-draw {
  0%, 56% { stroke-dashoffset: 0; opacity: 1; }
  61% { stroke-dashoffset: 0; opacity: 0; }
  61.5% { stroke-dashoffset: 100; opacity: 0; animation-timing-function: cubic-bezier(.55, 0, .25, 1); }
  62% { stroke-dashoffset: 100; opacity: 1; animation-timing-function: cubic-bezier(.55, 0, .25, 1); }
  86%, 100% { stroke-dashoffset: 0; opacity: 1; }
}

/* halo-breathe: a soft halo in the brand colour grows and shrinks (once: one breath per loop) */
.fx-halo-breathe { position: relative; --fx-d: calc(2.4s / var(--fx-s, 1)); --fx-at: calc(-1 * var(--fx-d)); }
.fx-halo-breathe.fx-once { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-at: calc((var(--fx-lag, 0) - 1) * var(--fx-d)); }
.fx-halo-breathe::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; animation: fx-halo-breathe var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
.fx-halo-breathe.fx-once::after { animation-name: fx-halo-breathe-once !important; }
@keyframes fx-halo-breathe {
  0%, 100% { box-shadow: 0 0 0 calc(3px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 30%, transparent); }
  50% { box-shadow: 0 0 0 calc(9px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 12%, transparent); }
}
@keyframes fx-halo-breathe-once {
  0%, 50% { box-shadow: 0 0 0 calc(3px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 30%, transparent); }
  67% { box-shadow: 0 0 0 calc(9px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 12%, transparent); }
  84%, 100% { box-shadow: 0 0 0 calc(3px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 30%, transparent); }
}

/* orbit-dot: a dot circles the photo on a faint track */
.fx-orbit-dot { position: relative; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-at: calc(-1 * var(--fx-d)); }
.fx-orbit-dot::before, .fx-orbit-dot::after { content: ""; position: absolute; inset: -8px; border-radius: 50%; pointer-events: none; }
.fx-orbit-dot::before { inset: -4.5px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fx-c, var(--acc)) 22%, transparent); }
.fx-orbit-dot::after { z-index: 2; background: radial-gradient(circle at 50% 4px, var(--fx-c, var(--acc)) 0 3.5px, transparent 4px); animation: fx-orbit-dot var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-orbit-dot { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* photo-zoom: the photo zooms in a little inside its still frame */
.fx-photo-zoom { overflow: hidden; border-radius: var(--fx-r, 50%); --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-at: calc(-1 * var(--fx-d)); }
.fx-photo-zoom > img { animation: fx-photo-zoom var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
@keyframes fx-photo-zoom { 0%, 100% { scale: 1; } 50% { scale: calc(1 + .08 * var(--fx-i, 1)); } }

/* float: up a few px and back, all the time */
.fx-float { --fx-d: calc(2.4s / var(--fx-s, 1)); --fx-at: calc(-1 * var(--fx-d)); animation: fx-float var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
@keyframes fx-float { 0%, 100% { translate: 0 0; } 50% { translate: 0 calc(-3px * var(--fx-i, 1)); } }

/* type-on: typed letter by letter, then it stays (the box shrinks to the text, so the steps follow the letters) */
.fx-type-on { width: fit-content; max-width: 100%; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-type-on var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-type-on {
  0%, 60% { clip-path: inset(-.3em -.4em -.3em 0); opacity: 1; }
  64% { clip-path: inset(-.3em -.4em -.3em 0); opacity: 0; }
  64.5% { clip-path: inset(-.3em 100% -.3em 0); opacity: 0; }
  65% { clip-path: inset(-.3em 100% -.3em 0); opacity: 1; animation-timing-function: steps(18, end); }
  86%, 100% { clip-path: inset(-.3em -.4em -.3em 0); opacity: 1; }
}

/* name-shine: a band of light in the brand colour sweeps across the text (--fx-ink: the text's colour) */
.fx-name-shine { width: fit-content; max-width: 100%; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); background: linear-gradient(100deg, var(--fx-ink, #111) 42%, color-mix(in srgb, var(--fx-c, var(--acc)) 70%, #fff) 50%, var(--fx-ink, #111) 58%) 100% 0 / 300% 100% no-repeat; -webkit-background-clip: text; background-clip: text; color: transparent !important; animation: fx-name-shine var(--fx-d) ease-in-out infinite var(--fx-at) !important; }
/* the band ends past the text and jumps back unseen at the loop's end (steps), so the loop starts where it ends */
@keyframes fx-name-shine { 0%, 55% { background-position: 100% 0; } 85% { background-position: 0 0; animation-timing-function: steps(1, end); } 100% { background-position: 100% 0; } }

/* marker: a highlighter stroke drawn behind the text */
.fx-marker { width: fit-content; max-width: 100%; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); background: linear-gradient(color-mix(in srgb, var(--fx-c, var(--acc)) 22%, transparent), color-mix(in srgb, var(--fx-c, var(--acc)) 22%, transparent)) 0 88% / 100% 42% no-repeat; animation: fx-marker var(--fx-d) cubic-bezier(.55, 0, .25, 1) infinite var(--fx-at) !important; }
@keyframes fx-marker {
  0%, 58% { background-size: 100% 42%; background-position: 0 88%; }
  64% { background-size: 0% 42%; background-position: 0 88%; }
  80%, 100% { background-size: 100% 42%; background-position: 0 88%; }
}

/* fade-up: the group's parts fade out together and rise back one after another */
.fx-fade-up { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-gap: .02; }
.fx-fade-up > * { animation: fx-fade-up var(--fx-d) ease-out infinite calc(var(--fx-at) + var(--fx-k, 0) * var(--fx-gap) * var(--fx-d)) !important; }
@keyframes fx-fade-up {
  0%, 56% { opacity: 1; translate: 0 0; }
  60% { opacity: 0; translate: 0 0; }
  60.5% { opacity: 0; translate: 0 calc(6px * var(--fx-i, 1)); }
  76%, 100% { opacity: 1; translate: 0 0; }
}

/* social-pop: each icon pops in turn, a small spring */
.fx-social-pop { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-gap: .025; }
.fx-social-pop > * { animation: fx-social-pop var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite calc(var(--fx-at) + var(--fx-k, 0) * var(--fx-gap) * var(--fx-d)) !important; }
@keyframes fx-social-pop {
  0%, 56% { scale: 1; animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  60.5% { scale: calc(1 + .18 * var(--fx-i, 1)); }
  65% { scale: calc(1 - .05 * var(--fx-i, 1)); }
  69% { scale: calc(1 + .015 * var(--fx-i, 1)); }
  73%, 100% { scale: 1; }
}

/* logo-shimmer: a soft light sweeps across the logo */
.fx-logo-shimmer { position: relative; overflow: hidden; border-radius: 0; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); }
.fx-logo-shimmer::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(100deg, transparent 35%, rgba(255, 255, 255, .8) 50%, transparent 65%); transform: translateX(-110%); animation: fx-logo-shimmer var(--fx-d) ease-in-out infinite var(--fx-at) !important; }
@keyframes fx-logo-shimmer { 0%, 55% { transform: translateX(-110%); } 85% { transform: translateX(110%); animation-timing-function: steps(1, end); } 100% { transform: translateX(-110%); } }

/* line-sweep: a short line in the brand colour above the element is drawn again */
.fx-line-sweep { position: relative; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); }
.fx-line-sweep::before { content: ""; position: absolute; left: 0; top: -8px; width: 44px; height: 2px; border-radius: 2px; background: var(--fx-c, var(--acc)); animation: fx-line-sweep var(--fx-d) cubic-bezier(.55, 0, .25, 1) infinite var(--fx-at) !important; }
@keyframes fx-line-sweep {
  0%, 58% { clip-path: inset(0 0 0 0); }
  64% { clip-path: inset(0 0 0 100%); }
  64.5% { clip-path: inset(0 100% 0 0); }
  80%, 100% { clip-path: inset(0 0 0 0); }
}

/* cta-pulse: a soft ring pulses around the button, which grows a touch */
.fx-cta-pulse { --fx-d: calc(2.4s / var(--fx-s, 1)); --fx-at: calc(-1 * var(--fx-d)); animation: fx-cta-pulse var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
@keyframes fx-cta-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--fx-c, var(--acc)) 0%, transparent); scale: 1; }
  50% { box-shadow: 0 0 0 calc(5px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 18%, transparent); scale: calc(1 + .03 * var(--fx-i, 1)); }
}

/* bounce: a hop from the bottom edge – a soft squash on take-off and landing and a tiny rebound, then rest */
.fx-bounce { transform-origin: 50% 100%; --fx-d: calc(2.4s / var(--fx-s, 1)); animation: fx-bounce var(--fx-d) linear infinite var(--fx-at) !important; }
.fx-bounce.fx-loop { --fx-at: calc(-1 * var(--fx-d)); animation-name: fx-bounce-loop !important; }
@keyframes fx-bounce {
  0%, 52% { translate: 0 0; scale: 1 1; animation-timing-function: cubic-bezier(.33, 1, .68, 1); }
  57% { translate: 0 0; scale: calc(1 + .04 * var(--fx-i, 1)) calc(1 - .05 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  68% { translate: 0 calc(-6px * var(--fx-i, 1)); scale: calc(1 - .02 * var(--fx-i, 1)) calc(1 + .03 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  77% { translate: 0 0; scale: calc(1 + .03 * var(--fx-i, 1)) calc(1 - .04 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  82% { translate: 0 calc(-1.2px * var(--fx-i, 1)); scale: 1 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  86%, 100% { translate: 0 0; scale: 1 1; }
}
/* loop: it keeps bouncing, with a short rest on the ground */
@keyframes fx-bounce-loop {
  0%, 100% { translate: 0 0; scale: 1 1; animation-timing-function: cubic-bezier(.33, 1, .68, 1); }
  8% { translate: 0 0; scale: calc(1 + .035 * var(--fx-i, 1)) calc(1 - .045 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  38% { translate: 0 calc(-6px * var(--fx-i, 1)); scale: calc(1 - .02 * var(--fx-i, 1)) calc(1 + .025 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  64% { translate: 0 0; scale: calc(1 + .03 * var(--fx-i, 1)) calc(1 - .04 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  74% { translate: 0 calc(-1.2px * var(--fx-i, 1)); scale: 1 1; animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  84% { translate: 0 0; scale: 1 1; }
}

/* drift: floats up and down while it sways a degree or two to each side (two sines a quarter apart) */
.fx-drift { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-at: calc(-1 * var(--fx-d)); animation: fx-drift var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at), fx-drift-sway var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-drift { 0%, 100% { translate: 0 0; } 50% { translate: 0 calc(-4px * var(--fx-i, 1)); } }
@keyframes fx-drift-sway {
  0%, 100% { rotate: 0deg; animation-timing-function: cubic-bezier(.61, 1, .88, 1); }
  25% { rotate: calc(1.5deg * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  75% { rotate: calc(-1.5deg * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.12, 0, .39, 0); }
}

/* wobble: tilts left and right, the swing dying out */
.fx-wobble { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-wobble var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
@keyframes fx-wobble {
  0%, 58% { rotate: 0deg; }
  61.5% { rotate: calc(-5deg * var(--fx-i, 1)); }
  65.5% { rotate: calc(4deg * var(--fx-i, 1)); }
  69.5% { rotate: calc(-2.5deg * var(--fx-i, 1)); }
  73.5% { rotate: calc(1.2deg * var(--fx-i, 1)); }
  77% { rotate: calc(-.4deg * var(--fx-i, 1)); }
  80%, 100% { rotate: 0deg; }
}

/* pop-in: shrinks away and pops back with a springy overshoot (the back curve) */
.fx-pop-in { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-pop-in var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-pop-in {
  0%, 58% { opacity: 1; scale: 1; animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  62% { opacity: 0; scale: calc(1 - .15 * var(--fx-i, 1)); }
  63% { opacity: 0; scale: calc(1 - .4 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.34, 1.56, .64, 1); }
  75%, 100% { opacity: 1; scale: 1; }
}

/* slide-in: fades away and slides back in from the side – a group part by part, a single element on its own */
.fx-slide-in { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-gap: .017; }
.fx-slide-in:has(> :nth-child(2)) > *, .fx-slide-in:not(:has(> :nth-child(2))) { animation: fx-slide-in var(--fx-d) ease-in-out infinite calc(var(--fx-at) + var(--fx-k, 0) * var(--fx-gap) * var(--fx-d)) !important; }
@keyframes fx-slide-in {
  0%, 56% { opacity: 1; translate: 0 0; }
  60% { opacity: 0; translate: 0 0; }
  60.5% { opacity: 0; translate: calc(var(--fx-x, 1) * -16px * var(--fx-i, 1)) calc(var(--fx-y, 0) * -10px * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  72%, 100% { opacity: 1; translate: 0 0; }
}

/* shake: a short, soft side-to-side shake that dies out */
.fx-shake { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-shake var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
@keyframes fx-shake {
  0%, 62% { translate: 0 0; }
  64% { translate: calc(-2px * var(--fx-i, 1)) 0; }
  66.5% { translate: calc(2px * var(--fx-i, 1)) 0; }
  69% { translate: calc(-1.5px * var(--fx-i, 1)) 0; }
  71.5% { translate: calc(1px * var(--fx-i, 1)) 0; }
  74% { translate: calc(-.5px * var(--fx-i, 1)) 0; }
  76.5%, 100% { translate: 0 0; }
}

/* heartbeat: two quick soft swells (lub-dub), then a pause */
.fx-heartbeat { --fx-d: calc(2.4s / var(--fx-s, 1)); animation: fx-heartbeat var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-heartbeat {
  0%, 56% { scale: 1; animation-timing-function: cubic-bezier(.33, 1, .68, 1); }
  61% { scale: calc(1 + .07 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  67% { scale: calc(1 + .015 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.33, 1, .68, 1); }
  72% { scale: calc(1 + .05 * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  84%, 100% { scale: 1; }
}

/* spin: one full turn with a smooth start and a soft settle past the end (right: clockwise) */
.fx-spin { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-spin var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-spin {
  0%, 58% { rotate: 0deg; animation-timing-function: cubic-bezier(.62, 0, .3, 1.1); }
  82%, 100% { rotate: calc(360deg * var(--fx-x, 1)); }
}

/* flip: one turn around the vertical axis, like a coin */
.fx-flip { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-flip var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-flip {
  0%, 58% { transform: perspective(600px) rotateY(0deg); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  80%, 100% { transform: perspective(600px) rotateY(calc(360deg * var(--fx-x, 1))); }
}

/* typewriter: typed with a cursor in the brand colour leading the letters; it blinks twice and goes before the loop
   ends. The text's clip ends where the cursor ends: both take the same 18 steps between the same two edges. */
.fx-typewriter { position: relative; width: fit-content; max-width: 100%; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); animation: fx-typewriter var(--fx-d) linear infinite var(--fx-at) !important; }
.fx-typewriter::after { content: ""; position: absolute; top: .12em; bottom: .12em; left: 0; width: 2px; border-radius: 1px; background: var(--fx-c, var(--acc)); opacity: 0; pointer-events: none; animation: fx-typewriter-caret var(--fx-d) linear infinite var(--fx-at) !important; }
@keyframes fx-typewriter {
  0%, 56% { clip-path: inset(-.3em -.6em -.3em 0); opacity: 1; }
  60% { clip-path: inset(-.3em -.6em -.3em 0); opacity: 0; }
  60.5% { clip-path: inset(-.3em calc(100% - 1px) -.3em 0); opacity: 0; }
  61% { clip-path: inset(-.3em calc(100% - 1px) -.3em 0); opacity: 1; animation-timing-function: steps(18, end); }
  79.5% { clip-path: inset(-.3em -3px -.3em 0); opacity: 1; }
  80%, 100% { clip-path: inset(-.3em -.6em -.3em 0); opacity: 1; }
}
@keyframes fx-typewriter-caret {
  0%, 60.5% { left: -1px; opacity: 0; }
  61% { left: -1px; opacity: 1; animation-timing-function: steps(18, end); }
  79.5% { left: calc(100% + 1px); opacity: 1; }
  82% { left: calc(100% + 1px); opacity: 0; }
  84.5% { left: calc(100% + 1px); opacity: 1; }
  87% { left: calc(100% + 1px); opacity: 0; }
  88%, 100% { left: -1px; opacity: 0; }
}

/* underline: a line in the brand colour under the text wipes away and draws itself again (right: from left to right) */
.fx-underline { position: relative; width: fit-content; max-width: 100%; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); }
.fx-underline::after { content: ""; position: absolute; left: 0; right: 0; bottom: -.08em; height: calc(2px * var(--fx-i, 1)); border-radius: 2px; background: var(--fx-c, var(--acc)); pointer-events: none; animation: fx-underline var(--fx-d) cubic-bezier(.65, 0, .35, 1) infinite var(--fx-at) !important; }
@keyframes fx-underline {
  0%, 56% { clip-path: inset(0 0 0 0); }
  63% { clip-path: inset(0 calc(50% - 50% * var(--fx-x, 1)) 0 calc(50% + 50% * var(--fx-x, 1))); }
  63.5% { clip-path: inset(0 calc(50% + 50% * var(--fx-x, 1)) 0 calc(50% - 50% * var(--fx-x, 1))); }
  80%, 100% { clip-path: inset(0 0 0 0); }
}

/* glow-pulse: a soft glow in the brand colour that follows the element's own shape (letters too) swells and fades */
.fx-glow-pulse { --fx-d: calc(2.4s / var(--fx-s, 1)); --fx-at: calc(-1 * var(--fx-d)); animation: fx-glow-pulse var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite var(--fx-at) !important; }
.fx-glow-pulse.fx-once { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-at: calc((var(--fx-lag, 0) - 1) * var(--fx-d)); animation-name: fx-glow-pulse-once !important; }
@keyframes fx-glow-pulse {
  0%, 100% { filter: drop-shadow(0 0 0 color-mix(in srgb, var(--fx-c, var(--acc)) 0%, transparent)); }
  50% { filter: drop-shadow(0 0 calc(7px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 60%, transparent)); }
}
@keyframes fx-glow-pulse-once {
  0%, 52% { filter: drop-shadow(0 0 0 color-mix(in srgb, var(--fx-c, var(--acc)) 0%, transparent)); }
  67% { filter: drop-shadow(0 0 calc(7px * var(--fx-i, 1)) color-mix(in srgb, var(--fx-c, var(--acc)) 60%, transparent)); }
  84%, 100% { filter: drop-shadow(0 0 0 color-mix(in srgb, var(--fx-c, var(--acc)) 0%, transparent)); }
}

/* ripple: a thin ring leaves the element's edge and spreads out, fading (once: one ripple per loop) */
.fx-ripple { position: relative; --fx-d: calc(2.4s / var(--fx-s, 1)); --fx-at: calc(-1 * var(--fx-d)); }
.fx-ripple.fx-once { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-at: calc((var(--fx-lag, 0) - 1) * var(--fx-d)); }
.fx-ripple::before { content: ""; box-sizing: border-box; position: absolute; inset: -1px; border-radius: inherit; border: 1.5px solid var(--fx-c, var(--acc)); pointer-events: none; animation: fx-ripple var(--fx-d) cubic-bezier(.22, 1, .36, 1) infinite var(--fx-at) !important; }
.fx-ripple.fx-once::before { animation-name: fx-ripple-once !important; }
@keyframes fx-ripple {
  0% { scale: 1; opacity: 0; }
  8% { scale: 1.02; opacity: .6; }
  72% { scale: calc(1 + .25 * var(--fx-i, 1)); opacity: 0; }
  72.5%, 100% { scale: 1; opacity: 0; }
}
@keyframes fx-ripple-once {
  0%, 54% { scale: 1; opacity: 0; }
  58% { scale: 1.02; opacity: .6; }
  84% { scale: calc(1 + .25 * var(--fx-i, 1)); opacity: 0; }
  84.5%, 100% { scale: 1; opacity: 0; }
}

/* icon-wave: the icons rise and settle one after another, a wave passing through the row */
.fx-icon-wave { --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); --fx-gap: .025; }
.fx-icon-wave > * { animation: fx-icon-wave var(--fx-d) cubic-bezier(.37, 0, .63, 1) infinite calc(var(--fx-at) + var(--fx-k, 0) * var(--fx-gap) * var(--fx-d)) !important; }
@keyframes fx-icon-wave {
  0%, 56% { translate: 0 0; animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
  61% { translate: 0 calc(-4px * var(--fx-i, 1)); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  66% { translate: 0 calc(.8px * var(--fx-i, 1)); }
  70%, 100% { translate: 0 0; }
}

/* sparkle: two tiny four-point stars twinkle at the element's top right corner */
.fx-sparkle { position: relative; --fx-d: calc(4.8s / max(1, var(--fx-s, 1))); }
.fx-sparkle::before, .fx-sparkle::after { content: ""; position: absolute; z-index: 2; pointer-events: none; background: var(--fx-c, var(--acc)); clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%); animation: fx-sparkle var(--fx-d) cubic-bezier(.22, 1, .36, 1) infinite var(--fx-at) !important; }
.fx-sparkle::before { top: -4px; right: -7px; width: calc(11px * var(--fx-i, 1)); height: calc(11px * var(--fx-i, 1)); }
.fx-sparkle::after { top: 9px; right: -13px; width: calc(6px * var(--fx-i, 1)); height: calc(6px * var(--fx-i, 1)); animation-delay: calc(var(--fx-at) + .025 * var(--fx-d)) !important; }
@keyframes fx-sparkle {
  0%, 58% { opacity: 0; scale: .2; rotate: 0deg; }
  64% { opacity: 1; scale: 1; rotate: 45deg; animation-timing-function: cubic-bezier(.55, 0, .75, .3); }
  74%, 100% { opacity: 0; scale: .2; rotate: 0deg; }
}
