/* ============================================================================
   Caledapt — motion layer
   ----------------------------------------------------------------------------
   Loaded AFTER styles.css. Everything here is enhancement, never structure.
   Delete this file and remove its <link> to remove every animation from the
   site; the page stays complete and readable.

   Zero dependencies. No GSAP, no Lenis, no scroll library. The whole layer is
   transform/opacity only, so nothing here can cause layout shift or reflow.

   Two deliberate choices:

   1. All reveal styles are scoped to .js, which script.js adds to <html>. If
      the script fails to load, is blocked, or the visitor has JS disabled, the
      .js class never appears and every section renders fully visible. An
      animation layer must never be able to hide content.

   2. Everything honours prefers-reduced-motion. The reduced block at the foot
      of this file sets every animated property to its settled value, so a
      reduced-motion visitor sees the finished composition instantly rather
      than a broken intermediate state.
   ========================================================================== */

:root {
  /* One easing curve for the whole site. Overshoot-free, fast out of the gate,
     long settle — reads as confident rather than bouncy. */
  --ease: cubic-bezier(.16, 1, .3, 1);
  --ease-io: cubic-bezier(.65, 0, .35, 1);
  /* Registered so a conic gradient can be animated through it. Browsers
     without @property simply get a static border, which still looks right. */
  --angle: 0deg;
}

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

/* ============================================================== reveals == */
/* One primitive. Anything can opt in; --i drives the stagger so a whole grid
   animates as a sequence without per-element inline styles. */

.js .reveal {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition:
    opacity .85s var(--ease),
    transform .85s var(--ease);
  transition-delay: calc(var(--i, 0) * 75ms);
  will-change: opacity, transform;
}

.js .reveal.in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* Slightly different travel for wide elements, so a full-width heading and a
   small chip do not slide in identically. */
.js .reveal[data-r="left"] {
  transform: translate3d(-26px, 0, 0);
}
.js .reveal[data-r="scale"] {
  transform: scale(.965);
}
.js .reveal[data-r="left"].in,
.js .reveal[data-r="scale"].in {
  transform: none;
}

/* ============================================================== masking == */
/* Line-by-line headline reveal. The outer span clips, the inner one slides up
   from below it. Used for the hero and the closing CTA.

   SAFETY: this hides the page's H1 at rest, so it must not depend on
   script.js to become visible. It is a plain CSS ANIMATION rather than a
   transform + class toggle, because `animation-fill-mode: both` leaves the
   final keyframe applied after the run ends. The line therefore animates in on
   load and then STAYS visible with no JavaScript involvement at all.

   Escape hatches, in order of what happens if something fails:
     - motion.js never loads / JS off  -> .js is absent, so none of this applies
                                          and the headline is plainly visible.
     - reduced motion                 -> `animation: none` below, transform none.
     - observer never delivered       -> the CSS animation already completed.

   Note: do NOT gate this on `paused`. A paused animation freezes at its
   current time (0 for a fresh one) and fill-mode `both` then applies the FROM
   keyframe - which is the hidden 108% offset. That inverts the whole thing and
   leaves the headline permanently invisible. */

