/* =========================================================
   Centrix Premium Scroll — home page only
   Scroll-scrubbed headline reveals + image parallax / clip wipes,
   on top of Lenis inertial smooth-scroll.

   Behaviour: assets/js/premium-scroll.js
   Enqueued by: inc/premium-scroll.php (front page only)

   EVERY hidden state below is gated behind html.cx-premium, which
   premium-scroll.js only adds once GSAP + ScrollTrigger + Lenis have
   all actually loaded and the visitor has not asked for reduced
   motion. If any of that fails the class is never set, no rule here
   applies, and the page renders exactly as it did before — content
   can never get stuck invisible. Same defensive pattern as
   motion.css's html.cx-anim gate.
   ========================================================= */

/* ---------------------------------------------------------
   1. Lenis
   ---------------------------------------------------------
   Lenis drives the real window scroll, so fixed chrome (the nav)
   keeps working untouched. Native smooth-scroll must be off or the
   two fight over the same gesture — motion.css sets
   `scroll-behavior: smooth`, so override it while Lenis is live. */
html.cx-premium,
html.cx-premium body {
    scroll-behavior: auto !important;
}

html.cx-premium.lenis-stopped {
    overflow: hidden;
}

/* Inner scrollers (the Our Work track) keep their own overscroll. */
html.cx-premium [data-lenis-prevent] {
    overscroll-behavior: contain;
}

/* ---------------------------------------------------------
   2. Headline word reveal
   ---------------------------------------------------------
   premium-scroll.js wraps each word of a section headline in
   .cx-word, walking text nodes only — the colour-carrying spans the
   theme already emits (.text-gradient, .text-stroke, .font-accent,
   and any inline style="color:…" from centrix_split_title) stay
   intact as parents, so every word inherits exactly the colour it
   had before. Nothing here sets a colour. */
html.cx-premium .cx-word {
    display: inline-block;
    /* Deliberately NO will-change. Every word of every section headline
       carries this class — on the order of a hundred elements — and a
       permanent will-change promotes each to its own compositor layer for
       the whole life of the page rather than just while it animates. Past
       a threshold Chrome starts ignoring the hint anyway. GSAP already
       promotes an element for the duration of a transform tween (force3D
       "auto") and releases it after, which is what this was approximating. */
}

html.cx-premium .cx-split {
    /* Contains the y-travel of the words inside. */
    overflow: hidden;
}

/* ---------------------------------------------------------
   2b. Hero: hidden on load, revealed by scroll
   ---------------------------------------------------------
   The hero copy starts invisible and is revealed by scrolling, while
   the hero itself is pinned — so the page does not advance to the next
   section until the copy has fully arrived. See initHero() in
   premium-scroll.js.

   Gated on html.cx-hero-pin, NOT html.cx-premium: cx-premium is only
   added once the footer scripts have run, which is well after first
   paint, so hiding the copy from there would let it flash in and then
   vanish. cx-hero-pin is set by an inline <head> snippet (printed by
   inc/premium-scroll.php) that runs before the body paints — and that
   same snippet removes the class again on a watchdog timer if GSAP
   never initialises, so the copy can never be left permanently
   invisible.

   The !important on animation kills the load-time heroFadeUp keyframes
   declared in front-page.php; without it those would still run and
   fight the scroll-driven reveal for control of opacity.

   The long selectors below are deliberate, not accidental. motion.js's
   site-wide reveal marks any h1/p it finds with .cx-r/.cx-in, and its
   `html.cx-anim .cx-r.cx-in { opacity: 1 }` scores 0,3,1 — which beat an
   earlier, shorter version of this rule at 0,2,1, faded the hero copy in
   on load, and left the scroll timeline to hide it again a beat later.
   That was a real ~1s flash on every refresh. Adding .fp-hero .fp-hero__text
   takes these to 0,4,1 so they win outright, whatever order the two
   stylesheets happen to load in.

   opacity deliberately has NO !important: GSAP drives the reveal by writing
   inline styles, and inline loses to a stylesheet !important — so
   !important here would pin the copy invisible forever. Plain-but-specific
   is exactly right; inline still wins, motion.js still loses.

   animation DOES need !important, to kill front-page.php's heroFadeUp
   keyframes. CSS animations outrank inline styles, so left alive they would
   override GSAP frame by frame. GSAP tweens via JS rather than CSS
   transitions, so `transition: none` costs the reveal nothing while
   stopping motion.js's 700ms fade from smearing the handover. */
