/* ==========================================================================
   EVO.AL — Animated profile backgrounds (Pro)
   Loaded only when a profile actually has a preset selected, and by the
   dashboard for the live preview. Every preset animates transform/opacity
   only, so the whole layer stays on the compositor.

   Preset families:
     Ambient   aurora · mesh · gradient-shift · orbs · nebula · beams
               · smoke · holo · spotlight
     Particles particles · bokeh · snow · meteors · bubbles · fireflies
               · rain · petals · confetti · warp
     Patterns  waves · grid · dots · ripples · equalizer · synthwave · grain
   ========================================================================== */

.anim-bg {
  position: fixed; inset: 0;
  z-index: -2;
  overflow: hidden;
  pointer-events: none;

  /* Preset palette. --ab-accent is the only thing a profile overrides (inline,
     from users.bg_color); the other four spread out from it so the presets
     keep the multi-hue richness they were designed with. --ab-soft is the pale
     tint the sparks, hairlines and streak trails use.

     This block is the fallback: mixing toward fixed hues. It is faithful for a
     blue-ish accent but goes muddy for one that is complementary to the mix
     target — a green accent crossed with pink lands in brown. The @supports
     block below replaces it with a hue rotation wherever that is available,
     which never mixes through grey. */
  --ab-accent: #2563EB;
  --ab-1: var(--ab-accent);
  --ab-2: color-mix(in oklch, var(--ab-accent) 58%, #a855f7);
  --ab-3: color-mix(in oklch, var(--ab-accent) 42%, #ec4899);
  --ab-4: color-mix(in oklch, var(--ab-accent) 52%, #06b6d4);
  --ab-soft: color-mix(in oklch, var(--ab-accent) 45%, #ffffff);
}

/* The offsets are picked so the default #2563EB reproduces the original
   blue / violet / pink / cyan palette almost exactly, and any other accent
   gets the same relationships rotated with it. */
@supports (color: oklch(from red l c h)) {
  .anim-bg {
    --ab-2: oklch(from var(--ab-accent) l c calc(h + 38));
    --ab-3: oklch(from var(--ab-accent) l c calc(h + 76));
    --ab-4: oklch(from var(--ab-accent) l c calc(h - 42));
    --ab-soft: oklch(from var(--ab-accent) calc(l + .28) calc(c * .55) h);
  }
}

/* -- Aurora: three blurred light blobs drifting -- */
.ab-blob {
  position: absolute;
  width: 55vmax; height: 55vmax;
  border-radius: 50%;
  filter: blur(70px);
  opacity: .38;
  will-change: transform;
}
.anim-aurora .ab-1 { top: -20%; left: -15%; background: var(--ab-1); animation: abDrift1 26s ease-in-out infinite alternate; }
.anim-aurora .ab-2 { bottom: -25%; right: -15%; background: var(--ab-2); animation: abDrift2 32s ease-in-out infinite alternate; }
.anim-aurora .ab-3 { top: 30%; left: 40%; width: 40vmax; height: 40vmax; background: var(--ab-3); opacity: .22; animation: abDrift3 38s ease-in-out infinite alternate; }
@keyframes abDrift1 { to { transform: translate(18vw, 12vh) scale(1.15); } }
@keyframes abDrift2 { to { transform: translate(-14vw, -10vh) scale(1.2); } }
@keyframes abDrift3 { to { transform: translate(-16vw, 8vh) scale(.85); } }

/* -- Mesh gradient: one large layer slowly swaying -- */
.anim-mesh::before {
  content: "";
  position: absolute; inset: -50%;
  background:
    radial-gradient(40% 40% at 25% 30%, color-mix(in srgb, var(--ab-1) 50%, transparent), transparent 70%),
    radial-gradient(35% 35% at 75% 25%, color-mix(in srgb, var(--ab-2) 45%, transparent), transparent 70%),
    radial-gradient(45% 45% at 70% 75%, color-mix(in srgb, var(--ab-3) 35%, transparent), transparent 70%),
    radial-gradient(35% 35% at 25% 80%, color-mix(in srgb, var(--ab-4) 35%, transparent), transparent 70%);
  animation: abMesh 30s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes abMesh { to { transform: translate(6%, -5%) rotate(8deg) scale(1.1); } }

/* -- Gradient shift: oversized gradient panning via transform -- */
.anim-gradient-shift::before {
  content: "";
  position: absolute; top: -50%; left: -50%;
  width: 200%; height: 200%;
  background: linear-gradient(120deg, color-mix(in srgb, var(--ab-1) 50%, transparent), color-mix(in srgb, var(--ab-2) 42%, transparent), color-mix(in srgb, var(--ab-3) 40%, transparent), color-mix(in srgb, var(--ab-1) 50%, transparent));
  animation: abPan 22s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes abPan { to { transform: translate(-18%, -12%); } }

/* -- Orbs: soft pulsing circles -- */
.ab-orb {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--ab-1) 50%, transparent), transparent 70%);
  will-change: transform, opacity;
  animation: abPulse 9s ease-in-out infinite;
}
.anim-orbs .ab-1 { width: 45vmin; height: 45vmin; top: 6%; left: 8%; }
.anim-orbs .ab-2 { width: 34vmin; height: 34vmin; top: 55%; right: 6%; background: radial-gradient(circle, color-mix(in srgb, var(--ab-2) 50%, transparent), transparent 70%); animation-delay: -3s; }
.anim-orbs .ab-3 { width: 26vmin; height: 26vmin; bottom: 8%; left: 22%; background: radial-gradient(circle, color-mix(in srgb, var(--ab-3) 45%, transparent), transparent 70%); animation-delay: -6s; }
.anim-orbs .ab-4 { width: 20vmin; height: 20vmin; top: 26%; right: 26%; background: radial-gradient(circle, color-mix(in srgb, var(--ab-4) 45%, transparent), transparent 70%); animation-delay: -1.5s; }
@keyframes abPulse {
  0%, 100% { transform: scale(1); opacity: .55; }
  50% { transform: scale(1.28); opacity: .9; }
}

/* -- Particles: tiny stars floating upward -- */
.ab-star {
  position: absolute;
  bottom: -3vh;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ab-soft) 80%, transparent);
  box-shadow: 0 0 8px 2px color-mix(in srgb, var(--ab-1) 35%, transparent);
  will-change: transform, opacity;
  animation: abFloat linear infinite;
  opacity: 0;
}
.anim-particles .ab-s1  { left: 6%;  animation-duration: 15s; }
.anim-particles .ab-s2  { left: 14%; animation-duration: 21s; animation-delay: -6s; width: 3px; height: 3px; }
.anim-particles .ab-s3  { left: 22%; animation-duration: 17s; animation-delay: -11s; }
.anim-particles .ab-s4  { left: 30%; animation-duration: 24s; animation-delay: -3s; width: 4px; height: 4px; }
.anim-particles .ab-s5  { left: 38%; animation-duration: 16s; animation-delay: -9s; width: 3px; height: 3px; }
.anim-particles .ab-s6  { left: 46%; animation-duration: 22s; animation-delay: -14s; }
.anim-particles .ab-s7  { left: 54%; animation-duration: 18s; animation-delay: -5s; width: 4px; height: 4px; }
.anim-particles .ab-s8  { left: 62%; animation-duration: 25s; animation-delay: -12s; width: 3px; height: 3px; }
.anim-particles .ab-s9  { left: 70%; animation-duration: 15s; animation-delay: -8s; }
.anim-particles .ab-s10 { left: 78%; animation-duration: 20s; animation-delay: -2s; width: 4px; height: 4px; }
.anim-particles .ab-s11 { left: 86%; animation-duration: 17s; animation-delay: -10s; width: 3px; height: 3px; }
.anim-particles .ab-s12 { left: 94%; animation-duration: 23s; animation-delay: -7s; }
.anim-particles .ab-s13 { left: 10%; animation-duration: 19s; animation-delay: -16s; width: 3px; height: 3px; }
.anim-particles .ab-s14 { left: 90%; animation-duration: 26s; animation-delay: -18s; width: 4px; height: 4px; }
@keyframes abFloat {
  0% { transform: translateY(0); opacity: 0; }
  8% { opacity: .9; }
  92% { opacity: .9; }
  100% { transform: translateY(-108vh); opacity: 0; }
}

/* -- Waves: two slow-spinning rounded sheets at the bottom -- */
.ab-wave {
  position: absolute;
  bottom: -130vmin;
  left: 50%;
  width: 160vmin; height: 160vmin;
  margin-left: -80vmin;
  border-radius: 43%;
  background: color-mix(in srgb, var(--ab-1) 28%, transparent);
  will-change: transform;
  animation: abSpin 24s linear infinite;
}
.anim-waves .ab-w2 {
  border-radius: 46%;
  background: color-mix(in srgb, var(--ab-2) 22%, transparent);
  animation-duration: 34s;
  animation-direction: reverse;
}
@keyframes abSpin { to { transform: rotate(360deg); } }

/* -- Nebula: deep cosmic clouds drifting behind twinkling stars -- */
.anim-nebula .ab-neb {
  position: absolute;
  border-radius: 50%;
  filter: blur(65px);
  opacity: .5;
  will-change: transform;
}
.anim-nebula .ab-n1 { width: 62vmax; height: 48vmax; top: -14%; left: -12%;
  background: radial-gradient(circle, var(--ab-1), transparent 70%);
  animation: abNeb1 34s ease-in-out infinite alternate; }
.anim-nebula .ab-n2 { width: 52vmax; height: 52vmax; bottom: -20%; right: -14%;
  background: radial-gradient(circle, var(--ab-3), transparent 70%);
  animation: abNeb2 42s ease-in-out infinite alternate; }
.anim-nebula .ab-n3 { width: 42vmax; height: 42vmax; top: 32%; left: 42%; opacity: .34;
  background: radial-gradient(circle, var(--ab-4), transparent 70%);
  animation: abNeb3 48s ease-in-out infinite alternate; }
@keyframes abNeb1 { to { transform: translate(14vw, 10vh) scale(1.2); } }
@keyframes abNeb2 { to { transform: translate(-12vw, -8vh) scale(1.15); } }
@keyframes abNeb3 { to { transform: translate(-11vw, 12vh) scale(.85); } }
.anim-nebula .ab-tw {
  position: absolute; left: var(--x); top: var(--y);
  width: var(--s); height: var(--s);
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 6px 1px color-mix(in srgb, var(--ab-soft) 70%, transparent);
  opacity: 0;
  animation: abTwinkle ease-in-out infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: opacity, transform;
}
@keyframes abTwinkle {
  0%, 100% { opacity: 0; transform: scale(.5); }
  50%      { opacity: .95; transform: scale(1.35); }
}

/* -- Beams: slow rotating rays of light -- */
.anim-beams::before {
  content: "";
  position: absolute; top: 50%; left: 50%;
  width: 220vmax; height: 220vmax; margin: -110vmax 0 0 -110vmax;
  background: conic-gradient(from 0deg,
    transparent 0deg,   color-mix(in srgb, var(--ab-1) 20%, transparent)  9deg, transparent  21deg,
    transparent 47deg,  color-mix(in srgb, var(--ab-2) 18%, transparent) 57deg, transparent  70deg,
    transparent 112deg, color-mix(in srgb, var(--ab-3) 16%, transparent) 122deg, transparent 135deg,
    transparent 182deg, color-mix(in srgb, var(--ab-4) 18%, transparent)  192deg, transparent 205deg,
    transparent 252deg, color-mix(in srgb, var(--ab-1) 16%, transparent) 263deg, transparent 276deg,
    transparent 320deg, color-mix(in srgb, var(--ab-2) 15%, transparent) 331deg, transparent 345deg,
    transparent 360deg);
  animation: abRotate 52s linear infinite;
  will-change: transform;
}
/* Fade the rays toward the edges so the text keeps its contrast. */
.anim-beams::after {
  content: "";
  position: absolute; inset: 0;
  background: radial-gradient(ellipse at 50% 38%, transparent 0%, transparent 30%,
              color-mix(in srgb, var(--bg) 78%, transparent) 100%);
}
@keyframes abRotate { to { transform: rotate(360deg); } }

/* -- Bokeh: soft blurred lights drifting upward -- */
.anim-bokeh .ab-bk {
  position: absolute; bottom: -25vh; left: var(--x);
  width: var(--s); height: var(--s);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, rgba(255, 255, 255, .55), color-mix(in srgb, var(--ab-soft) 20%, transparent) 45%, transparent 72%);
  filter: blur(var(--b));
  opacity: 0;
  animation: abRise linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abRise {
  0%   { transform: translate3d(0, 0, 0) scale(.75); opacity: 0; }
  14%  { opacity: .85; }
  86%  { opacity: .6; }
  100% { transform: translate3d(4vw, -130vh, 0) scale(1.2); opacity: 0; }
}

/* -- Retro grid: endless neon grid running to the horizon --
   The sheet pivots on its top edge (the horizon), so a positive rotateX tips
   its lower half toward the viewer and perspective does the rest. Scrolling it
   by exactly one tile keeps the loop seamless. */
.anim-grid { perspective: 280px; }
.anim-grid .ab-grid {
  position: absolute; left: -100%; right: -100%;
  top: 42%; bottom: -80%;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--ab-soft) 32%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--ab-soft) 32%, transparent) 1px, transparent 1px);
  background-size: 70px 70px;
  transform-origin: 50% 0;
  animation: abGrid 3s linear infinite;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 28%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 28%);
  will-change: transform;
}
@keyframes abGrid {
  from { transform: rotateX(78deg) translateY(0); }
  to   { transform: rotateX(78deg) translateY(70px); }
}
/* Glow sitting on the horizon line. */
.anim-grid::after {
  content: "";
  position: absolute; left: 0; right: 0; top: 10%;
  height: 34vh;
  background: radial-gradient(ellipse at 50% 100%, color-mix(in srgb, var(--ab-2) 34%, transparent), transparent 70%);
}

