(function() { "use strict"; const SITE_JS_VERSION = "4.12.3"; console.log(`[site] global.js v${SITE_JS_VERSION} loaded`); if (typeof gsap === "undefined") { console.error("[site] GSAP not found. Enable it in Webflow Project Settings → Integrations."); throw new Error("[site] Missing dependency: gsap"); } gsap.registerPlugin(ScrollTrigger); ScrollTrigger.config({ ignoreMobileResize: true }); const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const numOr = (v, fallback) => { const n = Number(v); return Number.isFinite(n) ? n : fallback; }; const isTouch = window.matchMedia("(pointer: coarse)").matches || navigator.maxTouchPoints > 0; const lenis = new Lenis({ lerp: .1, smoothWheel: !isTouch, syncTouch: false, wheelMultiplier: 1 }); gsap.ticker.add(time => lenis.raf(time * 1e3)); gsap.ticker.lagSmoothing(0); lenis.on("scroll", ScrollTrigger.update); if (prefersReduced) lenis.stop(); let _rTimer; const _refresh = () => { clearTimeout(_rTimer); _rTimer = setTimeout(() => ScrollTrigger.refresh(), 200); }; window.addEventListener("resize", _refresh, { passive: true }); window.addEventListener("orientationchange", _refresh, { passive: true }); const mm = gsap.matchMedia(); const DEFAULT_SETTINGS = { startIndex: 0, autoPlay: true, autoPlayDelay: 5e3, pauseOnHover: true, swipeThreshold: 50, fadeOutDuration: .2, fadeInDuration: .6, fadePause: .06, fadeEase: "power4.out", dotClass: "quote-dot", dotInactiveAlpha: .5, dotActiveAlpha: 1, dotActiveWidth: "2rem", dotCloseDuration: .3, dotOpenDuration: .5, dotPause: .04, dotEase: "power4.out", counterFormat: "index" }; function readSettings(el) { const s = { ...DEFAULT_SETTINGS }, d = el.dataset; if (d.autoplay !== undefined) s.autoPlay = d.autoplay !== "false"; if (d.autoplayDelay !== undefined) s.autoPlayDelay = numOr(d.autoplayDelay, DEFAULT_SETTINGS.autoPlayDelay); if (d.swipeThreshold !== undefined) s.swipeThreshold = numOr(d.swipeThreshold, DEFAULT_SETTINGS.swipeThreshold); if (d.startIndex !== undefined) s.startIndex = numOr(d.startIndex, 0); if (d.counterFormat !== undefined) s.counterFormat = d.counterFormat; return s; } const activeSliders = []; document.addEventListener("visibilitychange", () => { activeSliders.forEach(c => document.hidden ? c.stopAutoPlay() : c.resetAutoPlay()); }); function createSliderCore(sliderEl, slides, settings, onAfterTransition) { let currentIndex = Math.min(settings.startIndex, slides.length - 1); let autoPlayTimer = null, touchStartX = 0, isHovered = false, isAnimating = false; let navigateNext = () => goToSlide(currentIndex + 1); let navigatePrev = () => goToSlide(currentIndex - 1); slides.forEach((slide, i) => { const active = i === currentIndex; slide.setAttribute("aria-hidden", active ? "false" : "true"); slide.style.willChange = "opacity"; gsap.set(slide, { autoAlpha: active ? 1 : 0, display: active ? "block" : "none" }); }); function goToSlide(targetIndex) { if (slides.length <= 1 || isAnimating) return; const nextIndex = (targetIndex % slides.length + slides.length) % slides.length; if (nextIndex === currentIndex) return; const outSlide = slides[currentIndex], inSlide = slides[nextIndex]; outSlide.setAttribute("aria-hidden", "true"); inSlide.setAttribute("aria-hidden", "false"); gsap.killTweensOf([ outSlide, inSlide ]); isAnimating = true; const tl = gsap.timeline({ onComplete: () => { currentIndex = nextIndex; isAnimating = false; onAfterTransition?.(currentIndex); } }); tl.to(outSlide, { autoAlpha: 0, duration: settings.fadeOutDuration, ease: settings.fadeEase, onComplete: () => gsap.set(outSlide, { display: "none" }) }); if (settings.fadePause > 0) tl.to({}, { duration: settings.fadePause }); tl.set(inSlide, { display: "block", autoAlpha: 0 }); tl.to(inSlide, { autoAlpha: 1, duration: settings.fadeInDuration, ease: settings.fadeEase }); return tl; } const nextSlide = () => navigateNext(); const prevSlide = () => navigatePrev(); function startAutoPlay() { if (!settings.autoPlay || slides.length <= 1 || prefersReduced) return; stopAutoPlay(); autoPlayTimer = setInterval(() => { if (!isHovered && !isAnimating) nextSlide(); }, settings.autoPlayDelay); } function stopAutoPlay() { if (autoPlayTimer) { clearInterval(autoPlayTimer); autoPlayTimer = null; } } function resetAutoPlay() { if (settings.autoPlay && slides.length > 1) startAutoPlay(); } if (settings.pauseOnHover && slides.length > 1) { sliderEl.addEventListener("mouseenter", () => { isHovered = true; }); sliderEl.addEventListener("mouseleave", () => { isHovered = false; }); } if (slides.length > 1) { sliderEl.addEventListener("touchstart", e => { touchStartX = e.changedTouches[0].clientX; }, { passive: true }); sliderEl.addEventListener("touchend", e => { if (isAnimating) return; const dist = e.changedTouches[0].clientX - touchStartX; if (Math.abs(dist) < settings.swipeThreshold) return; dist < 0 ? nextSlide() : prevSlide(); resetAutoPlay(); }, { passive: true }); } const core = { goToSlide: goToSlide, nextSlide: nextSlide, prevSlide: prevSlide, startAutoPlay: startAutoPlay, stopAutoPlay: stopAutoPlay, resetAutoPlay: resetAutoPlay, getCurrentIndex: () => currentIndex, setNavigate: (next, prev) => { navigateNext = next; navigatePrev = prev; } }; activeSliders.push(core); return core; } function initQuoteSliders() { document.querySelectorAll("[data-quote-slider]").forEach(sliderEl => { if (sliderEl.dataset.initDone) return; sliderEl.dataset.initDone = "1"; const settings = readSettings(sliderEl); const dotsWrap = sliderEl.querySelector("[data-quote-dots]"); const allSlides = Array.from(sliderEl.querySelectorAll("[data-quote-slide]")); const slides = allSlides.filter(s => !dotsWrap?.contains(s)); if (!slides.length) return; const dots = buildDots(dotsWrap, slides, settings); dots.forEach((dot, i) => gsap.set(dot, { autoAlpha: i === 0 ? settings.dotActiveAlpha : settings.dotInactiveAlpha, width: i === 0 ? settings.dotActiveWidth : dot.dataset.baseWidth })); const core = createSliderCore(sliderEl, slides, settings, null); function goToWithDots(targetIndex) { const fromIndex = core.getCurrentIndex(); const tl = core.goToSlide(targetIndex); if (tl && dots.length) tl.add(buildDotAnimation(dots, fromIndex, targetIndex, settings), 0); } core.setNavigate(() => goToWithDots(core.getCurrentIndex() + 1), () => goToWithDots(core.getCurrentIndex() - 1)); dots.forEach((dot, i) => { dot.addEventListener("click", () => { goToWithDots(i); core.resetAutoPlay(); }); dot.addEventListener("keydown", e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); goToWithDots(i); core.resetAutoPlay(); } }); }); core.startAutoPlay(); }); } function initGallerySliders() { document.querySelectorAll("[data-gallery-slider]").forEach(sliderEl => { if (sliderEl.dataset.initDone) return; sliderEl.dataset.initDone = "1"; const settings = readSettings(sliderEl); const slides = Array.from(sliderEl.querySelectorAll("[data-gallery-slide]")); if (!slides.length) return; const prevBtn = sliderEl.querySelector("[data-gallery-prev]"); const nextBtn = sliderEl.querySelector("[data-gallery-next]"); const counterEl = sliderEl.querySelector("[data-gallery-counter]"); const total = slides.length; function updateCounter(i) { if (!counterEl) return; counterEl.textContent = settings.counterFormat === "fraction" ? `${i + 1} / ${total}` : `${i + 1} — ${total}`; counterEl.setAttribute("aria-label", `Image ${i + 1} of ${total}`); } updateCounter(0); const core = createSliderCore(sliderEl, slides, settings, updateCounter); if (prevBtn) prevBtn.addEventListener("click", () => { core.prevSlide(); core.resetAutoPlay(); }); if (nextBtn) nextBtn.addEventListener("click", () => { core.nextSlide(); core.resetAutoPlay(); }); sliderEl.addEventListener("keydown", e => { if (e.key === "ArrowLeft") { core.prevSlide(); core.resetAutoPlay(); } if (e.key === "ArrowRight") { core.nextSlide(); core.resetAutoPlay(); } }); core.startAutoPlay(); }); } function buildDots(dotsWrap, slides, settings) { if (!dotsWrap) return []; if (slides.length <= 1) { dotsWrap.style.display = "none"; return []; } const tmpl = dotsWrap.querySelector(`.${settings.dotClass}`) || dotsWrap.firstElementChild; dotsWrap.style.display = ""; dotsWrap.innerHTML = ""; return slides.map((_, i) => { const dot = tmpl ? tmpl.cloneNode(true) : document.createElement("div"); dot.classList.add(settings.dotClass); dot.setAttribute("role", "button"); dot.setAttribute("tabindex", "0"); dot.setAttribute("aria-label", `Go to slide ${i + 1}`); dotsWrap.appendChild(dot); dot.dataset.baseWidth = window.getComputedStyle(dot).width; return dot; }); } function buildDotAnimation(dots, fromIndex, toIndex, settings) { if (!dots.length) return gsap.timeline(); const to = (toIndex % dots.length + dots.length) % dots.length; const outDot = dots[fromIndex], inDot = dots[to]; const tl = gsap.timeline(); if (outDot && outDot !== inDot) tl.to(outDot, { width: outDot.dataset.baseWidth ?? "8px", autoAlpha: settings.dotInactiveAlpha, duration: settings.dotCloseDuration, ease: settings.dotEase }); if (settings.dotPause > 0) tl.to({}, { duration: settings.dotPause }); if (inDot) tl.to(inDot, { width: settings.dotActiveWidth, autoAlpha: settings.dotActiveAlpha, duration: settings.dotOpenDuration, ease: settings.dotEase }); return tl; } function initMenu() { const openButtons = document.querySelectorAll("[data-menu-open]"); const closeButtons = document.querySelectorAll("[data-menu-close]"); const overlay = document.querySelector("[data-menu-overlay]"); const panel = document.querySelector("[data-menu-panel]"); const site = document.querySelector(".site"); if (!overlay || !panel || !site) { console.warn("[menu] Missing elements"); return; } if (overlay.dataset.initDone) return; overlay.dataset.initDone = "1"; let menuOpen = false; gsap.set(overlay, { autoAlpha: 0, pointerEvents: "none", backdropFilter: "blur(24px) saturate(180%)", willChange: "opacity" }); gsap.set(panel, { autoAlpha: 1, clipPath: "inset(0 0 100% 0)", y: "0vh" }); gsap.set(site, { y: "0vh" }); const menuTl = gsap.timeline({ paused: true }).to(overlay, { autoAlpha: 1, pointerEvents: "auto", duration: .7, ease: "power4.inOut" }, 0).to(panel, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 1.2, ease: "power4.inOut" }, 0).to(site, { y: "15vh", duration: 1.2, ease: "power4.inOut" }, 0); function openMenu() { if (!menuOpen) { menuOpen = true; menuTl.timeScale(1).play(); } } function closeMenu() { if (menuOpen) { menuOpen = false; menuTl.timeScale(1.5).reverse(); } } openButtons.forEach(btn => btn.addEventListener("click", openMenu)); closeButtons.forEach(btn => btn.addEventListener("click", closeMenu)); document.addEventListener("keydown", e => { if (e.key === "Escape" && menuOpen) closeMenu(); }); overlay.addEventListener("click", closeMenu); } function initImageScroller() { document.querySelectorAll("[data-img-scroll]").forEach(scroller => { if (scroller.dataset.initDone) return; scroller.dataset.initDone = "1"; const track = scroller.querySelector("[data-img-track]"); const prevBtn = scroller.querySelector("[data-img-prev]"); const nextBtn = scroller.querySelector("[data-img-next]"); if (!track) return; if (isTouch) { scroller.style.overflowX = "scroll"; scroller.style.overflowY = "hidden"; scroller.style.touchAction = "pan-x"; track.querySelectorAll("img").forEach(img => img.setAttribute("draggable", "false")); return; } const originals = Array.from(track.children); const N = originals.length; if (!N) return; const preFrag = document.createDocumentFragment(); originals.forEach(item => { const c = item.cloneNode(true); c.setAttribute("aria-hidden", "true"); preFrag.appendChild(c); }); track.insertBefore(preFrag, track.firstChild); originals.forEach(item => { const c = item.cloneNode(true); c.setAttribute("aria-hidden", "true"); track.appendChild(c); }); track.querySelectorAll("img").forEach(img => { img.setAttribute("draggable", "false"); img.addEventListener("dragstart", e => e.preventDefault()); }); const allItems = Array.from(track.children); let scrollerW = 0, singleW = 0, allSnaps = [], origSnaps = []; const centerL = item => Math.round(item.offsetLeft - (scrollerW - item.offsetWidth) / 2); const nearest = sl => allSnaps.length ? allSnaps.reduce((b, s) => Math.abs(s - sl) < Math.abs(b - sl) ? s : b, allSnaps[0]) : sl; const nextSnap = sl => allSnaps.filter(s => s > sl + 2).sort((a, b) => a - b)[0] ?? nearest(sl); const prevSnap = sl => allSnaps.filter(s => s < sl - 2).sort((a, b) => b - a)[0] ?? nearest(sl); function loopCheck() { if (!origSnaps.length) return; const sl = scroller.scrollLeft; if (sl < origSnaps[0] - 2) scroller.scrollLeft += singleW; else if (sl > origSnaps[N - 1] + 2) scroller.scrollLeft -= singleW; } let pendingSL = null, rafId = 0; const flushSL = () => { if (pendingSL !== null) { scroller.scrollLeft = pendingSL; pendingSL = null; } rafId = 0; }; const writeSL = v => { pendingSL = v; if (!rafId) rafId = requestAnimationFrame(flushSL); }; const proxy = { v: 0 }; let tween = null; function snapTo(target, dur = .65) { if (tween) tween.kill(); proxy.v = scroller.scrollLeft; tween = gsap.to(proxy, { v: target, duration: dur, ease: "expo.out", onUpdate() { writeSL(proxy.v); }, onComplete() { flushSL(); loopCheck(); } }); } function calcSnaps() { scrollerW = scroller.clientWidth; singleW = Math.round(track.scrollWidth / 3); allSnaps = allItems.map(centerL); origSnaps = allSnaps.slice(N, 2 * N); } if (prevBtn) prevBtn.addEventListener("click", () => { if (allSnaps.length) snapTo(prevSnap(scroller.scrollLeft)); }); if (nextBtn) nextBtn.addEventListener("click", () => { if (allSnaps.length) snapTo(nextSnap(scroller.scrollLeft)); }); let active = false, axis = null, startX = 0, startY = 0, startSL = 0; scroller.addEventListener("pointerdown", e => { if (tween) { tween.kill(); tween = null; } active = true; axis = null; startX = e.clientX; startY = e.clientY; startSL = scroller.scrollLeft; scroller.setPointerCapture(e.pointerId); scroller.style.cursor = "grabbing"; }, { passive: true }); scroller.addEventListener("pointermove", e => { if (!active) return; const dx = e.clientX - startX, dy = e.clientY - startY; if (!axis) { if (Math.abs(dx) > Math.abs(dy) + 3) axis = "x"; else if (Math.abs(dy) > Math.abs(dx) + 3) axis = "y"; else return; } if (axis === "y") return; e.preventDefault(); writeSL(startSL - dx); }, { passive: false }); const onEnd = () => { if (!active) return; active = false; scroller.style.cursor = "grab"; flushSL(); if (axis !== "x" || !allSnaps.length) return; const sl = scroller.scrollLeft, absM = Math.abs(sl - startSL); if (absM < 20) snapTo(nearest(startSL), .45); else if (absM < scrollerW * .22) snapTo(sl > startSL ? nextSnap(startSL) : prevSnap(startSL), .6); else snapTo(nearest(sl), .7); }; scroller.addEventListener("pointerup", onEnd, { passive: true }); scroller.addEventListener("pointercancel", onEnd, { passive: true }); let centredIndex = N, roTimer; new ResizeObserver(() => { if (allSnaps.length) { const idx = allSnaps.indexOf(nearest(scroller.scrollLeft)); if (idx !== -1) centredIndex = idx; } clearTimeout(roTimer); roTimer = setTimeout(() => { calcSnaps(); scroller.scrollLeft = allSnaps[centredIndex] ?? origSnaps[0] ?? 0; loopCheck(); }, 150); }).observe(scroller); const allImgs = Array.from(track.querySelectorAll("img")); const decodeImg = img => img.decode ? img.decode().catch(() => {}) : Promise.resolve(); Promise.all(allImgs.map(img => { if (img.complete && img.naturalWidth > 0) return decodeImg(img); return new Promise(resolve => { img.addEventListener("load", () => decodeImg(img).then(resolve), { once: true }); img.addEventListener("error", resolve, { once: true }); }); })).then(() => requestAnimationFrame(() => requestAnimationFrame(() => { calcSnaps(); scroller.scrollLeft = origSnaps[0] ?? 0; loopCheck(); }))); }); } function initAccordion() { const groups = document.querySelectorAll("[data-accordion]"); if (!groups.length) return; const DURATION = prefersReduced ? 0 : .45; const EASE = "power2.inOut"; groups.forEach(group => { if (group.dataset.initDone) return; group.dataset.initDone = "1"; const independent = group.dataset.accordion === "independent"; const items = group.querySelectorAll("[data-accordion-item]"); if (!items.length) return; const state = []; items.forEach(item => { const header = item.querySelector("[data-accordion-header]"); const content = item.querySelector("[data-accordion-content]"); const icon = item.querySelector("[data-accordion-icon]"); if (!header || !content) return; gsap.set(content, { height: 0, overflow: "hidden", boxSizing: "border-box" }); if (icon) gsap.set(icon, { rotate: 0, transformOrigin: "50% 50%" }); header.setAttribute("aria-expanded", "false"); const entry = { item: item, header: header, content: content, icon: icon, open: false }; state.push(entry); header.addEventListener("click", () => toggle(entry)); if (header.tagName !== "BUTTON") { header.setAttribute("role", "button"); header.setAttribute("tabindex", "0"); header.addEventListener("keydown", e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(entry); } }); } }); function openEntry(entry) { if (entry.open) return; entry.open = true; entry.header.setAttribute("aria-expanded", "true"); entry.item.classList.add("is-open"); const target = entry.content.scrollHeight; gsap.to(entry.content, { height: target, duration: DURATION, ease: EASE, onComplete: () => { gsap.set(entry.content, { height: "auto" }); ScrollTrigger.refresh(); } }); if (entry.icon) gsap.to(entry.icon, { rotate: 45, duration: DURATION, ease: EASE }); } function closeEntry(entry) { if (!entry.open) return; entry.open = false; entry.header.setAttribute("aria-expanded", "false"); entry.item.classList.remove("is-open"); const current = entry.content.getBoundingClientRect().height; gsap.set(entry.content, { height: current }); gsap.to(entry.content, { height: 0, duration: DURATION, ease: EASE, onComplete: () => ScrollTrigger.refresh() }); if (entry.icon) gsap.to(entry.icon, { rotate: 0, duration: DURATION, ease: EASE }); } function toggle(entry) { if (entry.open) { closeEntry(entry); return; } if (!independent) state.forEach(other => { if (other !== entry) closeEntry(other); }); openEntry(entry); } state.forEach(entry => { if (entry.item.hasAttribute("data-open")) { entry.open = true; entry.header.setAttribute("aria-expanded", "true"); entry.item.classList.add("is-open"); gsap.set(entry.content, { height: "auto" }); if (entry.icon) gsap.set(entry.icon, { rotate: 45 }); if (!independent) { state.forEach(o => { if (o !== entry && o.open) closeEntry(o); }); } } }); }); } const CONSENT_KEY = "off_field_consent_done"; const CONSENT_DELAY = 1; function initConsent() { const form = document.querySelector(".consent_components"); if (!form) return; if (localStorage.getItem(CONSENT_KEY)) return; gsap.to(form, { autoAlpha: 1, duration: .6, ease: "power2.out", delay: CONSENT_DELAY }); form.querySelectorAll("[data-consent-accept]").forEach(btn => { btn.addEventListener("click", () => { localStorage.setItem(CONSENT_KEY, "1"); gsap.to(form, { autoAlpha: 0, duration: .4, ease: "power2.in", onComplete: () => { form.style.display = "none"; } }); }); }); } function safeInit(name, fn) { try { fn(); } catch (err) { console.error(`[site] init failed: ${name}`, err); } } function lazyInit(selector, name, fn, rootMargin = "300px") { const first = document.querySelector(selector); if (!first) return; const rect = first.getBoundingClientRect(); const margin = parseInt(rootMargin, 10) || 0; if (rect.top < window.innerHeight + margin && rect.bottom > -margin) { safeInit(name, fn); return; } if (!("IntersectionObserver" in window)) { safeInit(name, fn); return; } const io = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting)) { io.disconnect(); safeInit(name, fn); } }, { rootMargin: rootMargin }); io.observe(first); } function idleInit(name, fn) { const run = () => safeInit(name, fn); if ("requestIdleCallback" in window) { requestIdleCallback(run, { timeout: 2e3 }); } else { setTimeout(run, 1); } } document.addEventListener("DOMContentLoaded", () => { if (document.querySelector(".consent_components")) safeInit("consent", initConsent); if (document.querySelector("[data-menu-overlay]")) safeInit("menu", initMenu); if (document.querySelector("[data-hero]")) safeInit("parallax", initParallax); if (document.querySelector("[data-accordion]")) safeInit("accordion", initAccordion); if (document.querySelector('[data-list="dynamic"]')) safeInit("dynamicLists", initDynamicLists); if (document.querySelector("[data-list]")) safeInit("collectionLists", initCollectionLists); if (document.querySelector("[data-share-email],[data-share-copy]")) idleInit("share", initShare); lazyInit("[data-img-scroll]", "imageScroller", initImageScroller); lazyInit("[data-quote-slider]", "quoteSliders", initQuoteSliders); lazyInit("[data-gallery-slider]", "gallerySliders", initGallerySliders); lazyInit(".audio__player", "audioPlayer", initAudioPlayer); lazyInit("[data-footer-unveil]", "footerUnveil", initFooterUnveil); }); window.addEventListener("load", () => ScrollTrigger.refresh()); function initAudioPlayer() { document.querySelectorAll(".audio__player").forEach(player => { if (player.dataset.initDone) return; player.dataset.initDone = "1"; const audio = player.querySelector("audio"); const playBtn = player.querySelector("[data-pod-play]"); if (!audio || !playBtn) return; const timeEl = player.querySelector("[data-pod-time]"); const currentEl = player.querySelector("[data-pod-current]"); const remainingEl = player.querySelector("[data-pod-remaining]"); const durationEl = player.querySelector("[data-pod-duration]"); const speedBtn = player.querySelector("[data-pod-speed]"); const playIcon = player.querySelector("[data-pod-icon-play]"); const pauseIcon = player.querySelector("[data-pod-icon-pause]"); const scrubTrack = player.querySelector("[data-pod-scrub]"); const scrubFill = player.querySelector("[data-pod-progress]"); let shownDisplay = "inline-block"; if (playIcon) { const d = getComputedStyle(playIcon).display; if (d && d !== "none") shownDisplay = d; } const speeds = (player.dataset.podSpeeds || "1,1.5,2").split(",").map(s => parseFloat(s)).filter(n => n > 0); if (!speeds.length) speeds.push(1); let speedIndex = 0; const fmtTime = s => { s = Math.max(0, Math.floor(s)); const m = Math.floor(s / 60); const r = s % 60; return m + ":" + (r < 10 ? "0" : "") + r; }; const setPlaying = on => { player.setAttribute("data-pod-playing", on ? "true" : "false"); playBtn.setAttribute("aria-pressed", on ? "true" : "false"); if (playIcon) playIcon.style.display = on ? "none" : shownDisplay; if (pauseIcon) pauseIcon.style.display = on ? shownDisplay : "none"; }; const updateTime = () => { const dur = isFinite(audio.duration) ? audio.duration : 0; const cur = Math.max(0, Math.min(audio.currentTime, dur)); const remaining = Math.max(0, dur - cur); if (timeEl) timeEl.textContent = fmtTime(remaining); if (currentEl) currentEl.textContent = fmtTime(cur); if (remainingEl) remainingEl.textContent = "-" + fmtTime(remaining); if (durationEl) durationEl.textContent = fmtTime(dur); }; const updateScrub = () => { if (!scrubFill) return; const dur = isFinite(audio.duration) ? audio.duration : 0; const pct = dur > 0 ? audio.currentTime / dur * 100 : 0; scrubFill.style.width = pct + "%"; }; const updateSpeedLabel = () => { if (!speedBtn) return; const s = speeds[speedIndex]; const txt = (s % 1 === 0 ? s.toFixed(0) : String(s)) + "x"; const label = speedBtn.querySelector("[data-pod-speed-label]") || speedBtn; label.textContent = txt; }; setPlaying(false); updateSpeedLabel(); if (audio.readyState >= 1) { updateTime(); updateScrub(); } const tick = () => { updateTime(); updateScrub(); }; audio.addEventListener("loadedmetadata", tick); audio.addEventListener("durationchange", tick); audio.addEventListener("timeupdate", tick); audio.addEventListener("play", () => setPlaying(true)); audio.addEventListener("pause", () => setPlaying(false)); audio.addEventListener("ended", () => { setPlaying(false); tick(); }); playBtn.addEventListener("click", () => { if (audio.paused) audio.play(); else audio.pause(); }); if (speedBtn) { speedBtn.addEventListener("click", () => { speedIndex = (speedIndex + 1) % speeds.length; audio.playbackRate = speeds[speedIndex]; updateSpeedLabel(); }); } if (scrubTrack) { let isScrubbing = false; let wasPlaying = false; scrubTrack.style.touchAction = "none"; const seekFromPointer = e => { const rect = scrubTrack.getBoundingClientRect(); const ratio = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)); const dur = isFinite(audio.duration) ? audio.duration : 0; if (dur <= 0) return; audio.currentTime = ratio * dur; updateScrub(); updateTime(); }; scrubTrack.addEventListener("pointerdown", e => { isScrubbing = true; wasPlaying = !audio.paused; if (wasPlaying) audio.pause(); player.setAttribute("data-pod-scrubbing", "true"); try { scrubTrack.setPointerCapture(e.pointerId); } catch (_) {} seekFromPointer(e); }); scrubTrack.addEventListener("pointermove", e => { if (isScrubbing) seekFromPointer(e); }); const endScrub = e => { if (!isScrubbing) return; isScrubbing = false; player.setAttribute("data-pod-scrubbing", "false"); try { scrubTrack.releasePointerCapture(e.pointerId); } catch (_) {} if (wasPlaying) audio.play(); }; scrubTrack.addEventListener("pointerup", endScrub); scrubTrack.addEventListener("pointercancel", endScrub); scrubTrack.setAttribute("role", "slider"); scrubTrack.setAttribute("tabindex", "0"); if (!scrubTrack.hasAttribute("aria-label")) { scrubTrack.setAttribute("aria-label", "Audio scrubber"); } scrubTrack.addEventListener("keydown", e => { const step = e.shiftKey ? 15 : 5; const dur = isFinite(audio.duration) ? audio.duration : 0; if (e.key === "ArrowLeft") { e.preventDefault(); audio.currentTime = Math.max(0, audio.currentTime - step); tick(); } else if (e.key === "ArrowRight") { e.preventDefault(); audio.currentTime = Math.min(dur, audio.currentTime + step); tick(); } }); } }); } function initShare() { const emailBtn = document.querySelector("[data-share-email]"); const copyBtn = document.querySelector("[data-share-copy]"); if (emailBtn) { emailBtn.addEventListener("click", () => { window.location.href = "mailto:?subject=" + encodeURIComponent(document.title) + "&body=" + encodeURIComponent(window.location.href); }); } if (copyBtn) { copyBtn.addEventListener("click", function() { const orig = this.textContent; navigator.clipboard.writeText(window.location.href).then(() => { this.textContent = "Copied"; setTimeout(() => { this.textContent = orig; }, 2e3); }).catch(err => console.warn("[site] Clipboard write failed:", err)); }); } } function initCollectionLists() { document.querySelectorAll("[data-list]").forEach(wrapper => { if (wrapper.dataset.initDone) return; if (wrapper.getAttribute("data-list") === "") return; wrapper.dataset.initDone = "1"; const items = Array.from(wrapper.querySelectorAll("[data-list-item]")); if (!items.length) return; let loadBtn = wrapper.querySelector("[data-load-more]"); if (!loadBtn && wrapper.parentElement) { const candidates = wrapper.parentElement.querySelectorAll("[data-load-more]"); for (const btn of candidates) { const owner = btn.closest("[data-list]"); if (!owner || owner === wrapper) { loadBtn = btn; break; } } } const countVisible = wrapper.querySelector("[data-count-visible],[data-count-format]"); const countTotal = wrapper.querySelector("[data-count-total]"); const filterBtns = Array.from(wrapper.querySelectorAll("[data-filter]")); const mobileBreakpoint = numOr(wrapper.dataset.mobileBreakpoint, 767); const isMobile = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`).matches; const initialDesktop = numOr(wrapper.dataset.initialCount, Infinity); const initialMobile = numOr(wrapper.dataset.initialCountMobile, initialDesktop); const loadDesktop = numOr(wrapper.dataset.loadCount, 6); const loadMobile = numOr(wrapper.dataset.loadCountMobile, loadDesktop); const initialCount = isMobile ? initialMobile : initialDesktop; const loadCount = isMobile ? loadMobile : loadDesktop; const fadeDisabled = wrapper.hasAttribute("data-fade-disable") || prefersReduced; const fadeDuration = numOr(wrapper.dataset.fadeDuration, .9); const fadeStagger = numOr(wrapper.dataset.fadeStagger, .12); const fadeY = numOr(wrapper.dataset.fadeY, 20); let currentFilter = "all"; let visibleCount = initialCount; const setCount = (el, n) => { const fmt = el.dataset.countFormat; el.textContent = fmt ? fmt.replace("{n}", n) : String(n); }; const matchesFilter = item => { if (currentFilter === "all") return true; const cats = (item.dataset.category || "").split(",").map(s => s.trim()); return cats.includes(currentFilter); }; const getMatching = () => currentFilter === "all" ? items : items.filter(matchesFilter); function render() { const matching = getMatching(); const shown = matching.slice(0, visibleCount); items.forEach(item => { item.hidden = true; item.style.display = "none"; item.setAttribute("aria-hidden", "true"); }); shown.forEach(item => { item.hidden = false; item.style.display = ""; item.setAttribute("aria-hidden", "false"); }); if (countVisible) setCount(countVisible, shown.length); if (countTotal) setCount(countTotal, matching.length); if (loadBtn) { const allShown = shown.length >= matching.length; loadBtn.hidden = allShown; loadBtn.style.display = allShown ? "none" : ""; if (allShown) loadBtn.setAttribute("tabindex", "-1"); else loadBtn.removeAttribute("tabindex"); } filterBtns.forEach(btn => { const isActive = btn.dataset.filter === currentFilter; btn.setAttribute("aria-pressed", isActive ? "true" : "false"); btn.classList.toggle("is-active", isActive); }); } if (filterBtns.length) { wrapper.addEventListener("click", e => { const btn = e.target.closest("[data-filter]"); if (!btn || !wrapper.contains(btn)) return; e.preventDefault(); const next = btn.dataset.filter || "all"; if (next === currentFilter) return; currentFilter = next; visibleCount = initialCount; render(); ScrollTrigger.refresh(); }); } if (loadBtn) { loadBtn.addEventListener("click", e => { e.preventDefault(); const prevCount = visibleCount; const matching = getMatching(); visibleCount = Math.min(visibleCount + loadCount, matching.length); render(); ScrollTrigger.refresh(); const newItems = matching.slice(prevCount, visibleCount); if (newItems.length && !fadeDisabled) { gsap.fromTo(newItems, { opacity: 0, y: fadeY }, { opacity: 1, y: 0, duration: fadeDuration, stagger: fadeStagger, ease: "power3.out", clearProps: "transform,opacity" }); } const firstNew = newItems[0]; if (firstNew) { const focusable = firstNew.querySelector("a, button, [tabindex]") || firstNew; if (focusable === firstNew && !firstNew.hasAttribute("tabindex")) { firstNew.setAttribute("tabindex", "-1"); } focusable.focus({ preventScroll: true }); } }); } render(); }); } function initDynamicLists() { document.querySelectorAll('[data-list="dynamic"]').forEach(wrapper => { try { initDynamicList(wrapper); } catch (err) { console.error("[site] dynamic list failed:", wrapper, err); } }); } function initDynamicList(wrapper) { if (wrapper.dataset.initDone) return; wrapper.dataset.initDone = "1"; const loadBtn = wrapper.querySelector("[data-load-more]") || wrapper.parentElement?.querySelector("[data-load-more]"); const countVisible = wrapper.querySelector("[data-count-visible],[data-count-format]") || wrapper.parentElement?.querySelector("[data-count-visible],[data-count-format]"); const firstItem = wrapper.querySelector("[data-list-item]"); if (!firstItem) return; const itemContainer = firstItem.parentElement; const mobileBreakpoint = numOr(wrapper.dataset.mobileBreakpoint, 767); const isMobile = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`).matches; const loadDesktop = numOr(wrapper.dataset.loadCount, 6); const loadMobile = numOr(wrapper.dataset.loadCountMobile, loadDesktop); const loadCount = isMobile ? loadMobile : loadDesktop; const fadeDisabled = wrapper.hasAttribute("data-fade-disable") || prefersReduced; const fadeDuration = numOr(wrapper.dataset.fadeDuration, .9); const fadeStagger = numOr(wrapper.dataset.fadeStagger, .12); const fadeY = numOr(wrapper.dataset.fadeY, 20); const paginationEl = wrapper.querySelector(".w-pagination-wrapper") || document.querySelector(".w-pagination-wrapper"); const initialNextLink = paginationEl?.querySelector(".w-pagination-next"); let visibleCount = wrapper.querySelectorAll("[data-list-item]").length; let serverOffset = visibleCount; let useOffsetFallback = false; let nextPageUrl = initialNextLink?.href || null; let totalCount = null; if (!nextPageUrl && !paginationEl && visibleCount > 0) { nextPageUrl = `${window.location.pathname}?offset=${visibleCount}`; useOffsetFallback = true; } if (paginationEl) { const prevLink = paginationEl.querySelector(".w-pagination-previous"); const nextLink = paginationEl.querySelector(".w-pagination-next"); if (prevLink) prevLink.style.display = "none"; if (nextLink) nextLink.style.display = "none"; } updateCounters(); if (!nextPageUrl || !loadBtn) { totalCount = visibleCount; updateCounters(); hideLoadMore(); return; } loadBtn.hidden = false; loadBtn.style.display = ""; loadBtn.removeAttribute("tabindex"); let isLoading = false; let heldItems = []; loadBtn.addEventListener("click", async e => { e.preventDefault(); if (isLoading) return; if (heldItems.length > 0) { const batch = heldItems.splice(0, loadCount); appendAndAnimate(batch); if (heldItems.length === 0 && !nextPageUrl) hideLoadMore(); return; } if (!nextPageUrl) { hideLoadMore(); return; } isLoading = true; loadBtn.setAttribute("aria-busy", "true"); loadBtn.setAttribute("data-loading", "true"); try { const res = await fetch(nextPageUrl, { credentials: "same-origin" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const html = await res.text(); const doc = (new DOMParser).parseFromString(html, "text/html"); const fetchedWrapper = doc.querySelector('[data-list="dynamic"]'); const rawItems = Array.from((fetchedWrapper || doc).querySelectorAll("[data-list-item]")); if (!rawItems.length) { hideLoadMore(); return; } const existingHrefs = new Set(Array.from(itemContainer.querySelectorAll("a[href]")).map(a => a.href)); const uniqueItems = rawItems.filter(item => { const link = item.querySelector("a[href]"); return !link || !existingHrefs.has(link.href); }); if (!uniqueItems.length) { hideLoadMore(); return; } if (useOffsetFallback) { serverOffset += rawItems.length; nextPageUrl = rawItems.length >= loadCount ? `${window.location.pathname}?offset=${serverOffset}` : null; } else { nextPageUrl = doc.querySelector(".w-pagination-next")?.href || null; } const cloned = uniqueItems.map(item => document.importNode(item, true)); const toShow = cloned.splice(0, loadCount); heldItems = cloned; appendAndAnimate(toShow); if (!nextPageUrl && heldItems.length === 0) hideLoadMore(); } catch (err) { console.error("[site] dynamic list fetch failed:", err); } finally { isLoading = false; loadBtn.removeAttribute("aria-busy"); loadBtn.removeAttribute("data-loading"); } }); function appendAndAnimate(items, instant = false) { items.forEach(item => itemContainer.appendChild(item)); visibleCount += items.length; updateCounters(); history.replaceState(null, "", "#loaded=" + visibleCount); if (!instant && !fadeDisabled) { gsap.fromTo(items, { opacity: 0, y: fadeY }, { opacity: 1, y: 0, duration: fadeDuration, stagger: fadeStagger, ease: "power3.out", clearProps: "transform,opacity" }); } if (!instant) { const first = items[0]; if (first) { const focusable = first.querySelector("a, button, [tabindex]") || first; if (focusable === first && !first.hasAttribute("tabindex")) first.setAttribute("tabindex", "-1"); focusable.focus({ preventScroll: true }); } } ScrollTrigger.refresh(); } function hideLoadMore() { if (!loadBtn) return; loadBtn.hidden = true; loadBtn.style.display = "none"; } function updateCounters() { if (!countVisible) return; if (totalCount === null) return; const fmt = countVisible.dataset.countFormat; countVisible.textContent = fmt ? fmt.replace("{n}", totalCount) : String(totalCount); } (async function restoreFromHash() { const m = window.location.hash.match(/loaded=(\d+)/); if (!m) return; const target = parseInt(m[1], 10); if (target <= visibleCount) return; isLoading = true; if (loadBtn) { loadBtn.setAttribute("aria-busy", "true"); loadBtn.setAttribute("data-loading", "true"); } while (visibleCount < target) { if (heldItems.length > 0) { const need = target - visibleCount; const batch = heldItems.splice(0, need); appendAndAnimate(batch, true); if (heldItems.length === 0 && !nextPageUrl) { hideLoadMore(); break; } continue; } if (!nextPageUrl) break; try { const res = await fetch(nextPageUrl, { credentials: "same-origin" }); if (!res.ok) break; const html = await res.text(); const doc = (new DOMParser).parseFromString(html, "text/html"); const fetchedWrapper = doc.querySelector('[data-list="dynamic"]'); const rawItems = Array.from((fetchedWrapper || doc).querySelectorAll("[data-list-item]")); if (!rawItems.length) { hideLoadMore(); break; } const existingHrefs = new Set(Array.from(itemContainer.querySelectorAll("a[href]")).map(a => a.href)); const uniqueItems = rawItems.filter(item => { const link = item.querySelector("a[href]"); return !link || !existingHrefs.has(link.href); }); if (!uniqueItems.length) { hideLoadMore(); break; } if (useOffsetFallback) { serverOffset += rawItems.length; nextPageUrl = rawItems.length >= loadCount ? `${window.location.pathname}?offset=${serverOffset}` : null; } else { nextPageUrl = doc.querySelector(".w-pagination-next")?.href || null; } const cloned = uniqueItems.map(item => document.importNode(item, true)); const need = target - visibleCount; const toShow = cloned.splice(0, need); heldItems = cloned; appendAndAnimate(toShow, true); if (!nextPageUrl && heldItems.length === 0) { hideLoadMore(); break; } } catch (err) { console.error("[site] dynamic list hash-restore failed:", err); break; } } isLoading = false; if (loadBtn) { loadBtn.removeAttribute("aria-busy"); loadBtn.removeAttribute("data-loading"); } })(); (async function prefetchTotal() { const seen = new Set(Array.from(itemContainer.querySelectorAll("a[href]")).map(a => a.href)); let count = visibleCount; let url = nextPageUrl; let offset = serverOffset; while (url) { try { const res = await fetch(url, { credentials: "same-origin" }); if (!res.ok) break; const html = await res.text(); const doc = (new DOMParser).parseFromString(html, "text/html"); const fetchedWrapper = doc.querySelector('[data-list="dynamic"]'); const pageItems = Array.from((fetchedWrapper || doc).querySelectorAll("[data-list-item]")); if (!pageItems.length) break; const uniqueItems = pageItems.filter(item => { const link = item.querySelector("a[href]"); if (link && seen.has(link.href)) return false; if (link) seen.add(link.href); return true; }); if (!uniqueItems.length) break; count += uniqueItems.length; if (useOffsetFallback) { offset += pageItems.length; url = pageItems.length >= loadCount ? `${window.location.pathname}?offset=${offset}` : null; } else { url = doc.querySelector(".w-pagination-next")?.href || null; } } catch (err) { console.warn("[site] total count prefetch failed:", err); break; } } totalCount = count; updateCounters(); })(); } function initParallax() { const items = document.querySelectorAll("[data-hero]"); if (!items.length || prefersReduced) return; if (window.matchMedia("(min-width: 768px)").matches) { items.forEach(el => { if (el.dataset.hero === "top") gsap.set(el, { yPercent: 0 }); else if (el.dataset.hero === "middle") gsap.set(el, { yPercent: -8 }); else if (el.dataset.hero === "middle-top") gsap.set(el, { yPercent: 0 }); else if (el.dataset.hero === "lrg") gsap.set(el, { yPercent: -20 }); }); } mm.add("(min-width: 768px)", () => { items.forEach(el => { const type = el.dataset.hero; if (type === "top") { gsap.fromTo(el, { yPercent: 0 }, { yPercent: 30, ease: "none", force3D: true, scrollTrigger: { trigger: el.parentElement, start: "top top", end: "bottom top", scrub: true, invalidateOnRefresh: true } }); const aFrom = numOr(el.dataset.heroAlphaFrom, parseFloat(getComputedStyle(el).opacity ?? 1)); gsap.fromTo(el, { autoAlpha: aFrom }, { autoAlpha: numOr(el.dataset.heroAlphaTo, .4), ease: "none", scrollTrigger: { trigger: el.parentElement, start: el.dataset.heroFadeStart || "top top", end: el.dataset.heroFadeEnd || "bottom top", scrub: true, invalidateOnRefresh: true } }); } if (type === "middle") { gsap.fromTo(el, { yPercent: -8 }, { yPercent: 8, ease: "none", force3D: true, scrollTrigger: { trigger: el.parentElement, start: "top bottom", end: "bottom top", scrub: .3, invalidateOnRefresh: true } }); } if (type === "middle-top") { gsap.fromTo(el, { yPercent: 0 }, { yPercent: 8, ease: "none", force3D: true, scrollTrigger: { trigger: el.parentElement, start: "top top", end: "bottom top", scrub: true, invalidateOnRefresh: true } }); } if (type === "lrg") { gsap.fromTo(el, { yPercent: -20 }, { yPercent: 20, ease: "none", force3D: true, scrollTrigger: { trigger: el.parentElement, start: "top bottom", end: "bottom top", scrub: .3, invalidateOnRefresh: true } }); } }); }); mm.add("(max-width: 767px)", () => { items.forEach(el => gsap.set(el, { yPercent: 0 })); }); } function initFooterUnveil() { const footer = document.querySelector("[data-footer-unveil]"); if (!footer) return; if (prefersReduced) { gsap.set(footer, { yPercent: 0 }); return; } mm.add("(min-width: 768px)", () => { gsap.fromTo(footer, { yPercent: -40 }, { yPercent: 0, ease: "none", force3D: true, scrollTrigger: { trigger: footer.parentElement, start: "top bottom", end: "bottom bottom", scrub: .3, invalidateOnRefresh: true } }); }); mm.add("(max-width: 767px)", () => gsap.set(footer, { yPercent: 0 })); } })(); (function() { const COUNT_SELECTOR = "[data-cart-count]"; const ITEM_SELECTOR = ".w-commerce-commercecartitem"; const QTY_SELECTOR = "input.w-commerce-commercecartquantity"; const CART_CONTAINER = ".w-commerce-commercecartcontainerwrapper"; function totalQuantity() { let total = 0; document.querySelectorAll(ITEM_SELECTOR).forEach(item => { const input = item.querySelector(QTY_SELECTOR); if (!input) return; const v = parseInt(input.value, 10); if (!Number.isNaN(v)) total += v; }); return total; } function render() { const total = totalQuantity(); document.querySelectorAll(COUNT_SELECTOR).forEach(el => { const fmt = el.dataset.cartCountFormat; const text = fmt ? fmt.replace("{n}", total) : String(total); if (el.textContent !== text) el.textContent = text; if (el.hasAttribute("data-cart-count-hide-empty")) { el.style.display = total > 0 ? "" : "none"; } }); document.querySelectorAll(".w-commerce-commercecartopenlinkcount").forEach(el => { const text = total > 0 ? `(${total})` : ""; if (el.textContent !== text) el.textContent = text; el.style.display = total > 0 ? "" : "none"; }); } let moTimer; const mo = new MutationObserver(() => { clearTimeout(moTimer); moTimer = setTimeout(render, 30); }); function start() { const target = document.querySelector(CART_CONTAINER) || document.body; mo.observe(target, { childList: true, subtree: true, characterData: true }); document.addEventListener("input", e => { if (e.target.matches(QTY_SELECTOR)) render(); }); document.addEventListener("change", e => { if (e.target.matches(QTY_SELECTOR)) render(); }); render(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start); } else { start(); } window.Webflow = window.Webflow || []; window.Webflow.push(() => setTimeout(render, 300)); })();