/* ════════════════════════════════════════════════════════════════════════════════
   Niru — the landing motion layer (shared by getniru.com's pages, with motion.js, gradient.js
   and vendor/lenis.min.js). See info/archive/2026-09/build-notes/build-notes-landing-motion.md.

   THREE STATES, set on <html> by the gate script in each page's <head>, before first paint:
   · no class      JavaScript is off. Nothing in this file applies; the page reads as written.
   · .js           Scripts run: the enhanced, still design (the gradient hero) may apply.
   · .js.motion    ...and the visitor has not asked for reduced motion: start states (lowered,
                   masked, dim) and the scroll choreography. motion.js sets window.NiruMotion
                   when it arrives; the gate drops .motion after 3 s if it never does, so no
                   start state can outlive a script that failed to load.

   Every animation here moves transform and opacity only.
   ════════════════════════════════════════════════════════════════════════════════ */

:root{
  --m-ease:cubic-bezier(0.16,1,0.3,1);      /* entrances: fast out, long settle */
  --m-ease-io:cubic-bezier(0.76,0,0.24,1);  /* the curtain: slow start, slow finish */
  --m-dim:.2;                               /* a word the reader has not reached yet */
  --m-soft:2.5;                             /* how many words are mid-light at once */
}

/* Lenis rides the window's native scroll; it only needs html/body to size to their content. */
html.lenis,html.lenis body{height:auto}

