/* ==========================================================================
   Caching Out — Motion layer
   --------------------------------------------------------------------------
   Load AFTER tokens.css. Zero dependencies.

   Two tiers:
     DEMONSTRATIVE — motion that shows the product working. Earns its cost.
     EXPRESSIVE    — motion that adds polish. Cheap, or it doesn't ship.

   Every animated property is compositor-safe (transform / opacity / filter)
   except SVG line-drawing (stroke-dashoffset), which is allowlisted: tiny
   paint area, one-shot, never looped. Verified by check-motion.mjs.

   Nothing here animates width, height, top, left, margin, padding,
   background-position, background-size or box-shadow. Those force layout or
   full-area repaints and are banned on a site that sells page speed.
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */

:root {
  --mo-ease: cubic-bezier(0.2, 0, 0, 1);          /* brand ease — hard decel */
  --mo-ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
  --mo-ease-settle: cubic-bezier(0.16, 1, 0.3, 1); /* arrives, then settles  */

  --mo-fast: 120ms;      /* hover, focus, small state changes */
  --mo-base: 180ms;      /* toggles, tabs                     */
  --mo-slow: 240ms;      /* disclosure, accordion             */
  --mo-entrance: 320ms;  /* section entrance                  */
  --mo-stagger: 60ms;    /* delay between siblings            */

  --mo-logo: 720ms;      /* logo draw, once per session       */
  --mo-flow: 2400ms;     /* request-flow walkthrough, on demand */

  --mo-rise: 12px;       /* entrance travel — never more      */
}

/* ==========================================================================
   2. REDUCED MOTION — the kill switch
   Applied first so every rule below can assume motion is permitted.
   Elements land in their FINAL state, never their initial one.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .co-rise            { opacity: 1 !important; transform: none !important; }
  .co-logo [data-draw]{ stroke-dashoffset: 0 !important; }
  .co-glow            { animation: none !important; }
  .co-card            { transform: none !important; }
  .co-flow-dot        { opacity: 0 !important; }
  .co-flow            { --flow-static: 1; }
}

/* Same treatment when the user has asked the OS to save data. */
@media (prefers-reduced-data: reduce) {
  .co-glow { animation: none !important; }
}

/* ==========================================================================
   3. KEYFRAMES
   ========================================================================== */