/* -- Meteors: shooting stars crossing the sky -- */
.anim-meteors .ab-mt {
  position: absolute; top: var(--y); left: var(--x);
  width: 150px; height: 2px;
  border-radius: 999px;
  background: linear-gradient(270deg, color-mix(in srgb, var(--ab-soft) 95%, transparent), color-mix(in srgb, var(--ab-soft) 32%, transparent) 38%, transparent 78%);
  opacity: 0;
  animation: abMeteor linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
.anim-meteors .ab-mt::before {
  content: "";
  position: absolute; right: -2px; top: -2px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 10px 3px color-mix(in srgb, var(--ab-soft) 65%, transparent);
}
@keyframes abMeteor {
  0%   { transform: rotate(26deg) translateX(-40vmax); opacity: 0; }
  10%  { opacity: 1; }
  72%  { opacity: 1; }
  100% { transform: rotate(26deg) translateX(120vmax); opacity: 0; }
}

/* -- Snow: flakes drifting gently down -- */
.anim-snow .ab-sf {
  position: absolute; top: -8vh; left: var(--x);
  width: var(--s); height: var(--s);
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  box-shadow: 0 0 6px rgba(255, 255, 255, .45);
  opacity: 0;
  animation: abSnow linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abSnow {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  12%  { opacity: .9; }
  88%  { opacity: .9; }
  100% { transform: translate3d(var(--dx), 112vh, 0); opacity: 0; }
}

/* ==========================================================================
   Ambient presets, continued
   ========================================================================== */

/* -- Smoke: wide fog banks sliding past each other -- */
.anim-smoke .ab-sm {
  position: absolute;
  left: -30%; width: 160%; height: 62vh;
  border-radius: 50%;
  filter: blur(60px);
  opacity: .5;
  will-change: transform;
}
.anim-smoke .ab-1 { top: -12%;
  background: radial-gradient(ellipse at 42% 50%, rgba(148, 163, 184, .7), transparent 70%);
  animation: abSmoke1 46s ease-in-out infinite alternate; }
.anim-smoke .ab-2 { top: 26%;
  background: radial-gradient(ellipse at 62% 50%, color-mix(in srgb, var(--ab-soft) 55%, transparent), transparent 70%);
  animation: abSmoke2 38s ease-in-out infinite alternate; }
.anim-smoke .ab-3 { bottom: -14%;
  background: radial-gradient(ellipse at 38% 50%, color-mix(in srgb, var(--ab-2) 50%, transparent), transparent 70%);
  animation: abSmoke3 54s ease-in-out infinite alternate; }
@keyframes abSmoke1 { to { transform: translate3d(12vw, 4vh, 0) scaleY(1.25); } }
@keyframes abSmoke2 { to { transform: translate3d(-14vw, -3vh, 0) scaleY(.8); } }
@keyframes abSmoke3 { to { transform: translate3d(10vw, -5vh, 0) scaleY(1.15); } }

/* -- Holo: an iridescent wheel read through a sweeping sheen -- */
.anim-holo::before {
  content: "";
  position: absolute; top: 50%; left: 50%;
  width: 170vmax; height: 170vmax; margin: -85vmax 0 0 -85vmax;
  background: conic-gradient(from 0deg, #22d3ee, #a78bfa, #f472b6, #facc15, #4ade80, #22d3ee);
  filter: blur(80px);
  opacity: .3;
  animation: abRotate 44s linear infinite;   /* shared with beams */
  will-change: transform;
}
.anim-holo::after {
  content: "";
  position: absolute; inset: -40%;
  background: linear-gradient(115deg, transparent 42%, rgba(255, 255, 255, .16) 50%, transparent 58%);
  animation: abSheen 9s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes abSheen { from { transform: translateX(-22%); } to { transform: translateX(22%); } }

/* -- Spotlight: two stage beams sweeping from above.
   Both cones pivot on their top edge, so transform-origin: 50% 0 plus a
   rotation is all the sweep needs — no re-layout, pure compositing. -- */
.anim-spotlight::before,
.anim-spotlight::after {
  content: "";
  position: absolute; top: -26vh; left: 50%;
  width: 42vmax; height: 150vh;
  transform-origin: 50% 0;
  clip-path: polygon(45% 0, 55% 0, 100% 100%, 0 100%);
  filter: blur(18px);
  background: linear-gradient(180deg, color-mix(in srgb, var(--ab-soft) 42%, transparent), transparent 72%);
  animation: abSweepA 17s ease-in-out infinite alternate;
  will-change: transform;
}
.anim-spotlight::after {
  background: linear-gradient(180deg, color-mix(in srgb, var(--ab-2) 34%, transparent), transparent 72%);
  animation-name: abSweepB;
  animation-duration: 23s;
}
@keyframes abSweepA {
  from { transform: translateX(-50%) rotate(-27deg); }
  to   { transform: translateX(-50%) rotate(19deg); }
}
@keyframes abSweepB {
  from { transform: translateX(-50%) rotate(25deg); }
  to   { transform: translateX(-50%) rotate(-21deg); }
}

/* ==========================================================================
   Particle presets, continued
   ========================================================================== */

/* -- Rain: streaks falling along their own axis. Rotating *before* the
   translate keeps the travel parallel to the streak, so drops never skid. -- */
.anim-rain .ab-rd {
  position: absolute; top: -20vh; left: var(--x);
  width: 1.5px; height: var(--s);
  border-radius: 999px;
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--ab-soft) 85%, transparent));
  opacity: 0;
  animation: abRain linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abRain {
  0%   { transform: rotate(14deg) translate3d(0, 0, 0); opacity: 0; }
  12%  { opacity: .7; }
  88%  { opacity: .7; }
  100% { transform: rotate(14deg) translate3d(0, 138vh, 0); opacity: 0; }
}

/* -- Bubbles: outlined bubbles rising with a sway -- */
.anim-bubbles .ab-bb {
  position: absolute; bottom: -16vh; left: var(--x);
  width: var(--s); height: var(--s);
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--ab-soft) 70%, transparent);
  background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, .5), color-mix(in srgb, var(--ab-soft) 12%, transparent) 44%, transparent 64%);
  opacity: 0;
  animation: abBubble ease-in-out infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abBubble {
  0%   { transform: translate3d(0, 0, 0) scale(.7); opacity: 0; }
  12%  { opacity: .85; }
  50%  { transform: translate3d(var(--dx), -58vh, 0) scale(1); }
  86%  { opacity: .5; }
  100% { transform: translate3d(0, -122vh, 0) scale(1.1); opacity: 0; }
}

