/* ============================================================================
   reveals.css — two scroll-reveals lifted from Chug-SPYLT, on OUR tokens.

   Pairs with bto-js/reveals.js. Loaded from the carved shell
   (_notes/mokko-shell-head.html) so all three pages inherit the file; every
   selector below is scoped to a class that exists ONLY on index.html, so
   services.html and work.html compute nothing from it. See § 0.

   WHAT WAS LIFTED, AND WHAT WAS NOT
   The source is React 19 + Vite + Tailwind 4 and drives its scroll with
   ScrollSmoother. None of that comes across. ScrollSmoother in particular is
   permanently out: it transforms #smooth-content, which then becomes the
   containing block and kills `position: sticky` — the exact bug bto-js/
   no-smoother.js exists to prevent, and the hero's pin depends on sticky.
   What crosses over is two EFFECTS, expressed in GSAP + CSS we already load.

   THE COLOUR RULE
   SPYLT tints to #faeade — their brand cream. That hex does not exist in our
   system and is not used here. Both effects resolve entirely to MP071 tokens
   from palette.css (--fg-ink, --fg-mute, --fg-lead), so they follow the light/
   dark toggle and any future palette swap with no edit to this file.

   ============================================================================
   § 0 · HOW THIS FAILS  — read this before changing anything
   ============================================================================

   The project has already shipped one bug of this exact class: the Pixy
   fade-up class left content permanently invisible under Mokko because the
   script that was supposed to reveal it never ran.

   (That class is deliberately NOT named here. build-services.mjs guards the
   built page with /\bmil-[a-z0-9-]+/ and cannot tell a comment from a class
   attribute. This sheet is only ever LINKED, so it is not part of the string
   that guard tests — but if anyone ever inlines it, spelling the name out
   would fail the services build. Not worth the landmine.)

   Nothing here can repeat that bug, because
   NOTHING IN THIS FILE APPLIES UNTIL JAVASCRIPT SAYS SO. Two independent
   gates, both of which must be open:

     GATE 1  `html.bto-rv`      — added by a one-line inline <script> in <head>
                                  (the same idiom services.html uses for
                                  `rv-ready`). No JS engine, no class.
     GATE 2  `.bto-rv-wipe` /   — added to individual elements by reveals.js,
             `.bto-rv-w`          and only AFTER it has confirmed gsap +
                                  ScrollTrigger + SplitText are all present.
                                  reveals.js removes `.bto-rv-wipe` again the
                                  moment each reveal completes.

   So the failure matrix is:

     JS blocked / file 404      → neither gate opens → every heading is a plain
                                  unclipped heading, the lead paragraph is
                                  exactly the colour palette.css gives it today.
     GSAP or SplitText missing  → reveals.js returns before touching the DOM →
                                  same as above.
     reveals.js throws          → its try/catch removes the element classes →
                                  same as above.
     prefers-reduced-motion     → § 3: no clip, no scrub, end state only.

   There is no state in which this file can hide text. The collapsed clip-path
   in § 2 is only ever reachable through a class that JavaScript owns.
   ========================================================================== */


/* ============================================================================
   § 1 · WORD-BY-WORD COLOUR SCRUB  —  panel 2's lead paragraph
   ----------------------------------------------------------------------------
   SPYLT's signature move, and the only new motion panel 2 gets. Each word
   lights from muted grey to full ink as the reader scrolls, so the sentence
   reads as switching on line by line.

   TARGET: `.bto-banner .bto-banner-lead` — index.html only.

   HOW THE COLOUR IS DRIVEN — and why it is not a hex.
   reveals.js tweens ONE unitless custom property per word, `--rv-lit`, 0 → 1.
   The colour itself is mixed here, in CSS, from two live palette tokens. That
   matters: if the reader flips the theme mid-scroll, or the palette owner
   swaps a token, the mix recomputes from the new values. A hex resolved in JS
   at split time would be frozen against whichever theme happened to be active.

   `color-mix()` is not a new dependency — palette.css already uses it in ~20
   places (every `--on-dark` wash, the accent softening, the hairlines).

   CONTRAST, AT EVERY POINT IN THE SWEEP — not just at the ends.
   Because the mix only ever travels between two tokens, the worst case is one
   of the two endpoints. Against panel 2's surface (--bg-panel2, Polar Drift
   #CCD5DA in light):
       unlit  --fg-mute #555859   4.82:1   (palette.css § light block states
                                            this figure and notes it is chosen
                                            to clear 4.5:1 ON THE PANEL)
       lit    --fg-ink  #13181B  12.01:1
   Every intermediate frame sits between 4.82 and 12.01, so a reader who stops
   scrolling half way through the sweep is never left below the 4.5:1 floor.
   That is the reason the unlit end is --fg-mute and not something fainter:
   --fg-faint would look better and would drop the mid-scrub state under 4.5.

   ⚠ ONE VISUAL DELTA, FLAGGED FOR SIGN-OFF.
   The lit end state is --fg-ink, where palette.css § E2 currently paints this
   paragraph --fg-lead. So once the sweep completes the lead sits a step darker
   than it does today. This is deliberate — a 4.82 → 5.90 sweep (mute → lead)
   is too small a step to perceive, and an effect nobody can see is not worth a
   ScrollTrigger. TO REVERT: change --fg-ink to --fg-lead on the one line
   marked ★ below and the resting appearance is byte-identical to today; the
   effect stays, it just reads quieter.
   ---------------------------------------------------------------------------- */