.js .mask {
  display: block;
  overflow: hidden;
  /* Descenders get clipped without a little breathing room. */
  padding-bottom: .08em;
  margin-bottom: -.08em;
}
.js .mask-i {
  display: block;
  animation: mask-rise 1.05s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 95ms);
  will-change: transform;
}
@keyframes mask-rise {
  from { transform: translate3d(0, 108%, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* ========================================================= ambient drift == */
/* Three slow gradient masses behind the hero. Transform-only keyframes on
   composited layers, so this costs a GPU blit and nothing else. */

.amb {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  opacity: .5;
  pointer-events: none;
  will-change: transform;
}
.amb-1 {
  width: 46vw; height: 46vw; right: -6vw; top: 8%;
  background: radial-gradient(circle, rgba(54, 233, 216, .3), transparent 66%);
  animation: drift-a 26s var(--ease-io) infinite alternate;
}
.amb-2 {
  width: 38vw; height: 38vw; right: 18vw; top: 34%;
  background: radial-gradient(circle, rgba(113, 148, 255, .26), transparent 68%);
  animation: drift-b 32s var(--ease-io) infinite alternate;
}
.amb-3 {
  width: 30vw; height: 30vw; right: 4vw; bottom: 2%;
  background: radial-gradient(circle, rgba(54, 233, 216, .16), transparent 70%);
  animation: drift-c 22s var(--ease-io) infinite alternate;
}
@keyframes drift-a {
  to { transform: translate3d(-7%, 9%, 0) scale(1.14); }
}
@keyframes drift-b {
  to { transform: translate3d(10%, -12%, 0) scale(1.22); }
}
@keyframes drift-c {
  to { transform: translate3d(-13%, -7%, 0) scale(.9); }
}

/* A very slow grid pan. Cheap, and it stops the hero reading as a flat image. */
.js .intro-grid {
  animation: grid-pan 40s linear infinite;
}
@keyframes grid-pan {
  to { background-position: 56px 56px, 56px 56px; }
}

/* ====================================================== glowing borders == */
/* A conic sweep masked into a 1px ring. Only two elements carry it (the
   Gateway panel and the featured price card) so it stays an accent rather
   than a texture. */

.ring { position: relative; }
.ring::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(from var(--angle),
    transparent 0deg, rgba(54, 233, 216, .75) 52deg,
    transparent 118deg, 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;
  animation: ring-spin 7s linear infinite;
  pointer-events: none;
}
@keyframes ring-spin {
  to { --angle: 360deg; }
}

/* Hover state: the sweep speeds up and brightens, so the interaction reads
   even though the border is only one pixel wide. */
.ring:hover::after { animation-duration: 2.6s; }

/* ============================================================== shimmer == */
/* One pass of light across a surface, triggered on hover. Uses a child-free
   pseudo so no markup changes are needed. */

.shine { position: relative; overflow: hidden; }
.shine::before {
  content: "";
  position: absolute;
  top: 0; left: -120%;
  width: 55%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .09), transparent);
  transform: skewX(-16deg);
  pointer-events: none;
}
.shine:hover::before { animation: sweep 1.1s var(--ease) forwards; }
@keyframes sweep {
  to { left: 130%; }
}

/* ========================================================== hover lift == */
/* Cards rise on a small pointer-driven 3D tilt. The JS writes --rx/--ry; the
   transform itself stays in CSS so the browser owns the compositing. */

.tilt {
  transform:
    perspective(900px)
    translate3d(var(--tx, 0), var(--ty, 0), 0)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translate3d(0, var(--lift, 0), 0);
  transition:
    transform .5s var(--ease),
    box-shadow .5s var(--ease);
  will-change: transform;
}
.tilt:hover { --lift: -4px; }

/* ============================================================== counters = */
/* Bars grow from zero when their panel is first seen. Width is the only thing
   animated, and the values are written as custom properties in the HTML. */
.js .gw-bar b {
  width: 0;
  transition: width 1.15s var(--ease);
  transition-delay: calc(var(--i, 0) * 90ms + 120ms);
}
.js .gw-bar.in b { width: var(--w); }

/* ====================================================== magnetic control = */
/* Buttons drift a few pixels toward the cursor. Small enough to feel
   responsive rather than silly; JS sets --mx/--my. */
.magnetic { transform: translate3d(var(--mx, 0), var(--my, 0), 0); }

/* ======================================================== scroll progress = */
/* Hairline under the header that tracks document progress. */

.progress-line {
  position: fixed;
  inset: 76px 0 auto 0;
  height: 1px;
  z-index: 51;
  transform-origin: 0 50%;
  transform: scaleX(var(--p, 0));
  background: linear-gradient(90deg, var(--teal), var(--teal2));
  pointer-events: none;
}

/* ============================================================ header state = */
/* Gains a solid backing once the hero is behind you. */
.js .site-header[data-stuck] {
  background: rgba(6, 16, 20, .82);
  border-bottom-color: rgba(255, 255, 255, .07);
}

/* ============================================================ link micro = */
/* The underline wipes in from the left instead of existing statically. */
.ulink {
  position: relative;
  display: inline-block;
}
.ulink::after {
  content: "";
  position: absolute;
  left: 0; bottom: -2px;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .45s var(--ease);
}
.ulink:hover::after, .ulink:focus-visible::after { transform: scaleX(1); }

/* ========================================================== reduced motion */
/* Collapse to the settled composition. This is a full reset of the layer, not
   a reduction: every element lands on its final value with no travel. */

@media (prefers-reduced-motion: reduce) {
  .js .reveal,
  .js .mask-i {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  /* The bars are NOT grouped with the reveals above, and they must NOT be
     gated on `.in` here either.

     Two real defects came out of this rule, both measured live:

     1. It used `width: var(--w, auto) !important` on a selector list that
        included `.js .gw-bar b`. An !important declaration outranks source
        order, so it beat `.js .gw-bar.in b` and pinned every bar to its
        `auto` fallback - which on an empty block resolves to 0px. The score
        bars rendered as empty tracks at 346px of available width.

     2. Restoring it as `.js .gw-bar.in b` fixed the normal case but broke
        REDUCED MOTION: under `prefers-reduced-motion` the reveal observer is
        bypassed, so `.gw-bar` never receives `.in`, the selector did not
        match, and the bars collapsed to 0px again.

     The fix is to target the bar directly with NO `.in` requirement and NO
     `auto` fallback. The resting width in the base layer is 0 only because
     the animation starts there; under reduced motion nothing animates, so
     the correct value is simply --w. */
  .js .gw-bar b { width: var(--w) !important; transition: none !important; }
  /* The mask now animates via @keyframes mask-rise, not a transition. Setting
     transform:none alone would NOT settle it - an animation's computed value
     wins over the declared transform. The animation must be switched off, and
     because fill-mode is `both` the removal of the animation leaves the
     element at its natural position, which is fully visible. */
  .js .mask-i { animation: none !important; }
  .js .reveal[data-r="left"],
  .js .reveal[data-r="scale"] { transform: none !important; }
  .amb, .js .intro-grid { animation: none !important; }
  .ring::after { animation: none !important; }
  .shine::before { display: none; }
  .progress-line { transition: none !important; }
  html { scroll-behavior: auto; }
}