/* The motion layer — shared by the landing page and the member app.
 *
 * WHY THIS IS A SEPARATE FILE, AND WHY IT IS SURFACE-AGNOSTIC.
 * `launch.css` and `app/app.css` deliberately disagree about colour: the marketing page is a
 * poster and the app is a tool, and they carry different token names (`--paper`/`--ink` vs
 * `--bg`/`--card`) for that reason. Motion is the one thing they should NOT disagree about —
 * a reveal that rises 14px on the landing page and 40px in the app reads as two products.
 * So the primitives live here once, and they are built almost entirely out of `transform`
 * and `opacity`, which need no colour tokens at all. The two places that do need a colour
 * read `--motion-accent`, which each surface sets to its own green.
 *
 * COMPOSITOR-ONLY, ON PURPOSE. Every animation below moves `transform`, `opacity` or
 * `stroke-dashoffset`. Nothing here animates width, height, top, left or a box-shadow blur,
 * because those relayout or repaint on every frame and this runs on phones in a room with
 * bad wifi.
 *
 * THE NO-JAVASCRIPT STATE IS THE VISIBLE ONE. Reveals are keyed off `.js-motion` on <html>,
 * which is set by a one-line inline snippet in the document head. A member with JavaScript
 * blocked, or a crawler, gets the fully composed page with no hidden content — the failure
 * mode of a scroll-reveal library is a blank page, and that is not a failure mode this page
 * is allowed to have.
 */

:root{
  /* Overridden per surface. The fallback is the app's green so a page that forgets to set
     it still gets something in the family rather than a browser default. */
  --motion-accent:#3E9C63;

  /* One easing for everything that enters, one for everything that responds to a pointer.
     `--ease-out` is a gentle overshoot-free decelerate; UI that arrives should stop, not
     bounce. `--ease-spring` has a little life in it and is reserved for hover and press,
     where the user caused the motion and expects it to feel physical. */
  --ease-out:cubic-bezier(.22,1,.36,1);
  --ease-spring:cubic-bezier(.34,1.4,.64,1);

  --dur-fast:.18s;
  --dur:.5s;
  --dur-slow:.9s;
}

/* ------------------------------------------------------------------ reveal on scroll
 *
 * The workhorse. `data-reveal` on anything; motion.js adds `.is-in` when it crosses into
 * view and then stops watching it. Elements reveal ONCE — a section that re-fades every
 * time you scroll back past it turns a page into a slideshow.
 */

.js-motion [data-reveal]{
  opacity:0;
  /* 14px, not 40. The distance should read as "this settled into place", not as "this flew
     in from off-screen"; long travel on a page of text makes every scroll feel like a
     transition and it is the single most common way this effect is overdone. */
  transform:translate3d(0,14px,0);
  transition:opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
  /* Staggered children set `--i` (see .m-stagger). The bare element gets 0ms. */
  transition-delay:calc(var(--i,0) * 70ms);
}
.js-motion [data-reveal].is-in{ opacity:1; transform:none; }

/* Variants, for the few places a straight rise is the wrong gesture. */
.js-motion [data-reveal="left"] { transform:translate3d(-18px,0,0); }
.js-motion [data-reveal="right"]{ transform:translate3d(18px,0,0); }
.js-motion [data-reveal="scale"]{ transform:scale(.96); }

/* Stagger: motion.js numbers the direct children of `.m-stagger` into `--i`, so a grid of
   cards arrives as a sequence rather than as a slab. Capped in JS at 8 — beyond that the
   last card is waiting most of a second and the effect stops reading as intentional. */

/* ------------------------------------------------------------------ pointer tilt (3D)
 *
 * Real 3D: the card rotates about X and Y in a perspective space, so its far edge is
 * genuinely further away rather than being a skew that imitates one. motion.js writes
 * `--rx`/`--ry` in degrees from the pointer position; CSS does the rest, so there is no
 * per-frame style recalculation beyond two custom properties.
 *
 * `.m-tilt-in` is the child that lifts toward the viewer — separating it from the rotating
 * parent is what makes the card read as having depth instead of as a rotating rectangle.
 */
.m-tilt{ perspective:900px; }
.m-tilt-in{
  transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateZ(0);
  transform-style:preserve-3d;
  transition:transform .35s var(--ease-out), border-color var(--dur-fast) ease;
  will-change:transform;
}
/* While the pointer is actually moving, drop the transition or the card lags behind the
   cursor by a third of a second and feels broken. It comes back for the return-to-rest. */
