/* ============================================================ CSL Landing — Sections A: Header, Hero, Marquee, Founder, Work ============================================================ */ const { useState, useEffect, useRef } = React; const NS = window.ContentStackLabDesignSystem_cbb53c; const { Button } = NS; const Ic = window.Icons; const D = window.CSL; const A = window.CSLAdmin || {}; function adminHero(pageSlug) { return A.heroes && A.heroes[pageSlug] ? A.heroes[pageSlug] : null; } /* reveal-on-scroll using GSAP & ScrollTrigger for high-performance animations, falling back to a scroll event observer if GSAP is unavailable. */ function useReveal() { React.useLayoutEffect(() => { if (window.gsap && window.ScrollTrigger) { window.gsap.registerPlugin(window.ScrollTrigger); // All motion (including smooth scroll) is skipped for users who ask for // reduced motion — CSS reveals are already transform-only and harmless. const mm = window.gsap.matchMedia(); mm.add("(prefers-reduced-motion: no-preference)", () => { let lenis = null; let rafCallback = null; let handleAnchorClick = null; // Initialize Lenis smooth scroll if (window.Lenis) { lenis = new window.Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // easeOutExpo orientation: 'vertical', gestureOrientation: 'vertical', smoothWheel: true, }); // Sync ScrollTrigger on scroll lenis.on('scroll', window.ScrollTrigger.update); // Sync Lenis RAF with GSAP ticker rafCallback = (time) => { lenis.raf(time * 1000); }; window.gsap.ticker.add(rafCallback); window.gsap.ticker.lagSmoothing(0); // Intercept anchor links for smooth kinetic scrolling to target handleAnchorClick = (e) => { const href = e.currentTarget.getAttribute('href'); if (href && href.startsWith('#')) { e.preventDefault(); if (href === '#top') { lenis.scrollTo(0); } else { const target = document.querySelector(href); if (target) { lenis.scrollTo(target); } } } }; const anchors = document.querySelectorAll('a[href^="#"]'); anchors.forEach(a => a.addEventListener('click', handleAnchorClick)); } // 1. Initial page load stagger for Hero & Header const tl = window.gsap.timeline({ delay: 0.1 }); // Animate header components (logo, navigation links, and CTA button) tl.fromTo(".hdr__logo, .hdr__link, .hdr .btn", { opacity: 0, y: -15 }, { opacity: 1, y: 0, duration: 0.6, stagger: 0.05, ease: "power2.out" } ); // Animate Hero text and primary CTA buttons const heroInner = document.querySelector(".hero__inner"); if (heroInner) { heroInner.classList.remove("reveal"); // Remove standard reveal behavior heroInner.style.transition = "none"; tl.fromTo([".hero__copy h1, .hero-rich-heading", ".hero__lead", ".hero__cta .btn"], { opacity: 0, y: 35 }, { opacity: 1, y: 0, duration: 0.9, stagger: 0.12, ease: "power3.out" }, "-=0.45" ); } // Section-Specific Storytelling Animations // Marquee scrubbing const marquee = document.querySelector('.marq__track'); if (marquee) { window.gsap.to(marquee, { xPercent: -20, ease: "none", scrollTrigger: { trigger: ".marq", start: "top bottom", end: "bottom top", scrub: 1 } }); } // Founder photo parallax (the one continuous scroll effect we keep) const founderPhoto = document.querySelector('.founder__photo image-slot'); if (founderPhoto) { window.gsap.fromTo(founderPhoto, { yPercent: -15, scale: 1.1 }, { yPercent: 15, scale: 1, ease: "none", scrollTrigger: { trigger: ".founder__media", start: "top bottom", end: "bottom top", scrub: true } } ); } // Per-section entrances (see HOME-ANIMATIONS.md) — transform ONLY, // never opacity, same safety rule as the generic reveals below. const enter = (sel, from, to, trigger, start) => { const els = document.querySelectorAll(sel); if (!els.length) return; els.forEach((el) => { el.style.transition = "none"; }); window.gsap.fromTo(els, from, Object.assign({}, to, { clearProps: "transform", scrollTrigger: { trigger: trigger || els[0], start: start || "top 85%", once: true } })); }; enter(".founder__quote, .founder__body p", { x: -30 }, { x: 0, duration: 0.8, stagger: 0.15, ease: "power2.out" }, ".founder__copy", "top 80%"); enter(".work__card", { x: 50 }, { x: 0, duration: 0.8, stagger: 0.1, ease: "power3.out" }, ".work__scroller"); enter(".svc__item", { x: -20 }, { x: 0, duration: 0.7, stagger: 0.1, ease: "power2.out" }, ".svc", "top 80%"); enter(".rec__logo", { scale: 0.8 }, { scale: 1, duration: 0.7, stagger: 0.08, ease: "back.out(1.5)" }, ".rec__logos", "top 90%"); enter(".tst", { scale: 0.95 }, { scale: 1, duration: 1, ease: "power2.out" }); document.querySelectorAll(".step-row").forEach((el, i) => { el.style.transition = "none"; window.gsap.fromTo(el, { x: i % 2 ? 40 : -40 }, { x: 0, duration: 0.8, ease: "power2.out", clearProps: "transform", scrollTrigger: { trigger: el, start: "top 85%", once: true } } ); }); // Why cards keep their CSS resting rotation — clearProps hands the // transform back to the stylesheet after the entrance finishes. const whyCards = document.querySelectorAll('.why__card'); if (whyCards.length) { window.gsap.fromTo(whyCards, { y: 40, rotationX: -15 }, { y: 0, rotationX: 0, duration: 0.8, stagger: 0.1, ease: "power2.out", clearProps: "transform", scrollTrigger: { trigger: ".why__grid", start: "top 85%", once: true } } ); } // One motion vocabulary: everything else enters with the same slide-up. // NOTE: scroll-triggered tweens animate transform ONLY — never opacity. // If they included opacity:0, any ScrollTrigger stall (crawler render, // CDN hiccup) would leave the whole page invisible. See csl.css .reveal. const els = document.querySelectorAll(".reveal"); els.forEach((el) => { if (el.closest('.hero__inner') || el.closest('.why__card') || el.matches('.founder__copy, .svc__item, .step-row, .tst')) return; el.style.transition = "none"; window.gsap.fromTo(el, { y: 30 }, { y: 0, duration: 0.85, ease: "power2.out", scrollTrigger: { trigger: el, start: "top 90%", once: true } } ); }); return () => { if (lenis) { lenis.destroy(); } if (rafCallback) { window.gsap.ticker.remove(rafCallback); } if (handleAnchorClick) { const anchors = document.querySelectorAll('a[href^="#"]'); anchors.forEach(a => a.removeEventListener('click', handleAnchorClick)); } }; }); return () => mm.revert(); } else { // Fallback scroll check observer if GSAP is not available const els = [...document.querySelectorAll(".reveal")]; els.forEach((el) => el.classList.add("pre")); const check = () => { const vh = window.innerHeight || document.documentElement.clientHeight; els.forEach((el) => { if (!el.classList.contains("pre")) return; const r = el.getBoundingClientRect(); if (r.top < vh * 0.92 && r.bottom > 0) el.classList.remove("pre"); }); }; check(); window.addEventListener("scroll", check, { passive: true }); window.addEventListener("resize", check); const t = setTimeout(() => els.forEach((el) => el.classList.remove("pre")), 1400); return () => { window.removeEventListener("scroll", check); window.removeEventListener("resize", check); clearTimeout(t); }; } }, []); } window.useReveal = useReveal; const Arrow = (p) => Ic.arrow({ width: 18, height: 18, ...p }); function hasHtmlMarkup(value) { return /<\/?[a-z][\s\S]*>/i.test(String(value || "")); } function RichHeroHeading({ value, fallback }) { const heading = value || fallback; if (hasHtmlMarkup(heading)) { return
; } const text = String(heading || "").trim(); const needsDot = text && !/[.!?]$/.test(text); return
<\/p>/gi, "")
.replace(/<\/p>\s*
/gi, " {hero && hero.subheading ? hero.subheading : "Strategy, content, and visibility, built to generate pipeline, not just posts. For B2B tech companies done with random acts of marketing."} The human behind the strategy I've been the in-house marketer. The agency hire. The founder figuring it out. That's why I get it. {t} Recent work Real brands. Real problems. Real outcomes.
")
.replace(/<\/?(p|div)[^>]*>/gi, "")
.trim();
}
function RichDotTitle({ as = "h1", text, fallback, ...props }) {
const Tag = as;
const value = text || fallback || "";
if (hasHtmlMarkup(value)) {
const html = normalizeInlineHeroHtml(value);
return The proof is in the pipeline.
{w.brand}
{w.tag}