/* =====================================================================
   AI-abled motion layer  (motion.css + motion.js)
   ---------------------------------------------------------------------
   THE IDEA. The whole company sells measurement that resolves an unknown
   into a confident answer. So nothing here drifts, floats or fades in
   softly: elements ARRIVE, hard-edged, and settle once. That is why the
   entrance material is clip-path (a wipe against the neo-brutalist
   border) rather than the usual opacity-and-rise, and why the easing is
   ease-out-expo, which decelerates hard and stops with conviction.

   THE RULE THAT SHAPES THE FILE. Content is visible by DEFAULT. Every
   animation is opt-in behind `html.mo`, a class motion.js adds only when
   it has confirmed the browser can animate and the visitor has not asked
   it not to. If the script never runs, never loads, or the visitor sets
   prefers-reduced-motion, the page renders complete and readable, with
   movement removed and only a short opacity fade kept: fewer and gentler,
   not zero. The previous inline reveal did the
   opposite (it set opacity:0 from JS and waited for an observer), which
   means a headless renderer or a failed script left whole sections
   blank. Never gate content on a transition.

   SCOPE. Every selector is prefixed .mo- or sits under html.mo, so this
   file cannot alter the existing design when the layer is off.
   ===================================================================== */

:root{
  /* Decelerating curves only. No bounce, no elastic: they draw attention
     to the animation instead of the content. */
  --mo-out: cubic-bezier(0.16, 1, 0.3, 1);      /* expo, the signature */
  /* A SECOND CURVE, FOR OPACITY ONLY. Expo dumps almost all of its
     progress in the first third, which is right for movement (the thing
     arrives and stops) and wrong for a fade (it is over before the eye
     registers it). This one is close to linear early and eases at the
     end, so the fade is actually visible for most of its duration. */
  --mo-fade-ease: cubic-bezier(0.33, 0.1, 0.25, 1);
  --mo-fast: 150ms;   /* press + hover feedback, inside the 100-160ms band */

  /* THESE WERE SHORTER, AND THE SHORTENING WAS DELIBERATE. The notes that
     used to sit here argued reveals down from 420ms to 340ms and the hero
     from 620ms to 480ms, on the ground that a UI animation over 300ms
     reads as sluggish.

     That rule is about UI: a menu, a dropdown, a control someone is
     waiting on. It does not govern a marketing page's entrance, which the
     same design guidance puts in the "can be longer" bracket, because
     nobody is blocked on it. The owner's read was that the site arrived
     too briskly to register as an entrance at all, and on the one screen
     where a visitor is not yet reading, that is the more important signal.

     So the movement is still quick and the FADE is what got longer. See
     --mo-fade below, and the hero's two parallel animations: in both
     cases the travel lands first and the opacity keeps going after it,
     which is what makes the arrival read as a fade rather than a slow
     slide. */
  --mo-base: 460ms;   /* reveals: the movement */
  --mo-fade: 760ms;   /* reveals: the opacity, deliberately outlasting it */
  --mo-slow: 720ms;   /* the hero overture only */
}

/* ---------------------------------------------------------------------
   1. REVEALS
   The from-state lives ONLY under html.mo, so it can never strand
   content. `.mo-in` is what the observer adds; the transition is on the
   base element so the return trip is defined too.
   ------------------------------------------------------------------ */
/* The reveal drives a CUSTOM PROPERTY, never `transform` directly.
   Writing `transform:none` on the settled state looked harmless and was
   not: `html.mo [data-mo].mo-in` scores (0,3,1) and outranks
   `.tbcard:hover` at (0,2,0), so the card hover lift silently stopped
   working the moment a card revealed. Owning a variable instead leaves
   `transform` free for hover, and any future hover, to claim.

   clip-path is likewise gone. It clips PAINT, not just layout, so it
   erased the hard offset shadow that carries this brand, and an
   un-rounded inset() against a 22px radius left a square shadow nub
   poking out of every card corner. The wipe is not worth the signature. */