.m-tilt.is-tilting .m-tilt-in{ transition:border-color var(--dur-fast) ease; }

/* ------------------------------------------------------------------ SVG line drawing
 *
 * Used for the connector down the session steps and for the lattice's edges. The dash
 * length is set from `getTotalLength()` in JS because a hard-coded one is wrong the moment
 * the path or the viewport changes.
 */
.m-draw{
  stroke-dasharray:var(--len,1000);
  stroke-dashoffset:var(--len,1000);
  transition:stroke-dashoffset 1.4s var(--ease-out);
}
.m-draw.is-in{ stroke-dashoffset:0; }

/* ------------------------------------------------------------------ numbers that count
 *
 * `data-count="45"` counts up from zero when the element enters view. `tabular-nums` is not
 * optional here: without it the digits have different widths and the number visibly jitters
 * its own layout on every frame while counting.
 */
[data-count]{ font-variant-numeric:tabular-nums; }

/* ------------------------------------------------------------------ ambient
 *
 * The only motion on either surface that runs without being asked. Both are slow enough to
 * sit under text, and both are switched off entirely under reduced motion.
 */

/* A live indicator: something is genuinely happening on a timer. Used on the next-session
   marker, never as decoration. */
@keyframes m-pulse{
  0%        { transform:scale(1);   opacity:1; }
  70%,100%  { transform:scale(2.6); opacity:0; }
}
.m-live{ position:relative; }
.m-live::after{
  content:""; position:absolute; inset:0; border-radius:inherit;
  background:currentColor; animation:m-pulse 2.4s var(--ease-out) infinite;
  pointer-events:none;
}

/* A slow breath, for the hero glow. 18s so it is never the thing you are looking at. */
@keyframes m-drift{
  0%,100%{ transform:translate3d(0,0,0)      scale(1);    opacity:.65; }
  50%    { transform:translate3d(2%,-3%,0)   scale(1.12); opacity:1;   }
}

/* Chai steam. Three plumes on offset delays, rising and fading. */
@keyframes m-steam{
  0%  { transform:translate3d(0,6px,0) scaleX(.85); opacity:0; }
  22% { opacity:.55; }
  100%{ transform:translate3d(3px,-30px,0) scaleX(1.5); opacity:0; }
}

/* A one-shot sheen across a primary button on hover. Transform only — the element it moves
   is a fixed gradient, so nothing repaints. */
@keyframes m-sheen{
  from{ transform:translateX(-140%) skewX(-18deg); }
  to  { transform:translateX(260%)  skewX(-18deg); }
}

/* ------------------------------------------------------------------ press
 *
 * Buttons and cards acknowledge a press. 60ms and 1px — enough to feel connected to the
 * finger, not enough to be a bounce.
 */
.m-press{ transition:transform 60ms ease; }
.m-press:active{ transform:translateY(1px) scale(.995); }

/* ------------------------------------------------------------------ reduced motion
 *
 * NOT a dimming — a removal. Everything autonomous stops, everything that travels arrives
 * already in place, and the tilt flattens. The page keeps every word and every photograph;
 * it simply stops moving. This mirrors the reasoning already in `launch.css` about the
 * photo marquee: the content is never what is taken away.
 */
@media (prefers-reduced-motion:reduce){
  .js-motion [data-reveal],
  .js-motion [data-reveal].is-in{
    opacity:1 !important; transform:none !important; transition:none !important;
  }
  .m-draw{ stroke-dasharray:none !important; stroke-dashoffset:0 !important; transition:none !important; }
  .m-tilt-in{ transform:none !important; transition:none !important; }
  .m-live::after{ animation:none; display:none; }
  .m-press:active{ transform:none; }

  /* Anything that opted into an ambient keyframe stops where it is.
   *
   * THE TWO DELAY LINES ARE NOT DECORATION, they are the whole correctness of this block.
   * Collapsing only the DURATION leaves the delay intact, and an entrance animation written
   * `p-rise .5s both .385s` holds its hidden first frame for that entire delay before
   * snapping to visible. The result is content flashing in over a third of a second — which
   * is a worse experience than the animation was, delivered specifically to the people who
   * asked for less movement. Zeroing the delay is what makes "instant" actually instant. */
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-delay:0ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    transition-delay:0s !important;
    scroll-behavior:auto !important;
  }
}
