/* ═══════════════════════════════════════════════════════════════════
   FX — the motion and depth layer

   Contract with main.js
   ─────────────────────
   JS only ever writes CSS custom properties or toggles one class.
   It never sets a transform directly, so every effect below can be
   retuned here without touching a line of JavaScript.

     --mx / --my      pointer position inside an element, 0–1
     --rx / --ry      tilt rotation in degrees
     --cx / --cy      viewport cursor position in px
     --scroll         page scroll progress, 0–1
     --i              stagger index on a child
     .is-in           element has entered the viewport
   ═══════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════
   AURORA BACKDROP
   Three oversized blurred blobs drifting behind
   everything. Fixed, so it never scrolls away and
   glass panels always have something to refract.
   ═══════════════════════════════════════════════ */
.aurora {
  position: fixed;
  inset: 0;
  z-index: -10;
  overflow: hidden;
  pointer-events: none;
  background: var(--bg-0);
}

.aurora i {
  position: absolute;
  display: block;
  border-radius: 50%;
  /* One big blur is far cheaper than many small ones */
  filter: blur(90px);
  will-change: transform;
}

.aurora i:nth-child(1) {
  top: -18%;
  left: -8%;
  width: 58vw;
  height: 58vw;
  background: radial-gradient(circle at 40% 40%, var(--aurora-1), transparent 66%);
  animation: drift-a 26s var(--ease-in-out) infinite alternate;
}

.aurora i:nth-child(2) {
  top: 12%;
  right: -14%;
  width: 52vw;
  height: 52vw;
  background: radial-gradient(circle at 60% 40%, var(--aurora-2), transparent 66%);
  animation: drift-b 32s var(--ease-in-out) infinite alternate;
}

.aurora i:nth-child(3) {
  bottom: -22%;
  left: 26%;
  width: 46vw;
  height: 46vw;
  background: radial-gradient(circle at 50% 50%, var(--aurora-3), transparent 68%);
  animation: drift-c 38s var(--ease-in-out) infinite alternate;
}

/* Translate only — no scale. Scaling a layer that carries
   filter: blur(90px) forces the browser to re-blur it from
   scratch every frame, which on a 58vw element is one of the most
   expensive things a page can do. Pure translation lets it blur
   once, cache the texture, and move it on the compositor. */
@keyframes drift-a {
  0%   { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(14vw, 10vh, 0); }
}

@keyframes drift-b {
  0%   { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(-12vw, 16vh, 0); }
}

@keyframes drift-c {
  0%   { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(10vw, -14vh, 0); }
}

/* Faint grid over the aurora, fading toward the bottom */
.aurora-grid {
  position: fixed;
  inset: 0;
  z-index: -9;
  pointer-events: none;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.35) 60%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.35) 60%, transparent 100%);
}

/* Film grain. An inline feTurbulence SVG — no image request,
   and it kills the banding that big soft gradients produce. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 9500;
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--noise-blend);
  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='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Particle canvas sits above the aurora, below content */
#dust {
  position: fixed;
  inset: 0;
  z-index: -8;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .aurora i { animation: none; }
  #dust { display: none; }
}

/* ═══════════════════════════════════════════════
   CUSTOM CURSOR
   A precise dot plus a lagging ring. Pointer-events
   are off so it never eats a click.
   ═══════════════════════════════════════════════ */
.cursor-dot,
.cursor-ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9600;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  transition: opacity var(--t-base);
  will-change: transform;
}

.cursor-dot {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  background: var(--ac-1);
  box-shadow: 0 0 12px rgba(var(--ac-1-rgb), 0.9);
  transform: translate3d(var(--cx, -100px), var(--cy, -100px), 0);
}

.cursor-ring {
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border: 1.5px solid rgba(var(--ac-1-rgb), 0.55);
  /* Ring position is lerped in JS, hence its own vars */
  transform: translate3d(var(--rcx, -100px), var(--rcy, -100px), 0) scale(var(--rs, 1));
  transition: opacity var(--t-base), width var(--t-base), height var(--t-base),
              margin var(--t-base), border-color var(--t-base),
              background-color var(--t-base);
}

