/* ==========================================================================
   scroll-effects.css — HMM scroll-effects skill, static-HTML stack
   --------------------------------------------------------------------------
   Copy to site/assets/css/scroll-effects.css and link it AFTER styleguide.css.
   Every hiding rule is gated behind `html.js`, which the one-line inline
   script in <head> adds. With JavaScript off, nothing on the page is hidden.
   Colors come from Style Guide tokens; only the motion token layer is defined
   here. Map the three --color-* fallbacks below to real Style Guide tokens.
   ========================================================================== */

/* ---- Motion tokens (Foundations layer) ---------------------------------- */
:root {
  --motion-duration-fast: 200ms;
  --motion-duration-base: 600ms;
  --motion-duration-slow: 1000ms;
  --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --motion-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --motion-distance: 24px;          /* max reveal travel (vestibular-safe) */
  --motion-scale-from: 0.96;        /* min scale for scale reveals */
  --motion-stagger: 80ms;           /* per-item delay; 8-item cap = 640ms */
  --marquee-duration: 40s;
  --marquee-gap: 3rem;
  --pin-steps: 3;                   /* overwritten per .pin-wrap by the script */
  --header-height: 72px;
  --header-hide-after: 160px;       /* scroll depth before hide-on-down engages */
  --progress-height: 3px;
  --progress-color: var(--color-accent, currentColor);
  --highlight-color: var(--color-accent-soft, var(--color-accent, currentColor));
  --reveal-safety-delay: 2.5s;      /* content is never hidden longer than this */
}

/* Registered so a scroll timeline can interpolate it smoothly (pattern 16). */
@property --tint {
  syntax: '<color>';
  inherits: false;
  initial-value: transparent;
}

/* ---- 1 + 2. Reveal on enter, staggered reveal --------------------------- */
.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--motion-distance), 0);
  transition:
    opacity var(--motion-duration-base) var(--motion-ease-out),
    transform var(--motion-duration-base) var(--motion-ease-out);
  transition-delay: calc(var(--i, 0) * var(--motion-stagger));
  /* Safety net: if the observer never fires (slow hydration, script error),
     the element shows itself anyway after the delay. */
  animation: hmm-reveal-safety 1ms linear var(--reveal-safety-delay) forwards;
}
@keyframes hmm-reveal-safety {
  to { opacity: 1; transform: none; }
}
.js [data-reveal="fade"]  { transform: none; }
.js [data-reveal="left"]  { transform: translate3d(calc(var(--motion-distance) * -1), 0, 0); }
.js [data-reveal="right"] { transform: translate3d(var(--motion-distance), 0, 0); }
.js [data-reveal="scale"] { transform: scale(var(--motion-scale-from)); }
.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

/* T0 upgrade: elements that also carry data-reveal-native animate on a view()
   timeline where the browser supports it (bidirectional; no stagger). The
   script skips its observer for them. Animations beat the base rule above. */
@supports (animation-timeline: view()) {
  .js [data-reveal][data-reveal-native] {
    animation: hmm-reveal-in 1ms linear both;   /* 1ms: Firefox needs a duration */
    animation-timeline: view();
    animation-range: entry 0% entry 45%;
  }
}
@keyframes hmm-reveal-in {
  from { opacity: 0; transform: translate3d(0, var(--motion-distance), 0); }
  to   { opacity: 1; transform: none; }
}

