/* ============================================================================
   myk-motion.css — THE PAGE-WIDE ENTRANCE VOCABULARY
   ----------------------------------------------------------------------------
   Lane `r3:motion-vocab`. Founder items A01, A02, A06, A08, A09 of
   founder-trace/FT-20260819-jehnean-review.md.

   A01 is not "animate the hero lockup". It is, verbatim: *"GENERAL RULE: every
   overline animates in like other text."* A02 is *"Treat as best practice
   page-wide, not per-section."* Those two sentences are a request for a
   SYSTEM, and the whole design of this file follows from refusing to answer
   them with a list of per-section fixes.

   WHAT WAS MEASURED FIRST (review/motion-vocab/probe-before.json, captured on
   127.0.0.1:6620 with served-bytes SHA-256 verified equal to disk):

     · ALL 15 overlines on the page computed
         opacity: 1 · transform: none · animation: none · transition: all 0s
       They were not "animating badly". They were in NO entrance system at all.
     · 21 of 24 paragraphs (>=40 chars) computed the same.
     · The named A06/A08/A09 targets — `.mk-whowhyb__caption`,
       `.mk-whowhyb__duration`, `.mk-amenities__overline`, `.mk-ctacard__overline`,
       `.mk-ctacard__heading` — all static.

   So this is an ADDITION, not a retune. Nothing here replaces a working
   entrance; there was none to replace on these elements.

   ---------------------------------------------------------------------------
   THE FIVE DECISIONS, each with the reason it is not the obvious alternative
   ---------------------------------------------------------------------------

   1. `translate`, NEVER `transform`.
      `translate` is an independent transform property; the browser composes it
      with whatever `transform` the element already carries. This is not a style
      preference, it is a collision guarantee: `.mk-amenities__overline` is
      MEASURED carrying `transform: matrix(1, 0, 0, 1, 0, -186)` as its slider
      LAYOUT, and any entrance written as `transform: translateY(40px)` would
      silently drop that element 186px. Writing the vocabulary in `translate`
      means a section may keep, add or change its own `transform` forever and
      this file can never break it — and vice versa.

   2. The start state is gated on `html.mv`, which `js/myk-motion.js` adds
      SYNCHRONOUSLY from <head> before <body> is parsed.
      Fails OPEN. No JS, JS error, JS blocked, file 404 -> the class never
      lands, `[data-mv]` never gets `opacity: 0`, and the page renders exactly
      as it does today. The failure mode of the alternative (author `opacity:0`
      in CSS, remove it in JS) is a blank page, which is the single worst bug
      an entrance system can have. Salient's own `no-js` guard has that exact
      shape and the build already carries five section-level workarounds for it
      (01-who-why.css:255, 03-unique.css:574, 05-ecology-b.css:475,
      12-recovery-44.css:367, 13-recovery-45.css:113). Not repeating it.

   3. Adoption is BY SELECTOR, in JS, not by `data-mv` attributes hand-written
      into 17 section files.
      This is what makes A01 a rule. A new section that ships an `__eyebrow` or
      an `__overline` is animated the day it lands, by nobody. It also means
      this lane edited ZERO files belonging to another concurrent lane — the
      vocabulary reaches `14-amenities`, `13-recovery-45` and `07-building`
      without touching a byte of them.

   4. Three roles, three speeds — never one speed for a block.
      From review/omai/PACING.md §7.4, measured off omaivillas.com: a single
      block there runs 200 / 700 / 1000ms simultaneously, so it "resolves in
      layers rather than arriving as one slab". Our page ran ONE duration
      (1200ms) for every text element. Note this makes the page FASTER in
      seconds and SLOWER in feel — see 5.

   5. The easing is the fix. Not the duration.
      PACING §7.2, measured: at one tenth of the way through its animation our
      split-heading has already travelled 34.7% of its distance; Omai's has
      travelled 2.6%. A 13x difference in the first tenth — and the first tenth
      is what the eye locks onto. `easeOutQuart` starts at MAXIMUM velocity;
      there is no ramp, and the founder's "not so hard and fast" is the missing
      ramp, not excess seconds. `cubic-bezier(.4, 0, .2, 1)` is symmetric
      in-out: it starts at zero velocity, which is what "allows a little more
      time to set up" means in a curve.

   ---------------------------------------------------------------------------
   PROVENANCE OF EVERY NUMBER — all from review/omai/PACING.md §8.1, which read
   them out of omaivillas.com's shipped `theme.css` and then confirmed each one
   against a live per-animation-frame CDP sample (§2, probe-raw.json).
   Nothing below was chosen by taste.
   ========================================================================== */

