// INITIALIZE document.addEventListener("DOMContentLoaded", () => { const html = document.documentElement; html.classList.add("loaded"); setTimeout(() => { html.classList.add("ready"); }, 1000); }); // LENIS SMOOTH SCROLL let lenis = new Lenis({ lerp: 0.1, wheelMultiplier: 1, gestureOrientation: "vertical", normalizeWheel: false, smoothTouch: false, }); function raf(time) { lenis.raf(time); requestAnimationFrame(raf); } requestAnimationFrame(raf); $("[data-lenis-start]").on("click", function () { lenis.start(); }); $("[data-lenis-stop]").on("click", function () { lenis.stop(); }); $("[data-lenis-toggle]").on("click", function () { $(this).toggleClass("stop-scroll"); if ($(this).hasClass("stop-scroll")) { lenis.stop(); } else { lenis.start(); } }); // NAVBAR let prevScrollPos = window.pageYOffset; window.addEventListener("scroll", function () { let currentScrollPos = window.pageYOffset; var navElement = document.querySelector("[nav]"); if (!navElement) return; if (currentScrollPos > window.innerHeight * 0) { navElement.classList.add("scrolled"); } else { navElement.classList.remove("scrolled"); } if (prevScrollPos > currentScrollPos) { navElement.classList.remove("scroll-down"); } else { navElement.classList.add("scroll-down"); } prevScrollPos = currentScrollPos; }); let menuBtn = document.querySelector("[menu-btn]"); let nav = document.querySelector("[nav]"); if (menuBtn && nav) { menuBtn.addEventListener("click", function () { nav.classList.toggle("open"); }); } // ACCORDION initAccordionCSS(); function initAccordionCSS() { document .querySelectorAll('[data-accordion-list="css"]') .forEach((accordion) => { const closeSiblings = accordion.getAttribute("data-accordion-close-siblings") === "true"; const firstActive = accordion.getAttribute("data-accordion-first-active") === "true"; const collapsible = accordion.getAttribute("data-accordion-collapsible") === "true"; const eventType = accordion.getAttribute("data-accordion-event") || "click"; if (firstActive) { const first = accordion.querySelector("[data-accordion]"); if (first) first.setAttribute("data-accordion", "active"); } function toggleItem(item, open) { if (!open && !collapsible) { const activeCount = accordion.querySelectorAll( '[data-accordion="active"]', ).length; if (activeCount <= 1) return; } item.setAttribute("data-accordion", open ? "active" : "not-active"); if (closeSiblings && open) { accordion .querySelectorAll('[data-accordion="active"]') .forEach((sib) => { if (sib !== item) sib.setAttribute("data-accordion", "not-active"); }); } } if (eventType === "hover") { accordion .querySelectorAll("[data-accordion-toggle]") .forEach((toggle) => { const item = toggle.closest("[data-accordion]"); if (!item) return; toggle.addEventListener("mouseenter", () => { toggleItem(item, true); }); }); } else { accordion.addEventListener("click", (e) => { const toggle = e.target.closest("[data-accordion-toggle]"); if (!toggle) return; const item = toggle.closest("[data-accordion]"); if (!item) return; const isActive = item.getAttribute("data-accordion") === "active"; toggleItem(item, !isActive); }); } }); } // MARQUEE gsap.registerPlugin(ScrollTrigger); function initMarquee(scope = document) { scope .querySelectorAll("[data-marquee-scroll-direction-target]") .forEach((wrapper) => { const collection = wrapper.querySelector( "[data-marquee-collection-target]", ); const scroll = wrapper.querySelector("[data-marquee-scroll-target]"); if (!collection || !scroll) return; const { marqueeSpeed, marqueeDirection, marqueeDuplicate, marqueeScrollSpeed, marqueePause, } = wrapper.dataset; const speed = parseFloat(marqueeSpeed); const direction = marqueeDirection === "right" ? 1 : -1; const duplicate = parseInt(marqueeDuplicate || 0); const scrollSpeed = parseFloat(marqueeScrollSpeed); const scale = window.innerWidth < 479 ? 0.25 : window.innerWidth < 991 ? 0.5 : 1; let duration = speed * (collection.offsetWidth / window.innerWidth) * scale; scroll.style.marginLeft = `${-scrollSpeed}%`; scroll.style.width = `${100 + scrollSpeed * 2}%`; if (duplicate > 0) { const frag = document.createDocumentFragment(); for (let i = 0; i < duplicate; i++) { frag.appendChild(collection.cloneNode(true)); } scroll.appendChild(frag); } const items = wrapper.querySelectorAll( "[data-marquee-collection-target]", ); const loop = gsap .to(items, { xPercent: -100, repeat: -1, duration: duration, ease: "linear", }) .totalProgress(0.5); gsap.set(items, { xPercent: direction === 1 ? 100 : -100, }); loop.timeScale(direction); loop.play(); if (marqueePause === "hover") { wrapper.addEventListener("mouseenter", () => { loop.pause(); }); wrapper.addEventListener("mouseleave", () => { loop.play(); }); } ScrollTrigger.create({ trigger: wrapper, start: "top bottom", end: "bottom top", onUpdate: (self) => { const isScrollingDown = self.direction === 1; const newDir = isScrollingDown ? -direction : direction; if (marqueePause === "hover" && wrapper.matches(":hover")) { return; } loop.timeScale(newDir); }, }); const fromX = direction === -1 ? scrollSpeed : -scrollSpeed; const toX = -fromX; gsap.fromTo( scroll, { x: `${fromX}vw`, }, { x: `${toX}vw`, ease: "none", scrollTrigger: { trigger: wrapper, start: "0% 100%", end: "100% 0%", scrub: true, }, }, ); }); } initMarquee(); // PRICING document.querySelectorAll("[pricing-btn]").forEach((btn) => { btn.addEventListener("click", () => { document.querySelector("[pricing-btn].active")?.classList.remove("active"); btn.classList.add("active"); }); }); // SCROLL ANIMATIONS const popoutEase = "back.out(1.275)"; document.querySelectorAll('[scroll-anim="popout"]').forEach((el) => { const delay = parseFloat(el.getAttribute("scroll-anim-delay")) || 0.3; const type = el.getAttribute("scroll-anim-type"); const showTween = gsap.fromTo( el, { opacity: 0, scale: 0, }, { opacity: 1, scale: 1, duration: 0.5, delay, ease: popoutEase, paused: true, }, ); const hideTween = gsap.to(el, { opacity: 0, scale: 0, duration: 0.5, ease: popoutEase, paused: true, }); if (type === "restart") { ScrollTrigger.create({ trigger: el, start: "top 95%", end: "bottom 5%", onEnter: () => { hideTween.pause(); showTween.restart(); }, onLeave: () => { hideTween.restart(); }, onEnterBack: () => { hideTween.pause(); showTween.restart(); }, onLeaveBack: () => { hideTween.restart(); }, }); } else { ScrollTrigger.create({ trigger: el, start: "top 85%", once: true, onEnter: () => { showTween.restart(); }, }); } }); // MOUSE PARALLAX document.addEventListener("DOMContentLoaded", () => { const elements = document.querySelectorAll("[mouse-parallax]"); if (!elements.length) return; let mouseX = 0.5; let mouseY = 0.5; let currentX = 0.5; let currentY = 0.5; elements.forEach((el) => { const type = el.getAttribute("mouse-parallax-type"); // Default = scroll el._parallaxType = type || "scroll"; }); // MOUSE PARALLAX const mouseElements = [...elements].filter( (el) => el._parallaxType === "mouse", ); if (mouseElements.length) { window.addEventListener("mousemove", (e) => { mouseX = e.clientX / window.innerWidth; mouseY = e.clientY / window.innerHeight; }); function animateMouse() { currentX += (mouseX - currentX) * 0.08; currentY += (mouseY - currentY) * 0.08; mouseElements.forEach((el) => { const move = parseFloat(el.getAttribute("mouse-parallax-move")) || 10; const rotateZ = parseFloat(el.getAttribute("mouse-parallax-rotate-z")) || 0; const x = (currentX - 0.5) * move * 2; const y = (currentY - 0.5) * move * 2; const rotation = (currentX - 0.5) * rotateZ * 2; el.style.transform = `translate3d(${x}%, ${y}%, 0) rotateZ(${rotation}deg)`; }); requestAnimationFrame(animateMouse); } animateMouse(); } // SCROLL PARALLAX const scrollElements = [...elements].filter( (el) => el._parallaxType === "scroll", ); if (scrollElements.length) { let ticking = false; function updateScroll() { scrollElements.forEach((el) => { const move = parseFloat(el.getAttribute("mouse-parallax-move")) || 10; const rotateZ = parseFloat(el.getAttribute("mouse-parallax-rotate-z")) || 0; const rect = el.getBoundingClientRect(); const progress = (window.innerHeight - rect.top) / (window.innerHeight + rect.height); const value = Math.max(-1, Math.min(1, (progress - 0.5) * 2)); const y = value * move; const rotation = value * rotateZ; el.style.transform = `translate3d(0, ${y}%, 0) rotateZ(${rotation}deg)`; }); ticking = false; } function onScroll() { if (!ticking) { requestAnimationFrame(updateScroll); ticking = true; } } window.addEventListener("scroll", onScroll, { passive: true, }); window.addEventListener("resize", updateScroll); updateScroll(); } }); document.addEventListener("DOMContentLoaded", () => { // TRIAL SPOTS const elements = document.querySelectorAll("[trial-spots]"); const startDate = new Date("2026-09-08T00:00:00"); const today = new Date(); startDate.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); const daysPassed = Math.floor((today - startDate) / (1000 * 60 * 60 * 24)); elements.forEach((el) => { const maxSpots = parseInt(el.getAttribute("trial-spots"), 10); if (isNaN(maxSpots)) return; const currentSpots = Math.max(0, maxSpots - daysPassed * 5); el.textContent = `${currentSpots}/${maxSpots}`; }); }); // CURRENCY CONVERSION async function updatePrices() { console.log("🟢 Currency script started"); const priceElements = document.querySelectorAll("[price-text]"); console.log("🔎 Prices found:", priceElements.length); if (!priceElements.length) return; try { const locationResponse = await fetch("https://ipwho.is/"); const location = await locationResponse.json(); const country = location.country_code; console.log("🏳️ Country:", country); const currencyMap = { PK: "PKR", GB: "GBP", US: "USD", CA: "CAD", AU: "AUD", AE: "AED", SA: "SAR", QA: "QAR", KW: "KWD", DE: "EUR", FR: "EUR", IT: "EUR", ES: "EUR", NL: "EUR", IN: "INR", NZ: "NZD", SG: "SGD", MY: "MYR", }; const currency = currencyMap[country] || "GBP"; console.log("💱 Currency:", currency); const symbols = { PKR: "₨", GBP: "£", USD: "$", CAD: "C$", AUD: "A$", AED: "د.إ", SAR: "﷼", QAR: "﷼", KWD: "د.ك", EUR: "€", INR: "₹", NZD: "NZ$", SGD: "S$", MYR: "RM", }; const symbol = symbols[currency] || "£"; console.log("💲 Symbol:", symbol); const ratesResponse = await fetch("https://open.er-api.com/v6/latest/GBP"); const ratesData = await ratesResponse.json(); const rate = ratesData.rates[currency]; console.log(`💱 GBP → ${currency}:`, rate); if (!rate) return; priceElements.forEach((el) => { const originalText = el.textContent.trim(); const numericPrice = parseFloat(originalText.replace(/[^0-9.-]/g, "")); if (isNaN(numericPrice)) { console.error("❌ Invalid price:", originalText); return; } const convertedPrice = Math.round(numericPrice * rate); const formattedPrice = new Intl.NumberFormat("en-US").format( convertedPrice, ); const finalPrice = `${symbol}${formattedPrice}`; console.log(`💰 ${originalText} → ${finalPrice}`); el.textContent = finalPrice; }); console.log("✅ Prices updated!"); } catch (error) { console.error("🔥 Currency conversion failed:", error); } } window.addEventListener("load", updatePrices); document.addEventListener("DOMContentLoaded", function () { // MODAL document.querySelectorAll("[modal-content]").forEach((el) => { const original = el.style.display; el.style.display = ""; const cssDisplay = getComputedStyle(el).display; el.dataset.originalDisplay = cssDisplay; el.style.display = original; }); document.querySelectorAll("[modal-open]").forEach((trigger) => { trigger.addEventListener("click", () => { const modalName = trigger.getAttribute("modal-open"); const contentName = trigger.getAttribute("modal-open-content"); const modal = document.querySelector(`[modal="${modalName}"]`); if (!modal) return; modal.classList.add("open"); if (typeof lenis !== "undefined") { lenis.stop(); } modal.querySelectorAll("[modal-content]").forEach((el) => { el.style.display = "none"; }); if (contentName) { const content = modal.querySelector(`[modal-content="${contentName}"]`); if (!content) return; const display = content.dataset.originalDisplay || "block"; content.style.display = display; } }); }); document.querySelectorAll("[modal-close]").forEach((closeBtn) => { closeBtn.addEventListener("click", () => { const modal = closeBtn.closest("[modal]"); if (!modal) return; modal.classList.remove("open"); if (typeof lenis !== "undefined") { lenis.start(); } }); }); }); document.addEventListener("DOMContentLoaded", () => { // SCROLL ACCORDION ANIMATION if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return; document.querySelectorAll(".step-track").forEach((track) => { const blocks = track.querySelectorAll(".step-bg-block"); const toggles = track.querySelectorAll( ".accordion [data-accordion-toggle]", ); if (!blocks.length || !toggles.length) return; blocks.forEach((block, index) => { const toggle = toggles[index]; if (!toggle) return; ScrollTrigger.create({ trigger: block, start: "top top", onEnter: () => toggle.click(), onEnterBack: () => toggle.click(), }); }); }); }); // VIDEO SYSTEM (function () { if (!document.querySelector("[video-wrap]")) return; async function initializePlayers() { const players = document.querySelectorAll("video-source-player"); if (!players.length) return; [ "https://cdn.vidstack.io/player/theme.css", "https://cdn.vidstack.io/player/video.css", ].forEach((href) => { if (!document.querySelector(`link[href="${href}"]`)) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; document.head.appendChild(link); } }); const { VidstackPlayer, VidstackPlayerLayout } = await import("https://cdn.vidstack.io/player"); for (let i = 0; i < players.length; i++) { const el = players[i]; el.id = `video-player-${i}`; try { await VidstackPlayer.create({ target: `#${el.id}`, src: el.getAttribute("data-src"), title: el.getAttribute("data-title") || "", poster: el.getAttribute("data-poster") || "", playsinline: true, layout: new VidstackPlayerLayout(), }); } catch (err) { console.error("Vidstack init failed:", err); } } } document.addEventListener("DOMContentLoaded", () => { const loadVideo = (el) => { if (!el.dataset.src) return; el.src = el.dataset.src; el.removeAttribute("data-src"); if (el.hasAttribute("bg-poster")) { el.load(); return; } el.muted = true; el.loop = true; el.setAttribute("playsinline", ""); el.setAttribute("webkit-playsinline", ""); el.load(); el.play().catch(() => {}); }; const observeOnce = (el, cb) => { const io = new IntersectionObserver( (entries, obs) => { entries.forEach((entry) => { if (entry.isIntersecting) { cb(entry.target); obs.unobserve(entry.target); } }); }, { threshold: 0.2 }, ); io.observe(el); }; document .querySelectorAll("video[play-bg-video][data-src]") .forEach((v) => observeOnce(v, loadVideo)); document .querySelectorAll(".video-source-player[data-src]") .forEach((el) => { const modal = el.closest(".modal"); const tryLoad = () => { if (!el.dataset.loaded) { el.dataset.loaded = "true"; loadVideo(el); } }; if (modal) { const mo = new MutationObserver(() => { if (modal.classList.contains("open")) { tryLoad(); mo.disconnect(); } }); mo.observe(modal, { attributes: true, attributeFilter: ["class"] }); } else { observeOnce(el, tryLoad); } }); document .querySelectorAll("[video-wrap] video[data-src]") .forEach((el) => observeOnce(el, loadVideo)); }); function formatTime(secs) { if (!isFinite(secs) || isNaN(secs)) return "0:00"; const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = Math.floor(secs % 60); return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` : `${m}:${String(s).padStart(2, "0")}`; } function createNativeAdapter(video, { autoplay, loop } = {}) { if (autoplay) video.muted = true; return { mediaEl: video, loop, play: () => video.play().catch(() => {}), pause: () => video.pause(), isPaused: () => video.paused, isMuted: () => video.muted, setMuted: (m) => { video.muted = m; }, getCurrentTime: () => video.currentTime || 0, getDuration: () => video.duration || 0, seek: (t) => { video.currentTime = t; }, getFullscreenTarget: () => video, on: (evt, cb) => video.addEventListener(evt, cb), }; } const wraps = document.querySelectorAll("[video-wrap]"); const wrapAdapters = new WeakMap(); const wrapRenderers = new WeakMap(); function syncMute(wrap) { const adapter = wrapAdapters.get(wrap); if (!adapter) return; adapter.setMuted(wrap.getAttribute("video-wrap") === "mute"); } function initVideoWrap(wrap) { const video = wrap.querySelector("video"); if (!video) return; const autoplay = video.hasAttribute("autoplay"); const loop = video.hasAttribute("loop"); if (autoplay && wrap.getAttribute("video-wrap") !== "mute") { wrap.setAttribute("video-wrap", "mute"); } const adapter = createNativeAdapter(video, { autoplay, loop }); wrapAdapters.set(wrap, adapter); syncMute(wrap); const audioBtns = [...wrap.querySelectorAll("[audio-btn]")]; const ppBtns = [...wrap.querySelectorAll("[play-pause-btn]")]; const fsBtns = [...wrap.querySelectorAll("[full-screen-btn]")]; const progWrap = wrap.querySelector("[video-progress-wrap]"); const progBar = wrap.querySelector("[video-progress]"); const thumbEl = wrap.querySelector("[video-progress-thumb]"); const currentEl = wrap.querySelector("[video-current-duration]"); const totalEl = wrap.querySelector("[video-total-duration]"); let isScrubbing = false; if (ppBtns.length) { const syncAllPpBtns = () => { if (isScrubbing) return; const state = adapter.isPaused() ? "pause" : "play"; ppBtns.forEach((btn) => btn.setAttribute("play-pause-btn", state)); }; ppBtns.forEach((btn) => { btn.addEventListener("click", () => adapter.isPaused() ? adapter.play() : adapter.pause(), ); }); adapter.on("play", syncAllPpBtns); adapter.on("pause", syncAllPpBtns); syncAllPpBtns(); } audioBtns.forEach((btn) => { btn.addEventListener("click", () => { const state = wrap.getAttribute("video-wrap"); wrap.setAttribute("video-wrap", state === "mute" ? "unmute" : "mute"); }); }); if (fsBtns.length) { const setAllFsBtns = (state) => fsBtns.forEach((btn) => btn.setAttribute("full-screen-btn", state)); const fsTarget = wrap; const requestFS = (el) => el.requestFullscreen?.() || el.webkitRequestFullscreen?.() || el.mozRequestFullScreen?.() || el.msRequestFullscreen?.(); const exitFS = () => document.exitFullscreen?.() || document.webkitExitFullscreen?.() || document.mozCancelFullScreen?.() || document.msExitFullscreen?.(); const getFullscreenEl = () => document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement || null; const syncAllFsBtns = () => { const active = getFullscreenEl() === fsTarget || getFullscreenEl() === adapter.getFullscreenTarget(); setAllFsBtns(active ? "exit" : "enter"); }; fsBtns.forEach((btn) => { btn.addEventListener("click", () => { if (getFullscreenEl()) { exitFS(); } else { const req = requestFS(fsTarget); req?.catch?.(() => requestFS(adapter.getFullscreenTarget())); } }); }); document.addEventListener("fullscreenchange", syncAllFsBtns); document.addEventListener("webkitfullscreenchange", syncAllFsBtns); document.addEventListener("mozfullscreenchange", syncAllFsBtns); document.addEventListener("MSFullscreenChange", syncAllFsBtns); } let rafId = null; const renderOnce = () => { const duration = adapter.getDuration() || 0; const current = Math.min( adapter.getCurrentTime() || 0, duration || Infinity, ); const ratio = duration ? current / duration : 0; if (progBar) progBar.style.width = `${ratio * 100}%`; if (thumbEl) thumbEl.style.left = `${ratio * 100}%`; if (totalEl) totalEl.textContent = formatTime(duration); if (currentEl) currentEl.textContent = formatTime(current); }; const renderFrame = () => { renderOnce(); rafId = requestAnimationFrame(renderFrame); }; wrapRenderers.set(wrap, renderOnce); adapter.on("play", () => { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(renderFrame); }); adapter.on("pause", () => { cancelAnimationFrame(rafId); renderOnce(); }); adapter.on("ended", () => { cancelAnimationFrame(rafId); if (progBar) progBar.style.width = "100%"; if (thumbEl) thumbEl.style.left = "100%"; if (currentEl) currentEl.textContent = formatTime(adapter.getDuration()); }); adapter.on("loadedmetadata", renderOnce); renderOnce(); if (progWrap) { let dragging = false; let wasPlaying = false; const ratioFromEvent = (e) => { const rect = progWrap.getBoundingClientRect(); const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; return Math.min(Math.max(x / rect.width, 0), 1); }; const applyRatio = (ratio) => { if (progBar) progBar.style.width = `${ratio * 100}%`; if (thumbEl) thumbEl.style.left = `${ratio * 100}%`; const duration = adapter.getDuration() || 0; if (currentEl) currentEl.textContent = formatTime(ratio * duration); }; const startScrub = (e) => { dragging = true; isScrubbing = true; wasPlaying = !adapter.isPaused(); if (wasPlaying) adapter.pause(); progWrap.setAttribute("data-scrubbing", ""); cancelAnimationFrame(rafId); applyRatio(ratioFromEvent(e)); }; const moveScrub = (e) => { if (!dragging || !adapter.getDuration()) return; e.cancelable && e.preventDefault(); applyRatio(ratioFromEvent(e)); }; const endScrub = (e) => { if (!dragging) return; dragging = false; progWrap.removeAttribute("data-scrubbing"); const safeRatio = isNaN(ratioFromEvent(e)) ? 0 : ratioFromEvent(e); const duration = adapter.getDuration() || 0; adapter.seek(safeRatio * duration); applyRatio(safeRatio); if (wasPlaying) adapter.play(); isScrubbing = false; }; progWrap.addEventListener("mousedown", startScrub); window.addEventListener("mousemove", moveScrub); window.addEventListener("mouseup", endScrub); progWrap.addEventListener("touchstart", startScrub, { passive: true }); window.addEventListener("touchmove", moveScrub, { passive: false }); window.addEventListener("touchend", endScrub); } const attrObserver = new MutationObserver((mutations) => { mutations.forEach(({ target }) => { syncMute(target); if (target.getAttribute("video-wrap") === "unmute") { wraps.forEach((w) => { if (w !== target) w.setAttribute("video-wrap", "mute"); }); } }); }); wraps.forEach((w) => attrObserver.observe(w, { attributes: true, attributeFilter: ["video-wrap"], }), ); } const bgPosterAPI = new WeakMap(); const OVERLAY_PLAY_SVG = '' + ''; function initBgPoster(wrap) { if (bgPosterAPI.has(wrap)) return; const mainVideo = wrap.querySelector("video"); if (!mainVideo) return; const holder = wrap.querySelector("[bg-poster]") || wrap; const src = (holder.getAttribute("bg-poster") || "").trim(); if (!src) return; wrap.setAttribute("data-bg-poster", "idle"); const bg = document.createElement("video"); bg.className = "video-bg-poster"; bg.muted = true; bg.defaultMuted = true; bg.loop = true; bg.autoplay = true; bg.playsInline = true; ["muted", "loop", "autoplay", "playsinline", "webkit-playsinline"].forEach( (a) => bg.setAttribute(a, ""), ); bg.setAttribute("preload", "auto"); bg.setAttribute("aria-hidden", "true"); bg.tabIndex = -1; bg.src = src; const overlay = document.createElement("div"); overlay.className = "video-overlay"; overlay.innerHTML = '"; mainVideo.insertAdjacentElement("afterend", bg); bg.insertAdjacentElement("afterend", overlay); if (!mainVideo.hasAttribute("preload")) mainVideo.preload = "metadata"; let inView = false; let started = false; let fadeTimer = null; const bgPlay = () => { if (!started && inView) bg.play().catch(() => {}); }; const bgStop = () => bg.pause(); const markStarted = () => { if (started) return; started = true; wrap.setAttribute("data-bg-poster", "started"); clearTimeout(fadeTimer); fadeTimer = setTimeout(bgStop, 650); }; const play = () => { markStarted(); const adapter = wrapAdapters.get(wrap); if (adapter) adapter.play(); else mainVideo.play().catch(() => {}); }; const stop = () => { clearTimeout(fadeTimer); started = false; wrap.setAttribute("data-bg-poster", "idle"); const adapter = wrapAdapters.get(wrap); if (adapter) { adapter.pause(); try { adapter.seek(0); } catch (e) {} } else { mainVideo.pause(); try { mainVideo.currentTime = 0; } catch (e) {} } const render = wrapRenderers.get(wrap); if (render) render(); bgPlay(); }; overlay.addEventListener("click", play); mainVideo.addEventListener("play", markStarted); mainVideo.addEventListener("ended", () => { if (!mainVideo.loop) stop(); }); new IntersectionObserver( (entries) => { entries.forEach((e) => { inView = e.isIntersecting; inView ? bgPlay() : bgStop(); }); }, { threshold: 0.2 }, ).observe(wrap); bgPosterAPI.set(wrap, { play, stop, bg, overlay }); } function stopVideoWrap(wrap) { const ctl = bgPosterAPI.get(wrap); if (ctl) return ctl.stop(); const adapter = wrapAdapters.get(wrap); if (adapter) { if (!adapter.isPaused()) adapter.pause(); return; } const v = wrap.querySelector("video"); if (v && !v.paused) v.pause(); } function bindCarouselStops() { let allBound = true; document.querySelectorAll("[data-carousel]").forEach((root) => { if (root.dataset.bgpBound) return; const embla = window.CarouselControls && window.CarouselControls.getInstance ? window.CarouselControls.getInstance(root) : null; if (!embla || !embla.on) { allBound = false; return; } embla.on("select", () => root.querySelectorAll("[video-wrap]").forEach(stopVideoWrap), ); root.dataset.bgpBound = "true"; }); document.querySelectorAll("[carousel]").forEach((track) => { if (track.dataset.bgpBound) return; const mo = new MutationObserver((muts) => { muts.forEach((m) => { const hadActive = (m.oldValue || "") .split(/\s+/) .includes("is-active"); if (hadActive && !m.target.classList.contains("is-active")) { m.target.querySelectorAll("[video-wrap]").forEach(stopVideoWrap); } }); }); [...track.children].forEach((s) => mo.observe(s, { attributes: true, attributeFilter: ["class"], attributeOldValue: true, }), ); track.dataset.bgpBound = "true"; }); return allBound; } wraps.forEach(initVideoWrap); wraps.forEach(initBgPoster); initializePlayers(); bindCarouselStops(); let bgpTries = 0; const bgpTimer = setInterval(() => { if (bindCarouselStops() || ++bgpTries > 20) clearInterval(bgpTimer); }, 250); })(); // CAROUSEL (function () { // CAROUSEL "use strict"; function debounce(fn, wait) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), wait); }; } function handleError(error, context) { console.error(`[${context}]`, error); } const logger = { log: (...args) => console.log(...args), warn: (...args) => console.warn(...args), }; const _libraryPromises = {}; function isLibraryLoaded(name) { if (name === "embla") return typeof window.EmblaCarousel !== "undefined"; return false; } function loadLibrary(name) { if (isLibraryLoaded(name)) return Promise.resolve(); if (_libraryPromises[name]) return _libraryPromises[name]; const urls = { embla: "https://cdn.jsdelivr.net/npm/embla-carousel@8/embla-carousel.umd.js", }; const url = urls[name]; if (!url) return Promise.reject(new Error(`Unknown library: ${name}`)); _libraryPromises[name] = new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = url; script.onload = () => resolve(); script.onerror = () => reject(new Error(`Failed to load ${url}`)); document.head.appendChild(script); }); return _libraryPromises[name]; } let carouselLibraryLoaded = false; let loadPromise = null; let pendingSliders = []; let carouselObserver = null; let carouselUid = 0; const syncedSliderGroups = new Map(); const VIEWPORT_SELECTOR = "[carousel-wrapper]"; const CONTAINER_SELECTOR = "[carousel]"; const BTNS_WRAPPER_SELECTOR = "[carousel-btns]"; const PREV_BTN_SELECTOR = '[carousel-prev-btn], [carousel-btns] [aria-label="Previous slide"]'; const NEXT_BTN_SELECTOR = '[carousel-next-btn], [carousel-btns] [aria-label="Next slide"]'; const NAV_BTN_FALLBACK_SELECTOR = "[carousel-btn], button"; const DOTS_CONTAINER_SELECTOR = "[carousel-dots]"; const SLIDE_BTN_SELECTOR = "[data-slide-btn]"; const SLIDE_FALLBACK_SELECTOR = "[carousel-item]"; const PROGRESS_BAR_SELECTOR = "[carousel-progress-bar]"; const PROGRESS_FILL_SELECTOR = "[carousel-progress-fill]"; const MOBILE_MAX_WIDTH = 767; const TABLET_MAX_WIDTH = 991; function prefersReducedMotion() { return ( typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches ); } function resetFadeInlineStyles(slider) { const carouselContainer = slider.querySelector(CONTAINER_SELECTOR); if (!carouselContainer) return; carouselContainer.style.position = ""; carouselContainer.style.display = ""; carouselContainer.style.height = ""; carouselContainer.style.transform = ""; carouselContainer.style.transition = ""; Array.from(carouselContainer.children).forEach((slide) => { slide.style.position = ""; slide.style.inset = ""; slide.style.width = ""; slide.style.opacity = ""; slide.style.zIndex = ""; slide.style.willChange = ""; slide.style.transition = ""; slide.style.pointerEvents = ""; }); } function getCarouselTokens(slider) { const attrValue = slider.getAttribute("data-carousel"); if (!attrValue) return []; return attrValue .toLowerCase() .split(/[\s,|]+/) .map((token) => token.trim()) .filter(Boolean); } function isFadeCarousel(slider) { return ( slider.hasAttribute("data-fade") || slider.getAttribute("data-effect") === "fade" || getCarouselTokens(slider).includes("fade") ); } const ensureFadeCarouselAnimateStyles = (() => { let injected = false; const CSS_TEXT = `\n[carousel-text-animate] { opacity: 0; transform: translateY(12px); transition: opacity 0.5s ease, transform 0.5s ease; }\n.is-active [carousel-text-animate] { opacity: 1; transform: translateY(0); }\n.is-active [carousel-text-animate]:nth-child(1) { transition-delay: 0.3s; }\n.is-active [carousel-text-animate]:nth-child(2) { transition-delay: 0.45s; }\n.is-active [carousel-text-animate]:nth-child(3) { transition-delay: 0.6s; }\n[carousel-img-animate] { opacity: 0; transform: scale(0.95); transition: opacity 0.6s ease, transform 0.6s ease; }\n.is-active [carousel-img-animate] { opacity: 1; transform: scale(1); transition-delay: 0.2s; }\n`; return () => { if (injected || typeof document === "undefined") return; injected = true; const style = document.createElement("style"); style.setAttribute("data-carousel-fade-animate", "true"); style.textContent = CSS_TEXT; document.head.appendChild(style); }; })(); const ensureDotButtonResetStyles = (() => { let injected = false; const CSS_TEXT = `\n:where(button[carousel-dot]) {\n appearance: none; -webkit-appearance: none; background: none; border: 0;\n border-radius: 0; padding: 0; margin: 0; font: inherit; color: inherit;\n line-height: inherit; cursor: pointer;\n}\n`; return () => { if (injected || typeof document === "undefined") return; injected = true; const style = document.createElement("style"); style.setAttribute("data-carousel-dot-reset", "true"); style.textContent = CSS_TEXT; document.head.appendChild(style); }; })(); const ensureActiveInViewStyles = (() => { let injected = false; const CSS_TEXT = `\n[data-carousel][data-active-in-view] [carousel] > * { cursor: pointer; }\n[data-carousel][data-active-in-view] [carousel] > .is-active { position: relative; z-index: 2; }\n`; return () => { if (injected || typeof document === "undefined") return; injected = true; const style = document.createElement("style"); style.setAttribute("data-carousel-active-in-view-style", "true"); style.textContent = CSS_TEXT; document.head.appendChild(style); }; })(); function shouldInitForViewport(slider) { const tokens = getCarouselTokens(slider); const onlyMobile = tokens.includes("mobile"); const onlyTablet = tokens.includes("tablet"); if (!onlyMobile && !onlyTablet) return true; if (typeof window === "undefined") return true; if (onlyMobile) return window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH}px)`).matches; return window.matchMedia(`(max-width: ${TABLET_MAX_WIDTH}px)`).matches; } async function loadCarouselLibrary() { if (carouselLibraryLoaded || isLibraryLoaded("embla")) return Promise.resolve(); if (loadPromise) return loadPromise; loadPromise = (async () => { try { await loadLibrary("embla"); if (typeof window.EmblaCarousel === "undefined") { throw new Error("Carousel library failed to load"); } carouselLibraryLoaded = true; if (pendingSliders.length > 0) { logger.log( `Initializing ${pendingSliders.length} pending carousel(s)...`, ); initializeCarousels(pendingSliders); pendingSliders = []; } return true; } catch (error) { handleError(error, "Carousel Library Loader"); loadPromise = null; throw error; } })(); return loadPromise; } async function loadAndInitSlider(slider) { if (slider._carouselInitialized) return; if (!carouselLibraryLoaded && !pendingSliders.includes(slider)) { pendingSliders.push(slider); } if (!carouselLibraryLoaded) { await loadCarouselLibrary(); } if (carouselLibraryLoaded && !slider._carouselInitialized) { initializeCarousels([slider]); } } function initCarousel() { const sliders = document.querySelectorAll("[data-carousel]"); if (!sliders.length) return; if (Array.from(sliders).some(isFadeCarousel)) ensureFadeCarouselAnimateStyles(); if ( Array.from(sliders).some((s) => s.hasAttribute("data-active-in-view")) ) { ensureActiveInViewStyles(); } logger.log( `⏳ Found ${sliders.length} carousel(s) - will load when visible...`, ); if (carouselObserver) carouselObserver.disconnect(); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const slider = entry.target; if (!shouldInitForViewport(slider)) return; observer.unobserve(slider); slider.setAttribute("data-carousel-observed", "true"); loadAndInitSlider(slider); }); }, { root: null, rootMargin: "200px", threshold: 0, }, ); carouselObserver = observer; sliders.forEach((slider) => observer.observe(slider)); } function initializeCarousels(sliderList) { if (!sliderList || !sliderList.length) return; sliderList.forEach((slider) => { if (slider._carouselInitialized) return; if (!shouldInitForViewport(slider)) return; const carouselRoot = slider.querySelector(VIEWPORT_SELECTOR) || slider; const carouselViewport = slider.querySelector(VIEWPORT_SELECTOR) || carouselRoot; const carouselContainer = slider.querySelector(CONTAINER_SELECTOR); if (!carouselViewport) { logger.warn("Carousel viewport not found in slider:", slider); return; } const resolvedContainer = carouselContainer || carouselViewport.children[0]; if (!resolvedContainer) { logger.warn( "Carousel has no slides container — check DOM structure:", slider, ); return; } slider._carouselInitialized = true; const navButtonsWrapper = slider.querySelector(BTNS_WRAPPER_SELECTOR); const fallbackNavButtons = navButtonsWrapper ? Array.from( navButtonsWrapper.querySelectorAll(NAV_BTN_FALLBACK_SELECTOR), ) : []; const explicitNextBtn = slider.querySelector("[slider-next-btn]") || slider.querySelector(NEXT_BTN_SELECTOR); const explicitPrevBtn = slider.querySelector("[slider-prev-btn]") || slider.querySelector(PREV_BTN_SELECTOR); const prevBtn = explicitPrevBtn || fallbackNavButtons[0] || null; let nextBtn = explicitNextBtn || (fallbackNavButtons.length > 1 ? fallbackNavButtons[1] : fallbackNavButtons[0]) || null; if (prevBtn && nextBtn && prevBtn === nextBtn) nextBtn = null; let slideButtons = []; let slideButtonIndices = []; const customProgressBar = slider.querySelector(PROGRESS_BAR_SELECTOR); const syncId = slider.getAttribute("data-sync"); const centerMode = slider.hasAttribute("data-center"); const centerBounds = slider.hasAttribute("data-center-bounds"); const clickToCenter = slider.hasAttribute("data-click-center"); const activeInView = slider.hasAttribute("data-active-in-view"); const slideAllView = slider.hasAttribute("data-slide-all-view"); const loopMode = slider.hasAttribute("data-loop"); const disableDrag = slider.hasAttribute("data-no-drag"); const dragFree = slider.hasAttribute("data-drag-free"); const autoplayEnabled = slider.hasAttribute("data-autoplay"); const autoplayStopOnInteraction = slider.hasAttribute( "data-autoplay-stop-on-interaction", ); const pauseOnHover = slider.hasAttribute("data-pause-on-hover"); const fadeMode = isFadeCarousel(slider); const verticalMode = slider.hasAttribute("data-vertical"); const slideClassesEnabled = slider.hasAttribute("data-slide-classes"); const alignAttr = slider.getAttribute("data-align"); const autoplayDelayAttr = slider.getAttribute("data-autoplay-delay"); const autoplayDelay = Number.isFinite( Number.parseInt(autoplayDelayAttr, 10), ) ? Number.parseInt(autoplayDelayAttr, 10) : 8e3; const fadeDurationAttr = slider.getAttribute("data-fade-duration"); const fadeDuration = Number.isFinite( Number.parseInt(fadeDurationAttr, 10), ) ? Math.max(150, Number.parseInt(fadeDurationAttr, 10)) : 650; const fadeEase = slider.getAttribute("data-fade-ease") || "cubic-bezier(0.22, 1, 0.36, 1)"; const startIndexAttr = slider.getAttribute("data-start-index"); let requestedStartIndex = Number.parseInt(startIndexAttr, 10); if (!Number.isFinite(requestedStartIndex)) { if (centerMode) { const initialSlideCount = resolvedContainer.children.length; requestedStartIndex = initialSlideCount > 0 ? Math.floor((initialSlideCount - 1) / 2) : 0; } else { requestedStartIndex = 0; } } const carouselOptions = { container: resolvedContainer, align: alignAttr || (centerMode ? "center" : "start"), containScroll: fadeMode ? "keepSnaps" : centerMode && !centerBounds ? false : centerMode ? "keepSnaps" : "trimSnaps", loop: fadeMode ? false : loopMode, draggable: fadeMode ? false : !disableDrag, watchDrag: fadeMode ? false : !disableDrag, dragFree: fadeMode ? false : dragFree, axis: verticalMode ? "y" : "x", watchResize: !fadeMode, slidesToScroll: fadeMode ? 1 : slideAllView ? "auto" : 1, startIndex: requestedStartIndex, }; let carouselApi = null; const cleanupTasks = []; let scrollToIndex = () => {}; let restartAutoplay = null; let lastButtonStateKey = null; let progressRafId = null; let progressStartTime = null; let progressResumeOffset = 0; let interactionHold = false; let progressFillEl = null; let fadeLayoutSlideCount = 0; function getProgressFill() { if (!customProgressBar) return null; if (progressFillEl) return progressFillEl; progressFillEl = customProgressBar.querySelector( PROGRESS_FILL_SELECTOR, ); if (!progressFillEl && customProgressBar.children.length === 1) { progressFillEl = customProgressBar.firstElementChild; progressFillEl.setAttribute("carousel-progress-fill", ""); } return progressFillEl; } function cancelProgressBar(reset = true) { if (progressRafId !== null) { cancelAnimationFrame(progressRafId); progressRafId = null; } progressStartTime = null; if (reset && customProgressBar) { const fill = getProgressFill(); if (fill) { fill.style.transition = "none"; fill.style.width = "0%"; } } } function startProgressBar(resume = false) { if (!customProgressBar || !autoplayEnabled) return; if (prefersReducedMotion()) return; const fill = getProgressFill(); if (!fill) return; if (!resume) { progressResumeOffset = 0; fill.style.transition = "none"; fill.style.width = "0%"; fill.offsetWidth; } if (interactionHold) return; progressStartTime = null; function tick(timestamp) { if (progressStartTime === null) { progressStartTime = timestamp - progressResumeOffset; progressResumeOffset = 0; } const elapsed = timestamp - progressStartTime; const progress = Math.min((elapsed / autoplayDelay) * 100, 100); fill.style.width = `${progress}%`; if (carouselApi) { const totalSlides = carouselApi.scrollSnapList().length; const currentIndex = carouselApi.selectedScrollSnap(); customProgressBar.setAttribute( "data-progress", Math.round(progress), ); customProgressBar.setAttribute( "data-current-slide", currentIndex + 1, ); customProgressBar.setAttribute("data-total-slides", totalSlides); } if (progress < 100) { progressRafId = requestAnimationFrame(tick); return; } progressRafId = null; progressStartTime = null; if (carouselApi) { if (fadeMode) stepFade(1); else if (carouselApi.canScrollNext()) carouselApi.scrollNext(); else carouselApi.scrollTo(0); } } progressRafId = requestAnimationFrame(tick); } function updateStaticProgressBar() { if (!customProgressBar || autoplayEnabled || !carouselApi) return; const fill = getProgressFill(); if (!fill) return; const totalSlides = carouselApi.scrollSnapList().length; const currentIndex = carouselApi.selectedScrollSnap(); const progress = totalSlides > 1 ? Math.round((currentIndex / (totalSlides - 1)) * 100) : 100; fill.style.width = `${progress}%`; customProgressBar.setAttribute("data-progress", progress); customProgressBar.setAttribute("data-current-slide", currentIndex + 1); customProgressBar.setAttribute("data-total-slides", totalSlides); } function getSlides() { if (carouselContainer) return Array.from(carouselContainer.children); return Array.from(slider.querySelectorAll(SLIDE_FALLBACK_SELECTOR)); } function getSnapSlideIndices() { if (!carouselApi) return []; try { const engine = typeof carouselApi.internalEngine === "function" ? carouselApi.internalEngine() : null; if ( engine && Array.isArray(engine.slideRegistry) && engine.slideRegistry.length ) { return engine.slideRegistry; } } catch (_) {} return carouselApi.scrollSnapList().map((_, i) => [i]); } function getActiveIndices() { if (!carouselApi) return []; if ( slideAllView && !fadeMode && typeof carouselApi.slidesInView === "function" ) { const inView = carouselApi.slidesInView(); if (inView.length) return inView; } return [carouselApi.selectedScrollSnap()]; } function stepFade(direction) { if (!carouselApi) return; const totalSlides = carouselApi.scrollSnapList().length; if (totalSlides < 2) return; const currentIndex = carouselApi.selectedScrollSnap(); scrollToIndex( (currentIndex + direction + totalSlides) % totalSlides, true, ); } function applyFadeLayout(remeasure = false) { if (!fadeMode || !carouselContainer) return; const slides = getSlides(); if (!slides.length) return; carouselContainer.style.transform = "none"; carouselContainer.style.transition = "none"; const alreadyLaidOut = !remeasure && fadeLayoutSlideCount === slides.length && slides.every((s) => s.style.position === "absolute"); if (alreadyLaidOut) return; const previousHeight = carouselContainer.style.height; if (remeasure && fadeLayoutSlideCount > 0) { carouselContainer.style.height = ""; carouselContainer.style.display = ""; carouselContainer.style.position = ""; slides.forEach((slide) => { slide.style.position = ""; slide.style.inset = ""; slide.style.width = ""; }); } const maxHeight = slides.reduce( (max, slide) => Math.max(max, slide.offsetHeight), 0, ); carouselViewport.style.overflow = "hidden"; carouselContainer.style.position = "relative"; carouselContainer.style.display = "block"; if (maxHeight > 0) carouselContainer.style.height = `${maxHeight}px`; else if (previousHeight) carouselContainer.style.height = previousHeight; slides.forEach((slide) => { slide.style.position = "absolute"; slide.style.inset = "0"; slide.style.width = "100%"; slide.style.willChange = "opacity"; slide.style.transition = `opacity ${fadeDuration}ms ${fadeEase}`; }); fadeLayoutSlideCount = slides.length; } function ensureDots() { const dotsContainer = slider.querySelector(DOTS_CONTAINER_SELECTOR); const slides = getSlides(); if (!slides.length) return; const groupIndices = slideAllView && !fadeMode ? getSnapSlideIndices() : null; const dotCount = groupIndices && groupIndices.length ? groupIndices.length : slides.length; if (dotsContainer) { const existingDots = dotsContainer.querySelectorAll(SLIDE_BTN_SELECTOR); if (existingDots.length > 0) { if (existingDots.length !== dotCount) { logger.warn( `Carousel has ${dotCount} ${groupIndices ? "page(s)" : "slide(s)"} but ${existingDots.length} dot buttons. Button count should match.`, ); } } else { dotsContainer.innerHTML = ""; ensureDotButtonResetStyles(); const fragment = document.createDocumentFragment(); for (let index = 0; index < dotCount; index += 1) { const dot = document.createElement("button"); dot.type = "button"; dot.className = "carousel-dot"; dot.setAttribute("carousel-dot", ""); dot.setAttribute("data-slide-btn", ""); dot.setAttribute("aria-label", `Go to slide ${index + 1}`); fragment.appendChild(dot); } dotsContainer.appendChild(fragment); } } slideButtons = Array.from(slider.querySelectorAll(SLIDE_BTN_SELECTOR)); slideButtons.sort((a, b) => { const aIndex = a.hasAttribute("data-slide-index") ? Number.parseInt(a.getAttribute("data-slide-index"), 10) : null; const bIndex = b.hasAttribute("data-slide-index") ? Number.parseInt(b.getAttribute("data-slide-index"), 10) : null; if (aIndex !== null && bIndex !== null) return aIndex - bIndex; if (aIndex !== null) return -1; if (bIndex !== null) return 1; const position = a.compareDocumentPosition(b); return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; }); slideButtons = slideButtons.slice(0, dotCount); const claimedIndices = new Set(); slideButtons.forEach((button) => { if (button.hasAttribute("data-slide-index")) { claimedIndices.add( Number.parseInt(button.getAttribute("data-slide-index"), 10), ); } }); let nextFreeIndex = 0; slideButtonIndices = slideButtons.map((button, arrayIndex) => { if (button.hasAttribute("data-slide-index")) { return Number.parseInt(button.getAttribute("data-slide-index"), 10); } if (groupIndices) { const targetSlides = groupIndices[arrayIndex]; return targetSlides && targetSlides.length ? targetSlides[0] : arrayIndex; } while (claimedIndices.has(nextFreeIndex)) nextFreeIndex += 1; claimedIndices.add(nextFreeIndex); return nextFreeIndex; }); } function updateActiveSlides() { if (!carouselApi) return; const slides = getSlides(); if (!slides.length) return; if (fadeMode) { if (!carouselContainer) { logger.warn( "Fade mode requires a carousel container inside the slider.", ); return; } applyFadeLayout(); } const activeIndices = getActiveIndices(); const activeIndex = activeIndices.length ? activeIndices[0] : carouselApi.selectedScrollSnap(); const visibleIndices = slideClassesEnabled && carouselApi.slidesInView ? carouselApi.slidesInView() : null; slides.forEach((slide, index) => { Array.from(slide.classList).forEach((className) => { if (/^(upcoming|upcomming|passed)-\d+$/.test(className)) slide.classList.remove(className); }); slide.classList.toggle("is-active", activeIndices.includes(index)); if (fadeMode) { const isActive = index === activeIndex; slide.style.opacity = isActive ? "1" : "0"; slide.style.zIndex = isActive ? "2" : "1"; slide.style.pointerEvents = isActive ? "auto" : "none"; if (isActive) { slide.removeAttribute("aria-hidden"); slide.inert = false; } else { if (slide.contains(document.activeElement)) slider.focus({ preventScroll: true, }); slide.setAttribute("aria-hidden", "true"); slide.inert = true; } } if (!slideClassesEnabled) return; slide.classList.toggle( "is-visible", fadeMode ? index === activeIndex : visibleIndices.includes(index), ); slide.classList.toggle("is-passed", index < activeIndex); slide.classList.toggle("is-upcoming", index > activeIndex); slide.classList.toggle("is-prev", index === activeIndex - 1); slide.classList.toggle("is-next", index === activeIndex + 1); if (index > activeIndex) slide.classList.add(`upcoming-${index - activeIndex}`); else if (index < activeIndex) slide.classList.add(`passed-${activeIndex - index}`); }); updateSlideButtons(activeIndex); } function updateSlideButtons(activeIndex) { if (!slideButtons.length) return; let activeButton = null; slideButtons.forEach((button, arrayIndex) => { const buttonIndex = slideButtonIndices[arrayIndex] ?? arrayIndex; const isActive = buttonIndex === activeIndex; button.classList.toggle("is-active", isActive); if (isActive) { activeButton = button; button.setAttribute("aria-current", "true"); } else { button.removeAttribute("aria-current"); } }); keepActiveSlideButtonInView(activeButton); } function keepActiveSlideButtonInView(activeButton) { if (!activeButton) return; const scrollParent = findHorizontalScrollParent(activeButton); if (!scrollParent) return; const parentRect = scrollParent.getBoundingClientRect(); const buttonRect = activeButton.getBoundingClientRect(); const sidePadding = 8; const isOutOfView = buttonRect.left < parentRect.left + sidePadding || buttonRect.right > parentRect.right - sidePadding; if (!isOutOfView) return; const parentCenter = parentRect.left + parentRect.width / 2; const buttonCenter = buttonRect.left + buttonRect.width / 2; const delta = buttonCenter - parentCenter; const targetScrollLeft = Math.max( 0, Math.min( scrollParent.scrollWidth - scrollParent.clientWidth, scrollParent.scrollLeft + delta, ), ); scrollParent.scrollTo({ left: targetScrollLeft, behavior: prefersReducedMotion() ? "auto" : "smooth", }); } function findHorizontalScrollParent(element) { let current = element.parentElement; while (current && current !== slider) { const styles = window.getComputedStyle(current); const overflowX = styles.overflowX; const hasOverflowingContent = current.scrollWidth > current.clientWidth + 1; const canScrollX = hasOverflowingContent && (overflowX === "auto" || overflowX === "scroll" || overflowX === "hidden"); if (canScrollX) return current; current = current.parentElement; } return null; } function updateButtonStates() { if (!carouselApi) return; const canPrev = carouselApi.canScrollPrev(); const canNext = carouselApi.canScrollNext(); const slides = getSlides(); const effectiveCanPrev = fadeMode && slides.length > 1 ? true : canPrev; const effectiveCanNext = fadeMode && slides.length > 1 ? true : canNext; const bothDisabled = !effectiveCanPrev && !effectiveCanNext; const stateKey = JSON.stringify({ effectiveCanPrev: effectiveCanPrev, effectiveCanNext: effectiveCanNext, bothDisabled: bothDisabled, hasNavWrapper: Boolean(navButtonsWrapper), hasPrev: Boolean(prevBtn), hasNext: Boolean(nextBtn), }); if (stateKey === lastButtonStateKey) return; lastButtonStateKey = stateKey; if (navButtonsWrapper) { navButtonsWrapper.style.display = bothDisabled ? "none" : ""; navButtonsWrapper.setAttribute("aria-hidden", String(bothDisabled)); } if (prevBtn) { prevBtn.style.pointerEvents = effectiveCanPrev ? "auto" : "none"; prevBtn.style.opacity = effectiveCanPrev ? "1" : "0.5"; prevBtn.style.display = bothDisabled ? "none" : ""; prevBtn.setAttribute("aria-disabled", String(!effectiveCanPrev)); prevBtn.setAttribute("tabindex", effectiveCanPrev ? "0" : "-1"); } if (nextBtn) { nextBtn.style.pointerEvents = effectiveCanNext ? "auto" : "none"; nextBtn.style.opacity = effectiveCanNext ? "1" : "0.5"; nextBtn.style.display = bothDisabled ? "none" : ""; nextBtn.setAttribute("aria-disabled", String(!effectiveCanNext)); nextBtn.setAttribute("tabindex", effectiveCanNext ? "0" : "-1"); } } try { carouselApi = window.EmblaCarousel(carouselViewport, carouselOptions); carouselViewport._carousel = carouselApi; slider._carouselInstance = carouselApi; scrollToIndex = (index, jump = false) => { if (carouselApi) carouselApi.scrollTo(index, jump); }; ensureDots(); const slides = getSlides(); if ( !slideAllView && slideButtons.length !== slides.length && slideButtons.length > 0 ) { logger.warn( `Carousel has ${slides.length} slides but ${slideButtons.length} slide buttons. Button indices may not match slide indices.`, ); } if (!carouselViewport.id) { carouselUid += 1; carouselViewport.id = `carousel-viewport-${carouselUid}`; } resolvedContainer.setAttribute("aria-live", "polite"); slides.forEach((slide, index) => { if (!slide.hasAttribute("role")) slide.setAttribute("role", "group"); if (!slide.hasAttribute("aria-roledescription")) slide.setAttribute("aria-roledescription", "slide"); if (!slide.hasAttribute("aria-label")) slide.setAttribute( "aria-label", `${index + 1} of ${slides.length}`, ); }); updateButtonStates(); updateActiveSlides(); if (!autoplayEnabled) updateStaticProgressBar(); if (autoplayEnabled) { let autoplayIntervalId = null; let autoplayStoppedByInteraction = false; const setLiveRegion = (rotating) => resolvedContainer.setAttribute( "aria-live", rotating ? "off" : "polite", ); const startAutoplay = (resumeProgress = false) => { if (interactionHold) return; if (prefersReducedMotion()) return; setLiveRegion(true); if (customProgressBar) { startProgressBar(resumeProgress); return; } if (autoplayIntervalId) return; autoplayIntervalId = window.setInterval(() => { if (!carouselApi) return; if (fadeMode) { stepFade(1); return; } if (carouselApi.canScrollNext()) carouselApi.scrollNext(); else carouselApi.scrollTo(0); }, autoplayDelay); }; const stopAutoplay = () => { setLiveRegion(false); cancelProgressBar(true); if (autoplayIntervalId) { window.clearInterval(autoplayIntervalId); autoplayIntervalId = null; } }; restartAutoplay = () => { if (autoplayStopOnInteraction) return; stopAutoplay(); startAutoplay(); }; slider._carouselAutoplay = { suspend: stopAutoplay, resume: () => { stopAutoplay(); startAutoplay(); }, }; cleanupTasks.push(() => { delete slider._carouselAutoplay; }); startAutoplay(); if (autoplayStopOnInteraction) { const stopHandler = () => { autoplayStoppedByInteraction = true; stopAutoplay(); }; slider.addEventListener("pointerdown", stopHandler); slider.addEventListener("keydown", stopHandler); cleanupTasks.push(() => { slider.removeEventListener("pointerdown", stopHandler); slider.removeEventListener("keydown", stopHandler); }); } if (pauseOnHover) { let pointerOver = false; let focusWithin = false; const updateHold = () => { const shouldHold = pointerOver || focusWithin; if (shouldHold === interactionHold) return; interactionHold = shouldHold; if (shouldHold) { setLiveRegion(false); if (customProgressBar) { if (progressRafId !== null && progressStartTime !== null) { progressResumeOffset = performance.now() - progressStartTime; } cancelProgressBar(false); } else if (autoplayIntervalId) { window.clearInterval(autoplayIntervalId); autoplayIntervalId = null; } } else if (!autoplayStoppedByInteraction) { startAutoplay(true); } }; const pointerEnterHandler = () => { pointerOver = true; updateHold(); }; const pointerLeaveHandler = () => { pointerOver = false; updateHold(); }; const focusInHandler = () => { focusWithin = true; updateHold(); }; const focusOutHandler = (event) => { if (event.relatedTarget && slider.contains(event.relatedTarget)) return; focusWithin = false; updateHold(); }; slider.addEventListener("mouseenter", pointerEnterHandler); slider.addEventListener("mouseleave", pointerLeaveHandler); slider.addEventListener("focusin", focusInHandler); slider.addEventListener("focusout", focusOutHandler); cleanupTasks.push(() => { slider.removeEventListener("mouseenter", pointerEnterHandler); slider.removeEventListener("mouseleave", pointerLeaveHandler); slider.removeEventListener("focusin", focusInHandler); slider.removeEventListener("focusout", focusOutHandler); }); } cleanupTasks.push(() => stopAutoplay()); } const onSelect = () => { updateButtonStates(); updateActiveSlides(); if (autoplayEnabled && customProgressBar) { cancelProgressBar(false); startProgressBar(); } else if (autoplayEnabled && !customProgressBar && restartAutoplay) { restartAutoplay(); } else if (!autoplayEnabled) { updateStaticProgressBar(); } }; const onScroll = () => { if (fadeMode) applyFadeLayout(); }; const onReInit = () => { ensureDots(); updateButtonStates(); updateActiveSlides(); if (!autoplayEnabled) updateStaticProgressBar(); }; carouselApi.on("select", onSelect); carouselApi.on("scroll", onScroll); carouselApi.on("reInit", onReInit); const onSlidesInView = () => { if (slideClassesEnabled) updateActiveSlides(); }; if (slideClassesEnabled) carouselApi.on("slidesInView", onSlidesInView); cleanupTasks.push(() => { carouselApi.off("select", onSelect); carouselApi.off("scroll", onScroll); carouselApi.off("reInit", onReInit); if (slideClassesEnabled) carouselApi.off("slidesInView", onSlidesInView); }); if (fadeMode) { const SWIPE_THRESHOLD = 50; let swipeStartX = null; let swipeStartY = null; let isSwiping = false; const onPointerDown = (e) => { if (e.pointerType === "mouse" && e.button !== 0) return; swipeStartX = e.clientX; swipeStartY = e.clientY; isSwiping = true; }; const onPointerUp = (e) => { if (!isSwiping || swipeStartX === null) return; isSwiping = false; const deltaX = e.clientX - swipeStartX; const deltaY = e.clientY - swipeStartY; swipeStartX = null; swipeStartY = null; if (Math.abs(deltaY) > Math.abs(deltaX)) return; if (Math.abs(deltaX) < SWIPE_THRESHOLD) return; stepFade(deltaX < 0 ? 1 : -1); if (restartAutoplay) restartAutoplay(); }; const onPointerCancel = () => { isSwiping = false; swipeStartX = null; swipeStartY = null; }; carouselViewport.addEventListener("pointerdown", onPointerDown); carouselViewport.addEventListener("pointerup", onPointerUp); carouselViewport.addEventListener("pointercancel", onPointerCancel); cleanupTasks.push(() => { carouselViewport.removeEventListener("pointerdown", onPointerDown); carouselViewport.removeEventListener("pointerup", onPointerUp); carouselViewport.removeEventListener( "pointercancel", onPointerCancel, ); }); } if (dragFree && !fadeMode) { const DRAG_THRESHOLD = 0.05; const snapAfterDrag = () => { if (!carouselApi) return; const snapList = carouselApi.scrollSnapList(); const currentIndex = carouselApi.selectedScrollSnap(); const lastIndex = snapList.length - 1; const progress = carouselApi.scrollProgress(); const currentSnap = snapList[currentIndex]; const dragDelta = progress - currentSnap; let targetIndex = currentIndex; if (dragDelta > DRAG_THRESHOLD) { targetIndex = loopMode ? (currentIndex + 1) % snapList.length : Math.min(currentIndex + 1, lastIndex); } else if (dragDelta < -DRAG_THRESHOLD) { targetIndex = loopMode ? (currentIndex - 1 + snapList.length) % snapList.length : Math.max(currentIndex - 1, 0); } carouselApi.scrollTo(targetIndex); if (restartAutoplay) restartAutoplay(); }; carouselApi.on("pointerUp", snapAfterDrag); cleanupTasks.push(() => carouselApi.off("pointerUp", snapAfterDrag)); } const resizeHandler = debounce(() => { if (!fadeMode || !carouselApi) return; const currentIndex = carouselApi.selectedScrollSnap(); resetFadeInlineStyles(slider); carouselApi.reInit(); const total = carouselApi.scrollSnapList().length; if (currentIndex > 0 && currentIndex < total) carouselApi.scrollTo(currentIndex, true); }, 150); window.addEventListener("resize", resizeHandler); cleanupTasks.push(() => window.removeEventListener("resize", resizeHandler), ); if (nextBtn) { const nextHandler = () => { if (fadeMode) { stepFade(1); return; } carouselApi.scrollNext(); updateButtonStates(); if (restartAutoplay) restartAutoplay(); }; nextBtn.addEventListener("click", nextHandler); if (!nextBtn.hasAttribute("aria-label")) nextBtn.setAttribute("aria-label", "Next slide"); if (nextBtn.tagName !== "BUTTON") nextBtn.setAttribute("role", "button"); cleanupTasks.push(() => nextBtn.removeEventListener("click", nextHandler), ); } if (prevBtn) { const prevHandler = () => { if (fadeMode) { stepFade(-1); return; } carouselApi.scrollPrev(); updateButtonStates(); if (restartAutoplay) restartAutoplay(); }; prevBtn.addEventListener("click", prevHandler); if (!prevBtn.hasAttribute("aria-label")) prevBtn.setAttribute("aria-label", "Previous slide"); if (prevBtn.tagName !== "BUTTON") prevBtn.setAttribute("role", "button"); cleanupTasks.push(() => prevBtn.removeEventListener("click", prevHandler), ); } if (slideButtons.length) { slideButtons.forEach((button, index) => { const resolvedIndex = slideButtonIndices[index] ?? index; const targetIndex = Math.max( 0, Math.min(resolvedIndex, slides.length - 1), ); const clickHandler = () => { scrollToIndex(targetIndex, fadeMode); if (restartAutoplay) restartAutoplay(); }; const keyHandler = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); clickHandler(); } }; button.addEventListener("click", clickHandler); button.addEventListener("keydown", keyHandler); if (button.tagName !== "BUTTON") { button.setAttribute("role", "button"); if (!button.hasAttribute("tabindex")) button.setAttribute("tabindex", "0"); } if (!button.hasAttribute("aria-label")) button.setAttribute( "aria-label", `Go to slide ${targetIndex + 1}`, ); if (!button.hasAttribute("aria-controls")) button.setAttribute("aria-controls", carouselViewport.id); cleanupTasks.push(() => { button.removeEventListener("click", clickHandler); button.removeEventListener("keydown", keyHandler); }); }); } if (clickToCenter || activeInView) { getSlides().forEach((slide, index) => { const slideHandler = () => scrollToIndex(index, fadeMode); slide.addEventListener("click", slideHandler); cleanupTasks.push(() => slide.removeEventListener("click", slideHandler), ); }); } if (!slider._keyboardSetup) { const keyboardHandler = (event) => { const nextKey = verticalMode ? "ArrowDown" : "ArrowRight"; const prevKey = verticalMode ? "ArrowUp" : "ArrowLeft"; if (event.key === nextKey) { event.preventDefault(); if (fadeMode) stepFade(1); else carouselApi.scrollNext(); if (restartAutoplay) restartAutoplay(); } else if (event.key === prevKey) { event.preventDefault(); if (fadeMode) stepFade(-1); else carouselApi.scrollPrev(); if (restartAutoplay) restartAutoplay(); } }; slider.addEventListener("keydown", keyboardHandler); slider.tabIndex = 0; slider.setAttribute("role", "region"); slider.setAttribute("aria-roledescription", "carousel"); if ( !slider.hasAttribute("aria-label") && !slider.hasAttribute("aria-labelledby") ) { slider.setAttribute("aria-label", "Featured content"); } slider._keyboardSetup = true; cleanupTasks.push(() => slider.removeEventListener("keydown", keyboardHandler), ); } registerSyncedSlider(syncId, carouselApi); slider._carouselCleanup = cleanupTasks; } catch (error) { cleanupTasks.forEach((cleanup) => { try { cleanup(); } catch (_) {} }); if (carouselApi) { try { carouselApi.destroy(); } catch (_) {} } delete slider._keyboardSetup; slider._carouselInitialized = false; slider._carouselInstance = null; if (carouselViewport) carouselViewport._carousel = null; handleError(error, "Carousel Initialization"); } }); logger.log(`✅ ${sliderList.length} carousel(s) initialized`); } function getCarouselElementFromRoot(element) { if (!element) return null; return element.hasAttribute("data-carousel") ? element : element.querySelector?.("[data-carousel]") || null; } function getCarouselAutoplayControls(selector) { const element = typeof selector === "string" ? document.querySelector(selector) : selector; const carouselRoot = getCarouselElementFromRoot(element) || element; return carouselRoot?._carouselAutoplay || null; } function getCarouselInstance(selector) { const element = typeof selector === "string" ? document.querySelector(selector) : selector; if (!element) return null; return ( element._carouselInstance || element._carousel || element.querySelector?.(VIEWPORT_SELECTOR)?._carousel || null ); } async function ensureCarouselInitialized(selector) { const root = typeof selector === "string" ? document.querySelector(selector) : selector; const slider = getCarouselElementFromRoot(root) || root; if (!slider) return null; const existing = getCarouselInstance(slider); if (existing) return existing; await loadAndInitSlider(slider); return new Promise((resolve) => { let attempts = 0; const maxAttempts = 200; const tick = () => { const api = getCarouselInstance(slider); if (api) { resolve(api); return; } if (attempts >= maxAttempts) { resolve(null); return; } attempts += 1; window.setTimeout(tick, 50); }; tick(); }); } function reinitCarouselsIn(container) { container.querySelectorAll("[data-carousel]").forEach((slider) => { if (slider._carouselInitialized) { resetFadeInlineStyles(slider); if (Array.isArray(slider._carouselCleanup)) { slider._carouselCleanup.forEach((fn) => { try { fn(); } catch (_) {} }); slider._carouselCleanup = null; } const viewport = slider.querySelector(VIEWPORT_SELECTOR); const api = slider._carouselInstance || viewport?._carousel; if (api) { try { api.destroy(); } catch (_) {} } slider._carouselInitialized = false; slider._carouselInstance = null; if (viewport) viewport._carousel = null; delete slider._keyboardSetup; } loadAndInitSlider(slider); }); } function destroyCarousels() { if (carouselObserver) { carouselObserver.disconnect(); carouselObserver = null; } document.querySelectorAll("[data-carousel]").forEach((slider) => { if (Array.isArray(slider._carouselCleanup)) { slider._carouselCleanup.forEach((cleanup) => { try { cleanup(); } catch (error) { handleError(error, "Carousel Cleanup"); } }); slider._carouselCleanup = null; } const viewport = slider.querySelector?.(VIEWPORT_SELECTOR) || null; const carouselApi = slider._carouselInstance || viewport?._carousel || null; if (carouselApi) carouselApi.destroy(); resetFadeInlineStyles(slider); slider._carouselInitialized = false; slider._carouselInstance = null; if (viewport) viewport._carousel = null; delete slider._keyboardSetup; }); syncedSliderGroups.clear(); } function registerSyncedSlider(syncId, carouselApi) { if (!syncId || !carouselApi) return; if (!syncedSliderGroups.has(syncId)) syncedSliderGroups.set(syncId, new Set()); const group = syncedSliderGroups.get(syncId); group.add(carouselApi); const syncHandler = () => { const targetIndex = carouselApi.selectedScrollSnap(); group.forEach((otherCarousel) => { if (otherCarousel === carouselApi) return; if (otherCarousel.selectedScrollSnap() === targetIndex) return; otherCarousel.scrollTo(targetIndex); }); }; carouselApi.on("select", syncHandler); carouselApi.on("reInit", syncHandler); carouselApi.on("destroy", () => { carouselApi.off("select", syncHandler); carouselApi.off("reInit", syncHandler); group.delete(carouselApi); if (group.size === 0) syncedSliderGroups.delete(syncId); }); } window.CarouselControls = { getInstance: getCarouselInstance, getAutoplayControls: getCarouselAutoplayControls, ensureInitialized: ensureCarouselInitialized, reinitIn: reinitCarouselsIn, destroyAll: destroyCarousels, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initCarousel); } else { initCarousel(); } })(); // ELE INTOVIEW document.addEventListener("DOMContentLoaded", () => { const elements = gsap.utils.toArray("[ele-intoview]"); if (!elements.length) return; elements.forEach((el) => { ScrollTrigger.create({ trigger: el, start: "top 90%", onEnter: () => el.setAttribute("ele-intoview", "true"), }); }); });