/* ---- 3. Scroll progress indicator --------------------------------------- */
.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--progress-height);
  background: var(--progress-color);
  transform-origin: 0 50%;
  transform: scaleX(var(--scroll-progress, 0));
  z-index: 1000;
  pointer-events: none;
}
@supports (animation-timeline: scroll()) {
  .scroll-progress {
    transform: none;
    animation: hmm-progress-grow 1ms linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes hmm-progress-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* ---- 5. Section-aware header -------------------------------------------- */
/* Map both themes to Style Guide tokens and register both in the contrast
   manifest, including the focus ring. */
[data-header][data-theme="dark"] {
  background-color: var(--color-surface-dark, transparent);
  color: var(--color-on-dark, inherit);
}

/* ---- 14. Header scroll behavior (shrink, hide on scroll down) ----------- */
/* The script adds .is-compact once the 1px sentinel below the header leaves
   the viewport, and .is-hidden while scrolling down past --header-hide-after
   on headers that carry data-header-hide. Hidden by transform, never
   display: none, so focus inside the header is never lost. */
.header-sentinel { position: absolute; top: calc(var(--header-height) + 1px); height: 1px; width: 1px; pointer-events: none; }
[data-header] {
  transition:
    background-color var(--motion-duration-fast) var(--motion-ease-out),
    color var(--motion-duration-fast) var(--motion-ease-out),
    transform var(--motion-duration-fast) var(--motion-ease-out),
    height var(--motion-duration-fast) var(--motion-ease-out);
}
[data-header].is-compact { height: calc(var(--header-height) * 0.8); }
[data-header][data-header-hide].is-hidden:not(:focus-within) { transform: translate3d(0, -100%, 0); }

/* ---- 6. SVG line draw --------------------------------------------------- */
/* Every animated path carries pathLength="1" so dash values are unitless. */
.js [data-draw] :is(path, line, polyline, circle, rect, ellipse) {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset var(--motion-duration-slow) var(--motion-ease-in-out);
  transition-delay: calc(var(--i, 0) * var(--motion-stagger));
}
.js [data-draw].is-drawn :is(path, line, polyline, circle, rect, ellipse) {
  stroke-dashoffset: 0;
}

/* ---- 7. Marquee --------------------------------------------------------- */
[data-marquee] {
  display: flex;
  gap: var(--marquee-gap);
  overflow: hidden;
}
[data-marquee] .marquee-track {
  display: flex;
  flex: 0 0 auto;
  gap: var(--marquee-gap);
  min-width: 100%;
  animation: hmm-marquee var(--marquee-duration) linear infinite;
  animation-play-state: paused;        /* runs only while in view */
}
[data-marquee].is-inview .marquee-track { animation-play-state: running; }
[data-marquee]:hover .marquee-track,
[data-marquee]:focus-within .marquee-track { animation-play-state: paused; }
@keyframes hmm-marquee {
  to { transform: translateX(calc(-100% - var(--marquee-gap))); }
}

/* ---- 8. Pinned scrollytelling (sticky steps) ---------------------------- */
/* Markup:
   <section class="pin-wrap" data-pin-steps="3">
     <div class="pin-stage">
       <div class="pin-panel" data-step="1">…</div>
       <div class="pin-panel" data-step="2">…</div>
       <div class="pin-panel" data-step="3">…</div>
     </div>
     <div class="pin-sentinels" aria-hidden="true">
       <div data-pin-step="1"></div><div data-pin-step="2"></div><div data-pin-step="3"></div>
     </div>
   </section>
   Without JS the panels stack in normal flow and read top to bottom. */
.js .pin-wrap {
  position: relative;
  height: calc(var(--pin-steps) * 100vh);
  height: calc(var(--pin-steps) * 100lvh);
}
.js .pin-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100lvh;
  overflow: clip;
  contain: content;
}
.js .pin-panel {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--motion-duration-base) var(--motion-ease-in-out);
}
.js .pin-wrap[data-active-step="1"] .pin-panel[data-step="1"],
.js .pin-wrap[data-active-step="2"] .pin-panel[data-step="2"],
.js .pin-wrap[data-active-step="3"] .pin-panel[data-step="3"],
.js .pin-wrap[data-active-step="4"] .pin-panel[data-step="4"],
.js .pin-wrap[data-active-step="5"] .pin-panel[data-step="5"] {
  opacity: 1;
}
.js .pin-sentinels {
  position: absolute;
  inset: 0;
  display: grid;
  grid-auto-rows: 1fr;
  pointer-events: none;
}
.pin-sentinels { display: none; }          /* no-JS: sentinels take no space */
.js .pin-sentinels { display: grid; }

