/* ChairClick™ — motion layer
   ---------------------------------------------------------------------------
   Additive ONLY. This file never changes layout, spacing, colour or type.

   ⛔ THE RULE THAT MATTERS HERE: never animate `filter`, and never leave a
   blurred element animating forever.

   `transform` and `opacity` are composited — the GPU moves an already-painted
   texture and the main thread does nothing. `filter: blur()` is NOT. Every
   frame re-rasterises the element at a new blur radius, and a large blurred
   box costs the raster thread dearly whether or not it is on screen.

   The first version of this file got that wrong: it put `filter: blur(6px)`
   on all ~20 reveal targets and added four more `blur(60px)` blobs on infinite
   `alternate` loops, on top of the two `blur(80px)` ones the hero already had.
   Six permanently re-rasterising blurred layers is what "laggy" was. The blurs
   are gone; the movement is not.

   ⛔ Do not put a layout property (width/height/top/left/margin/padding/
   font-size) in this file either.

   Remove the two <link>/<script> tags for motion.css + motion.js and the site
   is exactly as it was.
   --------------------------------------------------------------------------- */

/* ===========================================================================
   1. Reveal upgrade — directional entry
   ---------------------------------------------------------------------------
   site.js owns the IntersectionObserver and tags elements `.rv` / `.in`.
   This only restyles the *pre-entry* pose. Every modifier is written
   `:not(.in)` so the resting state always wins and nothing can get stuck.

   Transform + opacity only. No filter. See the header.
   =========================================================================== */

.rv {
  transition:
    opacity   .62s var(--ease),
    transform .62s var(--ease);
}

/* Headings drop IN from above while the content below rises to meet them.
   Opposing directions are what read as depth rather than as a list sliding up. */
.rv.rv-down:not(.in) { transform: translate3d(0, -26px, 0); }
.rv.rv-up:not(.in)   { transform: translate3d(0,  32px, 0); }
.rv.rv-zoom:not(.in) { transform: scale(.972); }
.rv.rv-card:not(.in) { transform: translate3d(0, 28px, 0) scale(.985); }

/* ⚠ The base rule in styles.css pins `will-change` on every reveal for the
   life of the page — 20-40 permanent compositor layers on the long pages.
   Release each one the moment it has finished animating. */
.rv.in { will-change: auto; }

/* ===========================================================================
   2. Hero entrance
   ---------------------------------------------------------------------------
   The hero is above the fold, so an observer would fire instantly and look
   broken. It runs off `.hero-lit`, which motion.js sets on the first frame.
   Transform + opacity only, so there is no CLS — the elements occupy their
   final boxes from the very first paint.
   =========================================================================== */

.hero-anim .hero h1,
.hero-anim .hero .trust-line,
.hero-anim .hero .lede,
.hero-anim .hero .hero-ctas,
.hero-anim .hero .hero-note,
.hero-anim .hero .proofline {
  opacity: 0;
  transition:
    opacity   .7s var(--ease),
    transform .7s var(--ease);
  transition-delay: var(--hd, 0s);
}

/* eyebrow and headline arrive from above, everything under them from below */
.hero-anim .hero .trust-line { transform: translate3d(0, -16px, 0); }
.hero-anim .hero h1          { transform: translate3d(0, -22px, 0); }
.hero-anim .hero .lede       { transform: translate3d(0,  20px, 0); }
.hero-anim .hero .hero-ctas  { transform: translate3d(0,  24px, 0); }
.hero-anim .hero .hero-note  { transform: translate3d(0,  16px, 0); }
.hero-anim .hero .proofline  { transform: translate3d(0,  16px, 0); }

.hero-anim.hero-lit .hero h1,
.hero-anim.hero-lit .hero .trust-line,
.hero-anim.hero-lit .hero .lede,
.hero-anim.hero-lit .hero .hero-ctas,
.hero-anim.hero-lit .hero .hero-note,
.hero-anim.hero-lit .hero .proofline {
  opacity: 1;
  transform: none;
}

/* ⛔ Do NOT give `h1 .accent` its own entrance.
   It is an inline <span>, and transforms do not apply to non-replaced inline
   boxes — so animating it requires `display: inline-block`, which recomputes
   its height from `line-height: .9` (88px inline box -> 62px block box) and
   nudges the script word up inside the headline. The <h1> measures the same,
   so it does not shift the page, but the flourish itself lands in a different
   place, and the hero was explicitly not to move.
   The word already rides the headline's own entrance. Leave it alone. */

/* ===========================================================================
   3. The hand-drawn lines
   ---------------------------------------------------------------------------
   Each `.deco` path draws itself in ONCE, when it first scrolls into view,
   and then stays drawn.

   ⚠ It used to loop forever — draw, hold, erase, repeat. Reverted by owner
   decision 2026-08-18: the drawing reads as a flourish the first time and as a
   distraction every time after. **Do not reintroduce the loop.**

   How it works: the paths carry `pathLength="1"`, so the whole stroke is one
   unit long regardless of its real geometry. With `stroke-dasharray: 1 1`
   (one unit of dash, one unit of gap):
       offset  1  -> the gap covers the path, nothing visible
       offset  0  -> the dash covers the path, fully drawn
   So a single slide from 1 to 0 is the whole effect. `forwards` holds it at 0.

   ⭐ **Why it draws on scroll rather than on load.** The pause observer below
   fires on `observe()`, so any `.deco` below the fold starts with `.m-paused`
   and its animation is paused before it plays a frame. Scrolling near it
   removes the class and the stroke draws then. That is free — no extra code —
   and it is the reason the effect is still visible on a 8800px page instead of
   being long finished by the time you reach it.

   ⛔ `.nodraw` paths are excluded and must stay excluded. They live inside
   scaled <g> groups, and `pathLength` miscounts through a transform — the
   stroke renders at 1/scale of its true length and the line comes out
   visibly truncated. That is why the class exists. Do not "fix" it by
   removing it; bake the transform into the path coordinates instead.

   ⭐ `stroke-dashoffset` repaints the path every frame, unlike a composited
   transform. That cost is now **bounded** — each path repaints for a few
   seconds once, then never again — rather than forever on every path on the
   page, which is what the loop was doing.
   =========================================================================== */