:root {
  /* The single entrance easing. Omai's `--ease-in-out`, theme.css@1983 — the
     ONLY easing in their entire entrance system, used by every heading, eyebrow,
     paragraph and image on their homepage. One curve is the point. */
  --myk-ease-entrance: cubic-bezier(.4, 0, .2, 1);

  /* Three roles, three speeds. Omai `data-animate-text` / `-text-faster` /
     `-img`, PACING §2 table. */
  --myk-mv-dur-lead:   700ms;   /* overline, eyebrow, heading                */
  --myk-mv-dur-body:   200ms;   /* paragraph, caption, meta, duration text   */
  --myk-mv-dur-media: 1000ms;   /* declared for completeness; see §4 note    */

  --myk-mv-delay-lead: 100ms;
  --myk-mv-delay-body:   0ms;
  --myk-mv-delay-media: 100ms;
}

/* TOP-DOWN ORDER (founder, 2026-09-01: "things on the page should load from
   top down ... overline, then title, then paragraph"). Omai's own lanes land
   the fast body copy FIRST when a whole block arrives at once, and she read
   that as a bug ("I thought something was a little buggy"). So the ROLE
   delays are staggered by position in the block: an eyebrow/overline keeps
   the stock 100ms, a heading waits 220ms, body copy waits 440ms and takes
   400ms (Omai's 200ms read too abrupt once it stopped leading). These are set
   ON THE ELEMENT, so they override any section-level lane token that was
   tuned around the old order (01's body 220ms/700ms, for instance) — which is
   the intent: one order everywhere. A section that needs its own choreography
   still can: set the token on the element with higher specificity. */
html.mv :is(h1, h2, h3, h4, h5, h6)[data-mv="lead"]:not(.mk-eyebrow):not([class*="__eyebrow"]):not([class*="__overline"]) {
  --myk-mv-delay-lead: 220ms;
}
html.mv [data-mv="body"] {
  --myk-mv-delay-body: 440ms;
  --myk-mv-dur-body:   400ms;
}
:root {

  /* FLAT px, not em, and that is deliberate. Omai uses 40px at every type
     size. Ours was `1.3em`, which MEASURED 26px on a 20px eyebrow and 62.4px
     on a 48px headline — the biggest type moved 2.4x furthest and therefore
     fastest, which is exactly backwards from how a page should settle. */
  --myk-mv-travel: 40px;
}


/* ==========================================================================
   §1. THE PRIMITIVE
   --------------------------------------------------------------------------
   Everything is scoped under `html.mv`. See decision 2 in the header: that
   class is added by js/myk-motion.js from <head>, synchronously, before <body>
   exists, so there is no window in which an element is painted and then
   hidden. If the script does not run, none of this file applies.

   `.mv-in` is one-shot. js/myk-motion.js `unobserve`s on first intersection
   and never removes the class, so an element that has revealed NEVER re-hides
   on scroll-back. (Omai: same — `unobserve` in `As()`, PACING §2.)
   ========================================================================== */

html.mv [data-mv] {
  opacity: 0;
  /* `translate` and NOT `transform`. Decision 1. */
  translate: 0 var(--myk-mv-travel);
  transition:
    opacity   var(--myk-mv-dur-lead) var(--myk-ease-entrance) var(--myk-mv-delay-lead),
    translate var(--myk-mv-dur-lead) var(--myk-ease-entrance) var(--myk-mv-delay-lead);
}

/* BODY role — 3.5x faster than the lead and it starts 100ms EARLIER.
   PACING §2: "The body copy is 3.5x faster than the heading and starts 100ms
   earlier. It is settled and readable at t~200ms while the heading is still
   gliding through its last third. The block therefore resolves in layers
   rather than arriving as one slab. This is the single most transferable idea
   in the whole reference and it costs one extra class." */