/* ---- 9. Parallax: T0 view() timeline, T2 GSAP takes over when present ---- */
/* The script copies data-parallax into --parallax. Desktop pointer devices
   only; decorative layers only. No will-change on the T0 subject. */
@supports (animation-timeline: view()) {
  @media (min-width: 992px) and (pointer: fine) {
    .js [data-parallax] {
      animation: hmm-parallax 1ms linear both;
      animation-timeline: view();
      animation-range: cover;
    }
  }
}
@keyframes hmm-parallax {
  from { transform: translate3d(0, calc(var(--parallax, 0.2) * -50%), 0); }
  to   { transform: translate3d(0, calc(var(--parallax, 0.2) * 50%), 0); }
}
.js.has-gsap [data-parallax] { animation: none; will-change: transform; }

/* ---- 10. Horizontal scroll section -------------------------------------- */
/* T0 default: a native, keyboard-scrollable, snap-aligned track. */
[data-horizontal] .h-track {
  display: flex;
  gap: var(--space-6, 2rem);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--space-4, 1.5rem);
  scrollbar-width: thin;
  padding-bottom: var(--space-3, 1rem);
}
[data-horizontal] .h-track > * {
  flex: 0 0 auto;
  scroll-snap-align: start;
}
[data-horizontal] .h-track:focus-visible {
  outline: 2px solid var(--color-focus, currentColor);
  outline-offset: 4px;
}
/* T2 upgrade: the script adds .is-pinned on desktop when GSAP is present. */
.js.has-gsap [data-horizontal].is-pinned {
  overflow: clip;
  contain: content;
}
.js.has-gsap [data-horizontal].is-pinned .h-track {
  overflow: visible;
  width: max-content;
  scroll-snap-type: none;
  padding-bottom: 0;
  will-change: transform;
}

/* ---- 11. Masked media transition ---------------------------------------- */
/* The attribute goes on the frame; the clip applies to its first child (the
   media). The observer watches the unclipped frame, because a clip-path on
   the observed element itself shrinks its intersection box to nothing. */
.js [data-mask-reveal] > :first-child {
  clip-path: inset(0 100% 0 0);
  transition: clip-path var(--motion-duration-slow) var(--motion-ease-in-out);
}
.js [data-mask-reveal="circle"] > :first-child { clip-path: circle(0% at 50% 50%); }
.js [data-mask-reveal].is-revealed > :first-child { clip-path: inset(0 0 0 0); }
.js [data-mask-reveal="circle"].is-revealed > :first-child { clip-path: circle(75% at 50% 50%); }

/* ---- 12. Sticky stacked cards ------------------------------------------- */
/* <div class="stack" data-stack-scale><section class="stack-card">…</section>…</div>
   proximity, never mandatory, so long content never traps the reader. */
.stack {
  scroll-snap-type: y proximity;
  scroll-padding-top: var(--header-height);
}
.stack-card {
  position: sticky;
  top: 0;
  min-height: 100vh;
  min-height: 100lvh;
  scroll-snap-align: start;
  contain: content;
  transform-origin: 50% 0;
}
@supports (animation-timeline: view()) {
  .js .stack[data-stack-scale] .stack-card:not(:last-child) {
    animation: hmm-stack-shrink 1ms linear both;
    animation-timeline: view();
    animation-range: exit 0% exit 100%;
  }
}
@keyframes hmm-stack-shrink {
  to { transform: scale(var(--motion-scale-from)); }
}

/* ---- 13. Scroll-spy navigation ------------------------------------------ */
/* <nav data-scrollspy aria-label="On this page"><a href="#s1">…</a></nav>
   The script sets aria-current="true" and .is-current on the active link. */
[data-scrollspy] a[aria-current="true"] {
  font-weight: 600;
  text-decoration-thickness: 2px;
}

