/* motion */

/* ---------- Lenis owns scroll easing once active — the site-wide
   `html{scroll-behavior:smooth}` (theme-v2.css) fights Lenis's own
   easing curve and produces stuttering/inconsistent scroll positions,
   so turn native smooth-scroll off exactly when Lenis's marker class
   is present (per Lenis's own integration guidance). No-JS/reduced-
   motion pages never get the `lenis` class, so they keep native
   smooth-scroll untouched. ---------- */
html.lenis{scroll-behavior:auto;}

/* ---------- reveal timing: bring the shared [data-rv] sweep (defined
   in components.css, no-JS-safe) in line with the ui-ux-pro-max
   "standard" preset — opacity 0->1, y 24->0, 0.5s, 0.08 stagger,
   trigger ~85% into viewport. Only the distance/duration/delay values
   change here; the sweep mechanism itself is untouched. ---------- */
/* qa-fix C (2026-09-28, QA: copy still ~15% opacity 0.9s after it
   arrived — read as washed-out ghost text). Reveals now start from 35%
   opacity (never near-invisible), travel 10px and settle in .32s, with a
   tighter stagger — content is readable the moment it's on screen, the
   motion is just a soft settle. Mirrored in components.css's generic
   [data-rv] rule so pages that don't load this file match. */
html.js [data-rv]{
  opacity:.35;
  transform:translateY(10px);
  transition-duration:.32s;
  transition-timing-function:cubic-bezier(.2,.7,.3,1);
}
html.js [data-rv].in{opacity:1; transform:none;}
html.js [data-rv="2"]{transition-delay:.04s;}
html.js [data-rv="3"]{transition-delay:.08s;}
html.js [data-rv="4"]{transition-delay:.12s;}

/* ---------- section hand-off: the black services block slides over
   the hero, the paper work section rises over the black block —
   sticky stack, desktop/tablet only. Collapses to normal document
   flow under 761px so nothing pins/overlaps at 390px ("390 works
   with reduced effects"). ---------- */
@media (min-width:761px){
  [data-hero-pin]{position:relative; z-index:1;}
  .svc-panel{position:sticky; top:0; z-index:2;}
  .sec.work-tiles{position:relative; z-index:3; background:var(--paper);}
  /* .svc-panel's containing block is body (no dedicated pin wrapper in
     the markup), so its sticky range runs almost the full document
     height and it lands, released, on top of #words/.contact-cta at
     the bottom of the page — hiding the quote and contact form behind
     it with no way to scroll past. Give them the same rise-over
     treatment as .work-tiles so they paint above the panel once
     scrolled into view. Fixed 2026-09-22, verify.sh home-page pass. */
  #words{position:relative; z-index:3; background:var(--paper);}
  .contact-cta{position:relative; z-index:3; background:var(--paper);}
}

/* ---------- hero name/image motion hooks — GSAP drives the scrub via
   inline transforms during the pin; these just set transform-origin/
   will-change so the scrub doesn't jank. No-JS/reduced-motion: inert. ---------- */
[data-hero-name]{transform-origin:left center; will-change:transform, opacity;}
[data-hero-img]{will-change:transform;}

/* ---------- hero scroll-scrubbed image sequence: canvas sits over the
   static first-frame <img> (always visible, no-JS/reduced-motion safe)
   and only fades in once motion.js has preloaded + drawn a frame —
   see [data-hero-seq] in motion.js. Ungated by html.js on purpose: a
   blank/undrawn canvas must never show, JS-or-not. ---------- */
.hero-one-panel{position:relative;}
.hero-one-panel img[data-hero-img]{position:absolute; inset:0;}
.hero-one-panel canvas[data-hero-seq]{
  position:absolute; inset:0; width:100%; height:100%; display:block;
  filter:grayscale(1) contrast(1.15) brightness(.82);
  opacity:0; transition:opacity .4s ease-out;
}
.hero-one-panel canvas[data-hero-seq].is-ready{opacity:1;}

/* ---------- marquee: entrance (slides in from the fold, on load) —
   guarded behind html.js so no-JS users always see it. JS also takes
   over the loop speed (velocity-tied); the CSS keyframe in
   theme-v2.css remains the no-JS/reduced-motion fallback untouched. ---------- */
html.js .hero-marquee{opacity:0; transform:translateY(40px);}
html.js .hero-marquee.is-in{
  opacity:1; transform:none;
  transition:opacity .8s cubic-bezier(.16,1,.3,1), transform .8s cubic-bezier(.16,1,.3,1);
}
html.js [data-marquee-track]{animation:none; will-change:transform;}
@media (prefers-reduced-motion:reduce){
  html.js .hero-marquee{opacity:1; transform:none;}
  html.js [data-marquee-track]{animation:none;}
}

/* ---------- work tiles: scroll-in clip-path baseline (GSAP animates
   from this via [data-tile]) + hover scale 1.04 / darken, 300ms —
   overrides the 600ms hover already in components.css by loading
   after it, same specificity via the extra attribute selector. ---------- */
/* GSAP fully owns the tile's opacity/scale/clip-path entrance (see
   motion.js) — neutralize the generic [data-rv] fade-up base state on
   tiles so the two systems don't stack (extra attribute selector wins
   the cascade over the plain [data-rv] rule above). */
html.js [data-rv][data-tile]{transform:none; transition:none;}
.work-tile[data-tile]{clip-path:inset(0% 0% 0% 0%); will-change:transform, opacity, clip-path;}
.work-tile-img img[data-tile-img]{
  transition:transform .3s cubic-bezier(.25,.7,.35,1), filter .3s cubic-bezier(.25,.7,.35,1);
}
.work-tile[data-tile]:hover .work-tile-img img[data-tile-img],
.work-tile[data-tile]:focus-within .work-tile-img img[data-tile-img]{
  transform:scale(1.04);
  filter:grayscale(1) contrast(1.1) brightness(.88);
}

@media (prefers-reduced-motion:reduce){
  [data-hero-name], [data-hero-img], .svc-art, .work-tile[data-tile]{
    will-change:auto; clip-path:none;
  }
  .hero-one-panel canvas[data-hero-seq]{display:none;}
}