body.has-cursor .cursor-dot,
body.has-cursor .cursor-ring { opacity: 1; }

/* Ring swells over anything interactive */
body.cursor-hot .cursor-ring {
  width: 58px;
  height: 58px;
  margin: -29px 0 0 -29px;
  border-color: rgba(var(--ac-1-rgb), 0.85);
  background: rgba(var(--ac-1-rgb), 0.1);
}

body.cursor-hot .cursor-dot { opacity: 0.35; }

/* Hide the native cursor only where the custom one is active */
@media (hover: hover) and (pointer: fine) {
  body.has-cursor,
  body.has-cursor a,
  body.has-cursor button,
  body.has-cursor [role='button'] { cursor: none; }
}

/* ═══════════════════════════════════════════════
   SCROLL REVEAL
   Elements start hidden and animate in once. Under
   reduced-motion we skip straight to the end state
   rather than killing the animation, or content
   would be stuck invisible.
   ═══════════════════════════════════════════════ */
[data-reveal] {
  opacity: 0;
  transition: opacity 780ms var(--ease-glide), transform 780ms var(--ease-glide),
              filter 780ms var(--ease-glide);
  transition-delay: var(--reveal-delay, 0ms);
  /* No will-change here on purpose. A page can hold 40+ revealable
     elements; hinting every one of them permanently promotes 40
     compositor layers that stay allocated long after the 780ms
     entrance is over. Opacity and transform are composited anyway. */
}

[data-reveal='up'],
[data-reveal='']       { transform: translate3d(0, 38px, 0); }
[data-reveal='down']   { transform: translate3d(0, -38px, 0); }
[data-reveal='left']   { transform: translate3d(-46px, 0, 0); }
[data-reveal='right']  { transform: translate3d(46px, 0, 0); }
[data-reveal='scale']  { transform: scale(0.9); }
[data-reveal='blur']   { filter: blur(14px); transform: translate3d(0, 22px, 0); }

/* 3D entrance — the card swings in from below on its X axis */
[data-reveal='flip'] {
  transform: perspective(var(--perspective)) rotateX(-22deg) translate3d(0, 44px, 0);
  transform-origin: 50% 100%;
}

[data-reveal='tilt'] {
  transform: perspective(var(--perspective)) rotateY(-16deg) translate3d(38px, 0, 0);
  transform-origin: 100% 50%;
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
}

/* Stagger children of a revealed container */
[data-stagger] > * {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
}

[data-stagger].is-in > * {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--i, 0) * var(--stagger-step, 70ms));
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-stagger] > * {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
}

/* ═══════════════════════════════════════════════
   SPLIT TEXT
   JS wraps each character in .ch inside a .ln mask.
   Characters flip up on their X axis behind the mask.
   ═══════════════════════════════════════════════ */
.split { display: block; }

/* A line is a block so it stacks, but must NOT clip: spaces between
   characters are real text nodes, so a long heading can wrap inside
   one line and an overflow mask would eat the wrapped rows. */
.split .ln { display: block; }

.split .ch {
  display: inline-block;
  transform: translate3d(0, 0.55em, 0) rotateX(-68deg);
  opacity: 0;
  transform-origin: 50% 100%;
  transition: transform 900ms var(--ease-glide), opacity 620ms var(--ease-out);
  transition-delay: calc(var(--i, 0) * 26ms);
  /* will-change would apply per CHARACTER — a two-line heading is
     30+ glyphs, so hinting here allocated a layer for each one.
     That cost far exceeded any benefit on a one-shot entrance. */
}

.split.is-in .ch {
  transform: none;
  opacity: 1;
}

/* The 3D flip needs a shared perspective, applied on the parent so
   every character rotates about the same vanishing point. */
.split { perspective: 700px; }

/* Transforms off for one synchronous frame while JS measures
   character offsets — see paintGradientText in main.js. */
.split.is-measuring .ch {
  transform: none !important;
  transition: none !important;
}

