/* Motion layer — jeromebilaos.com variant, 2026-08-02.
 *
 * Loaded AFTER css/style.css. It adds motion; it does not restyle the site. Delete this file and
 * motion.js and the page is the live page.
 *
 * HOW MOTION IS GUARDED. One switch, --m-on, multiplies every duration:
 *     --m-on: 1  -> normal
 *     --m-on: 0  -> every duration becomes 0s, so animations land on their final state instantly
 * `prefers-reduced-motion: reduce` sets it to 0. A `?motion=force` URL flag (motion.js) sets it back
 * to 1 for review, because otherwise a machine with Windows animation effects turned off shows a
 * variant that is byte-identical to the live page and there is no way to tell the two apart.
 *
 * Why a multiplier rather than wrapping everything in a media query: with a media query, a rule
 * written outside it is unguarded and nothing complains. Here a duration that forgets the multiplier
 * is the ONLY way to escape the guard, and the test suite fails on exactly that.
 *
 * WHAT IS DELIBERATELY NOT ANIMATED:
 *   - Hero text opacity. The h1 is the LCP element; fading it in delays the largest paint by exactly
 *     the fade duration. It moves (transform only) and never fades.
 *   - Anything that changes layout. transform and opacity only, so nothing here can shift content
 *     after paint and cost CLS.
 */

:root {
  --m-on: 1;
  --m-fast: 160ms;
  --m-base: 320ms;
  --m-slow: 700ms;
  --m-stagger: 90ms;
  --m-rise: 28px;
  /* --ease already exists in style.css: cubic-bezier(0.2,0.8,0.2,1). Reused, not redefined. */
  --m-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  :root { --m-on: 0; }
}

/* Review override: /?motion=force. Deliberately beats the media query — it exists so the variant can
   be judged on a machine that has animations turned off at the OS level. */
html[data-motion-force] { --m-on: 1; }


/* Scroll progress — transform:scaleX only, never layout. */
.m-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 9999;
  background: linear-gradient(90deg, var(--primary), var(--primary-hover));
  transform: scaleX(0);
  transform-origin: 0 50%;
  pointer-events: none;
}

/* ---- section reveal ----------------------------------------------------------
 * The live site flips a whole section from opacity:0 to 1 in one step. Here the section still
 * governs its own visibility — so the existing IntersectionObserver and the existing noscript
 * fallback keep working untouched — and the CHILDREN arrive in sequence. --m-i is set per child by
 * motion.js; the 0 fallback keeps this valid on its own. */
section.visible > * {
  animation: m-rise calc(var(--m-slow) * var(--m-on)) var(--m-ease-out) both;
  animation-delay: calc(var(--m-i, 0) * var(--m-stagger) * var(--m-on));
}

@keyframes m-rise {
  from { opacity: 0; transform: translate3d(0, var(--m-rise), 0); }
  to   { opacity: 1; transform: none; }
}

/* Hero: movement without a fade, to protect LCP. */
#welcome.visible > * {
  animation: m-rise-noflash calc(var(--m-base) * var(--m-on)) var(--m-ease-out) both;
}

@keyframes m-rise-noflash {
  from { transform: translate3d(0, 12px, 0); }
  to   { transform: none; }
}

/* ---- micro-interactions ------------------------------------------------------
 * The page's REAL card classes, counted from index.html: .evo-card ×15, .feature-box ×16,
 * .trait-card ×6, .review-card ×4, .glass-card ×2. An earlier draft invented .card/.service-card and
 * silently animated nothing. .flip-card is excluded on purpose — it already owns a 3D flip transform
 * and a second transform would fight it. */
.evo-card, .trait-card, .review-card, .glass-card, .feature-box {
  transition: transform calc(var(--m-base) * var(--m-on)) var(--ease);
  will-change: transform;
}
.evo-card:hover, .trait-card:hover, .review-card:hover, .glass-card:hover, .feature-box:hover {
  transform: translate3d(0, calc(-6px * var(--m-on)), 0);
}

