Hero Load Sequence
Full homepage hero entrance. Title slides from left, chat window from right, then subtitle + CTA stagger up. Fires once on page load via a master timeline.
The CFO you never
had. Now you do.
Ask Luca analyses your store in real time — cash flow, margins, ad spend — and tells you exactly what to do next.
// Hero load — master timeline, fires once const tl = gsap.timeline({ defaults: { ease: "power3.out" } }); gsap.set(["#h-eyebrow", "#h-headline"], { x: -60, opacity: 0 }); gsap.set("#h-chat", { x: 60, opacity: 0 }); gsap.set("#h-sub", { y: 24, opacity: 0 }); gsap.set("#h-cta", { y: 20, opacity: 0 }); tl.to("#h-eyebrow", { x: 0, opacity: 1, duration: 0.7 }) .to("#h-headline", { x: 0, opacity: 1, duration: 0.8 }, "<0.1") .to("#h-chat", { x: 0, opacity: 1, duration: 0.9 }, "<0.15") .to("#h-sub", { y: 0, opacity: 1, duration: 0.6 }, "-=0.4") .to("#h-cta", { y: 0, opacity: 1, duration: 0.5 }, "<0.1");
Scroll Reveal Directions
Four direction variants used across the site. Scroll down to trigger. Direction is determined by element role and column position.
// Direction map — encode at render time via data attr const dirMap = { left: { x: -50, y: 0, opacity: 0 }, right: { x: 50, y: 0, opacity: 0 }, up: { x: 0, y: 40, opacity: 0 }, fade: { x: 0, y: 0, opacity: 0 }, }; document.querySelectorAll(".rv-card").forEach(el => { const dir = el.dataset.dir; gsap.set(el, dirMap[dir]); ScrollTrigger.create({ trigger: el, start: "top 82%", once: true, onEnter: () => gsap.to(el, { x: 0, y: 0, opacity: 1, duration: 0.75, ease: "power3.out", }), }); });
Staggered Grid Entrance
Two variants: (a) all cards fade-up with stagger, (b) first/middle/last get from-left/fade-up/from-right per row. Used on the Integrations grid and Features grid.
// Variant A — uniform stagger gsap.set(".ga-card", { y: 40, opacity: 0 }); ScrollTrigger.create({ trigger: "#grid-a", start: "top 78%", once: true, onEnter: () => gsap.to(".ga-card", { y: 0, opacity: 1, duration: 0.65, stagger: 0.08, ease: "power3.out", }), }); // Variant B — directional per column position const colDir = [ { x: -50, y: 0 }, // col 0 → from-left { x: 0, y: 40 }, // col 1 → fade-up { x: 50, y: 0 }, // col 2 → from-right ]; document.querySelectorAll(".gb-card").forEach(el => { const d = colDir[+el.dataset.col]; gsap.set(el, { ...d, opacity: 0 }); }); ScrollTrigger.create({ trigger: "#grid-b", start: "top 78%", once: true, onEnter: () => { document.querySelectorAll(".gb-card").forEach((el, i) => { gsap.to(el, { x: 0, y: 0, opacity: 1, duration: 0.7, delay: i * 0.07, ease: "power3.out", }); }); }, });
Counter Animation
Stat strip with animated counters. Each counter reads data attributes for prefix, suffix, target, and decimal places. Triggers once on scroll entry.
// Counter — data-attr driven, fires once function animateCounter(el) { const target = +el.dataset.target; const prefix = el.dataset.prefix || ""; const suffix = el.dataset.suffix || ""; const decimals = +el.dataset.decimals; const obj = { val: 0 }; gsap.to(obj, { val: target, duration: 1.6, ease: "power2.out", onUpdate() { el.textContent = prefix + obj.val.toFixed(decimals) + suffix; }, }); } ScrollTrigger.create({ trigger: "#stat-strip", start: "top 80%", once: true, onEnter: () => document.querySelectorAll(".stat-number").forEach(animateCounter), });
Scrub / Scroll-Driven
(a) Progress bar across section top — scrub: 0.25. (b) Integration pipeline slides horizontally as you scroll through — scrub: 1, pin: true.
// (a) Progress bar — scrub: 0.25 (slightly smoothed) gsap.set("#scrub-bar", { width: "0%" }); gsap.to("#scrub-bar", { width: "100%", ease: "none", scrollTrigger: { trigger: "#progress-demo", start: "top top", end: "bottom bottom", scrub: 0.25, }, }); // (b) Horizontal pipeline — scrub: 1, pinned const dist = document.querySelector("#pipeline-inner").scrollWidth - document.querySelector("#pipeline-outer").clientWidth; gsap.set("#pipeline-inner", { x: 0 }); gsap.to("#pipeline-inner", { x: -dist, ease: "none", scrollTrigger: { trigger: "#pipeline-outer", start: "top 70%", end: "+=500", scrub: 1, }, });
FAQ Accordion
GSAP height animation — not CSS display toggle. Animates from 0 to measured offsetHeight. Open/close logic with close-others behaviour.
// FAQ — GSAP height tween, close-others pattern let openItem = null; document.querySelectorAll(".faq-item").forEach(item => { const body = item.querySelector(".faq-body"); const icon = item.querySelector(".faq-icon"); const inner = item.querySelector(".faq-body-inner"); gsap.set(body, { height: 0 }); item.querySelector(".faq-trigger") .addEventListener("click", () => { const isOpen = openItem === item; // close current if (openItem) { const b = openItem.querySelector(".faq-body"); const ic = openItem.querySelector(".faq-icon"); gsap.to(b, { height: 0, duration: 0.35, ease: "power2.inOut" }); gsap.to(ic, { rotation: 0, duration: 0.25 }); openItem = null; } if (!isOpen) { const h = inner.offsetHeight; gsap.to(body, { height: h, duration: 0.4, ease: "power2.inOut" }); gsap.to(icon, { rotation: 45, duration: 0.25 }); openItem = item; } }); });
CTA Section Entrance + Glow
Headline fades up on scroll entry, then the CTA button pulses with a teal box-shadow glow. The glow animates from invisible to a soft 32px spread.
Your store's first fully autonomous CFO is one click away.
No setup fee. No contract. Cancel any time.
// CTA entrance + glow pulse gsap.set(["#cta-eyebrow", "#cta-headline", "#cta-sub", "#cta-btn"], { y: 30, opacity: 0 }); ScrollTrigger.create({ trigger: "#cta-stage", start: "top 75%", once: true, onEnter() { const tl = gsap.timeline({ defaults: { ease: "power3.out" } }); tl.to(["#cta-eyebrow", "#cta-headline", "#cta-sub"], { y: 0, opacity: 1, duration: 0.7, stagger: 0.12, }) .to("#cta-btn", { y: 0, opacity: 1, duration: 0.5 }, "-=0.2") .to("#cta-btn", { boxShadow: "0 0 32px 8px rgba(0,181,181,0.35)", duration: 0.55, ease: "power2.out", }, "+=0.1") .to("#cta-btn", { boxShadow: "0 0 18px 4px rgba(0,181,181,0.18)", duration: 0.8, ease: "power2.inOut", repeat: -1, yoyo: true, }); }, });
Hover Micro-Interactions
Four card hover states driven by GSAP (not CSS transitions). Hover over each card to see the effect. All use mouseenter/mouseleave pairs.
// (a) Border glow const a = document.querySelector("#hov-a"); a.addEventListener("mouseenter", () => gsap.to(a, { borderColor: "rgba(0,181,181,0.5)", duration: 0.25 })); a.addEventListener("mouseleave", () => gsap.to(a, { borderColor: "rgba(255,255,255,0.07)", duration: 0.3 })); // (b) Lift const b = document.querySelector("#hov-b"); b.addEventListener("mouseenter", () => gsap.to(b, { y: -4, boxShadow: "0 12px 32px rgba(0,0,0,0.35)", duration: 0.25 })); b.addEventListener("mouseleave", () => gsap.to(b, { y: 0, boxShadow: "0 0 0 rgba(0,0,0,0)", duration: 0.3 })); // (c) Stat colour shift const c = document.querySelector("#hov-c"); const num = document.querySelector("#hov-c-num"); c.addEventListener("mouseenter", () => gsap.to(num, { color: "#00D4C8", duration: 0.2 })); c.addEventListener("mouseleave", () => gsap.to(num, { color: "#8888A0", duration: 0.25 })); // (d) Arrow nudge const d = document.querySelector("#hov-d"); const arrow = document.querySelector("#hov-arrow"); gsap.set(arrow, { display: "inline-block" }); d.addEventListener("mouseenter", () => gsap.to(arrow, { x: 4, duration: 0.2, ease: "power2.out" })); d.addEventListener("mouseleave", () => gsap.to(arrow, { x: 0, duration: 0.25 }));
Chat Window Simulation
Messages appear sequentially on scroll entry: user message slides from right, Luca response shows typing dots then fades in from left. This is the hero visual pattern on Homepage and Agent page.
// Chat window — sequential timeline function buildChatTimeline() { const body = document.querySelector("#chat-body"); body.innerHTML = ""; const msgs = [ { role: "user", text: "Why did our ROAS drop last week?" }, { role: "luca", text: "Meta CPMs rose 28% — broad audiences hit hardest. Your retargeting held fine." }, { role: "user", text: "What should I do?" }, { role: "luca", text: "Pause BAU broad. Shift €3.8k to lookalike 2% and your abandoned-cart sequence." }, ]; const tl = gsap.timeline(); msgs.forEach((m, i) => { const row = makeRow(m); // creates DOM node const bubble = row.firstChild; body.appendChild(row); const isUser = m.role === "user"; gsap.set(row, { opacity: 0, x: isUser ? 40 : -40 }); if (!isUser) { // show typing dots first const dots = makeTypingDots(); body.appendChild(dots); tl.to(dots, { opacity: 1, duration: 0.2 }, "+=0.3") .to(dots, { opacity: 0, duration: 0.15, delay: 0.8 }, ">") .call(() => dots.remove()) .to(row, { opacity: 1, x: 0, duration: 0.4, ease: "power2.out" }); } else { tl.to(row, { opacity: 1, x: 0, duration: 0.35, ease: "power2.out" }, "+=0.5"); } }); return tl; }
Pinned Scroll Section
Left column stays sticky while right-side rows reveal on scroll. This is the Autonomy Levels pattern on the Agent page. Each right row fades in as it enters the viewport.
You decide how much Luca does.
From a second opinion to full autopilot — set the level per platform, per action type. Change it any time.
// Pinned left col + staggered right-side rows gsap.set(".pr-row", { y: 40, opacity: 0 }); document.querySelectorAll(".pr-row").forEach((row, i) => { ScrollTrigger.create({ trigger: row, start: "top 80%", once: true, onEnter: () => gsap.to(row, { y: 0, opacity: 1, duration: 0.65, delay: i * 0.06, ease: "power3.out", }), }); }); // Left col — sticky is handled via CSS (position: sticky) // No GSAP pin needed; native sticky is smoother and avoids // reflow conflicts with ScrollTrigger pin padding.