/* Per-character gradient. --gw is the group's full width and --gx
   this character's negative offset inside it, so the ramp lines up
   across the whole word or line instead of restarting per letter. */
.grad-group .ch {
  color: var(--ac-1);
  background-image: var(--grad-brand);
  background-size: var(--gw, 100%) 100%;
  background-position: var(--gx, 0) 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .grad-group .ch {
    -webkit-text-fill-color: currentColor;
    background-image: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .split .ch { transform: none !important; opacity: 1 !important; transition: none !important; }
}

/* ═══════════════════════════════════════════════
   3D TILT
   The element rotates toward the pointer while its
   children lift on the Z axis, so the card gains
   real parallax rather than just leaning over.
   ═══════════════════════════════════════════════ */
[data-tilt] {
  transform-style: preserve-3d;
  transform:
    perspective(var(--perspective))
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translate3d(0, 0, 0);
  transition: transform 260ms var(--ease-out);
}

/* While the pointer is inside, JS drives the vars every frame —
   the transition would fight it, so it is dropped. The layer hint
   also lives here rather than on [data-tilt], so a grid of 11 cards
   promotes one layer while hovered instead of 11 permanently. */
[data-tilt].is-tilting {
  transition: none;
  will-change: transform;
}

/* Depth layers. Higher Z = moves more = feels closer. */
[data-depth='1']  { transform: translateZ(18px); }
[data-depth='2']  { transform: translateZ(38px); }
[data-depth='3']  { transform: translateZ(62px); }
[data-depth='4']  { transform: translateZ(92px); }
[data-depth='-1'] { transform: translateZ(-24px); }

[data-depth] { transform-style: preserve-3d; }

/* Moving specular highlight. Tracks the pointer via --mx/--my. */
.sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-base);
  background: radial-gradient(
    circle 320px at calc(var(--mx, 0.5) * 100%) calc(var(--my, 0.5) * 100%),
    rgba(255, 255, 255, 0.16) 0%,
    rgba(var(--ac-1-rgb), 0.09) 32%,
    transparent 65%
  );
  mix-blend-mode: soft-light;
}

[data-tilt]:hover .sheen,
.card:hover .sheen,
.glass:hover .sheen { opacity: 1; }

/* Hard diagonal glare that sweeps across on hover */
.glare {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
}

.glare::after {
  content: '';
  position: absolute;
  inset: -60%;
  background: var(--grad-sheen);
  transform: translateX(-70%) rotate(8deg);
  transition: transform 1.1s var(--ease-glide);
}

[data-tilt]:hover .glare::after,
.card:hover .glare::after,
.glass:hover .glare::after { transform: translateX(70%) rotate(8deg); }

/* Spotlight that follows the pointer across a surface */
.spot {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-base);
  background: radial-gradient(
    circle 260px at calc(var(--mx, 0.5) * 100%) calc(var(--my, 0.5) * 100%),
    rgba(var(--ac-1-rgb), 0.13),
    transparent 70%
  );
}

*:hover > .spot { opacity: 1; }

/* ═══════════════════════════════════════════════
   MAGNETIC
   Element drifts toward the cursor. JS writes the
   offset; CSS owns the easing on release.
   ═══════════════════════════════════════════════ */
[data-magnetic] {
  transform: translate3d(var(--mgx, 0px), var(--mgy, 0px), 0);
  transition: transform 420ms var(--ease-spring);
}

[data-magnetic].is-pulled { transition: transform 120ms var(--ease-out); }

/* ═══════════════════════════════════════════════
   PARALLAX — offset driven by scroll position
   ═══════════════════════════════════════════════ */
[data-parallax] {
  transform: translate3d(0, var(--py, 0px), 0);
}

/* ═══════════════════════════════════════════════
   3D ORBIT
   Two counter-rotating rings of tech logos on a
   tilted stage.

   Why three nested elements per chip: the ring
   spins by t degrees, so a chip at angle θ ends up
   facing t+θ. To keep the logo square to the
   viewer we undo both — an animated -t on .orb-spin
   and a static -θ on .orb-face.
   ═══════════════════════════════════════════════ */
