/* ─── AI Superpower motion layer ──────────────────────────────────────
   One place for timing tokens and every page-level motion rule on the
   "Air" surfaces (app.css v3). Pairs with js/motion.js, which only adds
   classes and custom properties — it never restructures markup, because
   i18n.js owns the innerHTML of every [data-i18n] / [data-i18n-html]
   node and rewrites it on every language toggle. With JS off (or on a
   thrown error) .mo-ready is absent and nothing here hides content.

   Spec (frontend-motion skill, four layers):
   - hero     · page load  · staggered fade-up 10px, 60ms     · --dur-xl · RM: static
                (the [data-mo-lcp] child rises without fading — see § Hero)
   - blocks   · in view    · fade-up 12px, once, 60ms stagger · --dur-xl · RM: static
   - .btn     · press      · scale .97                        · --dur-xs
   - cards    · hover      · lift 2-3px (pointer:fine only)   · --dur-sm
   - a/button · focus      · visible accent ring              · none
   Deliberately NOT ported from the ClawBot reference: hero line-reveal
   and stat count-up. Both rewrite the inside of i18n-managed nodes, so
   a language toggle would wipe them (or they would wipe the 中文). */

:root {
    --dur-xs: 100ms;
    --dur-sm: 160ms;
    --dur-md: 220ms;
    --dur-lg: 320ms;
    --dur-xl: 600ms;
    --stagger: 60ms;
    --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
    --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}

/* ─── Press feedback on every primary action ───────────────────────── */
.btn, .card-cta, .fit-tab, .chip, .lang-toggle span {
    -webkit-tap-highlight-color: transparent;
}
.btn:active:not(:disabled), .card-cta:active, .fit-tab:active {
    transform: scale(0.97);
    transition-duration: var(--dur-xs);
}

/* ─── Focus: keyboard users get a ring the hover states never gave ── */
:is(a, button, summary, [tabindex]):focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 3px;
    border-radius: 8px;
}

/* ─── Reveal on scroll (only once motion.js marks the page ready) ──── */
.mo-ready .mo-reveal {
    opacity: 0;
    transform: translateY(12px);
    /* motion-audit: allow slow-ui — one-time explanatory reveal, not UI feedback */
    transition: opacity var(--dur-xl) var(--ease-out-strong),
                transform var(--dur-xl) var(--ease-out-strong);
    transition-delay: calc(var(--mo-i, 0) * var(--stagger));
}
.mo-ready .mo-reveal.mo-in {
    opacity: 1;
    transform: none;
}
/* Fade-only twin, for elements that already carry a transform of their own
   (the bleed figures on the talk pages sit at translateX(-530px) to centre a
   1060px diagram in a 760px column). transform is one property: declaring it
   here would REPLACE the page's own and shove the element off-centre, which
   pushed drone-agent.html to 1780px of document at a 1440px viewport. So this
   rule never mentions transform — motion.js picks the class per element. */
.mo-ready .mo-fade {
    opacity: 0;
    /* motion-audit: allow slow-ui — one-time explanatory reveal, not UI feedback */
    transition: opacity var(--dur-xl) var(--ease-out-strong);
    transition-delay: calc(var(--mo-i, 0) * var(--stagger));
}
.mo-ready .mo-fade.mo-in {
    opacity: 1;
}

/* ─── Hero: page-load entrance of the existing children ────────────────
   The marker is an ATTRIBUTE IN THE HTML ([data-mo-hero]) and the effect
   is a keyframe animation, not a class JS adds plus a transition. Both
   choices are load-bearing, and the earlier shape failed on both counts:

   · A transition needs a PAINTED "from" state. The old version added the
     hidden class and the visible class inside the same pre-paint window
     (DOMContentLoaded, then two nested rAFs), so the element's first
     resolved style was already the end state and the transition never
     ran. Measured on live course-ai.app 2026-09-18: all five hero
     children read opacity 1 at the first sampled frame (176.5ms) with
     .mo-hero-in already on the header. An animation has no such
     requirement — it plays from its own keyframes.
   · With the marker in the markup and .mo-ready set by the <head> script
     before the hero is even parsed, the entrance is anchored to FIRST
     PAINT rather than DOMContentLoaded. That is the earliest it can
     start, so the hero is never held hidden waiting on a script.

   JS off, blocked, or thrown -> no .mo-ready -> these rules never match
   and the hero renders fully visible and static, immediately. Nothing
   here touches the inside of an i18n node: the animation runs on the
   child ELEMENT, so i18n.js may rewrite its textContent/innerHTML
   mid-flight (a language toggle) without disturbing the entrance. */