/* -- Fireflies: sparks wandering on one timeline while breathing on a
   faster one; two animations on the same element, no wrapper needed. -- */
.anim-fireflies .ab-ff {
  position: absolute; left: var(--x); top: var(--y);
  width: var(--s); height: var(--s);
  border-radius: 50%;
  background: #fef3c7;
  box-shadow: 0 0 12px 3px rgba(251, 191, 36, .5);
  opacity: 0;
  animation: abWander var(--d) ease-in-out infinite alternate,
             abGlow calc(var(--d) / 6) ease-in-out infinite;
  animation-delay: var(--t), var(--t);
  will-change: transform, opacity;
}
@keyframes abWander {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(var(--dx), var(--dy), 0); }
}
@keyframes abGlow { 0%, 100% { opacity: .12; } 50% { opacity: 1; } }

/* -- Petals: blossom falling and fluttering -- */
.anim-petals .ab-pt {
  position: absolute; top: -12vh; left: var(--x);
  width: var(--s); height: calc(var(--s) * .8);
  border-radius: 50% 6% 50% 50%;
  background: linear-gradient(135deg, rgba(252, 231, 243, .95), rgba(244, 114, 182, .8));
  opacity: 0;
  animation: abPetal linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abPetal {
  0%   { transform: translate3d(0, 0, 0) rotate3d(1, 1, .3, 0deg); opacity: 0; }
  10%  { opacity: .95; }
  88%  { opacity: .85; }
  100% { transform: translate3d(var(--dx), 122vh, 0) rotate3d(1, 1, .3, 940deg); opacity: 0; }
}

/* -- Confetti: tumbling paper, for launch-day profiles -- */
.anim-confetti .ab-cf {
  position: absolute; top: -12vh; left: var(--x);
  width: var(--s); height: calc(var(--s) * 1.7);
  border-radius: 2px;
  background: var(--c);
  opacity: 0;
  animation: abConfetti linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abConfetti {
  0%   { transform: translate3d(0, 0, 0) rotate3d(.6, 1, .2, 0deg); opacity: 0; }
  8%   { opacity: 1; }
  90%  { opacity: .9; }
  100% { transform: translate3d(var(--dx), 124vh, 0) rotate3d(.6, 1, .2, 1100deg); opacity: 0; }
}

/* -- Warp: stars stretching away from the centre.
   Each streak is anchored by its left edge at the centre point, so one
   rotate picks the ray, translateX flies along it, scaleX draws it out. -- */
.anim-warp .ab-wp {
  position: absolute; top: 50%; left: 50%;
  width: 16vmax; height: 2px; margin-top: -1px;
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ab-soft) 95%, transparent));
  transform-origin: 0 50%;
  opacity: 0;
  animation: abWarp linear infinite;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform, opacity;
}
@keyframes abWarp {
  0%   { transform: rotate(var(--a)) translateX(0) scaleX(.05); opacity: 0; }
  14%  { opacity: .9; }
  100% { transform: rotate(var(--a)) translateX(64vmax) scaleX(1); opacity: 0; }
}
/* Keep the middle dark so the streaks read as coming out of deep space. */
.anim-warp::after {
  content: "";
  position: absolute; inset: 0;
  background: radial-gradient(circle at 50% 50%,
              color-mix(in srgb, var(--bg) 88%, transparent) 0%, transparent 34%);
}