html.mv [data-mv="body"] {
  transition:
    opacity   var(--myk-mv-dur-body) var(--myk-ease-entrance) var(--myk-mv-delay-body),
    translate var(--myk-mv-dur-body) var(--myk-ease-entrance) var(--myk-mv-delay-body);
}

/* MEDIA role — opacity ONLY, no travel. Omai never translates an image
   (PACING §2 table, `data-animate-img` transitions `opacity` alone).
   DECLARED BUT NOT ADOPTED BY THIS LANE: founder item A10 (images fade in as
   their section arrives) belongs to `r3:recovery-plan`. The role exists so
   that lane can opt an element in with one attribute and inherit this file's
   easing and clock instead of inventing a second vocabulary. */
html.mv [data-mv="media"] {
  translate: none;
  transition: opacity var(--myk-mv-dur-media) var(--myk-ease-entrance) var(--myk-mv-delay-media);
}

/* THE REST STATE IS SPLIT IN TWO, and the split is load-bearing.

   `opacity: 1` is safe for every adopted element — this file is the only thing
   that made it 0.

   `translate: 0 0` is NOT. Written as one rule
   (`html.mv [data-mv].mv-in { opacity: 1; translate: 0 0 }`) it is (0,3,1),
   which BEATS 04-ecology.css:229's (0,3,0) — so the moment an ecology element
   revealed, its 1px optical correction was overwritten with 0. MEASURED in the
   third verification pass: `translate` computed `0px` on the four ecology
   elements after reveal, `0px 1px` on their un-adopted siblings. The start
   state never had this problem, because §1 is (0,2,1) and correctly loses.

   So the translate reset is restricted to `:not([data-mv-t])` — exactly the
   elements for which the travel IS on `translate`, i.e. the ones where this
   file's own start state is the only thing to undo. `:not()` contributes its
   argument's specificity, so the selector is still (0,3,1) and still wins over
   anything a section might layer on those elements; it simply no longer
   MATCHES the elements a section already owns. Specificity is not the tool for
   deciding who owns a property — matching is. */
html.mv [data-mv].mv-in {
  opacity: 1;
}
html.mv [data-mv]:not([data-mv-t]).mv-in {
  translate: 0 0;
}


/* --- 1.1 WHEN `translate` IS ALREADY SPOKEN FOR -----------------------------
   Decision 1 puts the travel on `translate` so it can never replace a
   section's `transform`. That is necessary but not sufficient: a section may
   own `translate` itself, and one does —

       sections/04-ecology.css:229
       .mk-page .mk-ecology .mk-ecology__eyebrow,
       .mk-page .mk-ecology .mk-ecology__title,
       .mk-page .mk-ecology .mk-ecology__copy p,
       .mk-page .mk-ecology .mk-ecology__cta > span { translate: 0 1px; }

   a deliberate 1px optical correction at (0,3,0), which beats §1's (0,2,1).
   MEASURED in the first verification pass: those four groups computed
   `translate: 0px 1px` while carrying `opacity: 0` — they faded without
   travelling, and `.mv-in` could not move them either.

   THE FIX IS NOT MORE SPECIFICITY. Winning that cascade would delete another
   lane's pixel correction to buy this lane's motion. Instead js/myk-motion.js
   asks each element, once at adoption, which of `translate` / `transform` is
   free, and stamps `data-mv-t` when the answer is not the default. The rules
   below are those two answers. They are written at the SAME specificity as §1
   plus one attribute, so they can still be overridden by a section that wants
   to opt out — the escape hatch stays open in both directions. */

/* `translate` taken, `transform` free.

   NOTE THE ABSENCE OF `translate: none` HERE, because writing it was a real
   regression caught by the second verification pass and it is the obvious
   thing to write. These selectors are (0,3,1) — `html` + `.mv` + `[data-mv]` +
   `[data-mv-t]` — which BEATS 04-ecology's (0,3,0). A `translate: none` here
   would therefore not be neutralising §1's declaration, it would be deleting
   the SECTION's 1px optical correction: measured, the ecology eyebrow computed
   `translate: none` with the correction gone.

   It is also unnecessary. This branch is only ever reached when the element
   ALREADY owns `translate` — that is the test that put it here, evaluated
   before `data-mv` was set, so §1 was not applying at the time and is losing
   to the section now. There is nothing of ours to neutralise. Leaving the
   property alone lets the section's `translate` and this file's `transform`
   compose, which is the whole point of using two properties. */