@keyframes co-rise {
  from { opacity: 0; transform: translate3d(0, var(--mo-rise), 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes co-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* SVG line drawing. Paths carry pathLength="100" so the maths is trivial. */
@keyframes co-draw {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}

/* The request travelling a path. offset-distance is compositor-friendly and
   avoids animating cx/cy, which would force layout on every frame. */
@keyframes co-travel {
  from { offset-distance: 0%; }
  to   { offset-distance: 100%; }
}

/* Runs alongside co-travel for the same duration: visible for the journey,
   gone on arrival. */
@keyframes co-arrive {
  0%, 84% { opacity: 1; }
  100%    { opacity: 0; }
}

/* Drifting glow behind dark bands.
   NOT an animated gradient — that repaints the full band every frame. This is
   a static blurred element moved with transform only: compositor, ~0 cost. */
@keyframes co-drift {
  0%   { transform: translate3d(-4%, -2%, 0) scale(1);    }
  50%  { transform: translate3d( 5%,  3%, 0) scale(1.12); }
  100% { transform: translate3d(-4%, -2%, 0) scale(1);    }
}

@keyframes co-pulse-once {
  0%   { opacity: 0;   transform: scale(0.94); }
  60%  { opacity: 1;   transform: scale(1.02); }
  100% { opacity: 1;   transform: scale(1);    }
}

/* ==========================================================================
   4. DEMONSTRATIVE — motion that shows the product working
   ========================================================================== */

/* --- 4.1 Logo -------------------------------------------------------------
   The arc draws (the cycle being described), then the response leaves through
   the aperture and settles past the terminals. Runs once per session; the JS
   sets data-co-seen in sessionStorage so it never replays on navigation. */

.co-logo [data-draw] {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
}
.co-logo.is-playing [data-draw="arc"] {
  animation: co-draw 420ms var(--mo-ease-soft) forwards;
}
.co-logo.is-playing [data-draw="bar"] {
  animation: co-draw 340ms var(--mo-ease-settle) 300ms forwards;
}
/* Static state for anything that never plays (already seen, reduced motion) */
.co-logo:not(.is-playing) [data-draw] { stroke-dashoffset: 0; }

/* --- 4.2 (removed 2026-10-23) The hero's score dial left with the homepage
   rebuild; .co-dial, .co-dial-arc and .co-dial-value are gone. ------------ */

/* --- 4.3 Request flow ----------------------------------------------------
   A request travels the long path, then the short one. Plays on first scroll
   into view and on demand — never loops. */

.co-flow-dot {
  offset-rotate: 0deg;
  opacity: 0;
}
/* will-change only while playing — otherwise it pins a compositor layer for
   the life of the page for a dot that runs for one second. */
.co-flow.is-playing .co-flow-dot {
  opacity: 1;
  will-change: offset-distance;
  /* co-arrive fades the dot out as it reaches the response box. Left parked
     there it sat against the box's label, and how close depended on the
     rendered width of the figure; a request that has been delivered has
     nothing left to show anyway. */
  animation: co-travel var(--flow-dur, 1100ms) var(--mo-ease-soft) var(--flow-delay, 0ms) forwards,
             co-arrive var(--flow-dur, 1100ms) linear var(--flow-delay, 0ms) forwards;
}
.co-flow [data-stage] {
  transition: opacity var(--mo-slow) var(--mo-ease);
}
.co-flow.is-bypassed [data-stage="work"] { opacity: 0.28; }

/* --- 4.4 Before / after bars --------------------------------------------
   scaleX from a fixed origin, never width. Same visual, no layout. */

.co-bar {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 720ms var(--mo-ease-settle);
}
.co-bar.is-shown { transform: scaleX(var(--bar-scale, 1)); }

/* ==========================================================================
   5. ENTRANCE
   ========================================================================== */

/* The hidden start state is opt-in: motion.js adds .co-motion-ready to <html>
   before it starts observing. Without that class - no JS, blocked JS, or a
   script error upstream - .co-rise content simply renders visible. The 0.1.15
   plugin minifier once corrupted motion.js and the unconditional opacity:0
   made the whole hero vanish; the default state must be the safe one. */
.co-motion-ready .co-rise {
  opacity: 0;
  transform: translate3d(0, var(--mo-rise), 0);
  will-change: opacity, transform;
}
.co-rise.is-in {
  animation: co-rise var(--mo-entrance) var(--mo-ease) forwards;
}
/* Stagger children of a group. Capped at 6 — beyond that it reads as slow. */
.co-stagger > .co-rise.is-in:nth-child(1) { animation-delay: 0ms; }
.co-stagger > .co-rise.is-in:nth-child(2) { animation-delay: calc(var(--mo-stagger) * 1); }
.co-stagger > .co-rise.is-in:nth-child(3) { animation-delay: calc(var(--mo-stagger) * 2); }
.co-stagger > .co-rise.is-in:nth-child(4) { animation-delay: calc(var(--mo-stagger) * 3); }
.co-stagger > .co-rise.is-in:nth-child(5) { animation-delay: calc(var(--mo-stagger) * 4); }
.co-stagger > .co-rise.is-in:nth-child(n+6) { animation-delay: calc(var(--mo-stagger) * 5); }

/* Release the compositor hint once the animation is done — otherwise
   will-change pins a layer for the life of the page. JS removes .is-in's
   sibling class after animationend; this is the belt-and-braces version. */
.co-rise.is-done { will-change: auto; }

/* ==========================================================================
   6. EXPRESSIVE — polish. Cheap, or it doesn't ship.
   ========================================================================== */

/* --- 6.1 Card lift + pointer tilt ---------------------------------------
   Tilt is capped at 1.2°. Anything more reads as a gimmick and starts to
   fight the "restraint is confidence" positioning. Hover-capable pointers
   only — no tilt on touch, where it would fire on tap. */

/* No base transform here: .co-rise sets one for its start state and the two
   would collide on any element carrying both classes. */
.co-card {
  transition: transform var(--mo-fast) var(--mo-ease),
              border-color var(--mo-fast) var(--mo-ease);
}
@media (hover: hover) and (pointer: fine) {
  .co-card:hover {
    transform: translate3d(0, -2px, 0);
    border-color: var(--border-default);
  }
  .co-card.co-tilt {
    transform-style: preserve-3d;
    transition: transform var(--mo-base) var(--mo-ease-soft),
                border-color var(--mo-fast) var(--mo-ease);
  }
  .co-card.co-tilt:hover {
    transform:
      perspective(900px)
      rotateX(calc(var(--co-ty, 0) * -1.2deg))
      rotateY(calc(var(--co-tx, 0) * 1.2deg))
      translate3d(0, -2px, 0);
  }
}

/* --- 6.2 Drifting glow ---------------------------------------------------
   Sits inside a dark band. The parent needs position:relative and
   overflow:hidden. The blur is baked into a radial-gradient on a static
   element; only transform animates, so this costs one composited layer and
   no repaints. JS pauses it when the band scrolls out of view. */

.co-glow-wrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  border-radius: inherit;
}
.co-glow {
  position: absolute;
  top: -30%;
  left: 20%;
  width: 70%;
  height: 160%;
  background: radial-gradient(
    closest-side,
    color-mix(in oklab, var(--signal-700, #046345) 62%, transparent) 0%,
    transparent 100%
  );
  opacity: 0.5;
  will-change: transform;
  animation: co-drift 22s var(--mo-ease-soft) infinite;
}
.co-glow.is-paused { animation-play-state: paused; }

/* --- 6.3 Stat counters ---------------------------------------------------
   Tabular figures are mandatory. Without them the numeral width changes on
   every tick and the surrounding text reflows sixty times a second. */

.co-count {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* --- 6.4 Nav scroll state ----------------------------------------------- */

.co-nav {
  transition: background-color var(--mo-slow) var(--mo-ease),
              box-shadow var(--mo-slow) var(--mo-ease);
}
/* No backdrop-filter here: on .co-nav it makes the nav the containing block
   for core's position:fixed mobile drawer, which then opened 71px tall below
   the first 80px of scroll - the phone menu only worked at the top of a page
   (operator, 2026-10-26). The blur lives on .co-nav::before (components.css,
   which explains the same trap). */
.co-nav.is-scrolled {
  background-color: color-mix(in oklab, var(--surface-page, #fff) 92%, transparent);
  box-shadow: var(--shadow-sm);
}

/* --- 6.5 Reveal for one-shot emphasis ----------------------------------- */

.co-pop.is-in { animation: co-pulse-once 420ms var(--mo-ease-settle) forwards; }

/* ==========================================================================
   7. UTILITIES
   ========================================================================== */

.co-anim-off *,
.co-anim-off *::before,
.co-anim-off *::after {
  animation: none !important;
  transition: none !important;
}