/* ==========================================================================
   Pattern presets, continued
   ========================================================================== */

/* -- Dots: a halftone field with a soft light roaming over it -- */
.anim-dots::before {
  content: "";
  position: absolute; inset: -25%;
  background-image: radial-gradient(circle, color-mix(in srgb, var(--ab-soft) 50%, transparent) 1.5px, transparent 1.6px);
  background-size: 24px 24px;
  animation: abDotsPan 36s linear infinite;
  will-change: transform;
}
/* Travels an exact multiple of the tile, so the loop point is invisible. */
@keyframes abDotsPan { to { transform: translate3d(240px, 168px, 0); } }
.anim-dots .ab-dg {
  position: absolute;
  width: 72vmax; height: 72vmax; top: -14%; left: -16%;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--ab-1) 38%, transparent), transparent 66%);
  filter: blur(46px);
  animation: abDrift1 34s ease-in-out infinite alternate;   /* shared with aurora */
  will-change: transform;
}

/* -- Ripples: rings spreading out from the centre, radar style -- */
.anim-ripples .ab-rp {
  position: absolute; top: 50%; left: 50%;
  width: 46vmax; height: 46vmax; margin: -23vmax 0 0 -23vmax;
  border: 1px solid color-mix(in srgb, var(--ab-soft) 50%, transparent);
  border-radius: 50%;
  opacity: 0;
  animation: abRipple 10s cubic-bezier(.16, .7, .3, 1) infinite;
  will-change: transform, opacity;
}
.anim-ripples .ab-r2 { animation-delay: -2s; }
.anim-ripples .ab-r3 { animation-delay: -4s; }
.anim-ripples .ab-r4 { animation-delay: -6s; }
.anim-ripples .ab-r5 { animation-delay: -8s; }
@keyframes abRipple {
  0%   { transform: scale(.04); opacity: 0; }
  8%   { opacity: .75; }
  100% { transform: scale(1.7); opacity: 0; }
}
.anim-ripples::after {
  content: "";
  position: absolute; top: 50%; left: 50%;
  width: 34vmax; height: 34vmax; margin: -17vmax 0 0 -17vmax;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--ab-1) 35%, transparent), transparent 68%);
  filter: blur(30px);
}

