// 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"),
});
});
});