/*
 * Motion layer — ReactBits-style animations, ported to plain CSS + vanilla JS.
 *
 * ReactBits (reactbits.dev) ships React components built on framer-motion /
 * GSAP / OGL. This theme is PHP + Tailwind with no React and no JS bundler,
 * so the effects are re-implemented here as CSS transitions driven by
 * data-* attributes and a few CSS custom properties that assets/js/motion.js
 * writes. Same visuals, no runtime dependency, nothing added to the deploy.
 *
 * Hand-authored plain CSS (like critical.css) rather than Tailwind, for the
 * same reason: these are stateful/pseudo-element rules that utility classes
 * can't express, and they must survive a stale Tailwind build.
 *
 * PROGRESSIVE ENHANCEMENT CONTRACT — every hidden state below keys off an
 * `-init` attribute that motion.js adds at runtime, never off the
 * server-rendered markup. If JS never runs, every element stays fully
 * visible and readable. Do not move these hidden states onto the plain
 * data-ss-* attributes.
 *
 * Ported effects, by ReactBits name:
 *   SplitText / BlurText  -> [data-ss-split]      (.ss-word)
 *   ShinyText             -> [data-ss-shiny]
 *   GradientText          -> [data-ss-gradient-text]
 *   CountUp               -> [data-ss-count]      (JS only, no CSS state)
 *   AnimatedContent       -> [data-ss-reveal="..."] direction variants
 *   SpotlightCard         -> [data-ss-spotlight]
 *   GlareHover            -> [data-ss-glare]
 *   Magnet                -> [data-ss-magnet]
 *   StarBorder            -> [data-ss-star]
 *   TiltedCard            -> [data-ss-tilt]
 *   Aurora / Silk         -> .ss-aurora (printed once, see functions.php)
 */

/* ── AURORA BACKGROUND (ReactBits: Aurora / Silk) ────────────────────────────
   Three slow-drifting blurred colour fields behind the page. Printed into
   <body> by ssc_aurora_backdrop() so it exists without JS.

   z-index:-1 on a fixed child of <body> paints ABOVE the body background
   (the ivory wash + dot grid propagate to the canvas) and BELOW all in-flow
   content — no stacking-context tricks needed, and it can never intercept
   clicks. Kept at low opacity and a 40s+ period so it reads as ambient
   light on the ivory palette rather than a moving background. */
/* overflow:hidden keeps the oversized blobs from extending the document —
   html/body already run overflow-x:clip, and a vw-sized blob drifting past
   the right edge is exactly the kind of thing that reintroduces the
   horizontal scrollbar that critical.css exists to prevent.
   contain is layout/paint/style only — deliberately NOT `strict`, which
   would add size containment to an element whose size comes from inset:0. */
.ss-aurora{position:fixed;inset:0;z-index:-1;pointer-events:none;overflow:hidden;contain:layout paint style}
.ss-aurora span{position:absolute;display:block;border-radius:9999px;filter:blur(72px);opacity:.5;will-change:transform}
.ss-aurora span:nth-child(1){top:-14%;left:-10%;width:52vw;height:52vw;background:radial-gradient(circle,rgba(255,197,49,.34),transparent 68%);animation:ss-aurora-a 46s ease-in-out infinite}
.ss-aurora span:nth-child(2){top:22%;right:-16%;width:46vw;height:46vw;background:radial-gradient(circle,rgba(228,230,210,.55),transparent 68%);animation:ss-aurora-b 58s ease-in-out infinite}
.ss-aurora span:nth-child(3){bottom:-18%;left:26%;width:48vw;height:48vw;background:radial-gradient(circle,rgba(169,206,199,.34),transparent 68%);animation:ss-aurora-c 52s ease-in-out infinite}
@keyframes ss-aurora-a{0%,100%{transform:translate3d(0,0,0) scale(1)}33%{transform:translate3d(8vw,6vh,0) scale(1.14)}66%{transform:translate3d(-4vw,10vh,0) scale(.94)}}
@keyframes ss-aurora-b{0%,100%{transform:translate3d(0,0,0) scale(1)}33%{transform:translate3d(-9vw,8vh,0) scale(.9)}66%{transform:translate3d(-3vw,-7vh,0) scale(1.12)}}
@keyframes ss-aurora-c{0%,100%{transform:translate3d(0,0,0) scale(1)}33%{transform:translate3d(7vw,-9vh,0) scale(1.1)}66%{transform:translate3d(-8vw,-3vh,0) scale(.92)}}
/* Phones: the blobs cost more than they add on a small canvas, and vw-sized
   blur radii are the expensive part. Hold them still. */
@media (max-width:640px){.ss-aurora span{animation:none;opacity:.38}}

