(() => { "use strict"; const hasGSAP = typeof window.gsap !== "undefined"; const hasST = typeof window.ScrollTrigger !== "undefined"; const hasJQ = typeof window.$ !== "undefined"; const hasWebflow = typeof window.Webflow !== "undefined"; if (hasGSAP && hasST) gsap.registerPlugin(ScrollTrigger); function mountVanta(el) { if (!window.VANTA || !VANTA.TOPOLOGY) return null; if (el.__vantaEffect) return el.__vantaEffect; el.__vantaEffect = VANTA.TOPOLOGY({ el, mouseControls: true, touchControls: true, gyroControls: false, minHeight: 200.0, minWidth: 200.0, scale: 1.0, scaleMobile: 1.0, color: 0xea05f2, }); return el.__vantaEffect; } function unmountVanta(el) { const fx = el.__vantaEffect; if (fx && typeof fx.destroy === "function") fx.destroy(); delete el.__vantaEffect; } function initAurorasViewport() { if (!hasST) return; document.querySelectorAll(".aurora").forEach((el) => { if (el.closest(".nav")) return; if (el.__auroraST) return; el.__auroraST = ScrollTrigger.create({ trigger: el, start: "top bottom+=100", end: "bottom top-=100", onEnter: () => mountVanta(el), onEnterBack: () => mountVanta(el), onLeave: () => unmountVanta(el), onLeaveBack: () => unmountVanta(el), }); }); } function initAurorasNav() { const navMenu = document.querySelector(".nav-menu"); if (!navMenu) return; const navAuroras = Array.from(document.querySelectorAll(".aurora")).filter( (el) => el.closest(".nav") ); const sync = () => { const open = navMenu.classList.contains("open"); navAuroras.forEach((el) => { if (open) mountVanta(el); else unmountVanta(el); }); }; sync(); if (!navMenu.__auroraObserver) { const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.attributeName === "class") { sync(); break; } } }); observer.observe(navMenu, { attributes: true }); navMenu.__auroraObserver = observer; window.addEventListener("beforeunload", () => { try { observer.disconnect(); } catch (_) {} }); } } function initLenis() { if (typeof window.Lenis === "undefined") return null; const lenis = new Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), orientation: "vertical", gestureOrientation: "vertical", smoothWheel: true, wheelMultiplier: 1, smoothTouch: false, touchMultiplier: 2, infinite: false, }); document.documentElement.classList.add("lenis-smooth"); if (hasGSAP) { gsap.ticker.add((time) => lenis.raf(time * 1000)); gsap.ticker.lagSmoothing(0); } else { const raf = (t) => { lenis.raf(t); requestAnimationFrame(raf); }; requestAnimationFrame(raf); } lenis.on("scroll", () => { if (hasST) ScrollTrigger.update(); }); document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", function (e) { const href = this.getAttribute("href"); if (!href || href === "#") return; const target = document.querySelector(href); if (!target) return; e.preventDefault(); lenis.scrollTo(target, { offset: 0, duration: 1.5 }); }); }); return lenis; } function computeDelay(element) { let delay = 0; let parent = element.parentElement; if (parent && parent.hasAttribute("data-stagger")) delay += 0.2; let sibling = element.previousElementSibling; while (sibling) { if (sibling.hasAttribute("data-stagger")) delay += 0.2; sibling = sibling.previousElementSibling; } return delay; } function initAnimations() { if (!hasGSAP || !hasST) return; const staggerContainers = document.querySelectorAll('[data-stagger="true"]'); staggerContainers.forEach((container) => { if (container.__staggerInit) return; container.__staggerInit = true; const delay = computeDelay(container); const children = container.children; const tl = gsap.timeline({ delay, scrollTrigger: { trigger: container, start: "top bottom-=100", end: "bottom top+=100", toggleActions: "play none none reverse", }, }); gsap.set(children, { opacity: 0, y: 50 }); tl.to(children, { opacity: 1, y: 0, duration: 0.5, stagger: 0.1, }); }); } document.addEventListener("DOMContentLoaded", () => { if (hasJQ) { $(".w-tab-link").removeClass("w--current"); $(".w-tab-pane").removeClass("w--tab-active"); $(".w-tab-link:nth-child(1)").addClass("w--current"); $(".w-tab-pane:nth-child(1)").addClass("w--tab-active"); } if (hasWebflow && typeof Webflow.push === "function") { Webflow.push(() => { if (hasJQ) $(".copyright-year").text(new Date().getFullYear()); }); } else { const y = document.querySelector(".copyright-year"); if (y) y.textContent = new Date().getFullYear(); } document .querySelectorAll('a[data-fancybox="true"] > img[fancybox-image="true"]') .forEach((img) => { const anchor = img.closest('a[data-fancybox="true"]'); if (anchor && img.src) anchor.href = img.src; }); const elements = document.querySelectorAll("[data-countup]"); if (elements.length) { const observer = new IntersectionObserver( (entries, obs) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const el = entry.target; const target = +el.getAttribute("data-countup"); const duration = 2000; let startTime = null; const animate = (timestamp) => { if (!startTime) startTime = timestamp; const progress = timestamp - startTime; const value = Math.min( Math.floor((progress / duration) * target), target ); el.textContent = value.toLocaleString(); if (value < target) requestAnimationFrame(animate); }; requestAnimationFrame(animate); obs.unobserve(el); }); }, { threshold: 0.1 } ); elements.forEach((el) => observer.observe(el)); } document.querySelectorAll(".swiper").forEach((el) => { if (el.__swiper) return; const nextEl = el.parentElement?.querySelector(".cs-slider-arrow.next"); const prevEl = el.parentElement?.querySelector(".cs-slider-arrow.prev"); el.__swiper = new Swiper(el, { loop: true, navigation: { nextEl, prevEl }, }); }); if (hasJQ && hasGSAP) { $(() => { const $navMenu = $(".nav-menu"); const $lists = $(".nav-menu-cs-list"); const $leftLinks = $(".nav-menu-left-bot-link"); const $triggers = $('[id$="-click"]'); const staggerInCards = (listSelector) => { const items = $(`${listSelector} .nav-menu-cs-item`); return gsap.timeline().to(items, { duration: 0.6, opacity: 1, y: 0, stagger: 0.08, ease: "power2.out", }, 0); }; const staggerOutCards = (listSelector) => { const items = $(`${listSelector} .nav-menu-cs-item`); return gsap.timeline().to(items, { duration: 0.4, opacity: 0, y: 20, stagger: 0.05, ease: "power2.in", }, 0); }; const resetLeftState = () => { $lists.removeClass("active"); $leftLinks.removeClass("active"); $triggers.removeClass("active"); gsap.set(".nav-menu-cs-item", { opacity: 0, y: 20 }); }; const activateGroup = (baseId) => { const $targetList = $(`#${baseId}-list`); if (!$targetList.length) return; const $currentList = $lists.filter(".active").not($targetList); const doSwitch = () => { $lists.removeClass("active"); $targetList.addClass("active"); $leftLinks.removeClass("active"); $(`#${baseId}-link`).addClass("active"); staggerInCards(`#${baseId}-list`); }; if ($currentList.length) { staggerOutCards(`#${$currentList.attr("id")}`).then(doSwitch); } else { doSwitch(); } }; $(".hamburger-btn").on("click", () => { $navMenu.toggleClass("open"); if (!$navMenu.hasClass("open")) { resetLeftState(); } else { // activateGroup("personal"); } }); $(".back_button.button").on("click", () => { resetLeftState(); }); $triggers.on("click", function (e) { e.preventDefault(); const baseId = this.id.replace("-click", ""); const $targetList = $(`#${baseId}-list`); const isActive = $targetList.hasClass("active"); if (isActive) { staggerOutCards(`#${baseId}-list`).then(() => { $targetList.removeClass("active"); $(`#${baseId}-link`).removeClass("active"); }); } else { activateGroup(baseId); } }); $('[id$="-link"]').on("click", function () { const baseId = this.id.replace("-link", ""); $(`#${baseId}-click`).toggleClass("active"); }); const menuNode = $navMenu.get(0); if (menuNode) { new MutationObserver(() => { if (!$navMenu.hasClass("open")) resetLeftState(); }).observe(menuNode, { attributes: true, attributeFilter: ["class"] }); } resetLeftState(); }); } if (hasJQ) { $(".cs-slider-arrow").hover( function () { $(".follower-dot").addClass("grow"); }, function () { $(".follower-dot").removeClass("grow"); } ); } initLenis(); initAnimations(); initAurorasNav(); initAurorasViewport(); }); window.addEventListener("load", () => { if (!hasGSAP) return; gsap.utils.toArray(".logo-list").forEach((list) => { if (list.dataset.cloned === "true") return; list.dataset.cloned = "true"; const items = list.querySelectorAll(".logo-item"); if (!items.length) return; for (let i = 0; i < 3; i++) items.forEach((item) => list.appendChild(item.cloneNode(true))); const itemWidth = items[0].offsetWidth; const gap = parseFloat(getComputedStyle(items[0]).marginRight) || 0; const totalWidth = (itemWidth + gap) * items.length; gsap.set(list, { x: 0 }); const tl = gsap.timeline({ repeat: -1, defaults: { ease: "none" } }); tl.to(list, { x: -totalWidth, duration: totalWidth / 50, ease: "none" }); list.querySelectorAll(".logo-item").forEach((item) => { item.addEventListener("mouseenter", () => gsap.to(tl, { timeScale: 0, overwrite: true })); item.addEventListener("mouseleave", () => gsap.to(tl, { timeScale: 1, overwrite: true })); }); }); // ======================================== // BIDIRECTIONAL MARQUEE - SPLIT INTO TWO ROWS // ======================================== gsap.utils.toArray(".cs-hl-row.marquee").forEach((container) => { if (container.dataset.marqueeInit === "true") return; container.dataset.marqueeInit = "true"; // Find the inner row with all items, or use container directly const innerRow = container.querySelector(".cs-hl-row:not(.marquee)") || container; const allItems = Array.from(innerRow.querySelectorAll(".cs-hl-item")); if (!allItems.length) return; // Split items in half (first half gets extra item if odd) const midpoint = Math.ceil(allItems.length / 2); const firstHalf = allItems.slice(0, midpoint); const secondHalf = allItems.slice(midpoint); // Clear container and create two new rows container.innerHTML = ""; // Style the container container.style.display = "flex"; container.style.flexDirection = "column"; container.style.gap = "1rem"; container.style.overflow = "hidden"; // Create row 1 (left to right) const row1 = document.createElement("div"); row1.className = "cs-hl-row-inner cs-hl-row-1"; row1.style.display = "flex"; row1.style.gap = "1rem"; row1.style.width = "fit-content"; // Create row 2 (right to left) const row2 = document.createElement("div"); row2.className = "cs-hl-row-inner cs-hl-row-2"; row2.style.display = "flex"; row2.style.gap = "1rem"; row2.style.width = "fit-content"; // Clone items multiple times for seamless loop (4 sets) for (let i = 0; i < 4; i++) { firstHalf.forEach((item) => { const clone = item.cloneNode(true); clone.style.flexShrink = "0"; row1.appendChild(clone); }); secondHalf.forEach((item) => { const clone = item.cloneNode(true); clone.style.flexShrink = "0"; row2.appendChild(clone); }); } container.appendChild(row1); container.appendChild(row2); // Wait for layout to calculate dimensions requestAnimationFrame(() => { // Calculate total width for one set of items const gap = 16; // 1rem // Row 1 dimensions const row1Items = row1.querySelectorAll(".cs-hl-item"); const row1ItemWidth = row1Items[0]?.offsetWidth || 0; const row1SetWidth = (row1ItemWidth + gap) * firstHalf.length; // Row 2 dimensions const row2Items = row2.querySelectorAll(".cs-hl-item"); const row2ItemWidth = row2Items[0]?.offsetWidth || 0; const row2SetWidth = (row2ItemWidth + gap) * secondHalf.length; // Animation speed (pixels per second) const speed = 50; // Row 1: moves left (negative x), starts at 0 const tl1 = gsap.timeline({ repeat: -1 }); tl1.fromTo(row1, { x: 0 }, { x: -row1SetWidth, duration: row1SetWidth / speed, ease: "none" } ); // Row 2: moves right (positive x), starts offset left gsap.set(row2, { x: -row2SetWidth }); const tl2 = gsap.timeline({ repeat: -1 }); tl2.fromTo(row2, { x: -row2SetWidth }, { x: 0, duration: row2SetWidth / speed, ease: "none" } ); // Pause on hover for row 1 row1.querySelectorAll(".cs-hl-item").forEach((item) => { item.addEventListener("mouseenter", () => gsap.to(tl1, { timeScale: 0, overwrite: true })); item.addEventListener("mouseleave", () => gsap.to(tl1, { timeScale: 1, overwrite: true })); }); // Pause on hover for row 2 row2.querySelectorAll(".cs-hl-item").forEach((item) => { item.addEventListener("mouseenter", () => gsap.to(tl2, { timeScale: 0, overwrite: true })); item.addEventListener("mouseleave", () => gsap.to(tl2, { timeScale: 1, overwrite: true })); }); }); }); function horizontalLoop(items, config) { items = gsap.utils.toArray(items); config = config || {}; let tl = gsap.timeline({ repeat: config.repeat, paused: config.paused, defaults: { ease: "none" }, onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100), }), length = items.length, startX = items[0].offsetLeft, times = [], widths = [], xPercents = [], curIndex = 0, pixelsPerSecond = (config.speed || 1) * 100, snap = config.snap === false ? (v) => v : gsap.utils.snap(config.snap || 1), totalWidth, curX, distanceToStart, distanceToLoop, item, i; gsap.set(items, { xPercent: (i, el) => { let w = (widths[i] = parseFloat(gsap.getProperty(el, "width", "px"))); xPercents[i] = snap( (parseFloat(gsap.getProperty(el, "x", "px")) / w) * 100 + gsap.getProperty(el, "xPercent") ); return xPercents[i]; }, }); gsap.set(items, { x: 0 }); totalWidth = items[length - 1].offsetLeft + (xPercents[length - 1] / 100) * widths[length - 1] - startX + items[length - 1].offsetWidth * gsap.getProperty(items[length - 1], "scaleX") + (parseFloat(config.paddingRight) || 0); for (i = 0; i < length; i++) { item = items[i]; curX = (xPercents[i] / 100) * widths[i]; distanceToStart = item.offsetLeft + curX - startX; distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX"); tl.to( item, { xPercent: snap(((curX - distanceToLoop) / widths[i]) * 100), duration: distanceToLoop / pixelsPerSecond, }, 0 ) .fromTo( item, { xPercent: snap(((curX - distanceToLoop + totalWidth) / widths[i]) * 100), }, { xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false, }, distanceToLoop / pixelsPerSecond ) .add("label" + i, distanceToStart / pixelsPerSecond); times[i] = distanceToStart / pixelsPerSecond; } function toIndex(index, vars) { vars = vars || {}; Math.abs(index - curIndex) > length / 2 && (index += index > curIndex ? -length : length); let newIndex = gsap.utils.wrap(0, length, index), time = times[newIndex]; if (time > tl.time() !== index > curIndex) { vars.modifiers = { time: gsap.utils.wrap(0, tl.duration()) }; time += tl.duration() * (index > curIndex ? 1 : -1); } curIndex = newIndex; vars.overwrite = true; return tl.tweenTo(time, vars); } tl.next = (vars) => toIndex(curIndex + 1, vars); tl.previous = (vars) => toIndex(curIndex - 1, vars); tl.current = () => curIndex; tl.toIndex = (index, vars) => toIndex(index, vars); tl.times = times; if (config.reversed) { tl.vars.onReverseComplete(); tl.reverse(); } return tl; } if (!hasST) return; gsap.utils.toArray(".cs-hl-col").forEach((col, i) => { if (col.dataset.cloned === "true") return; col.dataset.cloned = "true"; const images = col.querySelectorAll(".cs-hl-item"); if (!images.length) return; for (let j = 0; j < 3; j++) images.forEach((img) => col.appendChild(img.cloneNode(true))); const itemHeight = images[0].clientHeight; gsap.set(col, { marginTop: -itemHeight }); let mod = gsap.utils.wrap(0, itemHeight); let direction = i === 0 ? "-=" + itemHeight : "+=" + itemHeight; function marquee(element, time, dir) { return gsap.to(element, { duration: time, ease: "none", y: dir, modifiers: { y: (y) => mod(parseFloat(y)) + "px" }, repeat: -1, }); } var master = gsap.timeline().add(marquee(col, 15, direction), 0); var tween = gsap.to(master, { duration: 1.5, timeScale: 1, paused: true }); var timeScaleClamp = gsap.utils.clamp(1, 6); ScrollTrigger.create({ start: 0, end: "max", onUpdate: (self) => { const velocity = Math.abs(self.getVelocity() / 200); master.timeScale(timeScaleClamp(velocity)); tween.invalidate().restart(); }, }); col.addEventListener("mouseenter", () => gsap.to(master, { timeScale: 0, overwrite: true })); col.addEventListener("mouseleave", () => gsap.to(master, { timeScale: 1, overwrite: true })); }); if (document.querySelector(".dark-trigger") && hasST) { ScrollTrigger.create({ trigger: ".dark-trigger", start: "top 50%", end: "top 50%", onEnter: () => { document.querySelectorAll('[data-swap="true"]').forEach((el) => { el.classList.add("theme-swap"); if (el.classList.contains("button")) { el.classList.add("w-variant-a9a5f232-3704-5995-87cf-fcae1d273319"); } if (el.classList.contains("link")) { el.classList.remove("w-variant-d177991f-d39e-af2f-5c98-d142ba3dc9a"); } }); }, onLeaveBack: () => { document.querySelectorAll('[data-swap="true"]').forEach((el) => { el.classList.remove("theme-swap"); if (el.classList.contains("button")) { el.classList.remove("w-variant-a9a5f232-3704-5995-87cf-fcae1d273319"); } if (el.classList.contains("link")) { el.classList.add("w-variant-d177991f-d39e-af2f-5c98-d142ba3dc9a"); } }); }, }); } if (hasST) ScrollTrigger.refresh(); }); })();