html.bto-rv .bto-banner .bto-banner-lead .bto-rv-w {
  /* ★ the lit end. Swap to var(--fg-lead) to keep today's resting colour.

     `--rv-lit` CARRIES ITS OWN UNIT — it is `0%` … `100%`, never a bare
     number. The obvious form is `calc(var(--rv-lit) * 100%)` with a unitless
     0…1, and it is avoided deliberately: that puts a calc() in color-mix's
     percentage slot, which is far less well-trodden than a plain var()
     substitution. Substituting a var() that already reads `42%` resolves to
     `color-mix(in srgb, #13181B 42%, #555859)` — unambiguously valid CSS
     Color 5. reveals.js tweens it as "0%" → "100%" to match.

     IF THIS DECLARATION EVER GOES INVALID (a browser without color-mix, a
     malformed token), it is invalid at computed-value time and `color` — an
     inherited property — falls back to the parent's, i.e. `.bto-banner-lead`'s
     --fg-lead. The failure direction is today's exact appearance. */
  color: color-mix(in srgb,
                   var(--fg-ink) var(--rv-lit, 100%),
                   var(--fg-mute));

  /* GEOMETRY GUARD — the whole reason panel 2 does not move.
     GSAP's SplitText wraps each word in a <div> and, by default, sets it
     `display: inline-block` so the word can be transformed. An inline-block
     run does not break or collapse trailing whitespace the way an inline run
     does, which is enough to change where a line wraps — and panel 2's copy /
     rail clearance was settled over nine measured QA passes, against this
     paragraph at its current height.

     We only ever animate COLOUR here, never a transform, so inline-block buys
     us nothing and costs us the risk. Forcing `display: inline` puts the words
     back into the parent's normal inline flow, so line breaking is identical
     to the unsplit paragraph.

     `!important` IS LOAD-BEARING HERE, not defensive habit. SplitText writes
     `display: inline-block` as an INLINE STYLE on each word, and a normal
     stylesheet declaration loses to an inline style. An author `!important`
     is the one thing in the cascade that outranks a non-important inline
     style, so without it this whole rule silently does nothing and the
     paragraph is left free to re-wrap. Do not "clean this up".

     VERIFY, do not assume: `.bto-banner-lead`'s client height and line count
     must be unchanged before vs after the split. That is M1 in qa-reveals.mjs
     and it is the check that protects panel 2. */
  display: inline !important;
}


/* ============================================================================
   § 2 · CENTRE-OUT CLIP WIPE  —  three section headings
   ----------------------------------------------------------------------------
   The heading opens from a vertical seam at its centre out to both edges, on
   `circ.inOut`. Applied to three of the four converged 68px headings:

       .bto-work .content__title    THE WORK
       .er-sec   .er-head           THE ENGINE ROOM
       .fq-sec   .content__title    STRAIGHT ANSWERS

   The fourth converged heading — panel 2's `.bto-banner-title` — is
   deliberately NOT in this list. See reveals.js § 1 for the reasoning.

   SCOPED BY CLASS, NOT BY ID, ON PURPOSE.
   The obvious selector is `#faq .content__title`. It is wrong: services.html
   also ships `<section class="svc-faq" id="faq">` containing its own
   `.content__title`, so an id-scoped rule reaches through the shared shell and
   silently animates a second page. `.fq-sec` / `.bto-work` / `.er-sec` exist
   on index.html and nowhere else (verified against all three built pages), so
   the class scope cannot leak. This is the same one-owner-per-thing failure
   _notes/16_shell-ownership.md was written about.

   WHY THE POLYGON OVERSHOOTS THE BOX.
   SPYLT's open state is `polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` — exactly
   the border box. That is safe for them because they only ever wipe a padded
   pill. Our targets are bare headings, and Mokko sets
   `.ms-ah-wrapper .content__title { line-height: 0.9 }`. At a line-height
   below 1 the glyph box is TALLER than the content box, so ascenders and
   descenders sit outside it — a clip at exactly 100% would shave them off, and
   because that is the END state it would shave them off permanently. The open
   polygon therefore runs -25% → 125% vertically and -5% → 105% horizontally
   (the horizontal padding covers the italic overhang on `.er-head em.s`,
   which is Fraunces italic).

   Both polygons are 4-point, which is what lets GSAP interpolate them.

   reveals.js also strips this class and clears the inline clip-path when each
   reveal finishes, so no heading is left carrying a clip-path — and therefore
   none is left carrying the stacking context a clip-path induces.
   ---------------------------------------------------------------------------- */

html.bto-rv .bto-rv-wipe {
  clip-path: polygon(50% -25%, 50% -25%, 50% 125%, 50% 125%);
  will-change: clip-path;
}


/* ============================================================================
   § 3 · REDUCED MOTION  —  end state, immediately, no scrub
   ----------------------------------------------------------------------------
   reveals.js checks the same query and returns before creating a single
   ScrollTrigger, so under reduced motion this file is doing the whole job.

   · The wipe needs nothing: no class is ever added, so the headings are plain
     headings. That IS the end state.
   · The scrub is given its end state here instead of being animated toward it,
     so a reduced-motion reader sees the same finished page as everyone else
     rather than the pre-effect page. No split, no word spans, no DOM churn —
     one declaration on the paragraph itself.

   The `.bto-rv-wipe` reset is belt-and-braces: it cannot normally be reached
   (reveals.js never adds the class under reduced motion) but if some future
   edit adds it, this guarantees the heading still renders unclipped.
   ---------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html.bto-rv .bto-banner .bto-banner-lead { color: var(--fg-ink); }

  html.bto-rv .bto-rv-wipe {
    clip-path: none;
    will-change: auto;
  }
}
