/* SPDX-License-Identifier: LicenseRef-PolyForm-Shield-1.0.0 */
/*
 * lsd-buttons.css — universal `.lsd-fx--*` interactive-element animations
 * ---------------------------------------------------------------------------
 * Despite the filename, this file is the home of the LSD universal interactive
 * animation library. The class prefix is `.lsd-fx--<name>` so the same
 * animations can chain onto buttons, inputs, chips, cards, links, or any
 * primitive that opts in to the contract below.
 *
 * CONTRACT for the host element (what your component must already provide):
 *
 *   1. `position: relative` (so absolutely-positioned ::before/::after and
 *      injected `.lsd-fx__ripple` / `.lsd-fx__ink` nodes are scoped).
 *   2. `display: inline-flex` or `inline-block` (so transforms / overflow
 *      hidden / pseudo-element layout works as expected).
 *   3. A baseline `transition` on transform / background / box-shadow / color
 *      (most effects layer onto an existing transition; if none exists they
 *      still work, but feel snappier when the host has one).
 *   4. For animations that use `overflow: hidden` (shimmer, slide-fill,
 *      ripple-click, ink-drop): the host should set `isolation: isolate` so
 *      the pseudo z-index stays scoped.
 *
 * Raw <input> elements don't ship `position: relative` by default — wrap in a
 * `.lsd-input` (or similar) shell, or add the base contract via your own
 * class. The animations themselves are surface-neutral; colors come from the
 * host's `--accent` / `--accent-2` / `--accent-3` token contract.
 *
 * Tokens consumed:   --accent, --accent-2, --accent-3, --ink, --ink-faint
 * Used pseudo elem classes: .lsd-fx__char, .lsd-fx__ripple, .lsd-fx__ink
 */