.btn, .btn-primary, .btn-outline-gold, .icon-btn, .sticky-audit-btn, button:not(.cred-chip-btn) {
  transition: transform calc(var(--m-fast) * var(--m-on)) var(--ease);
}
.btn:active, .btn-primary:active, .icon-btn:active, button:not(.cred-chip-btn):active {
  transform: scale(calc(1 - 0.03 * var(--m-on)));
}

/* ---- Verified Proficiency: marquee restored -----------------------------------
 * Jerome stopped the `cred-scroll` marquee on 2026-08-02, and the note left here predicted the
 * result verbatim: the track is `width: max-content`, so stopped it shows the first few chips and
 * the rest sit beyond the mask, cut mid-word. He called it on 2026-08-08 — "used to be a carousel
 * that autoscrolls, now it's stale and hard to see" — so the unconditional stop is gone and
 * css/style.css:2799 drives it again (32s linear infinite, paused on hover, 26s under 1280px).
 *
 * The reduced-motion stop below is the one that stays: that one is an accessibility guarantee, not
 * a preference. The press-scale kill stays too — Jerome's 2026-08-02 call was about chip motion,
 * which is a separate thing from the strip scrolling, and he did not ask for it back. */
.cred-chip, .cred-chip-btn, .cred-chip-link { transition: none !important; }

.nav-dot {
  transition: transform calc(var(--m-base) * var(--m-on)) var(--m-ease-out),
              background-color calc(var(--m-fast) * var(--m-on)) linear;
}
.nav-dot.active { transform: scale(calc(1 + 0.35 * var(--m-on))); }

/* ---- reduced motion: stop what the live site leaves running -------------------
 * The audit's headline finding: .cred-marquee-track animates from CSS unconditionally, so the JS
 * guard in script.js:563 never stopped it. Cancelled here, where it is started. */
@media (prefers-reduced-motion: reduce) {
  /* The marquee scrolls for everyone else now (the unconditional stop above was removed 2026-08-08);
     this is the only thing holding it still, and it is the guard that should.
     Force-scoped like every other rule here. It used to be the ONE deliberate exception, on the
     grounds that Jerome wanted the strip static for everyone — he reversed that on 2026-08-08, so
     the exception lost its reason and ?motion=force now previews the scroll like anything else.

     Stopping it is also not enough on its own: the track is width:max-content, so a halted strip
     shows the first few chips and hides the rest past the mask, cut mid-word — the exact state
     Jerome rejected. A reduced-motion visitor gets a wrapped grid instead, which shows every
     credential; the aria-hidden duplicate group exists only to make the loop seamless, so it comes
     out when there is no loop. No !important needed — motion.css loads after style.css, so equal
     specificity already wins on order. */
  html:not([data-motion-force]) .cred-marquee-track { animation: none !important; transform: none !important; }
  html:not([data-motion-force]) .cred-marquee { -webkit-mask-image: none; mask-image: none; }
  html:not([data-motion-force]) .cred-marquee-track { flex-wrap: wrap; width: 100%; justify-content: center; }
  html:not([data-motion-force]) .cred-marquee-group { flex-wrap: wrap; justify-content: center; }
  html:not([data-motion-force]) .cred-marquee-group[aria-hidden="true"] { display: none; }

  /* The rest are scoped to :not([data-motion-force]). !important beats an inline style, which is how
     ?motion=force ended up promising a preview it did not deliver: verified live 2026-08-02 —
     motion.js had correctly set the progress bar to scaleX(0.6511) and this rule silently computed it
     back to scaleX(0). A review flag that only half-works is the same defect as a guard that only
     half-guards. A real reduced-motion visitor never carries that attribute, so nothing changes for
     them. */
  html:not([data-motion-force]) .audio-pill,
  html:not([data-motion-force]) .audio-pill-icon,
  html:not([data-motion-force]) [class^="audio-"] { animation: none !important; }
  html:not([data-motion-force]) { scroll-behavior: auto; }
  html:not([data-motion-force]) .m-progress { transform: scaleX(0) !important; }
}