html.mo [data-mo]{
  opacity: 0;
  /* `translate` and `scale` are INDEPENDENT properties, not shorthands for
     `transform`. The browser composes all three, so the page's own
     `.tbcard:hover{transform:translateY(-6px)}` keeps working untouched.
     Declaring `transform` here instead, at any specificity that beats a
     bare `.card:hover`, silently killed that hover: the first attempt at
     this file did exactly that, and moving the declaration from .mo-in to
     the base rule did not help, because the base rule outranks it too.
     The only real fix is to stop competing for the property at all. */
  translate: var(--mo-x, 0) var(--mo-y-in, 0);
  scale: var(--mo-s, 1);
  /* Opacity runs longer than the movement and on the gentler curve, so the
     element settles into place and then finishes fading. Written as three
     separate transitions rather than one shorthand precisely so the two
     can differ. */
  transition:
    opacity var(--mo-fade) var(--mo-fade-ease) var(--mo-delay, 0ms),
    translate var(--mo-base) var(--mo-out) var(--mo-delay, 0ms),
    scale var(--mo-base) var(--mo-out) var(--mo-delay, 0ms);
  /* will-change is armed by .mo-arm just before the element reaches the
     viewport, not here. Declaring it on the base rule promoted every
     tagged element to its own compositor layer at load, on card-heavy
     pages dozens at once, none of them animating yet: exactly the memory
     pressure will-change exists to avoid. */
}
html.mo [data-mo].mo-in{ opacity: 1; --mo-x: 0px; --mo-y-in: 0px; --mo-s: 1; }
html.mo [data-mo].mo-arm{ will-change: opacity, translate; }

/* Differentiated per content type, which is the point: a stat band, an
   ordered sequence and a row of cards should not arrive the same way.
   Travel stays small; long travel reads as a slideshow. */
html.mo [data-mo="rise"]{ --mo-y-in: 12px; }
html.mo [data-mo="fall"]{ --mo-y-in: -10px; }
/* Cards enter laterally, across the reading direction, so a row of them
   reads as one motion rather than four things bobbing. */
html.mo [data-mo="wipe"]{ --mo-x: -14px; }
html.mo [data-mo="wipe-up"]{ --mo-y-in: 14px; }
/* The markers in a numbered sequence scale up in order. */
/* .96, not .82: nothing in the real world appears from almost nothing, and
   the harder pop drew attention to itself instead of to the content. It
   moved from .92 to .96 when the entrance lengthened, for the same reason
   every other offset shrank: over a longer duration the same distance
   stops reading as arrival and starts reading as drift. */
html.mo [data-mo="pop"]{ --mo-s: .96; }

/* Stagger. Set --i on each sibling; the cap stops a long list turning
   into a queue the reader waits on. 10 x 55ms = 550ms worst case. */
html.mo [data-mo][style*="--i"]{ --mo-delay: calc(min(var(--i, 0), 9) * 55ms); }

/* ---------------------------------------------------------------------
   2. THE HERO OVERTURE
   One rehearsed entrance, played once, on the only screen where the
   visitor is not yet reading. Runs on load rather than on scroll: the
   hero is already in view, so an observer would fire instantly anyway.
   ------------------------------------------------------------------ */
html.mo [data-mo-hero]{
  opacity: 0;
  /* TWO ANIMATIONS, NOT ONE, so opacity and movement can differ in both
     duration and curve exactly as they do on the scroll reveals above.
     The first attempt did this with a 55% keyframe inside one timeline,
     which does not work as intended: the element's easing is applied to
     EACH keyframe segment, so the opacity curve kinks where the transform
     keyframe sits. Two animations have no such interaction. */
  animation: moHeroMove var(--mo-slow) var(--mo-out) forwards,
             moHeroFade var(--mo-fade) var(--mo-fade-ease) forwards;
  /* 80ms, up from 65ms, and this is the outer edge on purpose. The old
     note here warned that past roughly 80ms a stagger stops reading as
     one gesture and starts reading as items queuing up, and that is still
     true of a SHORT entrance: at 480ms each step was almost finished
     before the next began, so a wider gap would have shown the seams.

     At 720ms the steps overlap for most of their lives, so the sequence
     still resolves as one movement while giving the eye long enough to
     follow it down the hero. Do not push this further without lengthening
     --mo-slow to match; the two are a ratio, not two free numbers. */
  animation-delay: calc(var(--i, 0) * 80ms);
}
/* THE MOVEMENT LANDS AT 55 PER CENT AND THE FADE CARRIES ON.

   A keyframe animation cannot give opacity and transform different
   durations the way a transition can, so the split is done inside the
   timeline instead: the travel is finished a little past halfway and the
   remaining 45 per cent is pure opacity. That is the whole difference
   between "it slid in slowly", which reads as sluggish, and "it faded
   in", which is what was asked for. Travel is 12px rather than 20px for
   the same reason: the fade is carrying the entrance now, so the movement
   only has to hint at direction. */