.deco path:not(.nodraw) {
  stroke-dasharray: 1 1;
  stroke-dashoffset: 1;
  animation: drawonce var(--draw-dur, 3.4s) var(--ease) forwards;
  animation-delay: var(--draw-delay, 0s);
}

@keyframes drawonce {
  from { stroke-dashoffset: 1; }   /* unwritten */
  to   { stroke-dashoffset: 0; }   /* the hand finishes the stroke, and stops */
}

/* ===========================================================================
   4. Pausing anything that animates off screen
   ---------------------------------------------------------------------------
   The single biggest reason this page felt heavy: infinite animations do not
   stop when they scroll out of view. The browser keeps ticking the marquee,
   the CTA pan, the hero blooms and now the line-art no matter where you are
   on an 8800px page.

   motion.js puts `.m-paused` on each host as it leaves the viewport. Covers
   pseudo-elements too, since the hero's blooms are ::before/::after and can
   only be reached through their host.
   =========================================================================== */

.m-paused,
.m-paused::before,
.m-paused::after,
.m-paused path,
.m-paused .track,
.m-paused .inner { animation-play-state: paused !important; }

/* The hero's two `blur(80px)` blooms predate this file. They animate transform
   only, so promoting them to their own layer means the expensive blur is
   rasterised ONCE and the loop just slides the finished texture around.
   Without this the browser is free to re-blur a 720px box every frame. */
.hero.wash::before,
.hero.wash::after { will-change: transform; }

/* ===========================================================================
   5. Scroll progress rail
   ---------------------------------------------------------------------------
   motion.js writes `--sp` (0 -> 1) once per frame; the bar is a scaleX on one
   fixed element, so the whole effect is a single composited transform.
   =========================================================================== */

.scroll-rail {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: 60;              /* over the sticky header (50) */
  pointer-events: none;
  transform-origin: 0 50%;
  transform: scaleX(var(--sp, 0));
  background: linear-gradient(90deg, var(--blue), #4d8bff);
  opacity: 0;
  transition: opacity .4s var(--ease);
}
.scroll-rail.on { opacity: 1; }

/* ===========================================================================
   6. Parallax
   ---------------------------------------------------------------------------
   motion.js writes `--py` in px from its single rAF loop. Hero decoration
   only, and kept small — more than ~40px of drift reads as a rendering fault.
   =========================================================================== */

[data-par] {
  transform: translate3d(0, var(--py, 0px), 0);
  will-change: transform;
}

/* ===========================================================================
   7. Pointer tactility
   ---------------------------------------------------------------------------
   A light that tracks the cursor across a card. Costs nothing at rest
   (`opacity: 0`, nothing painted) and only ever applies to the one card under
   the pointer — and hovering is not scrolling, so it cannot cost scroll
   frames.
   =========================================================================== */

@media (hover: hover) and (pointer: fine) {
  .card, .tier, .panel, .product-card, .tile, .glass, .form-card {
    position: relative;
  }
  .card::after, .tier::after, .panel::after,
  .product-card::after, .tile::after, .glass::after, .form-card::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity .3s var(--ease);
    background: radial-gradient(
      380px circle at var(--mx, 50%) var(--my, 50%),
      rgba(0, 87, 253, .07), transparent 62%);
  }
  .card:hover::after, .tier:hover::after, .panel:hover::after,
  .product-card:hover::after, .tile:hover::after,
  .glass:hover::after, .form-card:hover::after { opacity: 1; }
}

/* ===========================================================================
   8. Smooth-scroll handshake
   ---------------------------------------------------------------------------
   ⚠ `html { scroll-behavior: smooth }` and a JS easing loop fight each other:
   the browser eases toward one target while the loop eases toward another and
   anchor jumps stutter. motion.js sets `.js-scroll` when it takes over. If the
   script never runs the class is never set and native smoothing stays.
   =========================================================================== */

html.js-scroll { scroll-behavior: auto; }

/* ===========================================================================
   9. Reduced motion — one switch, kills everything above
   =========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .rv, .rv:not(.in) { transform: none !important; }
  .hero-anim .hero h1,
  .hero-anim .hero .trust-line,
  .hero-anim .hero .lede,
  .hero-anim .hero .hero-ctas,
  .hero-anim .hero .hero-note,
  .hero-anim .hero .proofline {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  /* leave the line-art drawn, just stop redrawing it */
  .deco path:not(.nodraw) {
    animation: none !important;
    stroke-dashoffset: 0 !important;
  }
  [data-par] { transform: none !important; }
  .scroll-rail { display: none; }
}
