(function() { "use strict"; const SITE_JS_VERSION = "4.56.0"; 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); const SITE_CSS = `.w-commerce-commercecartcontainerwrapper{background-color:rgba(0,0,0,.45);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:offCartFade .22s ease both}\n.w-commerce-commercecartcontainer{animation:offCartIn .34s cubic-bezier(.22,1,.36,1) both}\n.w-commerce-commercecartcontainerwrapper.is-cart-closing{display:flex!important;animation:offCartFade .24s ease both reverse}\n.w-commerce-commercecartcontainerwrapper.is-cart-closing .w-commerce-commercecartcontainer{animation:offCartIn .26s cubic-bezier(.55,0,.67,.2) both reverse}\n.w-commerce-commercecartitem{animation:offCartRow .34s cubic-bezier(.22,1,.36,1) both;animation-delay:.12s}\n.w-commerce-commercecartitem:nth-child(2){animation-delay:.16s}\n.w-commerce-commercecartitem:nth-child(3){animation-delay:.20s}\n.w-commerce-commercecartitem:nth-child(4){animation-delay:.24s}\n.w-commerce-commercecartitem:nth-child(n+5){animation-delay:.28s}\n.is-cart-closing .w-commerce-commercecartitem{animation:none}\nhtml.is-cart-open,html.is-cart-open body{overflow:hidden}\n.off-cart-count.is-cart-bump{animation:offCartBump .4s cubic-bezier(.22,1,.36,1) both}\n@keyframes offCartFade{from{opacity:0}to{opacity:1}}\n@keyframes offCartIn{from{transform:translateX(100%)}to{transform:translateX(0)}}\n@keyframes offCartRow{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}\n@keyframes offCartBump{0%{transform:scale(1)}35%{transform:scale(1.25)}100%{transform:scale(1)}}\n@media (prefers-reduced-motion:reduce){\n.w-commerce-commercecartcontainer,.w-commerce-commercecartitem,.off-cart-count.is-cart-bump{animation:offCartFade .12s ease both!important}\n.w-commerce-commercecartcontainerwrapper{backdrop-filter:none}}\n.off-nav-btn.off-nav-link.w--current{border-bottom:0 none transparent;text-decoration-color:currentColor}\n@media (max-width:767px){\n.par__con,.par__con-2,.xl__img__plx,.xl__img{will-change:auto}\n}\n@media (max-width:767px){\n[data-swipe="mobile"]{display:grid!important;grid-template-columns:none!important;grid-auto-flow:column;grid-auto-columns:var(--swipe-width,86vw);grid-template-rows:auto auto;gap:var(--swipe-gap,.75rem);overflow:auto hidden;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;scrollbar-width:none;-ms-overflow-style:none}\n[data-swipe="mobile"]::-webkit-scrollbar{display:none}\n[data-swipe="mobile"]>*{grid-row:1!important;grid-column:auto!important;min-width:0;scroll-snap-align:start}\n[data-swipe="mobile"]>* img{display:block;width:100%;height:auto}\n[data-swipe="mobile"]>.caption__con{grid-row:2!important;grid-column:1!important;position:sticky;left:0;width:var(--swipe-width,86vw);scroll-snap-align:none}\n\n[data-swipe="snap"]{scroll-snap-type:x mandatory;overscroll-behavior-x:contain;scrollbar-width:none;-ms-overflow-style:none}\n[data-swipe="snap"]::-webkit-scrollbar{display:none}\n[data-swipe="snap"]>*,[data-swipe="snap"] .w-dyn-item{scroll-snap-align:start}\n}\n:root{--bg-in:.55s;--bg-out:.36s}\n\n.off-page,.off-main,.off-story-wrapper,header.off-nav-bar,\n.off-nav-btn,.off-logo-link,.off-nav-link,\n.off-story-copy,.off-story-body,.off-story-body a,\n.standfirst,.standfirst-block,.story__cap{\ntransition:background-color var(--bg-out) ease,color var(--bg-out) ease}\n\n:root.is-bg-takeover .off-page,\n:root.is-bg-takeover .off-main,\n:root.is-bg-takeover .off-story-wrapper,\n:root.is-bg-takeover header.off-nav-bar,\n:root.is-bg-takeover .off-nav-btn,\n:root.is-bg-takeover .off-logo-link,\n:root.is-bg-takeover .off-nav-link,\n:root.is-bg-takeover .off-story-copy,\n:root.is-bg-takeover .off-story-body,\n:root.is-bg-takeover .off-story-body a,\n:root.is-bg-takeover .standfirst,\n:root.is-bg-takeover .standfirst-block,\n:root.is-bg-takeover .story__cap{\ntransition:background-color var(--bg-in) ease,color var(--bg-in) ease}\n\n:root.is-bg-takeover body,\n:root.is-bg-takeover .off-page,\n:root.is-bg-takeover .off-main,\n:root.is-bg-takeover .off-story-wrapper{background-color:var(--off-page-bg)}\n\n:root.is-bg-takeover body,\n:root.is-bg-takeover .off-page,\n:root.is-bg-takeover .off-main,\n:root.is-bg-takeover .off-story-wrapper,\n:root.is-bg-takeover .off-story-copy,\n:root.is-bg-takeover .off-story-body,\n:root.is-bg-takeover .off-story-body a,\n:root.is-bg-takeover .standfirst,\n:root.is-bg-takeover .standfirst-block,\n:root.is-bg-takeover header.off-nav-bar,\n:root.is-bg-takeover .off-nav-btn,\n:root.is-bg-takeover .off-logo-link,\n:root.is-bg-takeover .off-nav-link{color:var(--off-page-fg,#fcfcfc)}\n\n/* Captions sit back from the body copy at either polarity. */\n:root.is-bg-takeover .story__cap{color:var(--off-page-fg,#fcfcfc);opacity:.6}\n\n@media (prefers-reduced-motion:reduce){\n.off-page,.off-main,.off-story-wrapper,header.off-nav-bar,\n.off-nav-btn,.off-logo-link,.off-nav-link,\n.off-story-copy,.off-story-body,.off-story-body a,\n.standfirst,.standfirst-block,.story__cap{transition:none}\n}\n.slide--img{object-fit:contain;width:100%;height:100%}`; function injectSiteCSS() { if (document.getElementById("off-global-css")) return; const el = document.createElement("style"); el.id = "off-global-css"; el.textContent = SITE_CSS; (document.head || document.documentElement).appendChild(el); } try { injectSiteCSS(); } catch (e) { console.error("[site] CSS inject failed", e); } 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(); document.addEventListener("off:cart-open", () => lenis.stop()); document.addEventListener("off:cart-close", () => { if (!prefersReduced) lenis.start(); }); let _rTimer, _lastW = window.innerWidth; const _refresh = force => { if (!force && isTouch && window.innerWidth === _lastW) return; _lastW = window.innerWidth; clearTimeout(_rTimer); _rTimer = setTimeout(() => ScrollTrigger.refresh(), 200); }; window.addEventListener("resize", () => _refresh(false), { passive: true }); window.addEventListener("orientationchange", () => _refresh(true), { passive: true }); const mm = gsap.matchMedia(); const DEFAULT_SETTINGS = { startIndex: 0, autoPlay: false, 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", fadeFirst: true }; 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; if (d.fadeIn !== undefined) s.fadeInDuration = numOr(d.fadeIn, s.fadeInDuration); if (d.fadeOut !== undefined) s.fadeOutDuration = numOr(d.fadeOut, s.fadeOutDuration); if (d.fadePause !== undefined) s.fadePause = numOr(d.fadePause, s.fadePause); if (d.dotClass !== undefined) s.dotClass = d.dotClass; if (d.fadeFirst !== undefined) s.fadeFirst = d.fadeFirst !== "false"; 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 pendingSteps = 0, pendingAbsolute = null; 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" }); }); if (settings.fadeFirst) { const firstSlide = slides[currentIndex]; const firstImg = firstSlide?.querySelector("img"); if (firstImg) { gsap.set(firstSlide, { autoAlpha: 0 }); let revealed = false; const reveal = () => { if (revealed) return; revealed = true; gsap.to(firstSlide, { autoAlpha: 1, duration: settings.fadeInDuration, ease: settings.fadeEase }); }; if (firstImg.complete) reveal(); else { firstImg.addEventListener("load", reveal, { once: true }); firstImg.addEventListener("error", reveal, { once: true }); setTimeout(reveal, 3e3); } } } function preloadNeighbours(index) { if (!slides.length) return; const targets = slides.length <= 1 ? [ index ] : [ index, (index + 1) % slides.length, (index - 1 + slides.length) % slides.length ]; targets.forEach(i => slides[i].querySelectorAll("img").forEach(img => { if (i === index && !img.dataset.prioritised) { img.dataset.prioritised = "1"; img.fetchPriority = "high"; } if (img.dataset.preloaded) return; img.dataset.preloaded = "1"; img.loading = "eager"; })); } function flushPending() { if (pendingAbsolute !== null) { const t = pendingAbsolute; pendingAbsolute = null; pendingSteps = 0; goToSlide(t); return; } if (pendingSteps > 0) { pendingSteps--; navigateNext(); return; } if (pendingSteps < 0) { pendingSteps++; navigatePrev(); } } function goToSlide(targetIndex) { if (slides.length <= 1) return; if (isAnimating) { pendingAbsolute = targetIndex; 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; onAfterTransition?.(nextIndex); preloadNeighbours(nextIndex); const tl = gsap.timeline({ onComplete: () => { currentIndex = nextIndex; isAnimating = false; flushPending(); } }); 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 = () => { if (isAnimating) { pendingSteps++; return; } navigateNext(); }; const prevSlide = () => { if (isAnimating) { pendingSteps--; return; } 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 }); } preloadNeighbours(currentIndex); 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); if (sliderEl.dataset.fadeOut === undefined) settings.fadeOutDuration = .15; if (sliderEl.dataset.fadeIn === undefined) settings.fadeInDuration = .2; if (sliderEl.dataset.fadePause === undefined) settings.fadePause = 0; 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; if (!sliderEl.hasAttribute("tabindex")) sliderEl.setAttribute("tabindex", "0"); sliderEl.setAttribute("role", "region"); sliderEl.setAttribute("aria-roledescription", "carousel"); if (!sliderEl.hasAttribute("aria-label")) sliderEl.setAttribute("aria-label", "Product images"); 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 dotsWrap = sliderEl.querySelector("[data-gallery-dots]"); 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, updateCounter); function goToWithDots(targetIndex) { const fromIndex = core.getCurrentIndex(); const tl = core.goToSlide(targetIndex); if (tl && dots.length) tl.add(buildDotAnimation(dots, fromIndex, targetIndex, settings), 0); } if (dots.length) { 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(); } }); }); } 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(".off-page") || 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" }); const menuTl = gsap.timeline({ paused: true, onReverseComplete: () => gsap.set(site, { clearProps: "transform" }) }).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); } const BG_SEL = "[data-bg],[background],[data-background],[slider-bg]"; const BG_NAMES = { black: { bg: "#161817", fg: "#fcfcfc" }, white: { bg: "#fcfcfc", fg: "#161817" }, grey: { bg: "#e6e6e6", fg: "#161817" }, gray: { bg: "#e6e6e6", fg: "#161817" } }; const bgStack = []; const attrOf = (el, names) => { for (const n of names) { const v = el.getAttribute(n); if (v && v.trim()) return v.trim(); } return null; }; function resolveColour(v) { const probe = document.createElement("span"); probe.style.cssText = "position:absolute;left:-9999px;top:0;color:rgb(1, 2, 3)"; document.body.appendChild(probe); probe.style.color = v; const rgb = getComputedStyle(probe).color; probe.remove(); if (rgb === "rgb(1, 2, 3)") return null; const m = rgb.match(/[\d.]+/g); if (!m || m.length < 3) return null; return { css: v, r: +m[0], g: +m[1], b: +m[2] }; } const readableFg = c => { const lin = x => { x /= 255; return x <= .04045 ? x / 12.92 : Math.pow((x + .055) / 1.055, 2.4); }; const L = .2126 * lin(c.r) + .7152 * lin(c.g) + .0722 * lin(c.b); return L > .45 ? "#161817" : "#fcfcfc"; }; const BG_OFF = { off: true }; function readBgRequest(el) { const raw = attrOf(el, [ "data-bg", "background", "data-background", "slider-bg" ]); if (!raw) return null; const key = raw.toLowerCase(); if (key === "none" || key === "false" || key === "off") return BG_OFF; let bg, fg; if (BG_NAMES[key]) { bg = BG_NAMES[key].bg; fg = BG_NAMES[key].fg; } else { const c = resolveColour(raw); if (!c) { console.warn(`[site] background="${raw}" is not a colour — ignored`, el); return BG_OFF; } bg = raw; fg = readableFg(c); } return { bg: bg, fg: attrOf(el, [ "data-fg", "foreground", "data-foreground", "slider-fg" ]) || fg }; } function attachBgTakeover(trigger, req) { const root = document.documentElement; const token = req; const apply = () => { const top = bgStack[bgStack.length - 1]; if (top) { root.style.setProperty("--off-page-bg", top.bg); root.style.setProperty("--off-page-fg", top.fg); root.classList.add("is-bg-takeover"); } else { root.classList.remove("is-bg-takeover"); root.style.removeProperty("--off-page-bg"); root.style.removeProperty("--off-page-fg"); } }; const on = () => { if (bgStack.indexOf(token) === -1) { bgStack.push(token); apply(); } }; const off = () => { const i = bgStack.indexOf(token); if (i > -1) { bgStack.splice(i, 1); apply(); } }; const TALL = () => trigger.offsetHeight > window.innerHeight * 1.2; ScrollTrigger.create({ trigger: trigger, start: () => attrOf(trigger, [ "data-bg-start", "bg-start" ]) || (TALL() ? "top top" : "top center"), end: () => attrOf(trigger, [ "data-bg-end", "bg-end" ]) || (TALL() ? "bottom bottom" : "bottom center"), invalidateOnRefresh: true, onEnter: on, onEnterBack: on, onLeave: off, onLeaveBack: off }); } function initImageScroller(root) { const list = root instanceof Element ? [ root ] : Array.from(document.querySelectorAll("[data-img-scroll]")); list.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; const HOSTS = "[slider-type], [data-slider-type], [data-scroll-mode]"; const exclusiveHost = el => { const h = el.closest(HOSTS); return h && h.querySelectorAll("[data-img-scroll]").length === 1 ? h : null; }; function readMode(el) { const own = el.getAttribute("data-img-scroll"); if (own) return own; const host = el.closest(HOSTS); if (!host) return ""; return host.getAttribute("slider-type") || host.getAttribute("data-slider-type") || host.getAttribute("data-scroll-mode") || ""; } const raw = readMode(scroller).trim().toLowerCase(); const mode = raw === "pinned" || raw === "pin" ? "scroll" : raw; const loop = scroller.dataset.loop === "true" || scroller.closest("[data-img-loop]")?.dataset.imgLoop === "true"; const bgHost = exclusiveHost(scroller) || scroller.parentElement; let bgReq = readBgRequest(scroller) || (bgHost ? readBgRequest(bgHost) : null); if (!bgReq) { let n = scroller.parentElement; while (n && n !== document.body && n !== document.documentElement) { if (n.matches(BG_SEL)) { const hit = readBgRequest(n); if (hit) { bgReq = hit; break; } } n = n.parentElement; } } if (bgReq && bgReq !== BG_OFF) attachBgTakeover(bgHost || scroller, bgReq); let padTries = 0; function padEnds(done) { const first = track.firstElementChild, last = track.lastElementChild; if (!first || !last) return; const w = scroller.clientWidth; if ((!w || !first.offsetWidth) && padTries < 60) { padTries++; requestAnimationFrame(() => padEnds(done)); return; } padTries = 0; const firstGap = parseFloat(getComputedStyle(first).marginLeft) || 0; const lastGap = parseFloat(getComputedStyle(last).marginRight) || 0; track.style.paddingLeft = Math.max(0, (w - first.offsetWidth) / 2 - firstGap) + "px"; track.style.paddingRight = Math.max(0, (w - last.offsetWidth) / 2 - lastGap) + "px"; if (done) done(); } function whenReady(imgs, ms = 1500) { const dec = img => img.decode ? img.decode().catch(() => {}) : Promise.resolve(); const done = Promise.all(imgs.map(img => { if (img.complete && img.naturalWidth > 0) return dec(img); return new Promise(res => { img.addEventListener("load", () => dec(img).then(res), { once: true }); img.addEventListener("error", res, { once: true }); }); })); return Promise.race([ done, new Promise(res => setTimeout(res, ms)) ]); } const originalImgs = Array.from(track.querySelectorAll("img")); const noDrag = img => img.setAttribute("draggable", "false"); if (getComputedStyle(scroller).display.includes("flex")) { scroller.style.justifyContent = "flex-start"; } const numAttr = (name, fallback) => { const v = parseFloat(scroller.dataset[name]); return Number.isFinite(v) ? v : fallback; }; function findCounter(el) { const own = el.querySelector("[data-img-counter]"); if (own) return own; const pick = scope => scope ? Array.from(scope.querySelectorAll("[data-img-counter]")).find(c => !c.closest("[data-img-scroll]")) || null : null; return pick(exclusiveHost(el)) || pick(el.parentElement) || null; } const gateX = mode === "horizontal" || mode === "horizontal-scroll" || mode === "reveal"; const revealX = mode === "reveal"; const wheelX = mode === "horizontal-scroll"; function attachGateX(items) { if (!items.length || prefersReduced) return; const dim = Math.min(1, Math.max(0, numAttr("gateDim", .6))); const small = Math.min(1, Math.max(0, numAttr("gateScale", .94))); const depth = numAttr("revealDepth", .12); const spread = Math.max(.2, numAttr("gateSpread", 1.25)); const counterEl = findCounter(scroller); const total = items.length; let activeIdx = -1; const setCounter = i => { if (i === activeIdx) return; activeIdx = i; if (!counterEl) return; counterEl.textContent = `${i + 1} — ${total}`; counterEl.setAttribute("aria-label", `Image ${i + 1} of ${total}`); }; const setFrame = items.map(el => gsap.quickSetter(el, "css")); const imgs = items.map(el => el.querySelector("img")); const setImg = imgs.map(img => img ? gsap.quickSetter(img, "css") : null); gsap.set(items, { transformOrigin: "50% 50%" }); if (revealX) gsap.set(imgs.filter(Boolean), { scale: 1.25, force3D: true }); let ticking = false; function paint() { ticking = false; const box = scroller.getBoundingClientRect(); const mid = box.width / 2; if (mid < 1) return; let near = 0, best = Infinity; items.forEach((el, i) => { const r = el.getBoundingClientRect(); const off = r.left - box.left + r.width / 2 - mid; const d = Math.min(1, Math.abs(off) / (mid * spread)); const t = 1 - d; const e = t * t * (3 - 2 * t); setFrame[i]({ opacity: dim + (1 - dim) * e, scale: small + (1 - small) * e }); if (revealX && setImg[i]) setImg[i]({ x: -off * depth }); if (Math.abs(off) < best) { best = Math.abs(off); near = i; } }); setCounter(near); } const request = () => { if (!ticking) { ticking = true; requestAnimationFrame(paint); } }; scroller.addEventListener("scroll", request, { passive: true }); window.addEventListener("resize", request, { passive: true }); whenReady(originalImgs).then(() => { padEnds(); request(); }); request(); } if (mode === "scroll" && !isTouch && !prefersReduced) { scroller.style.overflow = "hidden"; scroller.style.cursor = "default"; track.querySelectorAll("img").forEach(noDrag); const distance = () => Math.max(0, track.scrollWidth - scroller.clientWidth); whenReady(originalImgs).then(() => { padEnds(); gsap.to(track, { x: () => -distance(), ease: "none", scrollTrigger: { trigger: scroller, start: "center center", end: () => "+=" + distance(), pin: true, pinType: "transform", scrub: 1, anticipatePin: 1, invalidateOnRefresh: true } }); ScrollTrigger.refresh(); }); return; } if (mode === "vertical") { const allFrames = Array.from(track.children); if (!allFrames.length) return; track.querySelectorAll("img").forEach(noDrag); scroller.style.overflow = "visible"; scroller.style.cursor = "default"; const isShown = el => el.offsetParent !== null && el.getBoundingClientRect().height > 1; const shown = () => allFrames.filter(isShown); const frames = shown(); if (!frames.length) return; if ("IntersectionObserver" in window) { const io = new IntersectionObserver(entries => { entries.forEach(e => { if (!e.isIntersecting) return; e.target.querySelectorAll("img").forEach(img => { img.loading = "eager"; }); io.unobserve(e.target); }); }, { rootMargin: "100% 0px" }); frames.forEach(it => io.observe(it)); } const moveDuration = numAttr("snapDuration", .6); const centreOf = el => { const r = el.getBoundingClientRect(); return r.top + window.scrollY + r.height / 2 - window.innerHeight / 2; }; const stops = () => { const maxY = ScrollTrigger.maxScroll(window); return shown().map(el => Math.max(0, Math.min(Math.round(centreOf(el)), maxY))); }; const moveTo = y => { if (typeof lenis?.scrollTo === "function") { lenis.scrollTo(y, { duration: moveDuration, easing: t => 1 - Math.pow(1 - t, 3) }); } else { window.scrollTo({ top: y, behavior: "smooth" }); } }; const ownsCentre = () => { const r = scroller.getBoundingClientRect(); const mid = window.innerHeight / 2; return r.top <= mid && r.bottom >= mid; }; function padColumn() { const vis = shown(); if (!vis.length) return; track.style.paddingTop = "0px"; track.style.paddingBottom = "0px"; const head = Math.max(0, -centreOf(vis[0])); if (head > 0) track.style.paddingTop = Math.ceil(head) + "px"; const tail = Math.max(0, centreOf(vis[vis.length - 1]) - ScrollTrigger.maxScroll(window)); if (tail > 0) track.style.paddingBottom = Math.ceil(tail) + "px"; } padColumn(); ScrollTrigger.addEventListener("refreshInit", padColumn); const counterEl = findCounter(scroller); const total = frames.length; let activeIdx = 0; const setCounter = i => { activeIdx = i; if (!counterEl) return; counterEl.textContent = `${i + 1} — ${total}`; counterEl.setAttribute("aria-label", `Image ${i + 1} of ${total}`); }; setCounter(0); if (prefersReduced) { gsap.set(frames, { opacity: 1, scale: 1 }); return; } const dim = Math.min(1, Math.max(0, numAttr("gateDim", .3))); const small = Math.min(1, Math.max(0, numAttr("gateScale", .94))); gsap.set(frames, { opacity: dim, scale: small, transformOrigin: "50% 50%" }); frames.forEach((item, index) => { const setGate = gsap.quickSetter(item, "css"); let canEnter = true, canExit = true; ScrollTrigger.create({ trigger: item, start: "top bottom", end: "bottom top", invalidateOnRefresh: true, onRefresh: self => { canEnter = self.start >= 0; canExit = self.end <= ScrollTrigger.maxScroll(window) + 1; }, onUpdate: self => { const p = self.progress; let t = 1 - Math.abs(p * 2 - 1); if (!canEnter && p < .5) t = 1; if (!canExit && p > .5) t = 1; const e = t * t * (3 - 2 * t); setGate({ opacity: dim + (1 - dim) * e, scale: small + (1 - small) * e }); if (t > .6 && index !== activeIdx) setCounter(index); } }); }); let quietUntil = 0; if (!isTouch && scroller.dataset.wheel === "page") { let lockUntil = 0, stepAt = 0; window.addEventListener("wheel", e => { if (!ownsCentre() || e.ctrlKey) return; if (Math.abs(e.deltaY) < 2) return; const marks = stops(); if (marks.length < 2) return; const now = performance.now(); if (now < lockUntil) { lockUntil = Math.min(Math.max(lockUntil, now + 100), stepAt + 1200); e.preventDefault(); e.stopPropagation(); return; } const dir = e.deltaY > 0 ? 1 : -1; const y = window.scrollY; let near = 0, best = Infinity; marks.forEach((c, i) => { const d = Math.abs(c - y); if (d < best) { best = d; near = i; } }); const idx = dir > 0 ? y < marks[near] - 8 ? near : near + 1 : y > marks[near] + 8 ? near : near - 1; if (idx < 0 || idx >= marks.length) { quietUntil = now + 900; return; } e.preventDefault(); e.stopPropagation(); stepAt = now; lockUntil = now + moveDuration * 1e3 + 150; quietUntil = now + moveDuration * 1e3 + 150; setCounter(idx); moveTo(marks[idx]); }, { capture: true, passive: false }); } if (scroller.dataset.snap !== "false") { const snapDelay = numAttr("snapDelay", 120); let idleTimer = 0; const settle = () => { if (!ownsCentre()) return; const marks = stops(); if (!marks.length) return; const y = window.scrollY; let target = null, best = Infinity; marks.forEach(c => { const d = Math.abs(c - y); if (d < best) { best = d; target = c; } }); if (target === null || best < 2) return; quietUntil = performance.now() + moveDuration * 1e3 + 120; moveTo(target); }; const queue = () => { if (performance.now() < quietUntil) return; clearTimeout(idleTimer); idleTimer = setTimeout(settle, snapDelay); }; window.addEventListener("scroll", queue, { passive: true }); [ "touchstart", "keydown" ].forEach(ev => window.addEventListener(ev, () => { quietUntil = 0; }, { passive: true })); } ScrollTrigger.refresh(); return; } if (mode === "carousel") { const slides = Array.from(track.children); if (!slides.length) return; track.querySelectorAll("img").forEach(noDrag); scroller.style.overflow = "hidden"; scroller.style.cursor = "default"; const settings = readSettings(scroller); if (scroller.dataset.fadeOut === undefined) settings.fadeOutDuration = .15; if (scroller.dataset.fadeIn === undefined) settings.fadeInDuration = .2; if (scroller.dataset.fadePause === undefined) settings.fadePause = 0; const counterEl = findCounter(scroller); const total = slides.length; const 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); if (!scroller.hasAttribute("tabindex")) scroller.setAttribute("tabindex", "0"); scroller.setAttribute("role", "region"); scroller.setAttribute("aria-roledescription", "carousel"); const core = createSliderCore(scroller, slides, settings, updateCounter); if (prevBtn) prevBtn.addEventListener("click", () => { core.prevSlide(); core.resetAutoPlay(); }); if (nextBtn) nextBtn.addEventListener("click", () => { core.nextSlide(); core.resetAutoPlay(); }); scroller.addEventListener("keydown", e => { if (e.key === "ArrowLeft") { core.prevSlide(); core.resetAutoPlay(); } if (e.key === "ArrowRight") { core.nextSlide(); core.resetAutoPlay(); } }); core.startAutoPlay(); return; } if (isTouch) { scroller.style.overflowX = "scroll"; scroller.style.overflowY = "hidden"; scroller.style.touchAction = "pan-x pan-y"; scroller.style.scrollSnapType = "x mandatory"; Array.from(track.children).forEach(item => { item.style.scrollSnapAlign = "center"; }); track.querySelectorAll("img").forEach(noDrag); whenReady(originalImgs).then(() => { padEnds(); scroller.scrollLeft = 0; }); if (gateX) attachGateX(Array.from(track.children)); return; } const originals = Array.from(track.children); const N = originals.length; if (!N) return; if (loop) { 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 => { noDrag(img); 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 (!loop || !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; if (!loop) padEnds(); singleW = loop ? Math.round(track.scrollWidth / 3) : 0; allSnaps = allItems.map(centerL); origSnaps = loop ? allSnaps.slice(N, 2 * N) : allSnaps; } 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; let pinnedX = false; scroller.addEventListener("pointerdown", e => { if (pinnedX) return; 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 (pinnedX || !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 = loop ? N : 0, 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); whenReady(originalImgs).then(() => requestAnimationFrame(() => requestAnimationFrame(() => { calcSnaps(); scroller.scrollLeft = origSnaps[0] ?? 0; loopCheck(); }))); if (gateX) attachGateX(allItems); window.addEventListener("load", () => setTimeout(() => { calcSnaps(); if (pinnedX) return; if (!loop && allSnaps.length) { const idx = allSnaps.indexOf(nearest(scroller.scrollLeft)); scroller.scrollLeft = allSnaps[idx === -1 ? 0 : idx]; } }, 50), { once: true }); if (wheelX && !isTouch && !prefersReduced) { const host = exclusiveHost(scroller) || scroller.parentElement; const distance = () => Math.max(0, scroller.scrollWidth - scroller.clientWidth); pinnedX = true; scroller.style.cursor = "default"; const sizeSection = () => { host.style.height = window.innerHeight + distance() + "px"; }; sizeSection(); ScrollTrigger.create({ trigger: host, start: "top top", end: "bottom bottom", invalidateOnRefresh: true, onRefreshInit: sizeSection, onUpdate: self => { scroller.scrollLeft = self.progress * distance(); }, onLeaveBack: () => { scroller.scrollLeft = 0; }, onLeave: () => { scroller.scrollLeft = distance(); } }); const remeasure = () => { sizeSection(); requestAnimationFrame(() => ScrollTrigger.refresh()); }; whenReady(originalImgs).then(() => padEnds(remeasure)); let roT = 0; new ResizeObserver(() => { clearTimeout(roT); roT = setTimeout(remeasure, 120); }).observe(track); } }); } function initProductCounter() { document.querySelectorAll("[data-product-gallery]").forEach(gallery => { if (gallery.dataset.initDone) return; gallery.dataset.initDone = "1"; const SEL = "[data-img-counter], [data-gallery-counter]"; const counter = gallery.parentElement?.querySelector(SEL) || gallery.closest("section, .off-product-media")?.querySelector(SEL) || gallery.querySelector(SEL); const items = Array.from(gallery.querySelectorAll(".off-product-item, .w-dyn-item")); if (!counter || items.length < 2) return; const total = items.length; let shown = -1; const write = i => { if (i === shown) return; shown = i; counter.textContent = `${i + 1} — ${total}`; counter.setAttribute("aria-label", `Image ${i + 1} of ${total}`); }; write(0); let ticking = false; const read = () => { ticking = false; const box = gallery.getBoundingClientRect(); const mid = box.left + box.width / 2; let near = 0, best = Infinity; items.forEach((el, i) => { const r = el.getBoundingClientRect(); const d = Math.abs(r.left + r.width / 2 - mid); if (d < best) { best = d; near = i; } }); write(near); }; gallery.addEventListener("scroll", () => { if (!ticking) { ticking = true; requestAnimationFrame(read); } }, { passive: true }); window.addEventListener("resize", () => { if (!ticking) { ticking = true; requestAnimationFrame(read); } }, { passive: true }); }); } 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 lazyInitEach(selector, name, fn, rootMargin = "300px") { const all = Array.from(document.querySelectorAll(selector)); if (!all.length) return; const margin = parseInt(rootMargin, 10) || 0; const run = el => safeInit(name + ":" + (all.indexOf(el) + 1), () => fn(el)); if (!("IntersectionObserver" in window)) { all.forEach(run); return; } const io = new IntersectionObserver(entries => { entries.forEach(e => { if (!e.isIntersecting) return; io.unobserve(e.target); run(e.target); }); }, { rootMargin: rootMargin }); all.forEach(el => { const r = el.getBoundingClientRect(); if (r.top < window.innerHeight + margin && r.bottom > -margin) run(el); else io.observe(el); }); } 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-reveal]")) safeInit("reveal", initReveal); 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); lazyInitEach("[data-img-scroll]", "imageScroller", initImageScroller); lazyInit("[data-product-gallery]", "productCounter", initProductCounter); lazyInit("[data-quote-slider]", "quoteSliders", initQuoteSliders); lazyInit("[data-gallery-slider]", "gallerySliders", initGallerySliders); lazyInit(".audio__player", "audioPlayer", initAudioPlayer); }); 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 initReveal() { const items = document.querySelectorAll("[data-reveal]"); if (!items.length || prefersReduced) return; const START_PCT = 92; const FAILSAFE_MS = 3e3; const FONT_WAIT_MS = 1200; const fontsReady = () => Promise.race([ document.fonts ? document.fonts.ready : Promise.resolve(), new Promise(r => setTimeout(r, FONT_WAIT_MS)) ]); const hasSplit = typeof SplitText !== "undefined"; if (hasSplit) gsap.registerPlugin(SplitText); const maskMetrics = el => { const cs = getComputedStyle(el); const fs = parseFloat(cs.fontSize), lh = parseFloat(cs.lineHeight); if (!(fs > 0) || !(lh > 0)) return { bleedEm: 0, risePct: 115 }; let fontBox = fs * 1.25; try { const c = document.createElement("canvas").getContext("2d"); c.font = cs.fontStyle + " " + cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily; const m = c.measureText("Hg"); if (m.fontBoundingBoxAscent && m.fontBoundingBoxDescent) fontBox = m.fontBoundingBoxAscent + m.fontBoundingBoxDescent; } catch (e) {} const bleed = Math.max(0, (fontBox - lh) / 2); return { bleedEm: bleed / fs, risePct: Math.ceil((lh + bleed) / lh * 100) + 5 }; }; const endOf = new Map; const findAfter = (el, sel) => { let n = el.parentElement, depth = 0; while (n && depth++ < 6) { let hit = null; try { hit = n.querySelector(sel); } catch (e) { return null; } if (hit && hit !== el) return hit; n = n.parentElement; } return null; }; items.forEach(el => { const rest = parseFloat(getComputedStyle(el).opacity ?? 1); const onLoad = el.getBoundingClientRect().top < window.innerHeight * (START_PCT / 100); const trigger = { trigger: el, start: "top " + START_PCT + "%", once: true, invalidateOnRefresh: true }; let settle; endOf.set(el, new Promise(r => { settle = r; })); const afterEl = el.dataset.revealAfter ? findAfter(el, el.dataset.revealAfter) : null; const wait = afterEl ? endOf.get(afterEl) || Promise.resolve(0) : Promise.resolve(0); const chars = el.dataset.reveal === "chars" && hasSplit && !!el.textContent.trim(); let armed = true; gsap.set(el, chars ? { autoAlpha: 0 } : { autoAlpha: 0, y: numOr(el.dataset.revealY, 14) }); const blockFade = delay => { const duration = numOr(el.dataset.revealDuration, .9); gsap.set(el, { autoAlpha: 0, y: numOr(el.dataset.revealY, 14) }); gsap.to(el, { autoAlpha: rest, y: 0, ease: "power2.out", duration: duration, delay: delay, scrollTrigger: trigger }); armed = false; settle(delay + duration); }; const ready = chars ? Promise.all([ wait, fontsReady() ]).then(a => a[0]) : wait; ready.then(prior => { const delay = (afterEl ? prior : onLoad ? numOr(el.dataset.revealLoadDelay, 1) : numOr(el.dataset.revealViewDelay, .3)) + numOr(el.dataset.revealDelay, 0); if (!chars) { blockFade(delay); return; } let parts = null; try { parts = new SplitText(el, { type: "words,chars", mask: "chars" }); if (!parts.chars || !parts.chars.length) throw new Error("no chars"); const masked = !!(parts.masks && parts.masks.length); let rise = numOr(el.dataset.revealCharY, 0), risePct = 0; if (masked) { const m = maskMetrics(el); if (m.bleedEm > 0) gsap.set(parts.masks, { paddingBottom: m.bleedEm + "em", marginBottom: "-" + m.bleedEm + "em" }); risePct = m.risePct; } const from = { autoAlpha: numOr(el.dataset.revealCharFade, masked ? 1 : 0) }; const to = { autoAlpha: 1 }; if (masked && !rise) { from.yPercent = risePct; to.yPercent = 0; } else { from.y = rise || 32; to.y = 0; } const duration = numOr(el.dataset.revealCharDuration, .95); const stagger = numOr(el.dataset.revealStagger, .012); const ease = el.dataset.revealCharEase || "power4.out"; gsap.set(parts.chars, from); gsap.set(el, { autoAlpha: rest, y: 0 }); gsap.to(parts.chars, Object.assign({}, to, { ease: ease, duration: duration, stagger: stagger, delay: delay, scrollTrigger: trigger })); armed = false; settle(delay + duration + stagger * (parts.chars.length - 1)); } catch (e) { try { if (parts) parts.revert(); } catch (e2) {} blockFade(delay); } }).catch(() => { gsap.set(el, { autoAlpha: rest, y: 0 }); armed = false; settle(0); }); if (onLoad) setTimeout(() => { if (armed && parseFloat(getComputedStyle(el).opacity) === 0) gsap.set(el, { autoAlpha: rest, y: 0 }); }, FAILSAFE_MS); }); } 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 }); else if (el.dataset.hero === "sticky") gsap.set(el, { yPercent: 0 }); else if (el.dataset.hero === "reveal") gsap.set(el, { yPercent: 0 }); }); } const introHero = (el, defaultBlur) => { const img = el.querySelector("img") || el.firstElementChild; if (!img) return; const rest = numOr(el.dataset.heroIntroAlpha, parseFloat(getComputedStyle(img).opacity ?? 1)); const blur = numOr(el.dataset.heroIntroBlur, defaultBlur); const from = { autoAlpha: 0, scale: numOr(el.dataset.heroIntroZoom, 1.12) }; const to = { autoAlpha: rest, scale: 1 }; if (blur > 0) { from.filter = "blur(" + blur + "px)"; to.filter = "blur(0px)"; } gsap.set(img, from); const play = () => gsap.to(img, Object.assign({}, to, { ease: "power2.out", duration: numOr(el.dataset.heroIntroDuration, 1.2), delay: numOr(el.dataset.heroIntroDelay, .2) })); if (img.complete) play(); else { img.addEventListener("load", play, { once: true }); img.addEventListener("error", play, { once: true }); } }; items.forEach(el => { if (el.dataset.hero === "sticky") introHero(el, 12); if (el.dataset.hero === "reveal") introHero(el, 12); }); 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 === "sticky") { const shift = numOr(el.dataset.heroShift, -8); gsap.fromTo(el, { yPercent: 0 }, { yPercent: shift, 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 === "reveal") { gsap.fromTo(el, { yPercent: 0 }, { yPercent: numOr(el.dataset.heroShift, 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() { "use strict"; const WRAP = ".w-commerce-commercecartcontainerwrapper"; const CLOSE = ".w-commerce-commercecartcloselink"; const EXIT_MS = 260; const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const wrap = () => document.querySelector(WRAP); const isOpen = w => !!w && getComputedStyle(w).display !== "none" && !w.dataset.cartClosing; function playExit(w) { if (!w || reduced || w.dataset.cartClosing) return; w.dataset.cartClosing = "1"; w.classList.add("is-cart-closing"); setTimeout(() => { w.classList.remove("is-cart-closing"); delete w.dataset.cartClosing; }, EXIT_MS); } function closeCart() { const w = wrap(); if (!isOpen(w)) return; const link = w.querySelector(CLOSE); if (link) link.click(); } document.addEventListener("click", e => { const w = wrap(); if (w && e.target === w) closeCart(); }); document.addEventListener("keydown", e => { if (e.key === "Escape" || e.key === "Esc") closeCart(); }); let open = false; function syncOpenState() { const w = wrap(); const now = isOpen(w); if (now === open) return; if (open && !now) playExit(w); open = now; document.documentElement.classList.toggle("is-cart-open", now); document.dispatchEvent(new CustomEvent(now ? "off:cart-open" : "off:cart-close")); } function watchCount() { document.querySelectorAll("[data-cart-count]").forEach(el => { if (el.dataset.bumpWatch) return; el.dataset.bumpWatch = "1"; let last = el.textContent.trim(); new MutationObserver(() => { const now = el.textContent.trim(); if (now === last) return; last = now; if (reduced) return; const host = el.closest(".off-cart-count") || el; host.classList.remove("is-cart-bump"); void host.offsetWidth; host.classList.add("is-cart-bump"); setTimeout(() => host.classList.remove("is-cart-bump"), 400); }).observe(el, { childList: true, characterData: true, subtree: true }); }); } function start() { const w = wrap(); if (!w) return; new MutationObserver(syncOpenState).observe(w, { attributes: true, attributeFilter: [ "style", "class" ] }); syncOpenState(); watchCount(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", start); } else { start(); } })(); (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)); })();