/* -- Equalizer: bars reacting along the bottom edge -- */
.anim-equalizer .ab-eq {
  position: absolute; bottom: 0; left: var(--x);
  width: var(--s); height: 26vh;
  transform-origin: 50% 100%;
  border-radius: 5px 5px 0 0;
  background: linear-gradient(180deg, color-mix(in srgb, var(--ab-soft) 85%, transparent), color-mix(in srgb, var(--ab-2) 15%, transparent));
  opacity: .55;
  animation: abEq ease-in-out infinite alternate;
  animation-duration: var(--d);
  animation-delay: var(--t);
  will-change: transform;
}
@keyframes abEq { from { transform: scaleY(.12); } to { transform: scaleY(1); } }
.anim-equalizer::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 30vh;
  background: linear-gradient(0deg, color-mix(in srgb, var(--ab-1) 30%, transparent), transparent 70%);
}

/* -- Synthwave: a striped sun sinking into the haze. The bands are drawn in
   the page colour on top of the disc, which keeps them correct in both
   themes without a second gradient to maintain. -- */
.anim-synthwave::before {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(76, 29, 149, .55) 0%, rgba(190, 24, 93, .3) 46%, transparent 72%);
}
.anim-synthwave .ab-sun,
.anim-synthwave .ab-bars {
  position: absolute; left: 50%; top: 20%;
  width: 48vmin; height: 48vmin; margin-left: -24vmin;
}
.anim-synthwave .ab-sun {
  border-radius: 50%;
  background: linear-gradient(180deg, #fbbf24 0%, #fb7185 52%, #a21caf 100%);
  animation: abSunPulse 7s ease-in-out infinite alternate;
  will-change: opacity;
}
.anim-synthwave .ab-bars {
  background: repeating-linear-gradient(180deg, transparent 0 13px, var(--bg) 13px 20px);
  -webkit-mask-image: linear-gradient(180deg, transparent 44%, #000 56%);
          mask-image: linear-gradient(180deg, transparent 44%, #000 56%);
}
.anim-synthwave .ab-haze {
  position: absolute; left: -10%; right: -10%; top: 40%;
  height: 46vh;
  background: radial-gradient(ellipse at 50% 0%, rgba(236, 72, 153, .38), transparent 68%);
  filter: blur(24px);
  animation: abHaze 18s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes abSunPulse { from { opacity: .82; } to { opacity: 1; } }
@keyframes abHaze { from { transform: translate3d(-4vw, 0, 0); } to { transform: translate3d(4vw, 2vh, 0); } }

/* -- Grain: film texture jittering over a slow colour wash -- */
.anim-grain::before {
  content: "";
  position: absolute; inset: -20%;
  background:
    radial-gradient(45% 45% at 30% 30%, color-mix(in srgb, var(--ab-1) 38%, transparent), transparent 70%),
    radial-gradient(45% 45% at 72% 68%, color-mix(in srgb, var(--ab-2) 30%, transparent), transparent 70%);
  animation: abMesh 34s ease-in-out infinite alternate;   /* shared with mesh */
  will-change: transform;
}
.anim-grain::after {
  content: "";
  position: absolute; inset: -30%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
  opacity: .22;
  animation: abGrain .7s steps(5) infinite;
  will-change: transform;
}
@keyframes abGrain {
  0%   { transform: translate3d(0, 0, 0); }
  20%  { transform: translate3d(-6%, 3%, 0); }
  40%  { transform: translate3d(4%, -5%, 0); }
  60%  { transform: translate3d(-3%, -2%, 0); }
  80%  { transform: translate3d(5%, 4%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* ==========================================================================
   Light theme: soften the layers so text stays readable
   ========================================================================== */
[data-theme="light"] .anim-bg { opacity: .5; }
/* White sparks disappear on a light page — recolour them to the brand blue. */
[data-theme="light"] .anim-nebula .ab-tw,
[data-theme="light"] .anim-snow .ab-sf {
  background: color-mix(in srgb, var(--ab-1) 75%, transparent);
  box-shadow: 0 0 6px color-mix(in srgb, var(--ab-1) 35%, transparent);
}
[data-theme="light"] .anim-meteors .ab-mt {
  background: linear-gradient(270deg, color-mix(in srgb, var(--ab-1) 90%, transparent), color-mix(in srgb, var(--ab-1) 30%, transparent) 38%, transparent 78%);
}
[data-theme="light"] .anim-meteors .ab-mt::before {
  background: var(--ab-1);
  box-shadow: 0 0 10px 3px color-mix(in srgb, var(--ab-1) 45%, transparent);
}
[data-theme="light"] .anim-bokeh .ab-bk {
  background: radial-gradient(circle at 35% 32%, color-mix(in srgb, var(--ab-1) 38%, transparent), color-mix(in srgb, var(--ab-2) 16%, transparent) 45%, transparent 72%);
}
[data-theme="light"] .anim-rain .ab-rd {
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--ab-1) 80%, transparent));
}
[data-theme="light"] .anim-warp .ab-wp {
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ab-1) 90%, transparent));
}
[data-theme="light"] .anim-bubbles .ab-bb {
  border-color: color-mix(in srgb, var(--ab-1) 38%, transparent);
  background: radial-gradient(circle at 32% 28%, color-mix(in srgb, var(--ab-1) 22%, transparent), color-mix(in srgb, var(--ab-2) 8%, transparent) 44%, transparent 64%);
}
[data-theme="light"] .anim-fireflies .ab-ff {
  background: #f59e0b;
  box-shadow: 0 0 12px 3px rgba(245, 158, 11, .45);
}
[data-theme="light"] .anim-holo::after,
[data-theme="light"] .anim-grain::after { opacity: .12; }

/* ==========================================================================
   Phones: trim the piece count and the blur radii. Every preset still reads
   the same, it just costs the battery a lot less.
   ========================================================================== */
@media (max-width: 520px) {
  .ab-tw:nth-child(2n), .ab-bk:nth-child(2n), .ab-sf:nth-child(3n),
  .ab-cf:nth-child(3n), .ab-rd:nth-child(3n), .ab-bb:nth-child(3n),
  .ab-pt:nth-child(3n), .ab-ff:nth-child(3n), .ab-wp:nth-child(3n) { display: none; }

  .ab-blob { filter: blur(48px); }
  .anim-nebula .ab-neb { filter: blur(46px); }
  .anim-smoke .ab-sm { filter: blur(42px); }
  .anim-holo::before { filter: blur(56px); }
  .anim-dots .ab-dg { filter: blur(34px); }
}

/* ==========================================================================
   Reduced motion: keep the mood, drop the movement. Pieces that only become
   visible part-way through their cycle would freeze as blank shapes, so they
   are removed rather than paused.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .anim-bg *,
  .anim-bg::before,
  .anim-bg::after { animation: none !important; }

  .ab-star, .ab-bk, .ab-mt, .ab-sf, .ab-tw, .ab-rd, .ab-bb,
  .ab-pt, .ab-cf, .ab-wp, .ab-rp, .ab-ff { display: none; }

  .ab-eq { transform: scaleY(.4); opacity: .35; }
}

[data-bg-intensity="normal"] .anim-bg { opacity: .7; }
[data-bg-intensity="subtle"] .anim-bg { opacity: .35; }
[data-bg-intensity="strong"] .anim-bg { opacity: 1; }
[data-theme="light"][data-bg-intensity="normal"] .anim-bg { opacity: .5; }
[data-theme="light"][data-bg-intensity="subtle"] .anim-bg { opacity: .2; }
[data-theme="light"][data-bg-intensity="strong"] .anim-bg { opacity: .8; }

/* PRO: orbit rings, light ribbons, crystals and rising embers. */
.anim-orbit .ab-orbit {
  position: absolute; left: 50%; top: 50%; width: 78vmin; height: 78vmin;
  margin: -39vmin; border: 2px solid var(--ab-1); border-radius: 50%;
  box-shadow: 0 0 24px color-mix(in srgb, var(--ab-1) 25%, transparent);
  opacity: .45; transform: rotate(0deg) scaleY(.45);
  animation: abOrbitTurn 38s linear infinite;
}
.anim-orbit .ab-layer-2 { width: 105vmin; height: 105vmin; margin: -52.5vmin; border-color: var(--ab-2); animation-delay: -12s; animation-duration: 49s; }
.anim-orbit .ab-layer-3 { width: 130vmin; height: 130vmin; margin: -65vmin; border-color: var(--ab-3); animation-delay: -25s; animation-direction: reverse; }
@keyframes abOrbitTurn { to { transform: rotate(360deg) scaleY(.45); } }
.anim-ribbons .ab-ribbons {
  position: absolute; left: -30%; top: 12%; width: 160%; height: 24vh;
  border-radius: 50%; opacity: .32;
  background: linear-gradient(100deg, transparent, var(--ab-1), var(--ab-2), transparent);
  filter: blur(28px); transform: rotate(-18deg);
  animation: abRibbonSway 24s ease-in-out infinite alternate;
}
.anim-ribbons .ab-layer-2 { top: 40%; background: linear-gradient(100deg, transparent, var(--ab-3), var(--ab-4), transparent); animation-delay: -9s; animation-direction: alternate-reverse; }
.anim-ribbons .ab-layer-3 { top: 70%; opacity: .22; animation-delay: -17s; animation-duration: 32s; }
@keyframes abRibbonSway { to { transform: translate3d(6vw, 6vh, 0) rotate(16deg) scaleY(.65); } }
.anim-crystals .ab-crystal {
  position: absolute; left: var(--x); top: var(--y);
  width: var(--s); height: var(--s); border: 1px solid var(--ab-1);
  background: linear-gradient(135deg, color-mix(in srgb, var(--ab-soft) 30%, transparent), transparent);
  opacity: .5; transform: rotate(45deg);
  animation: abCrystalDrift var(--d) ease-in-out var(--t) infinite alternate;
}
@keyframes abCrystalDrift { to { transform: translate3d(var(--dx), -12vh, 0) rotate(135deg); opacity: .2; } }
.anim-embers .ab-ember {
  position: absolute; left: var(--x); bottom: -12px;
  width: var(--s); height: calc(var(--s) * 1.8); border-radius: 50%;
  background: var(--ab-soft); box-shadow: 0 0 10px var(--ab-1);
  opacity: 0; animation: abEmberRise var(--d) linear var(--t) infinite;
}
.anim-embers::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(ellipse at 50% 110%, color-mix(in srgb, var(--ab-1) 30%, transparent), transparent 65%);
}
@keyframes abEmberRise {
  0% { transform: translate3d(0, 0, 0); opacity: 0; }
  15% { opacity: .8; }
  70% { opacity: .45; }
  100% { transform: translate3d(var(--dx), -105vh, 0); opacity: 0; }
}
@media (max-width: 520px) {
  .anim-crystals .ab-crystal:nth-child(3n), .anim-embers .ab-ember:nth-child(2n) { display: none; }
  .anim-ribbons .ab-ribbons { filter: blur(18px); }
}
@media (prefers-reduced-motion: reduce) {
  .anim-embers .ab-ember { display: none; }
  .anim-orbit .ab-layer-2 { transform: rotate(60deg) scaleY(.45); }
  .anim-orbit .ab-layer-3 { transform: rotate(120deg) scaleY(.45); }
}