html.cx-hero-pin .fp-hero .fp-hero__text .fp-hero__label,
html.cx-hero-pin .fp-hero .fp-hero__text .fp-hero__h1,
html.cx-hero-pin .fp-hero .fp-hero__text .fp-hero__desc,
html.cx-hero-pin .fp-hero .fp-hero__text .fp-hero__ctas {
    animation: none !important;
    transition: none !important;
    opacity: 0;
}

/* The readability veil (the left-to-right gradient + the blurred glass
   scrim) exists purely to make the hero copy legible. With the copy hidden
   until you scroll, a dark wash sitting over an otherwise bright carousel
   has nothing to justify it — it just dims the image for no reason. So the
   veil starts hidden too and fades in with the copy, slightly ahead of it,
   so the text always lands on a backdrop that is already there.

   No !important, for the same reason as the copy above: GSAP reveals these
   by writing inline styles, which must be able to win. */
html.cx-hero-pin .fp-hero .fp-hero__overlay,
html.cx-hero-pin .fp-hero .fp-hero__scrim {
    opacity: 0;
}

/* The scroll cue is the one thing that must be visible on load — it is
   the only affordance telling the visitor there is anything to scroll
   to. It fades out as the copy arrives (handled in JS). */
html.cx-hero-pin .fp-hero .fp-scroll-indicator {
    opacity: 1;
}

/* ---------------------------------------------------------
   3. Media reveals
   ---------------------------------------------------------
   The wipe runs on a wrapper, the parallax on the <img> inside it,
   so the two transforms never collide on one element. */
html.cx-premium .cx-wipe {
    clip-path: inset(0% 0% 100% 0%);
    will-change: clip-path;
}

html.cx-premium .cx-parallax {
    will-change: transform;
}

/* ---------------------------------------------------------
   3b. Transition lock for GSAP-driven elements
   ---------------------------------------------------------
   GSAP animates by writing an inline transform/opacity every frame. If
   the same element also carries a CSS `transition` covering those
   properties, the browser does not apply those writes — it *eases toward
   each one*, restarting the ease every frame. The rendered value then
   trails the scroll position permanently, which reads as stutter and
   rubber-banding rather than smooth scrubbing. An inline style cannot
   win this on its own: a transition is not overridden by the value it is
   transitioning, so the transition itself has to go.

   With motion.css no longer loaded on the front page its .cx-r
   transitions are gone, but the Tailwind utilities in the markup remain
   — `transition-all` on the answer cards, `transition-all duration-400`
   on the philosophy rows, the card/glass hover transitions — and those
   collide in exactly the same way.

   premium-scroll.js applies this class per element. Elements driven by a
   scrubbed tween keep it for good (they animate the whole time they are
   on screen); one-shot reveals drop it on completion so hover
   transitions behave normally afterwards. */
html.cx-premium .cx-anim-lock {
    transition: none !important;
}

/* ---------------------------------------------------------
   4. Scroll-morph blob background (inside "The Problem")
   ---------------------------------------------------------
   Sizing is handled by the absolute inset-0 z-0 utility classes on
   the wrapper in front-page.php now that it's a background layer
   inside .fp-problem rather than its own section. The shape itself
   lives entirely in the SVG "d" attribute, driven by
   initBlobDivider() in premium-scroll.js.

   opacity is the one thing making this safe regardless of exactly
   how much of the section the wave ends up covering at any given
   scroll position: the wave is the same navy (#090F18) as the
   section's own text, so at full strength it would wash the navy
   headline/paragraph out wherever the two overlap. Capped at ~22% it
   reads as a soft tint on the white background instead of a
   competing colour field, so the text stays legible in every frame
   of the scrub, not just the start and end. */
.fp-blob-divider {
    opacity: 0.22;
}

.fp-blob-divider__svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* ---------------------------------------------------------
   5. Reduced motion
   ---------------------------------------------------------
   Belt and braces — the JS already refuses to add .cx-premium at
   all under a reduce preference, so this only matters if the
   setting is changed after load. */
@media (prefers-reduced-motion: reduce) {

    html.cx-premium .cx-word,
    html.cx-premium .cx-wipe,
    html.cx-premium .cx-parallax {
        opacity: 1 !important;
        transform: none !important;
        clip-path: none !important;
        will-change: auto;
    }
}