/* ---- 15. Text highlight wipe -------------------------------------------- */
/* <mark data-highlight>phrase</mark> or <span data-highlight="underline">.
   A real <mark>, so the emphasis exists without CSS. The stroke is a
   pseudo-element scaled with transform (compositor), never background-size. */
[data-highlight] {
  position: relative;
  isolation: isolate;
  background: transparent;
  color: inherit;
}
[data-highlight]::before {
  content: "";
  position: absolute;
  inset: 0.1em -0.15em -0.05em;
  background: var(--highlight-color);
  border-radius: 0.2em;
  z-index: -1;
  transform-origin: 0 50%;
  transform: scaleX(1);                   /* final state: highlighted */
}
[data-highlight="underline"]::before {
  inset: auto -0.05em -0.05em;
  height: 0.18em;
  border-radius: 0.1em;
}
.js [data-highlight]::before {
  transform: scaleX(0);
  transition: transform var(--motion-duration-slow) var(--motion-ease-in-out);
  animation: hmm-highlight-safety 1ms linear var(--reveal-safety-delay) forwards;
}
.js [data-highlight].is-revealed::before { transform: scaleX(1); }
@keyframes hmm-highlight-safety { to { transform: scaleX(1); } }
@supports (animation-timeline: view()) {
  .js [data-highlight]::before {
    transition: none;
    animation: hmm-highlight-wipe 1ms linear both;
    animation-timeline: view();
    animation-range: entry 40% contain 60%;
  }
}
@keyframes hmm-highlight-wipe {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* ---- 16. Scroll-linked color shift (T0 only) ---------------------------- */
/* <section data-scroll-tint style="--tint-from: var(--color-bg); --tint-to: var(--color-surface-dark)">
   Text over it must clear 4.5:1 at the start, midpoint, and end. */
[data-scroll-tint] {
  --tint: var(--tint-to);
  background-color: var(--tint);          /* final state where unsupported */
}
@supports (animation-timeline: view()) {
  .js [data-scroll-tint] {
    animation: hmm-tint 1ms linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 60%;
  }
}
@keyframes hmm-tint {
  from { --tint: var(--tint-from); }
  to   { --tint: var(--tint-to); }
}

/* ---- Utilities ---------------------------------------------------------- */
/* Skip layout and paint for below-the-fold sections until they are near.
   Never on a section that holds a pinned stage or a timeline subject. */
.cv-auto {
  content-visibility: auto;
  contain-intrinsic-size: auto 100vh;
}

/* ---- Lenis (smooth scroll, optional; from lenis.css) -------------------- */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* ---- Reduced motion: final states, no tweens ---------------------------- */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js [data-mask-reveal] > :first-child {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    transition: none !important;
    animation: none !important;
  }
  .js [data-draw] :is(path, line, polyline, circle, rect, ellipse) {
    stroke-dashoffset: 0 !important;
    transition: none !important;
  }
  [data-marquee] .marquee-track { animation: none !important; }
  .js .pin-panel,
  [data-header],
  .scroll-progress { transition: none !important; }
  .js [data-parallax],
  .js .stack[data-stack-scale] .stack-card { animation: none !important; transform: none !important; }
  .js [data-highlight]::before { transform: scaleX(1) !important; transition: none !important; animation: none !important; }
  .js [data-scroll-tint] { animation: none !important; --tint: var(--tint-to); }
  .js.has-gsap [data-parallax],
  .js.has-gsap [data-horizontal].is-pinned .h-track { will-change: auto; }
}

/* ---- T2 custom scrub stage (GSAP timelines authored per build) ---------- */
/* .pin-scrub gets .is-scrubbing only while GSAP is pinning it on desktop with
   motion allowed. Position layers absolutely ONLY under .is-scrubbing so the
   fallback (mobile, reduced motion, no JS) is a normal stacked section. */
.js .pin-scrub.is-scrubbing .pin-scrub-stage {
  position: relative;
  height: 100vh;
  height: 100lvh;
  overflow: clip;
  contain: content;
}