.orbit-stage {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 1 / 1;
  max-width: 520px;
  margin-inline: auto;
  perspective: var(--perspective-deep);
  perspective-origin: 50% 42%;
}

.orbit {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  /* Tilt the whole assembly so the rings read as ellipses */
  transform: rotateX(-18deg);
}

.orbit-ring {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: orbit-spin var(--dur, 42s) linear infinite;
}

.orbit-ring--inner {
  inset: 22%;
  animation-direction: reverse;
  --dur: 30s;
}

@keyframes orbit-spin {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}

/* Position on the ring: swing out to angle θ, push out by the radius */
.orb {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 62px;
  height: 62px;
  margin: -31px 0 0 -31px;
  transform-style: preserve-3d;
  transform: rotateY(var(--a)) translateZ(var(--r, 220px));
}

.orbit-ring--inner .orb {
  width: 50px;
  height: 50px;
  margin: -25px 0 0 -25px;
}

/* Cancels the ring's rotation */
.orb-spin {
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  animation: orbit-spin var(--dur, 42s) linear infinite reverse;
}

.orbit-ring--inner .orb-spin { animation-direction: normal; }

/* Cancels this chip's own angle, and un-tilts the stage */
.orb-face {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  border-radius: var(--r-md);
  transform: rotateY(calc(var(--a) * -1)) rotateX(18deg);
  background: var(--glass-3);
  border: 1px solid var(--brd-2);
  backdrop-filter: blur(var(--blur-sm));
  -webkit-backdrop-filter: blur(var(--blur-sm));
  box-shadow: var(--sh-md), inset 0 1px 0 rgba(255, 255, 255, 0.14);
  transition: background-color var(--t-base), border-color var(--t-base),
              box-shadow var(--t-base), scale var(--t-base);
  cursor: pointer;
}

.orb-face img {
  width: 26px;
  height: 26px;
  filter: grayscale(1) brightness(1.9);
  opacity: 0.78;
  transition: filter var(--t-base), opacity var(--t-base);
}

.orbit-ring--inner .orb-face img { width: 21px; height: 21px; }

[data-theme='light'] .orb-face img { filter: grayscale(1) brightness(0.5); }

.orb-face:hover {
  background: var(--ac-1-dim);
  border-color: var(--ac-1);
  box-shadow: var(--sh-glow-1);
  scale: 1.18;
}

.orb-face:hover img {
  filter: none;
  opacity: 1;
}

/* Label appears above the chip on hover */
.orb-name {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  translate: -50% 0;
  padding: 0.25rem 0.55rem;
  border-radius: var(--r-xs);
  background: var(--bg-4);
  border: 1px solid var(--brd-2);
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: 0.04em;
  color: var(--tx-0);
  white-space: nowrap;
  opacity: 0;
  transition: opacity var(--t-fast), translate var(--t-fast);
  pointer-events: none;
}

.orb-face:hover .orb-name {
  opacity: 1;
  translate: -50% -4px;
}

/* Pausing on hover lets you actually read the logos */
.orbit-stage:hover .orbit-ring,
.orbit-stage:hover .orb-spin { animation-play-state: paused; }

/* Glowing core at the centre of the rings */
.orbit-core {
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 34%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, var(--glass-4), var(--glass-1));
  border: 1px solid var(--brd-2);
  backdrop-filter: blur(var(--blur-md));
  -webkit-backdrop-filter: blur(var(--blur-md));
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.14),
              0 0 70px rgba(var(--ac-1-rgb), 0.22);
  text-align: center;
  padding: var(--sp-4);
}

.orbit-core::before {
  content: '';
  position: absolute;
  inset: -14%;
  border-radius: 50%;
  border: 1px dashed var(--brd-2);
  animation: orbit-spin 60s linear infinite;
  opacity: 0.5;
}

.orbit-core-val {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: var(--ls-tight);
  background: var(--grad-brand);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: var(--ac-1);
}