/* ─── A1 · lift ────────────────────────────────────────────────────── */
.lsd-fx--lift {
  transition:
    transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 260ms cubic-bezier(0.22, 1, 0.36, 1),
    background 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.lsd-fx--lift:hover {
  transform: translateY(-3px);
  box-shadow:
    0 14px 28px -10px color-mix(in oklch, var(--accent) 70%, transparent),
    0 6px 14px -8px color-mix(in oklch, var(--accent-3) 40%, transparent);
}
.lsd-fx--lift:active { transform: translateY(0); transition-duration: 80ms; }

/* ─── A2 · magnetic ───────────────────────────────────────────────── */
.lsd-fx--magnetic {
  --mx: 0px; --my: 0px;
  transform: translate(var(--mx), var(--my));
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1),
              background 180ms cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.lsd-fx--magnetic:hover {
  box-shadow: 0 18px 32px -12px color-mix(in oklch, var(--accent) 70%, transparent);
}

/* ─── A3 · shimmer ────────────────────────────────────────────────── */
.lsd-fx--shimmer {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--shimmer::before {
  content: '';
  position: absolute; top: 0; bottom: 0; left: -120%;
  width: 70%;
  background: linear-gradient(115deg,
    transparent 30%,
    color-mix(in oklch, white 70%, transparent) 50%,
    transparent 70%);
  transform: skewX(-18deg);
  transition: left 700ms cubic-bezier(0.22, 1, 0.36, 1);
  z-index: 0;
  pointer-events: none;
}
.lsd-fx--shimmer:hover::before { left: 130%; }
.lsd-fx--shimmer > * { position: relative; z-index: 1; }

/* ─── A4 · squish ─────────────────────────────────────────────────── */
.lsd-fx--squish {
  transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.lsd-fx--squish:active {
  transform: scaleY(0.78) scaleX(1.08);
  transition-duration: 90ms;
  transition-timing-function: ease-out;
}

/* ─── A5 · pop ────────────────────────────────────────────────────── */
.lsd-fx--pop {
  transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1),
              box-shadow 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.lsd-fx--pop:hover { transform: scale(1.08); }
.lsd-fx--pop:active { transform: scale(0.96); transition-duration: 90ms; }

/* ─── A6 · wobble ─────────────────────────────────────────────────── */
@keyframes lsd-fx-wobble {
  0%   { transform: rotate(0deg) translateX(0); }
  25%  { transform: rotate(-3deg) translateX(-2px); }
  55%  { transform: rotate(3deg) translateX(2px); }
  80%  { transform: rotate(-1.5deg) translateX(-1px); }
  100% { transform: rotate(0deg) translateX(0); }
}
.lsd-fx--wobble:hover {
  animation: lsd-fx-wobble 520ms cubic-bezier(0.36, 1.4, 0.5, 1);
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--wobble:hover { animation: none; }
}

/* ─── A7 · tilt ───────────────────────────────────────────────────── */
.lsd-fx--tilt {
  --rx: 0deg; --ry: 0deg;
  transform-style: preserve-3d;
  transform: perspective(420px) rotateX(var(--rx)) rotateY(var(--ry));
  transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* ─── A8 · slide-fill ─────────────────────────────────────────────── */
.lsd-fx--slide-fill {
  position: relative; overflow: hidden; isolation: isolate;
  transition: color 280ms;
}
.lsd-fx--slide-fill::before {
  content: ''; position: absolute; inset: 0;
  background: var(--accent);
  transform: translateY(101%);
  transition: transform 360ms cubic-bezier(0.65, 0, 0.35, 1);
  z-index: -1;
  pointer-events: none;
}
.lsd-fx--slide-fill:hover { color: #0a0716; }
.lsd-fx--slide-fill:hover::before { transform: translateY(0); }

/* ─── A9 · border-draw ────────────────────────────────────────────── */
.lsd-fx--border-draw {
  position: relative;
  --bd-prog: 0deg;
}
.lsd-fx--border-draw::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  background: conic-gradient(from 270deg,
    var(--accent) 0deg,
    var(--accent) var(--bd-prog),
    color-mix(in oklch, var(--ink) 14%, transparent) var(--bd-prog),
    color-mix(in oklch, var(--ink) 14%, transparent) 360deg);
  padding: 2px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  transition: --bd-prog 600ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
@property --bd-prog {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
.lsd-fx--border-draw:hover,
.lsd-fx--border-draw:focus,
.lsd-fx--border-draw:focus-within { --bd-prog: 360deg; }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--border-draw::before { transition: none; }
}

/* ─── A10 · underline-grow ────────────────────────────────────────── */
.lsd-fx--underline-grow {
  position: relative;
}
.lsd-fx--underline-grow::after {
  content: ''; position: absolute;
  left: 12%; right: 12%;
  bottom: 6px; height: 2px;
  background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
.lsd-fx--underline-grow:hover::after,
.lsd-fx--underline-grow:focus::after,
.lsd-fx--underline-grow:focus-within::after { transform: scaleX(1); }

/* ─── A13 · letter-stagger ────────────────────────────────────────── */
/* JS splits the text into `.lsd-fx__char` spans with --lsd-stagger-index. */
.lsd-fx--letter-stagger .lsd-fx__char {
  display: inline-block;
  transition: transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
  transition-delay: calc(var(--lsd-stagger-index, 0) * 22ms);
}
.lsd-fx--letter-stagger:hover .lsd-fx__char,
.lsd-fx--letter-stagger:focus .lsd-fx__char,
.lsd-fx--letter-stagger:focus-within .lsd-fx__char {
  transform: translateY(-4px);
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--letter-stagger:hover .lsd-fx__char,
  .lsd-fx--letter-stagger:focus .lsd-fx__char,
  .lsd-fx--letter-stagger:focus-within .lsd-fx__char { transform: none; }
}

/* ─── A14 · letter-shuffle ────────────────────────────────────────── */
.lsd-fx--letter-shuffle { font-variant-numeric: tabular-nums; }

/* ─── A15 · glitch ────────────────────────────────────────────────── */
/* The host needs an inner element (e.g. `<span class="lsd-fx__text"
   data-text="WORD">WORD</span>`) carrying the text + `data-text`. */
.lsd-fx--glitch {
  position: relative; isolation: isolate;
}
.lsd-fx--glitch .lsd-fx__text {
  position: relative; display: inline-block;
}
.lsd-fx--glitch:hover .lsd-fx__text::before,
.lsd-fx--glitch:hover .lsd-fx__text::after,
.lsd-fx--glitch.is-active .lsd-fx__text::before,
.lsd-fx--glitch.is-active .lsd-fx__text::after {
  content: attr(data-text);
  position: absolute; left: 0; top: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}
.lsd-fx--glitch:hover .lsd-fx__text::before,
.lsd-fx--glitch.is-active .lsd-fx__text::before {
  color: var(--accent-2);
  animation: lsd-fx-glitch-a 400ms steps(2) infinite;
}
.lsd-fx--glitch:hover .lsd-fx__text::after,
.lsd-fx--glitch.is-active .lsd-fx__text::after {
  color: var(--accent-3);
  animation: lsd-fx-glitch-b 400ms steps(2) infinite;
}
@keyframes lsd-fx-glitch-a {
  0%   { transform: translate(0, 0); clip-path: inset(0 0 60% 0); }
  50%  { transform: translate(-2px, 1px); clip-path: inset(40% 0 20% 0); }
  100% { transform: translate(1px, -1px); clip-path: inset(70% 0 5% 0); }
}
@keyframes lsd-fx-glitch-b {
  0%   { transform: translate(0, 0); clip-path: inset(60% 0 0 0); }
  50%  { transform: translate(2px, -1px); clip-path: inset(20% 0 40% 0); }
  100% { transform: translate(-1px, 1px); clip-path: inset(5% 0 70% 0); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--glitch:hover .lsd-fx__text::before,
  .lsd-fx--glitch:hover .lsd-fx__text::after,
  .lsd-fx--glitch.is-active .lsd-fx__text::before,
  .lsd-fx--glitch.is-active .lsd-fx__text::after { animation: none; }
}

/* ─── A16 · ripple-click ──────────────────────────────────────────── */
.lsd-fx--ripple-click {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx__ripple {
  position: absolute; border-radius: 50%;
  background: color-mix(in oklch, white 50%, transparent);
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0);
  opacity: 0.55;
  animation: lsd-fx-ripple 600ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  z-index: 0;
}
.lsd-fx--ripple-click > *:not(.lsd-fx__ripple) { position: relative; z-index: 1; }
@keyframes lsd-fx-ripple {
  to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

/* ─── A17 · glow-pulse ────────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .lsd-fx--glow-pulse {
    animation: lsd-fx-glow-pulse 2.4s ease-in-out infinite;
  }
}
@keyframes lsd-fx-glow-pulse {
  0%, 100% {
    box-shadow:
      0 0 0 0 color-mix(in oklch, var(--accent) 0%, transparent),
      0 4px 14px -4px color-mix(in oklch, var(--accent) 50%, transparent);
  }
  50% {
    box-shadow:
      0 0 0 8px color-mix(in oklch, var(--accent) 0%, transparent),
      0 0 24px 4px color-mix(in oklch, var(--accent) 55%, transparent);
  }
}

/* ─── A19 · goo-morph ─────────────────────────────────────────────── */
/* Host page must mount the SVG `#lsd-goo` filter once (see lsd-buttons demo). */
.lsd-fx--goo-morph {
  position: relative; isolation: isolate;
  filter: url(#lsd-goo);
}
.lsd-fx--goo-morph::before,
.lsd-fx--goo-morph::after {
  content: ''; position: absolute;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent);
  z-index: -1;
  transition: transform 480ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
.lsd-fx--goo-morph::before { left: 4px; top: 50%; transform: translate(0, -50%) scale(0); }
.lsd-fx--goo-morph::after  { right: 4px; top: 50%; transform: translate(0, -50%) scale(0); }
.lsd-fx--goo-morph:hover::before { transform: translate(-18px, -50%) scale(1.4); }
.lsd-fx--goo-morph:hover::after  { transform: translate(18px, -50%) scale(1.4); }

/* ─── A20 · ink-drop ──────────────────────────────────────────────── */
.lsd-fx--ink-drop {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx__ink {
  position: absolute; border-radius: 50%;
  background: var(--accent-3);
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0);
  animation: lsd-fx-ink 900ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  z-index: 0;
  opacity: 0.85;
}
.lsd-fx--ink-drop > *:not(.lsd-fx__ink) { position: relative; z-index: 1; }
@keyframes lsd-fx-ink {
  to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}

/* ═══════════════════════════════════════════════════════════════════
   Entrance animations — `.lsd-fx--<name>` gated by `.lsd-scroll.lsd-in-view`
   -------------------------------------------------------------------
   These play once when the host element (which must also carry `.lsd-scroll`)
   crosses the viewport threshold. `lsd-scroll-trigger.js` adds `.lsd-in-view`
   on intersection; add `.lsd-scroll-once` to prevent re-trigger on scroll-out.
   The library is intentionally CSS-only — same vocabulary as
   `lsd-motion/_fx.scss` (which uses the `.lsd-fx--in` toggle for its own
   transition-driven engine). Use whichever fits the project. This file's
   set chains onto images, frames, cards, anything.
   ═════════════════════════════════════════════════════════════════ */

[class*="lsd-fx--"].lsd-scroll {
  --lsd-entr-duration: 700ms;
  --lsd-entr-delay:    0ms;
  --lsd-entr-ease:     cubic-bezier(0.22, 1, 0.36, 1);
  --lsd-entr-distance: 28px;
}

/* Shared transition wiring for entrance presets. */
.lsd-fx--fade-in.lsd-scroll,
.lsd-fx--slide-up.lsd-scroll,
.lsd-fx--slide-down.lsd-scroll,
.lsd-fx--slide-left.lsd-scroll,
.lsd-fx--slide-right.lsd-scroll,
.lsd-fx--scale-in.lsd-scroll,
.lsd-fx--scale-out.lsd-scroll,
.lsd-fx--blur-in.lsd-scroll,
.lsd-fx--mask-up.lsd-scroll,
.lsd-fx--mask-iris.lsd-scroll,
.lsd-fx--rotate-in.lsd-scroll,
.lsd-fx--flip-in.lsd-scroll,
.lsd-fx--curtain.lsd-scroll,
.lsd-fx--unfold.lsd-scroll {
  transition:
    opacity   var(--lsd-entr-duration) var(--lsd-entr-ease) var(--lsd-entr-delay),
    transform var(--lsd-entr-duration) var(--lsd-entr-ease) var(--lsd-entr-delay),
    filter    var(--lsd-entr-duration) var(--lsd-entr-ease) var(--lsd-entr-delay),
    clip-path var(--lsd-entr-duration) var(--lsd-entr-ease) var(--lsd-entr-delay);
  will-change: opacity, transform, filter, clip-path;
}

/* ── E1 · fade-in ─────────────────────────────────────────────── */
.lsd-fx--fade-in.lsd-scroll                 { opacity: 0; }
.lsd-fx--fade-in.lsd-scroll.lsd-in-view     { opacity: 1; }

/* ── E2 · slide-up / down / left / right ──────────────────────── */
.lsd-fx--slide-up.lsd-scroll                { opacity: 0; transform: translateY(var(--lsd-entr-distance)); }
.lsd-fx--slide-up.lsd-scroll.lsd-in-view    { opacity: 1; transform: none; }

.lsd-fx--slide-down.lsd-scroll              { opacity: 0; transform: translateY(calc(var(--lsd-entr-distance) * -1)); }
.lsd-fx--slide-down.lsd-scroll.lsd-in-view  { opacity: 1; transform: none; }

.lsd-fx--slide-left.lsd-scroll              { opacity: 0; transform: translateX(var(--lsd-entr-distance)); }
.lsd-fx--slide-left.lsd-scroll.lsd-in-view  { opacity: 1; transform: none; }

.lsd-fx--slide-right.lsd-scroll             { opacity: 0; transform: translateX(calc(var(--lsd-entr-distance) * -1)); }
.lsd-fx--slide-right.lsd-scroll.lsd-in-view { opacity: 1; transform: none; }

/* ── E3 · scale-in (zoom up) ──────────────────────────────────── */
.lsd-fx--scale-in.lsd-scroll                { opacity: 0; transform: scale(0.92); }
.lsd-fx--scale-in.lsd-scroll.lsd-in-view    { opacity: 1; transform: none; }

/* ── E4 · scale-out (zoom down) ───────────────────────────────── */
.lsd-fx--scale-out.lsd-scroll               { opacity: 0; transform: scale(1.08); }
.lsd-fx--scale-out.lsd-scroll.lsd-in-view   { opacity: 1; transform: none; }

/* ── E5 · blur-in ─────────────────────────────────────────────── */
.lsd-fx--blur-in.lsd-scroll                 { opacity: 0; filter: blur(20px); }
.lsd-fx--blur-in.lsd-scroll.lsd-in-view     { opacity: 1; filter: blur(0); }

/* ── E6 · mask-up (clip-path inset wipe from bottom) ──────────── */
.lsd-fx--mask-up.lsd-scroll                 { clip-path: inset(100% 0 0 0); }
.lsd-fx--mask-up.lsd-scroll.lsd-in-view     { clip-path: inset(0 0 0 0); }

/* ── E7 · mask-iris (circular reveal from center) ─────────────── */
.lsd-fx--mask-iris.lsd-scroll               { clip-path: circle(0% at 50% 50%); }
.lsd-fx--mask-iris.lsd-scroll.lsd-in-view   { clip-path: circle(75% at 50% 50%); }

/* ── E8 · rotate-in ───────────────────────────────────────────── */
.lsd-fx--rotate-in.lsd-scroll               { opacity: 0; transform: rotate(-8deg) translateY(12px); }
.lsd-fx--rotate-in.lsd-scroll.lsd-in-view   { opacity: 1; transform: none; }

/* ── E9 · flip-in (3D Y-axis) ─────────────────────────────────── */
.lsd-fx--flip-in.lsd-scroll                 {
  opacity: 0;
  transform: perspective(800px) rotateY(90deg);
  transform-origin: center;
}
.lsd-fx--flip-in.lsd-scroll.lsd-in-view     { opacity: 1; transform: perspective(800px) rotateY(0deg); }

/* ── E10 · curtain (scaleY from top) ──────────────────────────── */
.lsd-fx--curtain.lsd-scroll                 { transform: scaleY(0); transform-origin: top; }
.lsd-fx--curtain.lsd-scroll.lsd-in-view     { transform: scaleY(1); }

/* ── E11 · unfold (split-reveal: clip-path expanding from center) ─ */
.lsd-fx--unfold.lsd-scroll                  { clip-path: inset(50% 0 50% 0); }
.lsd-fx--unfold.lsd-scroll.lsd-in-view      { clip-path: inset(0 0 0 0); }

/* ── E12 · stagger-children — parent class. Children inherit
       --lsd-stagger-index from lsd-scroll-trigger or manual style.
       Each child's --lsd-entr-delay = index × 60ms. Combine with any
       entrance preset on the children. ─────────────────────────── */
.lsd-fx--stagger-children > * {
  --lsd-entr-delay: calc(var(--lsd-stagger-index, 0) * 60ms);
}
.lsd-fx--stagger-children[data-stagger-step="40"] > *  { --lsd-entr-delay: calc(var(--lsd-stagger-index, 0) * 40ms); }
.lsd-fx--stagger-children[data-stagger-step="80"] > *  { --lsd-entr-delay: calc(var(--lsd-stagger-index, 0) * 80ms); }
.lsd-fx--stagger-children[data-stagger-step="120"] > * { --lsd-entr-delay: calc(var(--lsd-stagger-index, 0) * 120ms); }

/* ── Reduced motion — snap to end state. ──────────────────────── */
@media (prefers-reduced-motion: reduce) {
  [class*="lsd-fx--"].lsd-scroll {
    transition: none !important;
    opacity:    1 !important;
    transform:  none !important;
    filter:     none !important;
    clip-path:  none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   Effect library — extension set (M1..M17)
   -------------------------------------------------------------------
   These extend the .lsd-fx--* vocabulary with ambient / particle /
   text / cursor effects. All values use LSD tokens (--accent,
   --accent-2, --accent-3, --bg, --ink). prefers-reduced-motion is
   honoured on every continuous animation. Helpers that need JS to
   spawn DOM nodes are described in the doc-comment above each block.
   ═════════════════════════════════════════════════════════════════ */

/* ─── M1 · warp-background ─ ambient skew/scale drift on a backdrop. */
.lsd-fx--warp-background {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--warp-background::before {
  content: ''; position: absolute; inset: -20%;
  background:
    radial-gradient(ellipse at 30% 30%, color-mix(in oklch, var(--accent) 35%, transparent), transparent 60%),
    radial-gradient(ellipse at 70% 70%, color-mix(in oklch, var(--accent-3) 30%, transparent), transparent 60%);
  filter: blur(40px);
  animation: lsd-fx-warp 14s ease-in-out infinite alternate;
  z-index: -1; pointer-events: none;
}
@keyframes lsd-fx-warp {
  0%   { transform: scale(1) skew(0deg, 0deg) translate(0, 0); }
  50%  { transform: scale(1.15) skew(4deg, -3deg) translate(2%, -1%); }
  100% { transform: scale(1.05) skew(-3deg, 4deg) translate(-2%, 1%); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--warp-background::before { animation: none; }
}

/* ─── M2 · line-shadow-text ─ moving offset shadow on text. */
.lsd-fx--line-shadow-text {
  --lsd-line-shadow-color: var(--accent-2);
  display: inline-block;
  animation: lsd-fx-line-shadow 3.6s linear infinite;
}
@keyframes lsd-fx-line-shadow {
  0%   { text-shadow: 0px 0px 0 var(--lsd-line-shadow-color); }
  50%  { text-shadow: -8px 8px 0 var(--lsd-line-shadow-color); }
  100% { text-shadow: 0px 0px 0 var(--lsd-line-shadow-color); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--line-shadow-text { animation: none; text-shadow: -4px 4px 0 var(--lsd-line-shadow-color); }
}

/* ─── M3 · meteors ─ diagonal particle shower across a positioned host.
   JS helper spawns N `.lsd-fx__meteor` spans inside the host. Each
   inherits its delay/duration via inline custom props (--m-delay/--m-dur). */
.lsd-fx--meteors {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx__meteor {
  position: absolute; top: -10%;
  width: 2px; height: 2px;
  background: color-mix(in oklch, white 90%, transparent);
  border-radius: 50%;
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent) 50%, transparent);
  pointer-events: none;
  animation: lsd-fx-meteor var(--m-dur, 4s) linear var(--m-delay, 0s) infinite;
}
.lsd-fx__meteor::after {
  content: ''; position: absolute; top: 50%; right: 0;
  width: 90px; height: 1px; transform: translateY(-50%);
  background: linear-gradient(90deg, color-mix(in oklch, var(--accent) 70%, transparent), transparent);
}
@keyframes lsd-fx-meteor {
  0%   { transform: translate3d(0, 0, 0) rotate(225deg); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translate3d(-700px, 700px, 0) rotate(225deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx__meteor { animation: none; opacity: 0; }
}

/* ─── M4 · flickering-grid ─ generative pixel-grid background. */
.lsd-fx--flickering-grid {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--flickering-grid::before {
  content: ''; position: absolute; inset: 0;
  background-image:
    linear-gradient(to right, color-mix(in oklch, var(--accent) 22%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklch, var(--accent) 22%, transparent) 1px, transparent 1px);
  background-size: 24px 24px;
  animation: lsd-fx-flicker 4s steps(8) infinite;
  pointer-events: none; z-index: -1;
}
@keyframes lsd-fx-flicker {
  0%, 100% { opacity: 0.6; }
  20%      { opacity: 0.85; }
  40%      { opacity: 0.4; }
  60%      { opacity: 0.95; }
  80%      { opacity: 0.55; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--flickering-grid::before { animation: none; opacity: 0.7; }
}

/* ─── M5 · particles ─ ambient particle background.
   JS spawns N `.lsd-fx__particle` spans with --p-x/--p-y/--p-delay. */
.lsd-fx--particles {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx__particle {
  position: absolute;
  left: var(--p-x, 50%); top: var(--p-y, 50%);
  width: 3px; height: 3px;
  background: color-mix(in oklch, var(--accent) 80%, transparent);
  border-radius: 50%;
  pointer-events: none;
  animation: lsd-fx-particle 6s ease-in-out var(--p-delay, 0s) infinite;
}
@keyframes lsd-fx-particle {
  0%, 100% { transform: translate(0, 0); opacity: 0; }
  50%      { transform: translate(0, -30px); opacity: 0.8; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx__particle { animation: none; opacity: 0.4; }
}

/* ─── M6 · retro-grid ─ synthwave perspective grid background. */
.lsd-fx--retro-grid {
  position: relative; overflow: hidden; isolation: isolate;
  perspective: 200px;
}
.lsd-fx--retro-grid::before {
  content: ''; position: absolute; inset: 0;
  background-image:
    linear-gradient(to right, color-mix(in oklch, var(--accent) 50%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklch, var(--accent) 50%, transparent) 1px, transparent 1px);
  background-size: 48px 48px;
  transform: rotateX(60deg) translateY(-10%);
  transform-origin: center top;
  animation: lsd-fx-retro 14s linear infinite;
  pointer-events: none; z-index: -1;
  height: 200%;
}
@keyframes lsd-fx-retro {
  0%   { background-position: 0 0; }
  100% { background-position: 0 48px; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--retro-grid::before { animation: none; }
}

/* ─── M7 · light-rays ─ radial sweep of rays from top center. */
.lsd-fx--light-rays {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--light-rays::before {
  content: ''; position: absolute; left: 50%; top: -50%;
  width: 200%; aspect-ratio: 1;
  transform: translateX(-50%);
  background: conic-gradient(from 0deg,
    transparent 0deg,
    color-mix(in oklch, var(--accent) 35%, transparent) 4deg,
    transparent 8deg,
    transparent 22deg,
    color-mix(in oklch, var(--accent-2) 28%, transparent) 26deg,
    transparent 30deg,
    transparent 360deg);
  filter: blur(8px); opacity: 0.6;
  animation: lsd-fx-light-rays 18s linear infinite;
  pointer-events: none; z-index: -1;
}
@keyframes lsd-fx-light-rays {
  to { transform: translateX(-50%) rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--light-rays::before { animation: none; }
}

/* ─── M8 · confetti ─ click-spawned burst of `.lsd-fx__confetti` pieces.
   JS appends absolutely-positioned span shards at the click point. */
.lsd-fx--confetti { position: relative; }
.lsd-fx__confetti {
  position: absolute;
  width: 8px; height: 14px;
  background: var(--c, var(--accent));
  pointer-events: none;
  animation: lsd-fx-confetti 900ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  border-radius: 1px;
}
@keyframes lsd-fx-confetti {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--cx, 60px), var(--cy, 120px)) rotate(var(--cr, 540deg)); opacity: 0; }
}

/* ─── M9 · sparkles-text ─ random sparkle dots around text bounds.
   JS spawns `.lsd-fx__sparkle` spans within the host. */
.lsd-fx--sparkles-text { position: relative; display: inline-block; }
.lsd-fx__sparkle {
  position: absolute;
  left: var(--s-x, 50%); top: var(--s-y, 50%);
  width: 6px; height: 6px; pointer-events: none;
  background: radial-gradient(circle, var(--accent) 30%, transparent 70%);
  border-radius: 50%;
  animation: lsd-fx-sparkle 2s ease-in-out var(--s-delay, 0s) infinite;
}
@keyframes lsd-fx-sparkle {
  0%, 100% { transform: scale(0); opacity: 0; }
  50%      { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx__sparkle { animation: none; opacity: 0.5; transform: scale(0.8); }
}

/* ─── M10 · comic-text ─ comic-book emphasis bounce on hover/in-view. */
.lsd-fx--comic-text {
  display: inline-block;
  font-weight: 900;
  letter-spacing: 0.02em;
  text-shadow:
    2px 2px 0 var(--accent-2),
    4px 4px 0 color-mix(in oklch, var(--accent-3) 70%, transparent);
  animation: lsd-fx-comic 2.4s cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}
@keyframes lsd-fx-comic {
  0%, 100% { transform: rotate(-2deg) scale(1); }
  50%      { transform: rotate(2deg)  scale(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--comic-text { animation: none; }
}

/* ─── M11 · animated-beam ─ SVG path with animated stroke-dashoffset.
   Host markup: <svg class="lsd-fx--animated-beam"><path d="..."/></svg>
   The path inherits stroke from --accent. */
.lsd-fx--animated-beam {
  overflow: visible;
}
.lsd-fx--animated-beam path,
.lsd-fx--animated-beam line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 8 14;
  stroke-dashoffset: 0;
  animation: lsd-fx-beam 1.6s linear infinite;
  filter: drop-shadow(0 0 6px color-mix(in oklch, var(--accent) 70%, transparent));
}
@keyframes lsd-fx-beam {
  to { stroke-dashoffset: -44; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--animated-beam path,
  .lsd-fx--animated-beam line { animation: none; }
}

/* ─── M12 · grid-beams ─ vertical beams sweeping across a grid. */
.lsd-fx--grid-beams {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--grid-beams::before {
  content: ''; position: absolute; inset: 0;
  background-image:
    linear-gradient(to right, color-mix(in oklch, var(--accent) 18%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklch, var(--accent) 18%, transparent) 1px, transparent 1px);
  background-size: 40px 40px;
  pointer-events: none; z-index: -2;
}
.lsd-fx--grid-beams::after {
  content: ''; position: absolute; inset: 0;
  background:
    linear-gradient(180deg,
      transparent 0%,
      color-mix(in oklch, var(--accent) 55%, transparent) 50%,
      transparent 100%);
  width: 2px; left: 30%;
  filter: blur(2px);
  animation: lsd-fx-grid-beam 4s ease-in-out infinite;
  pointer-events: none; z-index: -1;
}
@keyframes lsd-fx-grid-beam {
  0%, 100% { transform: translateY(-100%); opacity: 0; }
  50%      { transform: translateY(0%);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--grid-beams::after { animation: none; opacity: 0.4; transform: translateY(0); }
}

/* ─── M13 · typing-animation ─ char-by-char reveal driven by lsd-split + CSS.
   Add to a text element; the JS helper in lsd-split.js wraps chars in
   `.lsd-fx__char` and exposes `--lsd-stagger-index`. */
.lsd-fx--typing-animation .lsd-fx__char {
  opacity: 0;
  animation: lsd-fx-type 40ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(var(--lsd-stagger-index, 0) * 50ms);
}
@keyframes lsd-fx-type {
  to { opacity: 1; }
}
.lsd-fx--typing-animation::after {
  content: '|';
  display: inline-block; margin-inline-start: 2px;
  color: var(--accent);
  animation: lsd-fx-caret 1s steps(2) infinite;
}
@keyframes lsd-fx-caret {
  50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--typing-animation .lsd-fx__char { opacity: 1; animation: none; }
  .lsd-fx--typing-animation::after { animation: none; }
}

/* ─── M14 · number-ticker ─ alias class for data-lsd-count.
   Author writes <span class="lsd-fx--number-ticker" data-lsd-count="0:1234"></span>.
   lsd-count.js handles the rAF tween; this class is documentary + adds
   tabular numerals so the digits don't jitter. */
.lsd-fx--number-ticker {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ─── M15 · pixel-image ─ pixelation effect on hover via image-rendering. */
.lsd-fx--pixel-image img,
img.lsd-fx--pixel-image {
  transition: filter 320ms cubic-bezier(0.22, 1, 0.36, 1),
              image-rendering 0s;
}
.lsd-fx--pixel-image:hover img,
img.lsd-fx--pixel-image:hover {
  filter: contrast(1.4) saturate(1.2) blur(0.5px);
  image-rendering: pixelated;
  transform: scale(1.02);
}

/* ─── M16 · scratch-to-reveal ─ click-driven mask-reveal of the host.
   JS adds `.is-scratched` on pointer-down; CSS animates the mask away. */
.lsd-fx--scratch-to-reveal {
  position: relative; overflow: hidden; isolation: isolate;
  cursor: crosshair;
}
.lsd-fx--scratch-to-reveal::before {
  content: ''; position: absolute; inset: 0;
  background:
    repeating-linear-gradient(45deg,
      color-mix(in oklch, var(--accent-2) 60%, transparent) 0 6px,
      color-mix(in oklch, var(--accent-3) 60%, transparent) 6px 12px);
  transition: clip-path 700ms cubic-bezier(0.22, 1, 0.36, 1), opacity 700ms;
  clip-path: circle(80% at 50% 50%);
  pointer-events: none; z-index: 1;
}
.lsd-fx--scratch-to-reveal.is-scratched::before {
  clip-path: circle(0% at 50% 50%); opacity: 0;
}

/* ─── M17 · cool-mode ─ click-fling particle puff.
   JS spawns `.lsd-fx__fling` shards on pointer-down. */
.lsd-fx--cool-mode { position: relative; }
.lsd-fx__fling {
  position: absolute;
  width: 10px; height: 10px;
  background: var(--accent);
  border-radius: 50%;
  pointer-events: none;
  animation: lsd-fx-fling 700ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes lsd-fx-fling {
  0%   { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(var(--fx, 80px), var(--fy, -80px)) scale(0); opacity: 0; }
}

/* ─── Static pattern primitives ──────────────────────────────────────
   Decorative background-image surfaces. Chainable on any element.
   Tokens drive color via --line / --accent / --ink-faint.
   Each primitive ships a tone modifier `.lsd-fx--accent` that
   shifts the line color to var(--accent). */

/* M18 · grid-pattern — repeating subtle grid lines */
.lsd-fx--grid-pattern {
  --grid-cell-size: 32px;
  --grid-line-color: color-mix(in oklch, var(--line, currentColor) 70%, transparent);
  background-image:
    linear-gradient(to right, var(--grid-line-color) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line-color) 1px, transparent 1px);
  background-size: var(--grid-cell-size) var(--grid-cell-size);
}
.lsd-fx--grid-pattern.lsd-fx--accent {
  --grid-line-color: color-mix(in oklch, var(--accent) 40%, transparent);
}

/* M19 · dot-pattern — repeating dot field */
.lsd-fx--dot-pattern {
  --dot-cell-size: 24px;
  --dot-radius: 1px;
  --dot-color: color-mix(in oklch, var(--line, currentColor) 80%, transparent);
  background-image: radial-gradient(
    circle at center,
    var(--dot-color) var(--dot-radius),
    transparent calc(var(--dot-radius) + 0.5px)
  );
  background-size: var(--dot-cell-size) var(--dot-cell-size);
}
.lsd-fx--dot-pattern.lsd-fx--accent {
  --dot-color: color-mix(in oklch, var(--accent) 60%, transparent);
}

/* M20 · stripes — diagonal repeating gradient */
.lsd-fx--stripes {
  --stripe-angle: 45deg;
  --stripe-width: 12px;
  --stripe-a: color-mix(in oklch, var(--line, currentColor) 60%, transparent);
  --stripe-b: transparent;
  background-image: repeating-linear-gradient(
    var(--stripe-angle),
    var(--stripe-a) 0,
    var(--stripe-a) var(--stripe-width),
    var(--stripe-b) var(--stripe-width),
    var(--stripe-b) calc(var(--stripe-width) * 2)
  );
}
.lsd-fx--stripes.lsd-fx--accent {
  --stripe-a: color-mix(in oklch, var(--accent) 50%, transparent);
}

/* M21 · hexagons — pure CSS hex grid via stacked angled gradients.
   Uses 60deg conics to suggest a honeycomb without SVG bloat. */
.lsd-fx--hexagons {
  --hex-size: 28px;
  --hex-color: color-mix(in oklch, var(--line, currentColor) 55%, transparent);
  background-image:
    repeating-linear-gradient( 60deg, var(--hex-color) 0 1px, transparent 1px var(--hex-size)),
    repeating-linear-gradient(-60deg, var(--hex-color) 0 1px, transparent 1px var(--hex-size)),
    repeating-linear-gradient(  0deg, var(--hex-color) 0 1px, transparent 1px calc(var(--hex-size) * 1.732));
  background-size: calc(var(--hex-size) * 1.732) var(--hex-size);
}
.lsd-fx--hexagons.lsd-fx--accent {
  --hex-color: color-mix(in oklch, var(--accent) 50%, transparent);
}

/* M22 · noise-texture — SVG feTurbulence as a data-URI background.
   No JS, no performance cost; tiny inline SVG. */
.lsd-fx--noise-texture {
  --noise-opacity: 0.4;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.6 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-repeat: repeat;
  position: relative;
}
.lsd-fx--noise-texture::after { content: ''; position: absolute; inset: 0; opacity: var(--noise-opacity); pointer-events: none; mix-blend-mode: overlay; }

/* M23 · dotted-map — scattered radial-gradient dots suggesting
   continents. Two stacked layers at differing sizes/offsets give
   the "map of dots" silhouette without an SVG. */
.lsd-fx--dotted-map {
  --map-color: color-mix(in oklch, var(--ink-faint, var(--line, currentColor)) 60%, transparent);
  background-image:
    radial-gradient(circle at 20% 35%, var(--map-color) 1.2px, transparent 1.6px),
    radial-gradient(circle at 60% 45%, var(--map-color) 1.2px, transparent 1.6px),
    radial-gradient(circle at 40% 70%, var(--map-color) 1.2px, transparent 1.6px);
  background-size: 22px 22px, 26px 26px, 30px 30px;
  background-position: 0 0, 8px 4px, 16px 10px;
}
.lsd-fx--dotted-map.lsd-fx--accent {
  --map-color: color-mix(in oklch, var(--accent) 50%, transparent);
}

/* M24 · checkerboard — classic repeating square grid */
.lsd-fx--checkerboard {
  --check-size: 24px;
  --check-a: color-mix(in oklch, var(--line, currentColor) 25%, transparent);
  --check-b: transparent;
  background-image:
    linear-gradient(45deg, var(--check-a) 25%, var(--check-b) 25%, var(--check-b) 75%, var(--check-a) 75%),
    linear-gradient(45deg, var(--check-a) 25%, var(--check-b) 25%, var(--check-b) 75%, var(--check-a) 75%);
  background-size: var(--check-size) var(--check-size);
  background-position: 0 0, calc(var(--check-size) / 2) calc(var(--check-size) / 2);
}
.lsd-fx--checkerboard.lsd-fx--accent {
  --check-a: color-mix(in oklch, var(--accent) 25%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════
   Extended universal animations (R1–R10)
   -------------------------------------------------------------------
   Each chainable with any primitive (buttons, inputs, chips, cards,
   frames, images). Reveal classes pair with .lsd-scroll.lsd-scroll-once
   from lsd-scroll-trigger; parallax classes read --lsd-progress from
   any host wearing .lsd-scroll.lsd-scrub; magnetic-line reads
   --lsd-pointer-x from lsd-pointer.js.
   ═════════════════════════════════════════════════════════════════ */

/* ── R1 · reveal-mask-up ─────────────────────────────────────────
   Element revealed by an upward-sliding clip-path mask. Pair with
   `.lsd-scroll.lsd-scroll-once`; lsd-scroll-trigger adds .lsd-in-view
   when the element enters the viewport. */
.lsd-fx--reveal-mask-up.lsd-scroll {
  --lsd-rmask-duration: 900ms;
  --lsd-rmask-ease: cubic-bezier(0.77, 0, 0.175, 1);
  clip-path: inset(100% 0 0 0);
  transition: clip-path var(--lsd-rmask-duration) var(--lsd-rmask-ease);
  will-change: clip-path;
}
.lsd-fx--reveal-mask-up.lsd-scroll.lsd-in-view { clip-path: inset(0 0 0 0); }
.lsd-fx--reveal-mask-up.lsd-fx--accent.lsd-scroll       { background-color: color-mix(in oklch, var(--accent) 15%, transparent); }
.lsd-fx--reveal-mask-up.lsd-fx--accent-2.lsd-scroll     { background-color: color-mix(in oklch, var(--accent-2) 15%, transparent); }
.lsd-fx--reveal-mask-up.lsd-fx--accent-3.lsd-scroll     { background-color: color-mix(in oklch, var(--accent-3) 15%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--reveal-mask-up.lsd-scroll { transition: none; clip-path: inset(0); }
}

/* ── R2 · reveal-mask-iris ───────────────────────────────────────
   Circular iris expanding from center via clip-path circle().
   Same trigger pairing as reveal-mask-up. */
.lsd-fx--reveal-mask-iris.lsd-scroll {
  --lsd-iris-duration: 900ms;
  --lsd-iris-ease: cubic-bezier(0.65, 0, 0.35, 1);
  clip-path: circle(0% at 50% 50%);
  transition: clip-path var(--lsd-iris-duration) var(--lsd-iris-ease);
  will-change: clip-path;
}
.lsd-fx--reveal-mask-iris.lsd-scroll.lsd-in-view {
  clip-path: circle(140% at 50% 50%);
}
.lsd-fx--reveal-mask-iris.lsd-fx--accent.lsd-scroll   { background-color: color-mix(in oklch, var(--accent) 12%, transparent); }
.lsd-fx--reveal-mask-iris.lsd-fx--accent-2.lsd-scroll { background-color: color-mix(in oklch, var(--accent-2) 12%, transparent); }
.lsd-fx--reveal-mask-iris.lsd-fx--accent-3.lsd-scroll { background-color: color-mix(in oklch, var(--accent-3) 12%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--reveal-mask-iris.lsd-scroll { transition: none; clip-path: circle(140% at 50% 50%); }
}

/* ── R3 · curtain-split ──────────────────────────────────────────
   Two halves slide apart revealing content. Implemented as two
   stacked overlays (::before = left, ::after = right). Pair with
   .lsd-scroll.lsd-scroll-once. */
.lsd-fx--curtain-split.lsd-scroll {
  --lsd-curtain-duration: 900ms;
  --lsd-curtain-ease: cubic-bezier(0.77, 0, 0.175, 1);
  --lsd-curtain-color: color-mix(in oklch, var(--ink, currentColor) 12%, transparent);
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.lsd-fx--curtain-split.lsd-scroll::before,
.lsd-fx--curtain-split.lsd-scroll::after {
  content: '';
  position: absolute; top: 0; bottom: 0;
  width: 50%;
  background: var(--lsd-curtain-color);
  z-index: 2; pointer-events: none;
  transition: transform var(--lsd-curtain-duration) var(--lsd-curtain-ease);
  will-change: transform;
}
.lsd-fx--curtain-split.lsd-scroll::before { left: 0;  transform: translateX(0); }
.lsd-fx--curtain-split.lsd-scroll::after  { right: 0; transform: translateX(0); }
.lsd-fx--curtain-split.lsd-scroll.lsd-in-view::before { transform: translateX(-100%); }
.lsd-fx--curtain-split.lsd-scroll.lsd-in-view::after  { transform: translateX( 100%); }
.lsd-fx--curtain-split.lsd-fx--accent.lsd-scroll   { --lsd-curtain-color: color-mix(in oklch, var(--accent)   55%, transparent); }
.lsd-fx--curtain-split.lsd-fx--accent-2.lsd-scroll { --lsd-curtain-color: color-mix(in oklch, var(--accent-2) 55%, transparent); }
.lsd-fx--curtain-split.lsd-fx--accent-3.lsd-scroll { --lsd-curtain-color: color-mix(in oklch, var(--accent-3) 55%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--curtain-split.lsd-scroll::before,
  .lsd-fx--curtain-split.lsd-scroll::after { transition: none; transform: translateX(-100%); }
  .lsd-fx--curtain-split.lsd-scroll::after { transform: translateX(100%); }
}

/* ── R4 · parallax-soft ──────────────────────────────────────────
   Subtle parallax — translates up to 60px against scroll. Host or
   ancestor must carry .lsd-scroll.lsd-scrub (writes --lsd-progress).
   Good for inline images, decorative containers. */
.lsd-fx--parallax-soft {
  --lsd-parallax-amp: 60px;
  translate: 0 calc((var(--lsd-progress, 0) - 0.5) * var(--lsd-parallax-amp) * 2);
  will-change: translate;
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--parallax-soft { translate: 0; }
}

/* ── R5 · parallax-deep ──────────────────────────────────────────
   Deeper parallax — up to 200px. For hero backgrounds, oversized
   imagery, big-scale page-region drift. */
.lsd-fx--parallax-deep {
  --lsd-parallax-amp: 200px;
  translate: 0 calc((var(--lsd-progress, 0) - 0.5) * var(--lsd-parallax-amp) * 2);
  will-change: translate;
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--parallax-deep { translate: 0; }
}

/* ── R6 · rotate-on-hover ────────────────────────────────────────
   Slow 8s rotation that runs only while hovered. Paused at rest so
   it never spins idle. Good for stat-circle backdrops, decorative
   medallions, hover-affordance icons. */
@keyframes lsd-fx-rotate-slow {
  to { transform: rotate(360deg); }
}
.lsd-fx--rotate-on-hover {
  animation: lsd-fx-rotate-slow 8s linear infinite;
  animation-play-state: paused;
}
.lsd-fx--rotate-on-hover:hover,
.lsd-fx--rotate-on-hover:focus-visible { animation-play-state: running; }
.lsd-fx--rotate-on-hover.lsd-fx--accent   { color: var(--accent); }
.lsd-fx--rotate-on-hover.lsd-fx--accent-2 { color: var(--accent-2); }
.lsd-fx--rotate-on-hover.lsd-fx--accent-3 { color: var(--accent-3); }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--rotate-on-hover { animation: none; }
}

/* ── R7 · breathe ────────────────────────────────────────────────
   Continuous gentle scale 1 → 1.04 → 1 over 4s — a "living" feel
   for hero CTAs and standout primary actions. */
@keyframes lsd-fx-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); }
}
.lsd-fx--breathe {
  animation: lsd-fx-breathe 4s ease-in-out infinite;
  transform-origin: center;
}
.lsd-fx--breathe.lsd-fx--accent   { box-shadow: 0 0 0 0 color-mix(in oklch, var(--accent)   55%, transparent); animation: lsd-fx-breathe 4s ease-in-out infinite, lsd-fx-breathe-glow 4s ease-in-out infinite; }
.lsd-fx--breathe.lsd-fx--accent-2 { box-shadow: 0 0 0 0 color-mix(in oklch, var(--accent-2) 55%, transparent); animation: lsd-fx-breathe 4s ease-in-out infinite, lsd-fx-breathe-glow 4s ease-in-out infinite; }
.lsd-fx--breathe.lsd-fx--accent-3 { box-shadow: 0 0 0 0 color-mix(in oklch, var(--accent-3) 55%, transparent); animation: lsd-fx-breathe 4s ease-in-out infinite, lsd-fx-breathe-glow 4s ease-in-out infinite; }
@keyframes lsd-fx-breathe-glow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklch, currentColor 40%, transparent); }
  50%      { box-shadow: 0 0 0 14px color-mix(in oklch, currentColor 0%,  transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--breathe { animation: none; }
}

/* ── R8 · scan-line ──────────────────────────────────────────────
   Animated horizontal scan line traversing the element — CRT /
   terminal aesthetic. Pure CSS via ::before keyframe. */
@keyframes lsd-fx-scan {
  0%   { top: 0%;   opacity: 0; }
  10%  { opacity: 1; }
  90%  { opacity: 1; }
  100% { top: 100%; opacity: 0; }
}
.lsd-fx--scan-line {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  --lsd-scan-color: color-mix(in oklch, var(--accent, currentColor) 90%, white);
}
.lsd-fx--scan-line::before {
  content: '';
  position: absolute; left: 0; right: 0; top: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--lsd-scan-color), transparent);
  box-shadow: 0 0 12px var(--lsd-scan-color);
  pointer-events: none;
  z-index: 2;
  animation: lsd-fx-scan 3.2s linear infinite;
}
.lsd-fx--scan-line.lsd-fx--accent   { --lsd-scan-color: var(--accent); }
.lsd-fx--scan-line.lsd-fx--accent-2 { --lsd-scan-color: var(--accent-2); }
.lsd-fx--scan-line.lsd-fx--accent-3 { --lsd-scan-color: var(--accent-3); }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--scan-line::before { animation: none; opacity: 0; }
}

/* ── R9 · prismatic ──────────────────────────────────────────────
   Slow 12s color cycle through accent → accent-2 → accent-3 → accent.
   Applies to background by default; pair with .lsd-fx--prismatic-border
   to drive border-color instead. */
@keyframes lsd-fx-prismatic {
  0%   { background-color: var(--accent); }
  33%  { background-color: var(--accent-2); }
  66%  { background-color: var(--accent-3); }
  100% { background-color: var(--accent); }
}
@keyframes lsd-fx-prismatic-border {
  0%   { border-color: var(--accent); }
  33%  { border-color: var(--accent-2); }
  66%  { border-color: var(--accent-3); }
  100% { border-color: var(--accent); }
}
.lsd-fx--prismatic {
  animation: lsd-fx-prismatic 12s linear infinite;
}
.lsd-fx--prismatic.lsd-fx--prismatic-border {
  animation: lsd-fx-prismatic-border 12s linear infinite;
}
.lsd-fx--prismatic.lsd-fx--accent   { animation-delay: 0s; }
.lsd-fx--prismatic.lsd-fx--accent-2 { animation-delay: -4s; }
.lsd-fx--prismatic.lsd-fx--accent-3 { animation-delay: -8s; }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--prismatic { animation: none; background-color: var(--accent); }
}

/* ── R10 · magnetic-line ─────────────────────────────────────────
   Invisible underline draws in on hover, anchored at the cursor's
   x-position. Reads --lsd-pointer-x from lsd-pointer.js (0..1 along
   the element width). The line's transform-origin tracks the pointer
   so it grows outward from where the cursor is. */
.lsd-fx--magnetic-line {
  position: relative;
  display: inline-block;
  --lsd-pointer-x: 0;
}
.lsd-fx--magnetic-line::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 2px;
  background: var(--accent, currentColor);
  transform-origin: calc(var(--lsd-pointer-x, 0) * 100%) 50%;
  transform: scaleX(0);
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
.lsd-fx--magnetic-line:hover::after,
.lsd-fx--magnetic-line:focus-visible::after { transform: scaleX(1); }
.lsd-fx--magnetic-line.lsd-fx--accent::after   { background: var(--accent); }
.lsd-fx--magnetic-line.lsd-fx--accent-2::after { background: var(--accent-2); }
.lsd-fx--magnetic-line.lsd-fx--accent-3::after { background: var(--accent-3); }
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--magnetic-line::after { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   Effect library — extension set (M25..M37)
   -------------------------------------------------------------------
   Continues the M-series with backgrounds, text, and border effects
   that the upstream libraries ship and LSD previously didn't cover.
   Same token discipline, same reduced-motion etiquette.
   Pointer-driven items (M33 / M36) read --lsd-pointer-x/y written by
   lsd-pointer.js (host element needs class `lsd-pointer`).
   ═════════════════════════════════════════════════════════════════ */

/* ─── M25 · ripple-bg ─ expanding concentric waves from center. */
.lsd-fx--ripple-bg {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--ripple-bg::before,
.lsd-fx--ripple-bg::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 40px; aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid color-mix(in oklch, var(--accent) 55%, transparent);
  transform: translate(-50%, -50%);
  animation: lsd-fx-ripple-bg 3.6s ease-out infinite;
  pointer-events: none; z-index: -1;
}
.lsd-fx--ripple-bg::after { animation-delay: 1.8s; }
@keyframes lsd-fx-ripple-bg {
  0%   { width: 0;    opacity: 0.9; border-width: 2px; }
  100% { width: 220%; opacity: 0;   border-width: 1px; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--ripple-bg::before,
  .lsd-fx--ripple-bg::after { animation: none; opacity: 0.3; width: 60%; }
}

/* ─── M26 · shooting-stars ─ diagonal streaks with bright head + fading tail.
   JS helper (lsd-fx-spawn.js, optional) spawns N `.lsd-fx__star` spans with
   --s-delay / --s-dur / --s-y. Without JS, two default stars are emitted
   via ::before/::after for a baseline effect. */
.lsd-fx--shooting-stars {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--shooting-stars::before,
.lsd-fx--shooting-stars::after,
.lsd-fx__star {
  content: '';
  position: absolute; top: var(--s-y, 20%); left: -10%;
  width: 110px; height: 1px;
  background: linear-gradient(90deg,
    transparent,
    color-mix(in oklch, white 80%, transparent) 70%,
    var(--accent) 100%);
  filter: drop-shadow(0 0 4px color-mix(in oklch, var(--accent) 80%, transparent));
  animation: lsd-fx-shooting-star var(--s-dur, 3.2s) linear var(--s-delay, 0s) infinite;
  pointer-events: none; z-index: -1;
}
.lsd-fx--shooting-stars::before { top: 22%; --s-delay: 0s;   --s-dur: 3.2s; }
.lsd-fx--shooting-stars::after  { top: 64%; --s-delay: 1.6s; --s-dur: 4.4s; }
@keyframes lsd-fx-shooting-star {
  0%   { transform: translate(0, 0) rotate(20deg); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translate(160%, 60%) rotate(20deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--shooting-stars::before,
  .lsd-fx--shooting-stars::after,
  .lsd-fx__star { animation: none; opacity: 0; }
}

/* ─── M27 · wavy-bg ─ stacked sine waves drifting horizontally.
   Three SVG-shaped layers (via clip-path) move at different speeds; the
   gradient hue shifts so the layers read as separate "currents". */
.lsd-fx--wavy-bg {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--wavy-bg::before,
.lsd-fx--wavy-bg::after {
  content: ''; position: absolute; left: -25%; right: -25%;
  height: 70%; pointer-events: none; z-index: -1;
  background: linear-gradient(90deg,
    color-mix(in oklch, var(--accent)   55%, transparent),
    color-mix(in oklch, var(--accent-3) 55%, transparent));
  filter: blur(24px);
  animation: lsd-fx-wavy 12s ease-in-out infinite alternate;
}
.lsd-fx--wavy-bg::before {
  bottom: -10%;
  clip-path: polygon(0 60%, 12% 40%, 28% 70%, 44% 38%, 60% 66%, 78% 44%, 100% 60%, 100% 100%, 0 100%);
}
.lsd-fx--wavy-bg::after {
  bottom: -25%;
  background: linear-gradient(90deg,
    color-mix(in oklch, var(--accent-2) 55%, transparent),
    color-mix(in oklch, var(--accent)   55%, transparent));
  clip-path: polygon(0 55%, 15% 75%, 35% 45%, 55% 70%, 75% 40%, 92% 68%, 100% 50%, 100% 100%, 0 100%);
  animation-duration: 18s;
  animation-direction: alternate-reverse;
}
@keyframes lsd-fx-wavy {
  0%   { transform: translateX(0) translateY(0); }
  50%  { transform: translateX(-4%) translateY(-6px); }
  100% { transform: translateX(4%) translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--wavy-bg::before,
  .lsd-fx--wavy-bg::after { animation: none; }
}

/* ─── M28 · vortex-bg ─ swirling conic gradient. */
.lsd-fx--vortex-bg {
  position: relative; overflow: hidden; isolation: isolate;
}
.lsd-fx--vortex-bg::before {
  content: ''; position: absolute; inset: -50%;
  background:
    conic-gradient(from 0deg at 50% 50%,
      transparent 0deg,
      color-mix(in oklch, var(--accent)   40%, transparent) 60deg,
      transparent 120deg,
      color-mix(in oklch, var(--accent-2) 40%, transparent) 180deg,
      transparent 240deg,
      color-mix(in oklch, var(--accent-3) 40%, transparent) 300deg,
      transparent 360deg);
  filter: blur(40px);
  animation: lsd-fx-vortex 18s linear infinite;
  pointer-events: none; z-index: -1;
}
@keyframes lsd-fx-vortex {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--vortex-bg::before { animation: none; }
}

/* ─── M29 · shiny-text ─ gradient mask sweep across text on idle loop. */
.lsd-fx--shiny-text {
  display: inline-block;
  background:
    linear-gradient(90deg,
      currentColor 0%,
      currentColor 40%,
      color-mix(in oklch, white 70%, transparent) 50%,
      currentColor 60%,
      currentColor 100%);
  background-size: 220% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  animation: lsd-fx-shiny 3s linear infinite;
}
@keyframes lsd-fx-shiny {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--shiny-text { animation: none; color: inherit; background: none; -webkit-background-clip: initial; background-clip: initial; }
}

/* ─── M30 · animated-gradient-text ─ background-clip text cycling accents.
   Distinct from `.lsd-fx--hover-gradient-text` (hover-only). This one loops
   ambiently across the three accent tokens. */
.lsd-fx--animated-gradient-text {
  display: inline-block;
  background: linear-gradient(90deg,
    var(--accent), var(--accent-2), var(--accent-3), var(--accent));
  background-size: 300% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  animation: lsd-fx-gradient-text 6s linear infinite;
}
@keyframes lsd-fx-gradient-text {
  0%   { background-position: 0% 0; }
  100% { background-position: 300% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--animated-gradient-text { animation: none; background-position: 0 0; }
}

/* ─── M31 · spinning-text ─ text on a circular path via per-letter rotation.
   Author markup: <span class="lsd-fx--spinning-text" data-letters="12">
                    <span>H</span><span>E</span>… </span>
   Each child span is positioned along the orbit by :nth-child rotation;
   the parent rotates as a whole. JS helper (optional) can split text into
   spans; without JS, author writes them manually. Defaults assume up to
   16 letters; --orbit-radius / --orbit-size tune the circle. */
.lsd-fx--spinning-text {
  --orbit-size: 200px;
  --orbit-radius: 90px;
  position: relative;
  display: inline-block;
  width: var(--orbit-size); height: var(--orbit-size);
  animation: lsd-fx-orbit-spin 14s linear infinite;
}
.lsd-fx--spinning-text > * {
  position: absolute; left: 50%; top: 50%;
  transform-origin: 0 0;
  font-size: 0.9em;
  letter-spacing: 0.02em;
}
.lsd-fx--spinning-text > *:nth-child(1)  { transform: rotate(  0deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(2)  { transform: rotate( 22.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(3)  { transform: rotate( 45deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(4)  { transform: rotate( 67.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(5)  { transform: rotate( 90deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(6)  { transform: rotate(112.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(7)  { transform: rotate(135deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(8)  { transform: rotate(157.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(9)  { transform: rotate(180deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(10) { transform: rotate(202.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(11) { transform: rotate(225deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(12) { transform: rotate(247.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(13) { transform: rotate(270deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(14) { transform: rotate(292.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(15) { transform: rotate(315deg) translate(0, calc(var(--orbit-radius) * -1)); }
.lsd-fx--spinning-text > *:nth-child(16) { transform: rotate(337.5deg) translate(0, calc(var(--orbit-radius) * -1)); }
@keyframes lsd-fx-orbit-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--spinning-text { animation: none; }
}

/* ─── M32 · shine-border ─ gradient highlight travels around the border. */
.lsd-fx--shine-border {
  position: relative; isolation: isolate;
  --shine-thickness: 2px;
}
.lsd-fx--shine-border::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  padding: var(--shine-thickness);
  background: conic-gradient(from var(--shine-angle, 0deg),
    transparent 0deg,
    var(--accent) 50deg,
    color-mix(in oklch, var(--accent) 30%, transparent) 100deg,
    transparent 180deg,
    transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: lsd-fx-shine-border 3.6s linear infinite;
  pointer-events: none;
}
@property --shine-angle {
  syntax: '<angle>'; inherits: false; initial-value: 0deg;
}
@keyframes lsd-fx-shine-border {
  to { --shine-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--shine-border::before { animation: none; }
}

/* ─── M33 · glare-card ─ pointer-tracked specular highlight overlay.
   lsd-pointer.js auto-attaches when this class is on the host and writes
   --lsd-pointer-x/y as 0..1. No extra class needed. */
.lsd-fx--glare-card {
  position: relative; overflow: hidden; isolation: isolate;
  --lsd-pointer-x: 0.5; --lsd-pointer-y: 0.5;
}
.lsd-fx--glare-card::after {
  content: ''; position: absolute; inset: 0;
  pointer-events: none;
  background: radial-gradient(
    circle 200px at
      calc(var(--lsd-pointer-x, 0.5) * 100%)
      calc(var(--lsd-pointer-y, 0.5) * 100%),
    color-mix(in oklch, white 35%, transparent) 0%,
    transparent 70%);
  opacity: 0;
  transition: opacity 240ms cubic-bezier(0.22, 1, 0.36, 1);
  mix-blend-mode: overlay;
}
.lsd-fx--glare-card:hover::after,
.lsd-fx--glare-card:focus-within::after { opacity: 1; }

/* ─── M34 · border-beam ─ single bright spark traveling the border edge. */
.lsd-fx--border-beam {
  position: relative; isolation: isolate;
}
.lsd-fx--border-beam::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  padding: 2px;
  background: conic-gradient(from var(--beam-angle, 0deg),
    transparent 0deg,
    transparent 350deg,
    var(--accent) 358deg,
    color-mix(in oklch, white 80%, var(--accent)) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  filter: drop-shadow(0 0 6px color-mix(in oklch, var(--accent) 70%, transparent));
  animation: lsd-fx-border-beam 4s linear infinite;
  pointer-events: none;
}
@property --beam-angle {
  syntax: '<angle>'; inherits: false; initial-value: 0deg;
}
@keyframes lsd-fx-border-beam {
  to { --beam-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--border-beam::before { animation: none; }
}

/* ─── M35 · moving-border ─ perpetual conic-gradient border rotation. */
.lsd-fx--moving-border {
  position: relative; isolation: isolate;
}
.lsd-fx--moving-border::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(from var(--mb-angle, 0deg),
    var(--accent),
    var(--accent-2),
    var(--accent-3),
    var(--accent));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: lsd-fx-moving-border 6s linear infinite;
  pointer-events: none;
}
@property --mb-angle {
  syntax: '<angle>'; inherits: false; initial-value: 0deg;
}
@keyframes lsd-fx-moving-border {
  to { --mb-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--moving-border::before { animation: none; }
}

/* ─── M36 · cursor-border-gradient ─ pointer-following radial gradient on
   the border. Distinct from .lsd-fx--hover-border-draw (fixed progression).
   lsd-pointer.js auto-attaches when this class is on the host. */
.lsd-fx--cursor-border-gradient {
  position: relative; isolation: isolate;
  --lsd-pointer-x: 0.5; --lsd-pointer-y: 0.5;
}
.lsd-fx--cursor-border-gradient::before {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: radial-gradient(
    circle 120px at
      calc(var(--lsd-pointer-x, 0.5) * 100%)
      calc(var(--lsd-pointer-y, 0.5) * 100%),
    var(--accent),
    transparent 70%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity 220ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
.lsd-fx--cursor-border-gradient:hover::before,
.lsd-fx--cursor-border-gradient:focus-within::before { opacity: 1; }

/* ─── M37 · aurora-bg ─ multi-blob blurred mesh aurora background.
   Visual variant of an aurora; distinct from the lsd-aurora.js timeline
   engine. Three drifting blobs in accent hues create the "northern lights"
   look. */
.lsd-fx--aurora-bg {
  position: relative; overflow: hidden; isolation: isolate;
  background: var(--bg, transparent);
}
.lsd-fx--aurora-bg::before,
.lsd-fx--aurora-bg::after {
  content: ''; position: absolute; inset: -20%;
  background:
    radial-gradient(ellipse 50% 40% at 20% 30%,
      color-mix(in oklch, var(--accent)   60%, transparent), transparent 70%),
    radial-gradient(ellipse 45% 35% at 80% 60%,
      color-mix(in oklch, var(--accent-2) 60%, transparent), transparent 70%),
    radial-gradient(ellipse 40% 30% at 50% 80%,
      color-mix(in oklch, var(--accent-3) 60%, transparent), transparent 70%);
  filter: blur(48px) saturate(1.3);
  animation: lsd-fx-aurora 22s ease-in-out infinite alternate;
  pointer-events: none; z-index: -1;
}
.lsd-fx--aurora-bg::after {
  animation-duration: 30s;
  animation-direction: alternate-reverse;
  mix-blend-mode: screen;
  opacity: 0.7;
}
@keyframes lsd-fx-aurora {
  0%   { transform: translate(0, 0)        scale(1)   rotate(0deg); }
  50%  { transform: translate(-3%, 4%)     scale(1.1) rotate(8deg); }
  100% { transform: translate(4%, -3%)     scale(1.05) rotate(-6deg); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--aurora-bg::before,
  .lsd-fx--aurora-bg::after { animation: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   Wave 3 — scroll-tied effects (M38..M40)
   -------------------------------------------------------------------
   Each reads --lsd-progress (0..1) written by lsd-scroll-trigger.js
   onto any host carrying .lsd-scroll.lsd-scrub. Same contract as the
   existing R4/R5 parallax primitives.
   ═════════════════════════════════════════════════════════════════ */

/* ─── M38 · progressive-blur ─ blur increases with scroll progress.
   Combine with .lsd-scroll.lsd-scrub on the host or an ancestor. */
.lsd-fx--progressive-blur {
  --lsd-blur-max: 24px;
  filter: blur(calc(var(--lsd-progress, 0) * var(--lsd-blur-max)));
  will-change: filter;
}
.lsd-fx--progressive-blur.lsd-fx--invert {
  /* Sharpest when fully in view (progress=0.5), blurrier at the edges. */
  filter: blur(calc(abs(var(--lsd-progress, 0.5) - 0.5) * var(--lsd-blur-max) * 2));
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--progressive-blur { filter: none; }
}

/* ─── M39 · container-3d-scroll ─ 3D rotateX opens as scroll progresses.
   The host element starts tilted away (rotateX 30°) and rotates upright
   over the scroll range. Pair with .lsd-scroll.lsd-scrub on an outer
   wrapper so --lsd-progress drives the unfurl. */
.lsd-fx--container-3d-scroll {
  --lsd-3d-rotate-from: 28deg;
  --lsd-3d-perspective: 1200px;
  transform-style: preserve-3d;
  transform: perspective(var(--lsd-3d-perspective))
             rotateX(calc(var(--lsd-3d-rotate-from) * (1 - var(--lsd-progress, 0))))
             scale(calc(0.92 + 0.08 * var(--lsd-progress, 0)));
  transform-origin: center top;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--container-3d-scroll {
    transform: none;
  }
}

/* ─── M40 · tracing-beam ─ SVG path stroke draws as scroll progresses.
   Host markup:
     <svg class="lsd-fx--tracing-beam" viewBox="0 0 4 800" preserveAspectRatio="none">
       <path d="M 2 0 V 800" pathLength="1"/>
     </svg>
   Place inside (or as a child of) a .lsd-scroll.lsd-scrub container.
   The stroke uses pathLength=1 so we don't have to measure it; the
   stroke-dashoffset interpolates from 1 → 0 over scroll progress. */
.lsd-fx--tracing-beam path,
.lsd-fx--tracing-beam line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  pathLength: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--lsd-progress, 0));
  filter: drop-shadow(0 0 6px color-mix(in oklch, var(--accent) 70%, transparent));
  transition: stroke-dashoffset 80ms linear;
  will-change: stroke-dashoffset;
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--tracing-beam path,
  .lsd-fx--tracing-beam line { stroke-dashoffset: 0; transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   Tween/curve effects (M41..M44)
   -------------------------------------------------------------------
   Native CSS motion-path + parametric wiggle/bounce/rough. Zero JS —
   readable, parameterizable via custom properties.
   ═════════════════════════════════════════════════════════════════ */

/* ─── M41 · motion-path ─ animate the element along an SVG path.
   Author supplies the path via --offset-path:
     <span class="lsd-fx--motion-path"
           style="--offset-path:path('M 0 0 Q 200 -120 400 0')"></span>
   Built-in shape modifiers: --circle / --infinity / --zigzag use a preset path. */
.lsd-fx--motion-path {
  --offset-path: path('M 0 0 L 100 0');
  --motion-duration: 6s;
  offset-path: var(--offset-path);
  offset-distance: 0%;
  offset-rotate: auto;
  animation: lsd-fx-motion-path var(--motion-duration) linear infinite;
}
.lsd-fx--motion-path.lsd-fx--motion-circle  { --offset-path: path('M 100 0 a 100 100 0 1 0 0.001 0'); }
.lsd-fx--motion-path.lsd-fx--motion-infinity { --offset-path: path('M -100 0 C -100 -100, 100 100, 100 0 S -100 100, -100 0 Z'); }
.lsd-fx--motion-path.lsd-fx--motion-zigzag  { --offset-path: path('M 0 0 L 60 -30 L 120 0 L 180 -30 L 240 0'); }
@keyframes lsd-fx-motion-path {
  from { offset-distance: 0%; }
  to   { offset-distance: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--motion-path { animation: none; offset-distance: 50%; }
}

/* ─── M42 · wiggle ─ parametric wiggle (CustomWiggle parity).
   Vars: --wiggle-amp (px), --wiggle-cycles (count), --wiggle-duration. */
.lsd-fx--wiggle {
  --wiggle-amp: 8px;
  --wiggle-cycles: 6;
  --wiggle-duration: 800ms;
  animation: lsd-fx-wiggle var(--wiggle-duration) cubic-bezier(0.36, 0, 0.66, -0.56) both;
}
.lsd-fx--wiggle:hover { animation: lsd-fx-wiggle var(--wiggle-duration) cubic-bezier(0.36, 0, 0.66, -0.56) both; }
@keyframes lsd-fx-wiggle {
  0%   { transform: translateX(0); }
  10%  { transform: translateX(calc(var(--wiggle-amp) * -1)); }
  20%  { transform: translateX(var(--wiggle-amp)); }
  30%  { transform: translateX(calc(var(--wiggle-amp) * -0.8)); }
  40%  { transform: translateX(calc(var(--wiggle-amp) * 0.8)); }
  50%  { transform: translateX(calc(var(--wiggle-amp) * -0.6)); }
  60%  { transform: translateX(calc(var(--wiggle-amp) * 0.6)); }
  70%  { transform: translateX(calc(var(--wiggle-amp) * -0.4)); }
  80%  { transform: translateX(calc(var(--wiggle-amp) * 0.4)); }
  90%  { transform: translateX(calc(var(--wiggle-amp) * -0.2)); }
  100% { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--wiggle, .lsd-fx--wiggle:hover { animation: none; }
}

/* ─── M43 · custom-bounce ─ parameterizable bounce (CustomBounce parity).
   Vars: --bounce-amp (px), --bounce-squash (0..1 — how much to flatten on
   contact), --bounce-duration. Five-decay bounce that settles to rest. */
.lsd-fx--custom-bounce {
  --bounce-amp: 80px;
  --bounce-squash: 0.18;
  --bounce-duration: 1400ms;
  animation: lsd-fx-custom-bounce var(--bounce-duration) cubic-bezier(0.5, 0, 0.5, 1) both;
}
@keyframes lsd-fx-custom-bounce {
  0%   { transform: translateY(0)                              scaleY(1); }
  8%   { transform: translateY(calc(var(--bounce-amp) * -1))   scaleY(1); }
  16%  { transform: translateY(0)                              scaleY(calc(1 - var(--bounce-squash))); }
  20%  { transform: translateY(0)                              scaleY(1); }
  35%  { transform: translateY(calc(var(--bounce-amp) * -0.55)) scaleY(1); }
  44%  { transform: translateY(0)                              scaleY(calc(1 - var(--bounce-squash) * 0.6)); }
  48%  { transform: translateY(0)                              scaleY(1); }
  60%  { transform: translateY(calc(var(--bounce-amp) * -0.28)) scaleY(1); }
  68%  { transform: translateY(0)                              scaleY(calc(1 - var(--bounce-squash) * 0.35)); }
  72%  { transform: translateY(0)                              scaleY(1); }
  82%  { transform: translateY(calc(var(--bounce-amp) * -0.12)) scaleY(1); }
  88%  { transform: translateY(0)                              scaleY(calc(1 - var(--bounce-squash) * 0.18)); }
  92%  { transform: translateY(0)                              scaleY(1); }
  100% { transform: translateY(0)                              scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--custom-bounce { animation: none; }
}

/* ─── M44 · rough-ease ─ chaotic stuttering motion (RoughEase parity).
   Author drives any transform — class adds an extra "noisy" wiggle layer
   via steps() to break smooth interpolation into chunks. */
.lsd-fx--rough {
  --rough-amp: 4px;
  --rough-duration: 1200ms;
  animation: lsd-fx-rough var(--rough-duration) steps(18, end) both;
}
@keyframes lsd-fx-rough {
  0%   { transform: translate(0, 0)                   rotate(0deg); }
  10%  { transform: translate(calc(var(--rough-amp) * -1), calc(var(--rough-amp) *  0.6)) rotate(-1deg); }
  20%  { transform: translate(calc(var(--rough-amp) *  0.8), calc(var(--rough-amp) * -0.4)) rotate( 1deg); }
  30%  { transform: translate(calc(var(--rough-amp) * -0.5), calc(var(--rough-amp) *  0.8)) rotate(-1.5deg); }
  40%  { transform: translate(calc(var(--rough-amp) *  1), calc(var(--rough-amp) *  0.2)) rotate( 0.5deg); }
  50%  { transform: translate(calc(var(--rough-amp) * -0.3), calc(var(--rough-amp) * -0.9)) rotate(-0.5deg); }
  60%  { transform: translate(calc(var(--rough-amp) *  0.6), calc(var(--rough-amp) *  0.4)) rotate( 1deg); }
  70%  { transform: translate(calc(var(--rough-amp) * -0.7), calc(var(--rough-amp) * -0.2)) rotate(-1deg); }
  80%  { transform: translate(calc(var(--rough-amp) *  0.3), calc(var(--rough-amp) *  0.6)) rotate( 0.5deg); }
  90%  { transform: translate(calc(var(--rough-amp) * -0.2), calc(var(--rough-amp) * -0.3)) rotate( 0deg); }
  100% { transform: translate(0, 0)                   rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .lsd-fx--rough { animation: none; }
}

/* ═══════════════════════════════════════════════════════════════════
   Section patterns (M45..M47) — sticky-stage scroll recipes.
   Paired with lsd-stack-cards.js for the stacking variants. Bento and
   marquee patterns are pure CSS + existing lsd-scroll-trigger.js.
   ═════════════════════════════════════════════════════════════════ */

/* ─── M45 · stack-cards ─ sticky-panel scroll stack.
   The runtime (lsd-stack-cards.js) writes --enter / --exit per panel.
   Authors compose their own transforms using those vars. The base recipe
   below covers the common title + image case. */
.lsd-stack-cards {
  position: relative;
  width: 100%;
}
.lsd-stack-panel {
  position: sticky; top: 0;
  width: 100%;
  height: 100vh; height: 100svh;
  overflow: hidden;
  border-radius: 12px 12px 0 0;
  background: var(--bg, currentColor);
}
.lsd-stack-panel:first-child { border-radius: 0; }
.lsd-stack-panel__title {
  position: absolute; z-index: 2;
  left: 50%; top: clamp(66px, 9.5vh, 92px);
  width: min(1180px, 92vw);
  margin: 0; text-align: center; text-transform: uppercase;
  pointer-events: none;
  transform: translate3d(-50%, calc((1 - var(--enter, 0)) * 70px - var(--exit, 0) * 190px), 0);
  opacity: var(--content-opacity, 1);
  will-change: transform, opacity;
}
.lsd-stack-panel__image {
  position: absolute; z-index: 3;
  left: 50%; top: clamp(255px, 34vh, 330px);
  width: clamp(280px, 29vw, 420px);
  aspect-ratio: 1; border-radius: 8px; overflow: hidden;
  transform-origin: center;
  transform:
    translate3d(-50%, calc((1 - var(--enter, 0)) * 85px - var(--exit, 0) * 320px), 0)
    rotate(calc(var(--exit, 0) * var(--tilt, -1.6deg)))
    scale(calc(0.96 + var(--enter, 0) * 0.04 + var(--exit, 0) * 0.06));
  opacity: var(--content-opacity, 1);
  will-change: transform, opacity;
}
.lsd-stack-panel:nth-child(even) .lsd-stack-panel__image { --tilt: 1.6deg; }
@media (prefers-reduced-motion: reduce) {
  .lsd-stack-panel { position: relative; }
  .lsd-stack-panel__title,
  .lsd-stack-panel__image {
    transform: translate3d(-50%, 0, 0) !important;
    opacity: 1 !important;
  }
}

/* ─── M46 · bento-hero ─ sticky stage + scroll-driven bento scale-up.
   Use with .lsd-scroll.lsd-scrub on a tall outer wrapper so lsd-scroll-trigger
   writes --lsd-progress 0..1. The stage stays centered; cells grow. */
.lsd-bento-hero {
  position: relative;
  height: 350vh;
  min-height: 2200px;
}
.lsd-bento-hero__stage {
  position: sticky; top: 0; left: 0;
  width: 100%; height: 100vh;
  min-height: 620px;
  overflow: hidden;
  display: grid; place-items: center;
}
.lsd-bento-hero__copy {
  position: relative; z-index: 10;
  width: min(100% - 32px, 640px);
  text-align: center;
  transform:
    translate3d(0, calc(var(--lsd-progress, 0) * -84px), 0)
    scale(calc(1 - var(--lsd-progress, 0) * 0.24));
  opacity: clamp(0, calc(1 - var(--lsd-progress, 0) * 1.35), 1);
  will-change: transform, opacity;
}
.lsd-bento-hero__grid {
  position: absolute; inset: 0; z-index: 0;
  padding: 1rem;
  display: grid; gap: 1rem;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  grid-template-rows: 1fr 0.5fr 0.5fr 1fr;
}
.lsd-bento-hero__cell {
  position: relative; margin: 0;
  overflow: hidden; border-radius: 0.75rem;
  background: color-mix(in oklch, currentColor 8%, transparent);
  opacity: calc(0.58 + var(--lsd-progress, 0) * 0.42);
  transform: scale(calc(0.18 + var(--lsd-progress, 0) * 0.82));
  will-change: transform, opacity;
}
.lsd-bento-hero__cell:nth-child(1) { grid-column: 1/7; grid-row: 1/4; transform-origin: top right; }
.lsd-bento-hero__cell:nth-child(2) { grid-column: 7/9; grid-row: 1/3; transform-origin: center; }
.lsd-bento-hero__cell:nth-child(3) { grid-column: 7/9; grid-row: 3/5; transform-origin: bottom right; }
.lsd-bento-hero__cell:nth-child(4) { grid-column: 1/4; grid-row: 4/5; transform-origin: top right; }
.lsd-bento-hero__cell:nth-child(5) { grid-column: 4/7; grid-row: 4/5; transform-origin: center; }
@media (prefers-reduced-motion: reduce) {
  .lsd-bento-hero { height: 100vh; min-height: 620px; --lsd-progress: 1; }
  .lsd-bento-hero__stage { position: relative; }
}
@media (max-width: 767px) {
  .lsd-bento-hero__cell:nth-child(1) { grid-column: 1/9; }
  .lsd-bento-hero__cell:nth-child(2),
  .lsd-bento-hero__cell:nth-child(3) { display: none; }
  .lsd-bento-hero__cell:nth-child(4) { grid-column: 1/5; }
  .lsd-bento-hero__cell:nth-child(5) { grid-column: 5/9; }
}

/* ─── M47 · marquee-hero ─ dual-column vertical marquee hero recipe.
   Composes existing primitives — content uses .lsd-fx--fade-in and
   .lsd-fx--magnetic for the CTA, marquee columns animate via these
   keyframes. Authors duplicate the slide content so the loop seams. */
.lsd-marquee-hero {
  position: relative; isolation: isolate;
  overflow: hidden;
  height: 100vh;
}
.lsd-marquee-hero__shell {
  position: absolute;
  inset: -14vh -18vw -16vh auto;
  z-index: 0;
  width: min(960px, 78vw);
  min-height: 128vh;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  transform: rotate(-7deg) scale(1.08);
  transform-origin: center right;
  mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);
}
.lsd-marquee-hero__col { position: relative; overflow: hidden; min-height: 128vh; }
.lsd-marquee-hero__track {
  display: flex; flex-direction: column; gap: 18px;
  will-change: transform;
}
.lsd-marquee-hero__col--up   .lsd-marquee-hero__track { animation: lsd-marquee-hero-up   26s linear infinite; }
.lsd-marquee-hero__col--down .lsd-marquee-hero__track { animation: lsd-marquee-hero-down 28s linear infinite; }
.lsd-marquee-hero__col:hover .lsd-marquee-hero__track { animation-play-state: paused; }
@keyframes lsd-marquee-hero-up   { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@keyframes lsd-marquee-hero-down { from { transform: translateY(-50%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .lsd-marquee-hero__track { animation: none !important; }
}