/* ---------------------------------------------------------
   6. "Proof beats pitches" — pinned horizontal travel
   ---------------------------------------------------------
   The section pins and the project strip moves sideways as the page
   scrolls down. Behaviour: initWorkHorizontal() in premium-scroll.js.

   Everything here is gated on html.cx-work-h, which that function only
   adds once it has decided the effect actually applies — GSAP present,
   not reduced motion, wide enough viewport, and more than one card's
   worth of overflow to travel. Below that gate the section keeps the
   native scroll-snap carousel exactly as authored, which is still the
   right interaction on a phone and the correct fallback everywhere.

   The track keeps its real horizontal overflow; GSAP drives scrollLeft
   rather than a transform. That deliberately preserves scroll-snap,
   the arrows, the dots and the counter — work-carousel.js reads
   track.scrollLeft on its own scroll listener, so it stays in sync for
   free instead of needing to know this effect exists. */
/* The whole section is pinned, not just the strip, so the heading, the
   counter and the dots stay on screen with the cards instead of scrolling
   away above them, and the block is centred in the viewport rather than
   sitting under the fixed nav.

   height, not min-height. The section has to be exactly one viewport tall
   while pinned: min-height lets it grow to whatever its content needs, and
   the content here is taller than 100vh (a 405px card at 4/5 is ~506px,
   plus the heading block, the dots row and py-24 top and bottom). That
   overflow is what pushed the cards below the fold and left the empty band
   under the heading — the section was centring content that did not fit.
   Fixing the height and letting the strip size itself to what is actually
   left keeps every card fully on screen. */
html.cx-work-h .fp-work-section {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* The section's own py-14/py-24 is Tailwind's, and at 96px top and
       bottom it is most of the budget the cards need. Trimmed while
       pinned only. */
    padding-top: 2rem;
    padding-bottom: 2rem;
}

/* Cards size to the height actually left over after the heading and dots,
   instead of a fixed 4/5 of their width. Without this the aspect-ratio
   wins and the strip overflows the pinned viewport again. */
html.cx-work-h .fp-work-carousel {
    flex: 1 1 auto;
    min-height: 0;
    /* The strip is driven by page scroll while pinned; a stray trackpad
       swipe scrolling it independently would desync it from the pin. */
    overflow: hidden;
}

html.cx-work-h .fp-work-card {
    height: 100%;
}

html.cx-work-h .fp-work-card__frame {
    height: 100%;
    aspect-ratio: auto;
}

/* The strip keeps the page's own left gutter so the first card lines up
   with the heading above it, and gets no right pad at all — the last card
   should run out at the right edge, not stop short of it.

   An earlier version centred the strip by padding both ends with half the
   leftover viewport width. That is right for a one-card-at-a-time carousel
   and wrong for a travelling strip: it opened a half-viewport of empty
   space to the left of the first card, which is the gap visible at rest.
   A strip reads as continuous, so it should start flush and end flush. */
html.cx-work-h .fp-work-track {
    height: 100%;
    align-items: stretch;
    padding-right: 0;
    scroll-padding-left: 0;

    /* Snap fights a scrubbed scrollLeft: every frame GSAP writes, the
       snap tries to pull to the nearest card, so the strip stutters and
       never lands where the scrub says. The pin supplies the pacing now. */
    scroll-snap-type: none;
    overflow-x: hidden;
}

/* Arrows and dots stay for keyboard/assistive use but stop advertising
   an interaction that page scroll is now driving. */
html.cx-work-h .fp-work-arrow {
    opacity: 0;
    pointer-events: none;
}

/* ---------------------------------------------------------
   7. Final CTA — morph intro, then content
   ---------------------------------------------------------
   Four gradient shapes morph in, then fade out and hand over to the
   headline/button. Behaviour: initCtaMorph() in premium-scroll.js.

   The hidden state is gated on html.cx-cta-gate, set by the same inline
   <head> snippet that hides the hero copy (inc/premium-scroll.php), for
   the same reason: premium-scroll.js runs from the footer, far too late
   to hide anything before first paint, so gating this on .cx-premium
   would let the CTA flash in and then vanish. That snippet's watchdog
   also strips the class if GSAP never initialises, so this content can
   never be left permanently invisible. */
html.cx-cta-gate .fp-cta-content {
    opacity: 0;
}

/* Sized in its own right rather than filling the section: the shapes are
   an intro, not a background wash, and at full-bleed the stroke would be
   a thin line lost across the whole width. */
.fp-cta-morph__svg {
    width: clamp(90px, 12vw, 150px);
    height: auto;
    overflow: visible;
}

/* Nothing to show until the morph is actually driving it — otherwise the
   starting diamonds sit on top of the CTA on any page where the effect
   does not run. */
.fp-cta-morph {
    opacity: 0;
}