.orbit-core-lbl {
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--tx-2);
  margin-top: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .orbit-ring, .orb-spin, .orbit-core::before { animation: none; }
}

/* On small screens the ring radius has to shrink with the stage */
@media (max-width: 560px) {
  .orb { --r: 140px !important; width: 46px; height: 46px; margin: -23px 0 0 -23px; }
  .orb-face img { width: 20px; height: 20px; }
  .orbit-ring--inner .orb { --r: 82px !important; }
}

/* ═══════════════════════════════════════════════
   EXPLODING 3D STACK
   Layers sit flat and collapsed, then fan apart in
   Z on hover — an architecture diagram you can feel.
   ═══════════════════════════════════════════════ */
.stack-3d {
  position: relative;
  display: grid;
  place-items: center;
  min-height: clamp(340px, 46vw, 480px);
  perspective: var(--perspective-deep);
  perspective-origin: 50% 50%;
}

.stack-3d-inner {
  position: relative;
  width: min(100%, 520px);
  /* Explicit height: every layer is absolutely positioned, so without
     this the wrapper collapses to 0 and the stack centres wrongly. */
  height: 96px;
  transform-style: preserve-3d;
  transform: rotateX(56deg) rotateZ(-38deg);
  transition: transform 900ms var(--ease-glide);
}

/* Flatten toward the viewer when the user engages */
.stack-3d:hover .stack-3d-inner,
.stack-3d.is-open .stack-3d-inner {
  transform: rotateX(44deg) rotateZ(-30deg);
}

.stack-layer {
  position: absolute;
  inset: 0;
  height: 96px;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-lg);
  background: var(--glass-2);
  border: 1px solid var(--brd-2);
  backdrop-filter: blur(var(--blur-md));
  -webkit-backdrop-filter: blur(var(--blur-md));
  box-shadow: var(--sh-lg), inset 0 1px 0 rgba(255, 255, 255, 0.12);
  transform: translateZ(calc(var(--z, 0) * 26px));
  transition: transform 900ms var(--ease-glide), background-color var(--t-base),
              border-color var(--t-base), box-shadow var(--t-base);
  transform-style: preserve-3d;
}

.stack-3d:hover .stack-layer,
.stack-3d.is-open .stack-layer {
  transform: translateZ(calc(var(--z, 0) * 62px));
}

.stack-layer:hover {
  background: var(--glass-4);
  border-color: var(--ac-1);
  box-shadow: var(--sh-xl), var(--sh-glow-1);
}

/* Text column inside a layer. The children are spans, so they need
   to be blocks or the three lines run together. */
.stack-layer > span:last-child {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.stack-layer-num {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  letter-spacing: 0.1em;
  color: var(--ac-1);
  opacity: 0.8;
}

.stack-layer-title {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--tx-0);
  line-height: 1.3;
}

.stack-layer-sub {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-3xs);
  color: var(--tx-2);
  letter-spacing: 0.04em;
}

@media (prefers-reduced-motion: reduce) {
  .stack-3d-inner { transform: none; }
  .stack-layer {
    position: relative;
    inset: auto;
    height: auto;
    transform: none;
    margin-bottom: var(--sp-3);
  }
}

/* ═══════════════════════════════════════════════
   ANIMATED SVG FLOW
   Dashes travel along a path to show data moving.
   ═══════════════════════════════════════════════ */
.flow-path {
  fill: none;
  stroke: var(--brd-2);
  stroke-width: 1.5;
}

.flow-dash {
  fill: none;
  stroke: var(--ac-1);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 10 190;
  filter: drop-shadow(0 0 6px rgba(var(--ac-1-rgb), 0.85));
  animation: dash-flow 3.2s linear infinite;
  animation-delay: var(--delay, 0s);
}

@keyframes dash-flow {
  from { stroke-dashoffset: 200; }
  to   { stroke-dashoffset: 0; }
}

/* One-shot draw-on for outline paths */
.draw-path {
  stroke-dasharray: var(--len, 1000);
  stroke-dashoffset: var(--len, 1000);
  transition: stroke-dashoffset 1.8s var(--ease-glide);
}