/* ── The hero's gradient field (gradient.js paints a canvas into it; this is also its fallback) ── */
html.js [data-niru-gradient]{
  /* The field's palette: the page ground and brand teal, with two deep neighbours chosen by eye
     between them (hue 178 and 197). The shader reads these same four tokens. */
  --g-ink:var(--bg,#0B0C0F); --g-deep:#0A2530; --g-teal:#0E4A48; --g-accent:var(--accent,#2DD4BF);
  display:block;position:absolute;left:0;right:0;bottom:0;top:calc(-1 * var(--nav-h,0px));
  z-index:-1;overflow:hidden;pointer-events:none;
  /* fade the whole field out at its foot, so it hands over to whatever ground lies beneath it
     (a fade to solid ink left a band one step darker than the page's own glow) */
  -webkit-mask-image:linear-gradient(180deg,#000 70%,transparent);mask-image:linear-gradient(180deg,#000 70%,transparent);
  /* the still gradient that stands when WebGL cannot: the same light, drawn in CSS */
  background:
    radial-gradient(30% 20% at 44% 3%,color-mix(in srgb,var(--g-accent) 34%,transparent),transparent 72%),
    radial-gradient(50% 62% at 88% 46%,color-mix(in srgb,var(--g-teal) 92%,transparent),transparent 74%),
    radial-gradient(62% 52% at 34% 16%,color-mix(in srgb,var(--g-deep) 90%,transparent),transparent 72%),
    linear-gradient(0deg,var(--g-ink) 0%,transparent 40%),
    var(--g-ink);
}
html.js [data-niru-gradient] canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
html.js [data-niru-gradient].is-fallback canvas{display:none}
/* shade under the copy (left) and the nav (top), so text keeps its contrast whatever drifts by.
   Measured, not guessed: the hero's text is checked against the field's lightest pixels across
   four minutes of drift (see the build note); these stops hold every line at WCAG AA. */
html.js [data-niru-gradient]::before{content:'';position:absolute;inset:0;z-index:1;
  background:
    linear-gradient(180deg,color-mix(in srgb,var(--g-ink) 72%,transparent) 0,transparent calc(var(--nav-h,0px) + 7rem)),
    /* stops ride the centred text column (50% + n rem), not the viewport's width, so the shade
       ends where the copy ends at every size */
    linear-gradient(90deg,color-mix(in srgb,var(--g-ink) 74%,transparent) 0%,color-mix(in srgb,var(--g-ink) 66%,transparent) calc(50% + 4rem),
      color-mix(in srgb,var(--g-ink) 50%,transparent) calc(50% + 16rem),color-mix(in srgb,var(--g-ink) 14%,transparent) calc(50% + 26rem),transparent calc(50% + 34rem))}
/* narrow screens: the copy spans the whole width, so the whole field sits under text */
@media (max-width:720px){
  html.js [data-niru-gradient]::before{background:
    linear-gradient(180deg,color-mix(in srgb,var(--g-ink) 72%,transparent) 0,transparent calc(var(--nav-h,0px) + 7rem)),
    linear-gradient(90deg,color-mix(in srgb,var(--g-ink) 74%,transparent),color-mix(in srgb,var(--g-ink) 68%,transparent))}
}
/* No grain over the field: noise over a gradient is the vibe audit's tell 20, and measured, the grain
   hid no banding (the field shows the same steps with or without it, and only under a contrast stretch). */

/* ── The curtain: one brand-colour panel lifts off the page on load. Under 1 s, never blocks a
      click (pointer-events:none), never runs again on that page view. ── */
html.motion body::after{content:'';position:fixed;inset:0;z-index:100;pointer-events:none;
  background:var(--m-curtain,var(--accent,#2DD4BF));border-radius:0 0 50% 50% / 0 0 5vh 5vh;
  animation:m-curtain .76s var(--m-ease-io) .1s both}
@keyframes m-curtain{from{transform:translate3d(0,0,0)}to{transform:translate3d(0,-106%,0)}}

/* ── Rise and settle on entry (the page's own .reveal / .float, now hidden only with motion) ── */
html.motion .reveal{opacity:0;transform:translate3d(0,18px,0);transition:opacity .7s var(--m-ease),transform .8s var(--m-ease)}
html.motion .reveal.d1{transition-delay:.08s}
html.motion .reveal.d2{transition-delay:.16s}
html.motion .reveal.d3{transition-delay:.24s}
html.motion .reveal.in{opacity:1;transform:none}
html.motion .float{opacity:0;transform:translate3d(0,32px,0) rotate(1.4deg) scale(.985);transition:opacity .9s var(--m-ease),transform 1.1s var(--m-ease)}
html.motion .float.left{transform:translate3d(0,32px,0) rotate(-1.4deg) scale(.985)}
html.motion .float.in{opacity:1;transform:none}

/* ── Staggered rise: the children of [data-stagger] settle one after another. An animation with
      backwards fill, so once it ends the card is back on its own styles (its hover still works). ── */
html.motion [data-stagger]:not(.in)>*{opacity:0}
html.motion [data-stagger].in>*{animation:m-rise .9s var(--m-ease) backwards;animation-delay:calc(var(--si,0) * 110ms + 60ms)}
@keyframes m-rise{from{opacity:0;transform:translate3d(0,28px,0)}}

/* ── Headings that rise out of a mask: motion.js wraps each word in .mw (the clip) > .mwi (the
      word). Inline-blocks keep the heading's own line breaks; the clip is a static clip-path,
      so the word's baseline and the heading's layout do not move. ── */
html.motion .mw{display:inline-block;clip-path:inset(-.35em -.25em -.24em -.25em)}
html.motion .mwi{display:inline-block}
html.motion [data-mask].reveal{opacity:1;transform:none}
html.motion [data-mask] .mwi{transform:translate3d(0,125%,0);transition:transform .9s var(--m-ease);transition-delay:calc(var(--wi,0) * 45ms)}
html.motion [data-mask].in .mwi{transform:none}

/* ── Word-by-word light: motion.js wraps each word in .lw with its index (--wi) and writes the
      paragraph's --lit (words lit so far, fractional) as the reader scrolls. Opacity only. ── */
html.motion [data-light]{color:var(--text,#F4F5F7)}
html.motion [data-light] .lw{opacity:calc(var(--m-dim) + (1 - var(--m-dim)) * clamp(0, (var(--lit,0) - var(--wi,0)) / var(--m-soft), 1))}

/* ── The pinned, numbered story ([data-story] > [data-story-step] > [data-story-copy] +
      [data-story-media]). Wide screens only; narrower ones read it stacked, with the reveals
      above. The copies all share one sticky cell on the left and cross-fade by state classes
      (is-past / is-active / is-future, from motion.js); the media scroll past on the right.
      position:sticky, never scroll-jacking. --pinned is how motion.js learns this layout is live
      (it reads the value; the breakpoint lives only here). Spacing reads the page's scale
      (--space-*, falling back to the same values). ── */
@media (min-width:1024px) and (min-height:600px){
  html.motion [data-story]{
    --pinned:1;
    --story-num:clamp(4.5rem,12vh,7.5rem);   /* the big numeral */
    --story-lead:calc(var(--story-num) * .82 + var(--space-32,2rem));   /* the numeral, then the heading */
    --story-top:max(calc(var(--nav-h,0px) + var(--space-24,1.5rem) + var(--story-lead)),
                    calc(50vh - (var(--story-lead) + var(--story-copy-h,15rem)) / 2 + var(--story-lead)));
    display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);column-gap:clamp(var(--space-48,3rem),5vw,var(--space-96,6rem));
    width:100%;max-width:var(--maxw,72rem);margin:0 auto;
    padding:clamp(var(--space-32,2rem),6vh,var(--space-64,4rem)) clamp(var(--space-16,1rem),4vw,var(--space-32,2rem)) clamp(var(--space-48,3rem),8vh,var(--space-64,4rem));
    counter-reset:story;
  }
  html.motion [data-story]>[data-story-step],html.motion [data-story]>[data-story-step]>.wrap{display:contents}

  /* the left column: one sticky cell, four copies stacked in it */
  html.motion [data-story-copy]{grid-column:1;grid-row:1 / span 4;align-self:start;position:sticky;top:var(--story-top);
    margin-top:var(--story-lead);min-height:var(--story-copy-h,15rem);counter-increment:story}
  html.motion [data-story-copy]:not(.is-active){pointer-events:none}
  html.motion [data-story-copy].is-active{z-index:1}

  /* the step number, 1-4, with the story's length beside it ("3 / 4"): the story's one marker. No
     leading zero ("03" is the template's move). Decorative to a screen reader (empty alt text), since
     the heading itself changes. The total is motion.js's step count (--story-n). */
  html.motion [data-story-copy]::before{content:counter(story);content:counter(story) / "";
    position:absolute;left:-.04em;top:calc(-1 * var(--story-lead));font:600 var(--story-num)/.82 var(--heading,inherit);
    letter-spacing:-.051em;color:var(--text,#F4F5F7);opacity:0;transform:translate3d(0,.3em,0);
    transition:opacity .45s var(--m-ease),transform .75s var(--m-ease)}
  html.motion [data-story-copy].is-past::before{transform:translate3d(0,-.3em,0)}
  html.motion [data-story-copy].is-active::before{opacity:.13;transform:none;transition-delay:.06s}
  /* the total: small and legible (text3, 5.1:1), on the numeral's baseline. Radio Canada's digits are
     tabular (.6em), so it sits at a fixed offset. Baseline from a line box's top = line-height / 2 + .345
     (ascent .945, descent .255): .755 of the numeral at /.82, .845 of the total at /1. It is the same
     glyphs in every copy, so it switches without a fade. */
  html.motion [data-story-copy]{counter-reset:story-total var(--story-n)}
  html.motion [data-story-copy]::after{content:"/ " counter(story-total);content:"/ " counter(story-total) / "";
    position:absolute;left:calc(var(--story-num) * .62);top:calc(-1 * var(--story-lead) + var(--story-num) * .755 - 1.1rem * .845);
    font:600 1.1rem/1 var(--heading,inherit);color:var(--text3,#7A8391);opacity:0}
  html.motion [data-story-copy].is-active::after{opacity:1}

  /* The state rules below name the state explicitly, so they outrank the entry rules above
     (.reveal / .reveal.in / [data-mask].reveal) whatever order the classes arrive in. The step
     number is the story's one marker: no label list, and no caps line above a heading. */

  /* the words: the active heading rises out of its mask, the leaving one fades as its words exit */
  html.motion [data-story] [data-story-copy]:not(.is-active) h2{opacity:0;transition:opacity .28s var(--m-ease);transition-delay:0s}
  html.motion [data-story] [data-story-copy].is-active h2{opacity:1;transition:opacity .15s var(--m-ease);transition-delay:0s}
  html.motion [data-story] [data-story-copy].is-future h2 .mwi{transform:translate3d(0,125%,0);transition-delay:0s}
  html.motion [data-story] [data-story-copy].is-past h2 .mwi{transform:translate3d(0,-125%,0);transition-delay:0s}
  html.motion [data-story] [data-story-copy].is-active h2 .mwi{transform:none;transition-delay:calc(.1s + var(--wi,0) * 40ms)}
  html.motion [data-story] [data-story-copy]:not(.is-active) .line{opacity:0;transform:translate3d(0,12px,0);
    transition:opacity .3s var(--m-ease),transform .6s var(--m-ease);transition-delay:0s}
  html.motion [data-story] [data-story-copy].is-past .line{transform:translate3d(0,-12px,0)}
  html.motion [data-story] [data-story-copy].is-active .line{opacity:1;transform:none;
    transition:opacity .45s var(--m-ease),transform .7s var(--m-ease);transition-delay:.16s}

  /* the right column: each step's picture centred in a screen-tall row, scrolling past */
  html.motion [data-story-media]{grid-column:2;display:flex;flex-direction:column;align-items:center;justify-content:center;
    min-height:min(86vh,58rem);min-height:min(86svh,58rem)}   /* capped, so a tall monitor does not stretch the story */
  html.motion [data-story-media]>*{width:100%}
}

/* Print: every start state lands, whatever the scroll position. */
@media print{
  html.motion .reveal,html.motion .float,html.motion [data-stagger]>*,html.motion .mwi,html.motion [data-light] .lw,
  html.motion [data-story-copy] h2,html.motion [data-story-copy] .line{opacity:1!important;transform:none!important;animation:none!important}
  html.motion body::after{display:none}
}