/* ── SCROLL REVEAL DIRECTION VARIANTS (ReactBits: AnimatedContent) ───────────
   The base rise+fade lives in critical.css ([data-ss-reveal-init]); these
   only swap the starting transform, so untagged elements keep the exact
   behaviour they had before this file existed. Attribute selectors here are
   0,2,0 vs critical.css's 0,1,0, so they win regardless of load order. */
[data-ss-reveal-init][data-ss-reveal="up"]{transform:translateY(28px)}
[data-ss-reveal-init][data-ss-reveal="down"]{transform:translateY(-28px)}
[data-ss-reveal-init][data-ss-reveal="left"]{transform:translateX(-34px)}
[data-ss-reveal-init][data-ss-reveal="right"]{transform:translateX(34px)}
[data-ss-reveal-init][data-ss-reveal="scale"]{transform:scale(.94)}
/* The blur variant also has to re-declare the transition, because the base
   rule in critical.css only animates opacity and transform — filter would
   otherwise snap from blurred to sharp in a single frame. */
[data-ss-reveal-init][data-ss-reveal="blur"]{transform:translateY(18px);filter:blur(10px);transition:opacity .7s cubic-bezier(.4,0,.2,1),transform .7s cubic-bezier(.4,0,.2,1),filter .7s cubic-bezier(.4,0,.2,1);transition-delay:var(--ss-delay,0ms)}
[data-ss-reveal-init][data-ss-reveal].is-visible{transform:none;filter:none}

/* ── SPLIT TEXT (ReactBits: SplitText / BlurText) ────────────────────────────
   motion.js wraps each word of the heading in <span class="ss-word"> and
   stamps --ss-i (word index) on it; the stagger is pure CSS from there.
   The delay is capped in JS, not here, so a long heading can't end up with
   a two-second tail.

   inline-block is what makes the transform apply per word; motion.js keeps
   the real whitespace text nodes between spans so lines still wrap normally. */
[data-ss-split-init] .ss-word{display:inline-block;will-change:transform,opacity}
[data-ss-split-init]:not(.is-visible) .ss-word{opacity:0;transform:translateY(.42em)}
[data-ss-split-init] .ss-word{transition:opacity .55s cubic-bezier(.22,1,.36,1),transform .55s cubic-bezier(.22,1,.36,1),filter .55s cubic-bezier(.22,1,.36,1);transition-delay:calc(var(--ss-i,0) * 34ms)}
/* Blur variant — the ReactBits BlurText look. */
[data-ss-split-init][data-ss-split="blur"]:not(.is-visible) .ss-word{filter:blur(9px);transform:translateY(.3em)}
/* Once revealed, drop the compositing hints so long pages don't hold dozens
   of promoted layers alive after the animation has finished. */
[data-ss-split-init].is-visible .ss-word{will-change:auto}

/* ── SHINY TEXT (ReactBits: ShinyText) ───────────────────────────────────────
   A highlight sweeps across the glyphs. background-clip:text needs a painted
   background, so the element's own colour is re-declared as the gradient's
   base — that's why this is opt-in per element rather than auto-applied. */
[data-ss-shiny]{background:linear-gradient(110deg,currentColor 0%,currentColor 38%,rgba(255,255,255,.92) 50%,currentColor 62%,currentColor 100%);background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:ss-shiny 4.2s linear infinite}
@keyframes ss-shiny{0%{background-position:180% 0}100%{background-position:-80% 0}}

/* ── GRADIENT TEXT (ReactBits: GradientText) ─────────────────────────────────
   Animates the position of a brand gradient under clipped text. Sized 220%
   so the sweep has somewhere to travel.

   padding-right fixes a clipping bug that predates this file: background-clip
   :text paints only within the element's background box, but an italic
   glyph's tail leans PAST that box. On the homepage hero the accent word is
   italic, so the final "r" of "faster" was being sliced off vertically — with
   the static Tailwind gradient too, not just the animated one. A little right
   padding widens the background box enough to cover the overhang, and it also
   restores the space the lean was eating before the next word. */