@media (prefers-reduced-motion: no-preference) {
    .mo-ready :is([data-mo-hero], [data-mo-hero-fade]) > * {
        /* motion-audit: allow slow-ui — one-time marketing hero entrance */
        animation: moHeroUp var(--dur-xl) var(--ease-out-strong) both;
        animation-delay: calc(var(--mo-i, 0) * var(--stagger));
    }
    /* Fade-only twin, same split as .mo-reveal / .mo-fade above: for a
       container whose children carry a transform of their own or sit in a
       sticky column, where declaring transform here would REPLACE theirs. */
    .mo-ready [data-mo-hero-fade] > * {
        animation-name: moHeroFade;
    }

    /* The LCP element rises WITHOUT fading ([data-mo-lcp] in the markup).
       Chromium does not count an element as painted while its opacity is 0,
       so a faded headline defers the Largest Contentful Paint entry until
       the fade clears -- measured here at +188ms on / (116 -> 304ms). A
       transform-only animation carries no such penalty: the element is
       opaque from first paint, so it is counted immediately and still
       travels the same 10px on the same beat as its siblings.

       Do NOT "fix" this by starting at opacity:.01 instead. Chromium's test
       is === 0, so that restores the NUMBER while the user still looks at an
       invisible headline -- it games the metric and is explicitly rejected.

       Which element to mark is a MEASURED fact, not the h1 by assumption:
       on /courses the largest element is the section-head's <p>. Marking the
       wrong child leaves the real LCP faded and the penalty in place. */
    .mo-ready [data-mo-hero] > [data-mo-lcp] {
        animation-name: moHeroRise;
    }
    /* In a fade-only container there is no transform to fall back on -- its
       children are sticky columns (/community's .rail), and translating one
       fights the sticky offset, which is the whole reason for the deviation.
       Opacity is what costs the LCP and transform is unavailable, so the
       marked child simply does not animate; its two siblings still fade. */
    .mo-ready [data-mo-hero-fade] > [data-mo-lcp] {
        animation: none;
    }
    /* Reading order, capped at the 4th beat so a hero with many children
       still finishes on time: 3 * 60ms + 600ms = 780ms after first paint. */
    .mo-ready :is([data-mo-hero], [data-mo-hero-fade]) > :nth-child(1) { --mo-i: 0; }
    .mo-ready :is([data-mo-hero], [data-mo-hero-fade]) > :nth-child(2) { --mo-i: 1; }
    .mo-ready :is([data-mo-hero], [data-mo-hero-fade]) > :nth-child(3) { --mo-i: 2; }
    .mo-ready :is([data-mo-hero], [data-mo-hero-fade]) > :nth-child(n+4) { --mo-i: 3; }
}
@keyframes moHeroUp {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: none; }
}
@keyframes moHeroFade {
    from { opacity: 0; }
    to { opacity: 1; }
}
@keyframes moHeroRise {
    from { transform: translateY(10px); }
    to { transform: none; }
}

/* ─── Hero spotlight: motion.js writes --mx/--my on the hero ───────── */
.hero-air { position: relative; }
.hero-air > * { position: relative; z-index: 1; }
.hero-air::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 0;
    background: radial-gradient(520px circle at var(--mx, 50%) var(--my, 40%),
                rgba(124, 58, 237, 0.10), transparent 60%);
    opacity: 0;
    transition: opacity var(--dur-lg) ease;
}
.hero-air.mo-lit::after { opacity: 1; }

/* ─── Reading progress (scroll-linked, progressive enhancement) ───── */
.mo-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    z-index: 1002;
    background: var(--grad, var(--accent));
    transform-origin: 0 50%;
    transform: scaleX(0);
    pointer-events: none;
}
@supports (animation-timeline: scroll()) {
    .mo-progress {
        animation: moGrow linear both;
        animation-timeline: scroll(root);
    }
}
@keyframes moGrow { to { transform: scaleX(1); } }

/* ─── Reduced motion: keep state changes, drop travel and loops ───── */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 1ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 1ms !important;
        transition-delay: 0ms !important;
        animation-delay: 0ms !important;
    }
    html { scroll-behavior: auto !important; }
    .mo-ready .mo-reveal,
    .mo-ready :is([data-mo-hero], [data-mo-hero-fade]) > * {
        opacity: 1 !important;
        transform: none !important;
    }
    .mo-ready .mo-fade { opacity: 1 !important; }
    .hero-air::after, .mo-progress { display: none; }
}