html.mv [data-mv][data-mv-t="transform"] {
  transform: translateY(var(--myk-mv-travel));
  transition:
    opacity   var(--myk-mv-dur-lead) var(--myk-ease-entrance) var(--myk-mv-delay-lead),
    transform var(--myk-mv-dur-lead) var(--myk-ease-entrance) var(--myk-mv-delay-lead);
}
html.mv [data-mv="body"][data-mv-t="transform"] {
  transition:
    opacity   var(--myk-mv-dur-body) var(--myk-ease-entrance) var(--myk-mv-delay-body),
    transform var(--myk-mv-dur-body) var(--myk-ease-entrance) var(--myk-mv-delay-body);
}
html.mv [data-mv][data-mv-t="transform"].mv-in {
  transform: translateY(0);
}

/* Both taken — fade only. NOT a failure case: A02 asks that "paragraph text
   fades in", and a fade with no travel satisfies it. Clobbering a section's
   layout to add 40px of drift would not. */
html.mv [data-mv][data-mv-t="none"] {
  /* Same reasoning as the branch above: no `translate: none`, no `transform:
     none`. Both are already owned by the section, both are already beating §1,
     and asserting either here would clobber a layout this file did not write. */
  transition: opacity var(--myk-mv-dur-lead) var(--myk-ease-entrance) var(--myk-mv-delay-lead);
}
html.mv [data-mv="body"][data-mv-t="none"] {
  transition: opacity var(--myk-mv-dur-body) var(--myk-ease-entrance) var(--myk-mv-delay-body);
}


/* ==========================================================================
   §2. REDUCED MOTION
   --------------------------------------------------------------------------
   Two independent guards, because one of them can be defeated by a setting
   change AFTER load and the other cannot be defeated at all.

   Guard A (js/myk-motion.js): if `prefers-reduced-motion: reduce` matches at
   boot, the script never adds `html.mv`, so §1 never applies and there is no
   start state to recover from. That is the strongest possible form — the
   content is simply never hidden.

   Guard B (below): if the visitor changes the OS setting while the page is
   open, elements already carrying `opacity: 0` from §1 must be released. This
   writes the END state rather than cancelling the transition, which is the
   rule stated at the top of css/reduced-motion.css: "reduced motion means SHOW
   THE FINAL STATE, never hide the content".

   `!important` is required here and only here: it has to beat §1's own
   declarations at equal specificity from a later position in the same file,
   which source order alone would already do — but css/reduced-motion.css is
   linked AFTER this file by _tools/compose.py and carries a global
   `transition-duration: 0.01ms !important` floor. Without `!important` on the
   opacity itself, that floor would freeze the element at `opacity: 0` in
   0.01ms rather than release it.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  html.mv [data-mv],
  html.mv [data-mv].mv-in {
    opacity: 1 !important;
    translate: none !important;
  }
}


/* ==========================================================================
   §3. WHAT THIS FILE DELIBERATELY DOES NOT DO
   --------------------------------------------------------------------------
   · It does not touch `scroll-snap-type`, and it registers no `wheel`
     listener anywhere. REQ-SCROLL-01. The trigger is IntersectionObserver,
     which is not a scroll handler at all.
   · It does not restyle `.nectar-split-heading`. Salient's own line reveal is
     LANDED behaviour the founder named as working. The ONE exception is
     documented in js/myk-motion.js §2: split headings pinned
     `data-text-effect="none"` are frozen at `opacity: 1 !important` by
     00-hero.css:54 and therefore never animate at all — for those, and only
     those, the CHILD heading element is adopted.
   · It does not animate `.mk-hero` anything. The hero title sequence is in the
     founder's "WHAT LANDED" list and is excluded wholesale by
     js/myk-motion.js. A01's hero half is fixed inside the hero's own measured
     system instead (sections/00-hero.html, the lockup column's
     `data-animation`), so the hero keeps ONE clock.
   · It sets no `will-change`. `opacity` and `translate` are both compositor
     properties and Chrome promotes them for the duration of the transition on
     its own; a standing `will-change` on 40+ elements costs layers for the
     whole session to buy nothing.
   ========================================================================== */