.is-in .draw-path { stroke-dashoffset: 0; }

@media (prefers-reduced-motion: reduce) {
  .flow-dash { animation: none; opacity: 0.55; stroke-dasharray: none; }
  .draw-path { stroke-dashoffset: 0; transition: none; }
}

/* ═══════════════════════════════════════════════
   AMBIENT KEYFRAMES
   ═══════════════════════════════════════════════ */

/* Slow vertical bob */
.fx-float { animation: float 7s var(--ease-in-out) infinite; }

@keyframes float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-14px); }
}

.fx-float--slow { animation-duration: 11s; }
.fx-float--delay { animation-delay: -3.5s; }

/* Pulsing accent glow */
.fx-pulse { animation: pulse-glow 3.4s var(--ease-in-out) infinite; }

@keyframes pulse-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--ac-1-rgb), 0.34); }
  50%      { box-shadow: 0 0 30px 6px rgba(var(--ac-1-rgb), 0.14); }
}

/* Gradient that slides across a fill */
.fx-shift {
  background-size: 220% 100%;
  animation: grad-shift 7s var(--ease-in-out) infinite alternate;
}

@keyframes grad-shift {
  from { background-position: 0% 50%; }
  to   { background-position: 100% 50%; }
}

/* Light sweeping down a vertical rule */
.fx-scan { position: relative; overflow: hidden; }

.fx-scan::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: -40%;
  height: 40%;
  background: linear-gradient(180deg, transparent, rgba(var(--ac-1-rgb), 0.7), transparent);
  animation: scan 3.6s var(--ease-in-out) infinite;
}

@keyframes scan {
  0%   { top: -40%; opacity: 0; }
  18%  { opacity: 1; }
  82%  { opacity: 1; }
  100% { top: 100%; opacity: 0; }
}

/* Text shimmer for headline accents */
.fx-shimmer {
  background: linear-gradient(
    100deg,
    var(--tx-0) 32%,
    var(--ac-1) 46%,
    var(--ac-2) 54%,
    var(--tx-0) 68%
  );
  background-size: 280% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: shimmer 5.5s linear infinite;
}

@keyframes shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -80% 0; }
}

/* Rotating conic border. The mask carves out the middle so
   only a 1px ring of the spinning gradient shows. */
.fx-ring-border {
  position: relative;
  isolation: isolate;
}

.fx-ring-border::before {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(
    from var(--ang, 0deg),
    transparent 0deg,
    var(--ac-1) 70deg,
    var(--ac-2) 130deg,
    transparent 200deg,
    transparent 360deg
  );
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  -webkit-mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  animation: rotate-ang 5s linear infinite;
  opacity: 0;
  transition: opacity var(--t-base);
  pointer-events: none;
  z-index: 1;
}

.fx-ring-border:hover::before { opacity: 1; }

@property --ang {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@keyframes rotate-ang {
  to { --ang: 360deg; }
}

/* Typewriter caret */
.caret {
  display: inline-block;
  width: 2px;
  height: 1.05em;
  margin-left: 3px;
  vertical-align: text-bottom;
  background: var(--ac-1);
  box-shadow: 0 0 8px rgba(var(--ac-1-rgb), 0.9);
  animation: blink 1.05s steps(2, jump-none) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .fx-float, .fx-pulse, .fx-shift, .fx-shimmer,
  .fx-scan::after, .fx-ring-border::before, .caret { animation: none; }
  .fx-shimmer { -webkit-text-fill-color: currentColor; background: none; }
}

/* ═══════════════════════════════════════════════
   PAGE TRANSITION — curtain wipe on internal links
   ═══════════════════════════════════════════════ */
.curtain {
  position: fixed;
  inset: 0;
  z-index: 9800;
  background: var(--bg-0);
  transform: translateY(100%);
  pointer-events: none;
  transition: transform 620ms var(--ease-glide);
}

.curtain.is-up { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .curtain { display: none; }
}
