diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 889498dd..141768c0 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -29,6 +29,8 @@ body { -webkit-font-smoothing: antialiased; font-family: $font-family-base; padding-top: 90px; // navbar height + overflow-x: hidden; // Prevent horizontal scroll + } .logo-img { diff --git a/static/scripts/main.js b/static/scripts/main.js index d27f8096..7a0cd3e9 100644 --- a/static/scripts/main.js +++ b/static/scripts/main.js @@ -1,6 +1,9 @@ // Register Plugin gsap.registerPlugin(ScrollTrigger); +// Detect small-device viewport once on load (used by scroll-animation helpers) +const isMobile = window.matchMedia('(max-width: 768px)').matches; + // 1. Header Timeline let tl = gsap.timeline({ defaults: { ease: "power4.out" } @@ -68,13 +71,12 @@ const initMarquee = () => { // Helper: per-element scrub entrance const scrubEach = (elements, props, triggerEl, startBase, endBase, offsetPer) => { elements.forEach((el, i) => { + const startVal = startBase - i * offsetPer; + const endVal = endBase - i * offsetPer; gsap.from(el, Object.assign({}, props, { - scrollTrigger: { - trigger: triggerEl, - start: 'top ' + (startBase - i * offsetPer) + '%', - end: 'top ' + (endBase - i * offsetPer) + '%', - scrub: 1, - } + scrollTrigger: isMobile + ? { trigger: triggerEl, start: 'top ' + Math.min(startVal + 5, 100) + '%', toggleActions: "play none none none" } + : { trigger: triggerEl, start: 'top ' + startVal + '%', end: 'top ' + endVal + '%', scrub: 1 }, })); }); }; @@ -356,6 +358,16 @@ const initScrollPieces = () => { } }; +// Mobile-aware ScrollTrigger config helper. +// On small devices: fires ~5% earlier and plays once (no scrub-reversal when scrolling back up). +const stConfig = (trigger, start, end, scrubVal = 1) => { + if (isMobile) { + const mStart = start.replace(/(\d+)%/, (_, n) => Math.min(+n + 5, 100) + '%'); + return { trigger, start: mStart, toggleActions: "play none none none" }; + } + return { trigger, start, end, scrub: scrubVal }; +}; + const initScrollAnimations = () => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; @@ -395,15 +407,17 @@ const initScrollAnimations = () => { hero.parentNode.insertBefore(wrapper, hero); wrapper.appendChild(hero); - gsap.to(wrapper, { - scale: 0.92, opacity: 1, y: -40, - scrollTrigger: { - trigger: wrapper, - start: 'bottom 30%', // recession begins when bottom of hero reaches 60% viewport - end: 'bottom top', - scrub: 1, - } - }); + if (!isMobile) { + gsap.to(wrapper, { + scale: 0.92, opacity: 1, y: -40, + scrollTrigger: { + trigger: wrapper, + start: 'bottom 30%', + end: 'bottom top', + scrub: 1, + } + }); + } } // ── Customers Section ── @@ -411,7 +425,7 @@ const initScrollAnimations = () => { if (customersSection) { gsap.from('.customers-title', { y: 30, opacity: 0, - scrollTrigger: { trigger: customersSection, start: 'top 90%', end: 'top 55%', scrub: 1 } + scrollTrigger: stConfig(customersSection, 'top 90%', 'top 55%'), }); } @@ -420,19 +434,19 @@ const initScrollAnimations = () => { if (heroSection) { gsap.from('.hero-badge', { y: 40, opacity: 0, - scrollTrigger: { trigger: heroSection, start: 'top 90%', end: 'top 50%', scrub: 1 } + scrollTrigger: stConfig(heroSection, 'top 90%', 'top 50%'), }); gsap.from('.hero-title', { y: 80, opacity: 0, - scrollTrigger: { trigger: heroSection, start: 'top 88%', end: 'top 40%', scrub: 1 } + scrollTrigger: stConfig(heroSection, 'top 88%', 'top 40%'), }); gsap.from('.hero-subtitle', { y: 100, opacity: 0, - scrollTrigger: { trigger: heroSection, start: 'top 85%', end: 'top 35%', scrub: 1 } + scrollTrigger: stConfig(heroSection, 'top 85%', 'top 35%'), }); gsap.from('.hero-actions', { y: 60, opacity: 0, - scrollTrigger: { trigger: heroSection, start: 'top 80%', end: 'top 35%', scrub: 1 } + scrollTrigger: stConfig(heroSection, 'top 80%', 'top 35%'), }); const statItems = heroSection.querySelectorAll('.stat-item'); @@ -442,16 +456,18 @@ const initScrollAnimations = () => { ScrollTrigger.create({ trigger: heroSection, - start: 'top 70%', - once: true, + start: isMobile ? 'top 75%' : 'top 70%', + toggleActions: "play none none none", onEnter: animateCounters, }); - // Recession — only after scrolling well past - gsap.to(heroSection, { - opacity: 0.4, y: -30, - scrollTrigger: { trigger: heroSection, start: 'bottom 10%', end: 'bottom top', scrub: 1 } - }); + // Recession — desktop only (no reverse scrub on mobile) + if (!isMobile) { + gsap.to(heroSection, { + opacity: 0.4, y: -30, + scrollTrigger: { trigger: heroSection, start: 'bottom 10%', end: 'bottom top', scrub: 1 } + }); + } } // ── Demo Section ── @@ -459,14 +475,14 @@ const initScrollAnimations = () => { if (demoSection) { gsap.from('.demo-header', { y: 50, opacity: 0, - scrollTrigger: { trigger: demoSection, start: 'top 88%', end: 'top 50%', scrub: 1 } + scrollTrigger: stConfig(demoSection, 'top 88%', 'top 50%'), }); const demoContainer = document.querySelector('.demo-container'); if (demoContainer) { gsap.from(demoContainer, { scale: 0.88, opacity: 0, y: 60, - scrollTrigger: { trigger: demoContainer, start: 'top 90%', end: 'top 35%', scrub: 1 } + scrollTrigger: stConfig(demoContainer, 'top 90%', 'top 35%'), }); } @@ -475,10 +491,13 @@ const initScrollAnimations = () => { scrubEach(personaCards, { y: 60, opacity: 0, scale: 0.94 }, '.demo-personas', 90, 50, 5); } - gsap.to(demoSection, { - opacity: 0.5, y: -20, - scrollTrigger: { trigger: demoSection, start: 'bottom 40%', end: 'bottom top', scrub: 1 } - }); + // Recession — desktop only (no reverse scrub on mobile) + if (!isMobile) { + gsap.to(demoSection, { + opacity: 0.5, y: -20, + scrollTrigger: { trigger: demoSection, start: 'bottom 40%', end: 'bottom top', scrub: 1 } + }); + } } // ── Capabilities Section ── @@ -500,7 +519,7 @@ const initScrollAnimations = () => { if (ctaBox) { gsap.from(ctaBox, { y: 40, opacity: 0, scale: 0.96, - scrollTrigger: { trigger: ctaBox, start: 'top 90%', end: 'top 55%', scrub: 1 } + scrollTrigger: stConfig(ctaBox, 'top 90%', 'top 55%'), }); } } @@ -527,7 +546,7 @@ const initScrollAnimations = () => { if (browserShell) { gsap.from(browserShell, { scale: 0.82, opacity: 0, y: 80, - scrollTrigger: { trigger: '.browser', start: 'top 90%', end: 'top 30%', scrub: 1 } + scrollTrigger: stConfig('.browser', 'top 90%', 'top 30%'), }); } @@ -535,7 +554,7 @@ const initScrollAnimations = () => { if (browserStand) { gsap.from(browserStand, { scaleX: 0.5, opacity: 0, - scrollTrigger: { trigger: browserStand, start: 'top 95%', end: 'top 65%', scrub: 1 } + scrollTrigger: stConfig(browserStand, 'top 95%', 'top 65%'), }); } @@ -553,7 +572,7 @@ const initScrollAnimations = () => { gsap.from(divider, { opacity: 0, scaleX: 0.3, - scrollTrigger: { trigger: divider, start: 'top 90%', end: 'top 70%', scrub: 1 } + scrollTrigger: stConfig(divider, 'top 90%', 'top 70%'), }); }); }; @@ -588,4 +607,4 @@ document.addEventListener("DOMContentLoaded", () => { onLeaveBack: () => header.classList.remove("scrolled"), }); } -}); +}); \ No newline at end of file