[data-ss-gradient-text]{background:linear-gradient(90deg,#F2B41C,#FFC531,#C4C89A,#FFC531,#F2B41C);background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;padding-right:.12em;animation:ss-gradient-text 7s linear infinite}
@keyframes ss-gradient-text{0%{background-position:0 0}100%{background-position:220% 0}}

/* ── COUNT UP (ReactBits: CountUp) ───────────────────────────────────────────
   Proportional digits change width as they tick, which makes the whole stats
   row jitter sideways while counting. Tabular figures pin the width. */
[data-ss-count]{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}

/* ── SPOTLIGHT CARD (ReactBits: SpotlightCard) ───────────────────────────────
   A soft radial highlight tracks the pointer across the glass cards.
   motion.js writes --ss-x/--ss-y (percentages) from one delegated
   pointermove listener on the document, so this costs no per-card listeners.

   inset:0 + border-radius:inherit keeps the glow inside the card's rounded
   box WITHOUT needing overflow:hidden — several of these cards deliberately
   let shadows and badges bleed outside their bounds.

   z-index:-1 is what keeps the glow behind the card's content, and it is
   safe here for a specific reason: motion.js only ever tags elements that
   carry backdrop-filter (the glass panels), and a non-none backdrop-filter
   makes the element its own stacking context. So -1 lands above the card's
   own translucent background and below everything inside it. Do NOT extend
   this attribute to non-glass elements — without that stacking context the
   glow would slide behind the card entirely.

   The same backdrop-filter is also why there is no `[data-ss-spotlight] > *`
   rule forcing children to position:relative: these cards are already the
   containing block for their absolutely positioned descendants (the floating
   badges and stat chips), and adding a positioning context on the direct
   children would re-anchor those to the wrong box.

   ⚠ AND IT IS WHY THIS RULE DOES NOT SET `position:relative` ON THE CARD.
   That looks like the obvious way to anchor an absolutely positioned
   ::before, and it broke the homepage. This file is UNLAYERED, while every
   Tailwind utility lives in @layer utilities — and unlayered CSS beats any
   layered CSS no matter how specific the layered selector is. So a bare
   `position:relative` here silently outranked `min-[481px]:absolute` on the
   About section's three overlapping dashboard cards and dropped them into
   normal flow as a plain vertical stack.
   No anchor is needed: backdrop-filter already establishes the containing
   block, exactly as it does for the z-index reasoning above. Anything given
   this attribute must carry backdrop-filter (motion.js enforces that by
   matching the `backdrop-blur-xl` class token). */
[data-ss-spotlight]::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:-1;opacity:0;transition:opacity .35s ease;background:radial-gradient(320px circle at var(--ss-x,50%) var(--ss-y,50%),rgba(255,197,49,.30),rgba(228,230,210,.15) 42%,transparent 68%)}
[data-ss-spotlight].is-lit::before{opacity:1}

/* ── GLARE HOVER (ReactBits: GlareHover) ─────────────────────────────────────
   A diagonal sheen crosses the element once per hover. Needs its own clip,
   so this one DOES take overflow:hidden — only applied to media frames.

   ⚠ Unlike the spotlight above, these targets have no backdrop-filter, so the
   ::after really does need an anchor here. That makes this rule subject to the
   unlayered-beats-@layer-utilities trap documented above: it will override a
   Tailwind `absolute`/`fixed`/`sticky` utility on whatever it is applied to.
   Only put data-ss-glare on statically positioned media frames (motion.js
   auto-applies it to .ss-blog-thumb only). */
[data-ss-glare]{position:relative;overflow:hidden}
[data-ss-glare]::after{content:"";position:absolute;top:-60%;bottom:-60%;left:-90%;width:55%;pointer-events:none;z-index:2;transform:rotate(18deg) translateX(0);background:linear-gradient(90deg,transparent,rgba(255,255,255,.55),transparent);opacity:0;transition:transform .85s cubic-bezier(.22,1,.36,1),opacity .2s ease}
[data-ss-glare]:hover::after,[data-ss-glare]:focus-within::after{opacity:1;transform:rotate(18deg) translateX(370%)}

/* ── MAGNET (ReactBits: Magnet) ──────────────────────────────────────────────
   Buttons drift a few px toward the pointer. .ssc-btn already animates
   transform on hover (a -2px lift) in critical.css, so the magnet offset is
   composed with that lift instead of replacing it — the :hover rule below
   re-states the lift inside the same translate3d. Specificity is 0,3,0
   against critical.css's 0,2,0, so it wins without !important. */
[data-ss-magnet]{transform:translate3d(var(--ss-mx,0px),var(--ss-my,0px),0);transition:transform .28s cubic-bezier(.22,1,.36,1)}
/* .ssc-btn's own rule in critical.css is `transition:transform .2s ease,
   box-shadow .2s ease`. Both selectors weigh 0,1,0 and this file loads
   later, so the generic rule above would win and quietly drop box-shadow
   from the list — the hover shadow would snap on instead of easing. Restate
   the whole list here rather than leaving that to load order. */
.ssc-btn[data-ss-magnet]{transition:transform .28s cubic-bezier(.22,1,.36,1),box-shadow .2s ease}
.ssc-btn[data-ss-magnet]:hover,.ssc-btn[data-ss-magnet]:focus{transform:translate3d(var(--ss-mx,0px),calc(var(--ss-my,0px) - 2px),0)}

/* ── STAR BORDER (ReactBits: StarBorder) ─────────────────────────────────────
   A light travels around the button's edge. Drawn as a conic gradient in a
   ::before that is masked to a 1.5px ring, so it never repaints the button's
   dark fill. The mask pair (composite:xor) is the standard gradient-border
   trick; -webkit-mask is still required for Safari.

   ⚠ Same caveat as the glare rule: this sets position, so keep it to the
   pill buttons (.ssc-btn), which are never Tailwind-positioned. */
[data-ss-star]{position:relative;isolation:isolate}
[data-ss-star]::before{content:"";position:absolute;inset:0;border-radius:inherit;padding:1.5px;pointer-events:none;z-index:-1;opacity:0;transition:opacity .3s ease;background:conic-gradient(from var(--ss-angle,0deg),transparent 0deg,rgba(255,197,49,.15) 40deg,#FFC531 90deg,rgba(255,255,255,.95) 110deg,#FFC531 130deg,rgba(255,197,49,.15) 180deg,transparent 220deg,transparent 360deg);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude}
[data-ss-star]:hover::before,[data-ss-star]:focus-visible::before{opacity:1;animation:ss-star 2.4s linear infinite}
/* @property makes the conic angle a real animatable <angle>; without it the
   custom property is an untyped string and the keyframe below simply snaps.
   Browsers that don't support @property just get the static (still visible)
   border, which is why the ring is drawn at full opacity above. */
@property --ss-angle{syntax:"<angle>";initial-value:0deg;inherits:false}
@keyframes ss-star{to{--ss-angle:360deg}}

/* ── TILTED CARD (ReactBits: TiltedCard) ─────────────────────────────────────
   3D tilt following the pointer. motion.js writes --ss-rx/--ss-ry in degrees.
   Kept to a few degrees; the big glass cards look broken past ~8deg.

   ⚠ This sets transform, and by the unlayered-vs-@layer rule above it will
   replace any Tailwind transform utility (rotate-*, scale-*, translate-*) on
   the same element. Tag a wrapper instead of an element that already leans. */
[data-ss-tilt]{transform:perspective(900px) rotateX(var(--ss-rx,0deg)) rotateY(var(--ss-ry,0deg));transition:transform .4s cubic-bezier(.22,1,.36,1);transform-style:preserve-3d}
[data-ss-tilt].is-tilting{transition:transform .12s linear}

/* ── PRINT ───────────────────────────────────────────────────────────────────
   Printing never runs transitions, so any element still holding its armed
   hidden state at print time would come out blank — including headings below
   the fold that were never scrolled to. Force every animated state to its
   final value and drop the decorative layers. */
@media print{
  .ss-aurora{display:none!important}
  [data-ss-reveal-init],
  [data-ss-reveal-init][data-ss-reveal],
  [data-ss-split-init] .ss-word,
  [data-ss-split-init]:not(.is-visible) .ss-word{opacity:1!important;transform:none!important;filter:none!important}
  [data-ss-spotlight]::before,[data-ss-glare]::after,[data-ss-star]::before{display:none!important}
  [data-ss-shiny],[data-ss-gradient-text]{background:none!important;color:#181713!important;-webkit-text-fill-color:#181713!important}
}

/* ── REDUCED MOTION ──────────────────────────────────────────────────────────
   One master switch. Everything lands in its final, fully legible state:
   text at full opacity and no transform, sheens and sweeps parked, the
   aurora held still (kept visible — it's part of the page's colour, not an
   animation). motion.js reads the same media query and skips its pointer
   listeners entirely, so nothing writes the custom properties either. */
@media (prefers-reduced-motion:reduce){
  .ss-aurora span{animation:none!important}
  [data-ss-reveal-init][data-ss-reveal]{transition:none!important;transform:none!important;filter:none!important;opacity:1!important}
  [data-ss-split-init] .ss-word,
  [data-ss-split-init]:not(.is-visible) .ss-word{transition:none!important;transform:none!important;filter:none!important;opacity:1!important}
  [data-ss-shiny]{animation:none!important;background:none!important;color:inherit!important;-webkit-text-fill-color:currentColor!important}
  [data-ss-gradient-text]{animation:none!important;background-position:0 0!important}
  [data-ss-spotlight]::before{display:none!important}
  [data-ss-glare]::after{display:none!important}
  [data-ss-magnet],.ssc-btn[data-ss-magnet],.ssc-btn[data-ss-magnet]:hover,.ssc-btn[data-ss-magnet]:focus{transform:none!important;transition:none!important}
  [data-ss-star]::before{animation:none!important}
  [data-ss-tilt]{transform:none!important;transition:none!important}
}