@keyframes moHeroMove{
  from{ transform: translate3d(0, 12px, 0); }
  to  { transform: none; }
}
@keyframes moHeroFade{
  from{ opacity: 0; }
  to  { opacity: 1; }
}
/* The headline arrives with more travel and a touch of scale, so the
   biggest type on the page lands rather than fades. It used to use a
   clip-path wipe; that is gone for the same reason it left the cards.
   The animation is fill-mode:forwards, so the to-state persists for the
   life of the page, and a persisting clip-path clips PAINT: the lime
   highlight and any shadow behind the headline would be permanently
   cropped to the text box. A reveal must not leave a trap behind it. */
/* Only the MOVEMENT differs for the headline; it shares the same fade, so
   the whole overture finishes together rather than the biggest type
   settling on its own clock. */
html.mo [data-mo-hero="head"]{ animation-name: moHeroHeadMove, moHeroFade; }
@keyframes moHeroHeadMove{
  from{ transform: translate3d(0, 16px, 0) scale(.99); }
  to  { transform: none; }
}

/* ---------------------------------------------------------------------
   3. PRESS PHYSICS  (deliberately absent)
   Every page already defines its own, and they are TUNED: index.html uses
   .btn:active{transform:translate(3px,3px);box-shadow:2px 2px} against a
   6px shadow, so the surface travels almost exactly as far as the shadow
   retracts and the shadow's outer corner stays planted. That is what makes
   the button read as pressing INTO the page.

   This file previously shipped a generic .mo-press that loaded later at the
   same specificity, won the tie, and moved the surface 1px while the page's
   own rule still retracted the shadow 4px. The shadow detached and the
   button read as shrinking away from a sticker behind it: soft and rubbery,
   the opposite of the personality it was supposed to reinforce.

   The lesson is that a shared layer must not compete with a design system
   that already solved the problem per page. Reveals are what this file adds.
   ------------------------------------------------------------------ */

/* ---------------------------------------------------------------------
   5. NUMBER COUNT-UP
   Tabular figures stop the width jittering while the digits change,
   which is the thing that makes most count-ups look cheap.
   ------------------------------------------------------------------ */
html.mo [data-mo-count]{ font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------
   PRINT
   Printing does not scroll, so an observer-driven reveal never fires and
   whole sections print blank. Verified: a headless print before the
   failsafe dropped the entire stat band, the four-step journey and the
   toolbox cards out of the PDF. Print always shows everything.
   ------------------------------------------------------------------ */
@media print{
  html.mo [data-mo],
  html.mo [data-mo].mo-in,
  html.mo [data-mo-hero]{
    opacity: 1 !important;
    translate: none !important;
    scale: none !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }
}

/* ---------------------------------------------------------------------
   6. REDUCED MOTION
   Not a downgrade: the same page, arriving instantly. Everything is
   reset rather than merely sped up, so no from-state can survive.
   motion.js also refuses to add html.mo, so this is belt and braces.
   ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce){
  /* Reduced motion means FEWER AND GENTLER animations, not none. Killing
     everything was the wrong reading: a soft opacity fade carries no motion
     signal, cannot trigger vestibular discomfort, and still tells the reader
     that a new block has arrived. What has to go is MOVEMENT, so translate
     and scale are neutralised while the fade is kept and shortened. */
  html.mo [data-mo]{
    translate: none !important;
    scale: none !important;
    transform: none !important;
    transition: opacity 180ms var(--mo-out) !important;
  }
  html.mo [data-mo].mo-in{ opacity: 1 !important; }

  /* The hero keyframes move as well as fade, so they are replaced outright
     by a fade of the same shape rather than merely disabled. */
  html.mo [data-mo-hero]{
    /* opacity:1 !important is load-bearing. A CSS animation declaration
       outranks the base opacity AND any inline style, so without this the
       block's own !important animation defeated the JS hero failsafe: with
       a stalled clock (prerender, hidden tab, headless capture) the
       headline painted at opacity 0 with nothing left to rescue it. The
       @media print block above already asserts this; this one did not. */
    opacity: 1 !important;
    animation: moFadeOnly 180ms var(--mo-out) forwards !important;
    animation-delay: calc(var(--i, 0) * 40ms) !important;
    transform: none !important;
    clip-path: none !important;
  }
  @keyframes moFadeOnly{ from{ opacity: 0 } to{ opacity: 1 } }

}
