(() => { const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const MOBILE_RE = /^\d{10,15}$/; function resolveApiBase() { if (window.EVENTBUZZ_API_BASE) { return String(window.EVENTBUZZ_API_BASE).replace(/\/$/, ""); } const host = window.location.hostname; if (host === "localhost" || host === "127.0.0.1") { return "http://localhost:3001/api"; } return "https://api.eventbuzz.live/api"; } const API_BASE = resolveApiBase(); function getHashId(anchor) { const raw = anchor.getAttribute("href") || ""; if (!raw || raw === "#") return ""; try { const url = new URL(anchor.href, window.location.href); if (!url.hash || url.hash === "#") return ""; return decodeURIComponent(url.hash.slice(1)); } catch { return raw.charAt(0) === "#" ? raw.slice(1) : ""; } } let activeNavId = ""; let navSpyLocked = false; let navSpyUnlockTimer = 0; function setActiveNav(id) { activeNavId = id || ""; document .querySelectorAll(".navlinks-desktop a, .nav-drawer-links a") .forEach((a) => { if (a.target === "_blank") return; const linkId = getHashId(a); if (!linkId) return; const on = Boolean(activeNavId) && linkId === activeNavId; a.classList.toggle("is-active", on); if (on) a.setAttribute("aria-current", "location"); else a.removeAttribute("aria-current"); }); } function lockActiveNav(id) { navSpyLocked = true; setActiveNav(id); window.clearTimeout(navSpyUnlockTimer); navSpyUnlockTimer = window.setTimeout(() => { navSpyLocked = false; }, 900); } /* ---------- Mobile nav drawer ---------- */ function initNav() { const nav = document.querySelector("nav"); const menuBtn = document.querySelector(".nav-menu-btn"); if (!nav || !menuBtn) return; const mq = window.matchMedia("(max-width: 1024px)"); let drawerOpen = false; let drawerEls = null; const links = Array.from( document.querySelectorAll(".navlinks-desktop a") ).map((a) => ({ href: a.getAttribute("href") || "#", label: a.textContent.trim(), target: a.getAttribute("target") || null, })); function syncDrawerMode() { nav.classList.toggle("is-drawer-mode", mq.matches); if (!mq.matches) closeDrawer(true); } function ensureDrawer() { if (drawerEls) return drawerEls; const overlay = document.createElement("div"); overlay.className = "nav-drawer-overlay"; overlay.setAttribute("role", "presentation"); const aside = document.createElement("aside"); aside.className = "nav-drawer"; aside.id = "nav-drawer"; aside.setAttribute("role", "dialog"); aside.setAttribute("aria-modal", "true"); aside.setAttribute("aria-label", "Navigation menu"); aside.innerHTML = ` Request demo `; overlay.appendChild(aside); document.body.appendChild(overlay); overlay.addEventListener("click", (e) => { if (e.target === overlay) closeDrawer(); }); aside .querySelector(".nav-drawer-close") ?.addEventListener("click", () => closeDrawer()); aside.querySelectorAll("a").forEach((a) => { a.addEventListener("click", () => closeDrawer()); }); drawerEls = { overlay, aside }; return drawerEls; } function openDrawer() { const { overlay, aside } = ensureDrawer(); setActiveNav(activeNavId); drawerOpen = true; menuBtn.setAttribute("aria-expanded", "true"); menuBtn.setAttribute("aria-label", "Close menu"); requestAnimationFrame(() => { overlay.classList.add("is-open"); aside.classList.add("is-open"); }); document.body.style.overflow = "hidden"; } function closeDrawer(instant) { if (!drawerEls) return; drawerOpen = false; menuBtn.setAttribute("aria-expanded", "false"); menuBtn.setAttribute("aria-label", "Open menu"); drawerEls.overlay.classList.remove("is-open"); drawerEls.aside.classList.remove("is-open"); document.body.style.overflow = ""; if (instant) { drawerEls.overlay.style.transition = "none"; drawerEls.aside.style.transition = "none"; requestAnimationFrame(() => { drawerEls.overlay.style.transition = ""; drawerEls.aside.style.transition = ""; }); } } menuBtn.addEventListener("click", () => { if (drawerOpen) closeDrawer(); else openDrawer(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && drawerOpen) closeDrawer(); }); syncDrawerMode(); mq.addEventListener("change", syncDrawerMode); } /* ---------- In-page section links (no #hash in the URL) ---------- */ function initInPageNav() { const reduceMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; const PENDING_KEY = "eb-scroll-to"; function normalizePath(path) { return path.replace(/\/index\.html$/i, "/") || "/"; } function scrollToId(id) { const el = document.getElementById(id); if (!el) return false; lockActiveNav(id); el.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start", }); return true; } function consumeHash() { if (!window.location.hash || window.location.hash === "#") return ""; const id = decodeURIComponent(window.location.hash.slice(1)); history.replaceState( null, "", window.location.pathname + window.location.search ); return id; } let pending = ""; try { pending = sessionStorage.getItem(PENDING_KEY) || ""; if (pending) sessionStorage.removeItem(PENDING_KEY); } catch { pending = ""; } const fromUrl = consumeHash(); const target = pending || fromUrl; if (target) { const go = () => scrollToId(target); if (document.readyState === "complete") requestAnimationFrame(go); else window.addEventListener("load", () => requestAnimationFrame(go), { once: true }); } document.addEventListener("click", (e) => { if ( e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ) { return; } const a = e.target.closest("a[href]"); if (!a || a.target === "_blank" || a.hasAttribute("download")) return; if (a.hasAttribute("data-demo-open") || a.hasAttribute("data-talk-open")) { return; } const raw = a.getAttribute("href") || ""; if (!raw || raw === "#") return; let url; try { url = new URL(a.href); } catch { return; } if (!url.hash || url.hash === "#") return; const id = decodeURIComponent(url.hash.slice(1)); if (!id) return; if ( url.origin === window.location.origin && normalizePath(url.pathname) === normalizePath(window.location.pathname) ) { if (!document.getElementById(id)) return; e.preventDefault(); scrollToId(id); return; } if (url.origin !== window.location.origin) return; try { sessionStorage.setItem(PENDING_KEY, id); } catch { return; } e.preventDefault(); window.location.assign(url.pathname + url.search); }); } /* ---------- Highlight nav link for the section in view ---------- */ function initNavSpy() { const links = Array.from( document.querySelectorAll(".navlinks-desktop a") ).filter((a) => a.target !== "_blank" && getHashId(a)); const items = links .map((a) => { const id = getHashId(a); const el = document.getElementById(id); return el ? { id, el } : null; }) .filter(Boolean) .sort((a, b) => { const pos = a.el.compareDocumentPosition(b.el); if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); if (!items.length) return; function currentId() { const nav = document.querySelector("nav"); const marker = (nav ? nav.offsetHeight : 58) + 20; let id = ""; items.forEach((item) => { if (item.el.getBoundingClientRect().top <= marker) id = item.id; }); return id; } let ticking = false; function update() { ticking = false; if (navSpyLocked) return; setActiveNav(currentId()); } function onScroll() { if (ticking) return; ticking = true; requestAnimationFrame(update); } window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); update(); } /* ---------- Demo modal ---------- */ const EVENT_TYPES = [ "Webinar", "Conference", "Exhibition", "Internal Sales Meet", "Others", ]; function validateDemo(data) { const errors = {}; const name = (data.name || "").trim(); const mobile = (data.mobile || "").trim(); const email = (data.email || "").trim(); const eventType = (data.eventType || "").trim(); const message = (data.message || "").trim(); const captchaAnswer = (data.captchaAnswer || "").trim(); if (!name) errors.name = "Full name is required."; if (!mobile) errors.mobile = "Mobile is required."; else { const digits = mobile.replace(/[\s\-().]/g, ""); if (!MOBILE_RE.test(digits)) { errors.mobile = "Enter a valid mobile number (10-15 digits)."; } } if (!email) errors.email = "Email is required."; else if (!EMAIL_RE.test(email)) { errors.email = "Enter a valid email address."; } if (!eventType) errors.eventType = "Select an event type."; else if (!EVENT_TYPES.includes(eventType)) { errors.eventType = "Select a valid event type."; } if (!message) errors.message = "Message is required."; if (!captchaAnswer) errors.captcha = "Please solve the captcha."; return { ok: Object.keys(errors).length === 0, errors, data: { name, mobile: mobile.replace(/[\s\-().]/g, ""), email, eventType, message, captchaAnswer, }, }; } function bindDemoForm(root, options = {}) { const idPrefix = options.idPrefix || "demo"; let captchaToken = ""; let captchaLoading = false; let submitting = false; let success = false; async function loadCaptcha() { if (!root) return; const questionEl = root.querySelector("[data-demo-captcha-question]"); const answerEl = root.querySelector('[name="captchaAnswer"]'); const reloadBtn = root.querySelector("[data-demo-captcha-reload]"); if (!questionEl) return; captchaLoading = true; questionEl.textContent = "Loading…"; if (answerEl) answerEl.value = ""; if (reloadBtn) reloadBtn.disabled = true; try { const res = await fetch(`${API_BASE}/public/captcha/challenge`, { cache: "no-store", }); const body = await res.json(); if (!res.ok || !body?.success || !body?.data?.captchaToken) { throw new Error(body?.error?.message || "Captcha unavailable"); } captchaToken = body.data.captchaToken; questionEl.textContent = body.data.question || "Solve the sum"; } catch { captchaToken = ""; questionEl.textContent = "Unavailable — tap reload"; } finally { captchaLoading = false; if (reloadBtn) reloadBtn.disabled = false; } } function setFieldError(form, key, message) { const inputName = key === "captcha" ? "captchaAnswer" : key; const errId = key === "captchaAnswer" || key === "captcha" ? `${idPrefix}-captcha-err` : `${idPrefix}-${key}-err`; const field = form .querySelector(`[name="${inputName}"]`) ?.closest(".demo-field"); const errEl = document.getElementById(errId); const input = form.querySelector(`[name="${inputName}"]`); if (message) { field?.classList.add("has-error"); if (errEl) { errEl.hidden = false; errEl.textContent = message; } input?.setAttribute("aria-invalid", "true"); } else { field?.classList.remove("has-error"); if (errEl) { errEl.hidden = true; errEl.textContent = ""; } input?.removeAttribute("aria-invalid"); } } function showSuccess() { success = true; const formView = root.querySelector(".demo-modal-form-view"); const successView = root.querySelector(".demo-modal-success"); if (formView) formView.hidden = true; if (successView) successView.hidden = false; } function reset() { success = false; captchaToken = ""; const form = root.querySelector(".demo-modal-form"); form?.reset(); root.querySelectorAll(".demo-field-error").forEach((el) => { el.hidden = true; el.textContent = ""; }); root.querySelectorAll(".demo-field").forEach((el) => { el.classList.remove("has-error"); }); const formErr = root.querySelector(".demo-form-error"); if (formErr) { formErr.hidden = true; formErr.textContent = ""; } const formView = root.querySelector(".demo-modal-form-view"); const successView = root.querySelector(".demo-modal-success"); if (formView) formView.hidden = false; if (successView) successView.hidden = true; void loadCaptcha(); } const form = root.querySelector(".demo-modal-form"); if (!form) { return { loadCaptcha, reset, isSuccess: () => success }; } root .querySelector("[data-demo-captcha-reload]") ?.addEventListener("click", () => { void loadCaptcha(); }); root.querySelector("[data-demo-reset]")?.addEventListener("click", reset); form.addEventListener("submit", async (e) => { e.preventDefault(); if (submitting || captchaLoading) return; const fd = new FormData(form); const result = validateDemo({ name: fd.get("name"), mobile: fd.get("mobile"), email: fd.get("email"), eventType: fd.get("eventType"), message: fd.get("message"), captchaAnswer: fd.get("captchaAnswer"), }); ["name", "mobile", "email", "eventType", "message", "captchaAnswer"].forEach( (key) => { const errKey = key === "captchaAnswer" ? "captcha" : key; setFieldError(form, key, result.errors[errKey] || ""); } ); const formErr = root.querySelector(".demo-form-error"); if (formErr) { formErr.hidden = true; formErr.textContent = ""; } if (!result.ok) { const first = ["name", "mobile", "email", "eventType", "message", "captcha"].find( (k) => result.errors[k] ) || "name"; const focusName = first === "captcha" ? "captchaAnswer" : first; form.querySelector(`[name="${focusName}"]`)?.focus(); return; } if (!captchaToken) { setFieldError( form, "captchaAnswer", "Captcha is not ready. Please reload and try again." ); void loadCaptcha(); return; } const submitBtn = root.querySelector(".demo-submit-btn"); submitting = true; if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = "Sending…"; } try { const res = await fetch(`${API_BASE}/public/demo-request`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: result.data.name, mobile: result.data.mobile, email: result.data.email, eventType: result.data.eventType, message: result.data.message, captchaToken, captchaAnswer: result.data.captchaAnswer, }), }); let body = null; try { body = await res.json(); } catch { body = null; } if (!res.ok || !body?.success) { const msg = body?.error?.message || "Could not send your request. Please try again."; if (/captcha/i.test(msg)) { setFieldError(form, "captchaAnswer", msg); void loadCaptcha(); } else if (formErr) { formErr.hidden = false; formErr.textContent = msg; } return; } showSuccess(); } catch { if (formErr) { formErr.hidden = false; formErr.textContent = "Could not reach the server. Please try again in a moment."; } } finally { submitting = false; if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = "Request demo"; } } }); return { loadCaptcha, reset, isSuccess: () => success }; } function initDemoPage() { const root = document.querySelector("[data-demo-page]"); if (!root) return; bindDemoForm(root, { idPrefix: "help" }).reset(); } function initDemoModal() { let overlay = null; let open = false; let formApi = null; function build() { if (overlay) return overlay; overlay = document.createElement("div"); overlay.className = "demo-modal-overlay"; overlay.setAttribute("role", "presentation"); overlay.innerHTML = ` `; document.body.appendChild(overlay); overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); overlay .querySelector(".demo-modal-close") ?.addEventListener("click", close); overlay .querySelector(".demo-success-close") ?.addEventListener("click", close); formApi = bindDemoForm(overlay, { idPrefix: "demo" }); return overlay; } function openModal() { const el = build(); if (!open) formApi.reset(); open = true; document.body.style.overflow = "hidden"; requestAnimationFrame(() => { el.classList.add("is-open"); el.querySelector(".demo-modal")?.classList.add("is-open"); if (!formApi.isSuccess()) { el.querySelector('[name="name"]')?.focus(); } }); } function close() { if (!overlay) return; open = false; overlay.classList.remove("is-open"); overlay.querySelector(".demo-modal")?.classList.remove("is-open"); document.body.style.overflow = ""; } document.addEventListener("click", (e) => { const trigger = e.target.closest("[data-demo-open]"); if (!trigger) return; e.preventDefault(); openModal(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && open) close(); }); } const PLANS = ["Start", "Grow", "Scale", "Enterprise"]; function validateTalk(data) { const errors = {}; const name = (data.name || "").trim(); const mobile = (data.mobile || "").trim(); const email = (data.email || "").trim(); const selectedPlan = (data.selectedPlan || "").trim(); const message = (data.message || "").trim(); const captchaAnswer = (data.captchaAnswer || "").trim(); if (!name) errors.name = "Full name is required."; if (!mobile) errors.mobile = "Mobile is required."; else { const digits = mobile.replace(/[\s\-().]/g, ""); if (!MOBILE_RE.test(digits)) { errors.mobile = "Enter a valid mobile number (10-15 digits)."; } } if (!email) errors.email = "Email is required."; else if (!EMAIL_RE.test(email)) { errors.email = "Enter a valid email address."; } if (!selectedPlan) errors.selectedPlan = "Selected plan is required."; else if (!PLANS.includes(selectedPlan)) { errors.selectedPlan = "Select a valid plan."; } if (!message) errors.message = "Message is required."; if (!captchaAnswer) errors.captcha = "Please solve the captcha."; return { ok: Object.keys(errors).length === 0, errors, data: { name, mobile: mobile.replace(/[\s\-().]/g, ""), email, selectedPlan, message, captchaAnswer, }, }; } function initTalkModal() { let overlay = null; let open = false; let success = false; let captchaToken = ""; let captchaLoading = false; let submitting = false; let selectedPlan = ""; async function loadCaptcha() { if (!overlay) return; const questionEl = overlay.querySelector("[data-talk-captcha-question]"); const answerEl = overlay.querySelector('[name="captchaAnswer"]'); const reloadBtn = overlay.querySelector("[data-talk-captcha-reload]"); if (!questionEl) return; captchaLoading = true; questionEl.textContent = "Loading…"; if (answerEl) answerEl.value = ""; if (reloadBtn) reloadBtn.disabled = true; try { const res = await fetch(`${API_BASE}/public/captcha/challenge`, { cache: "no-store", }); const body = await res.json(); if (!res.ok || !body?.success || !body?.data?.captchaToken) { throw new Error(body?.error?.message || "Captcha unavailable"); } captchaToken = body.data.captchaToken; questionEl.textContent = body.data.question || "Solve the sum"; } catch { captchaToken = ""; questionEl.textContent = "Unavailable — tap reload"; } finally { captchaLoading = false; if (reloadBtn) reloadBtn.disabled = false; } } function setFieldError(form, key, message) { const inputName = key === "captcha" ? "captchaAnswer" : key; const errId = key === "captchaAnswer" || key === "captcha" ? "talk-captcha-err" : `talk-${key}-err`; const field = form.querySelector(`[name="${inputName}"]`)?.closest(".demo-field"); const errEl = document.getElementById(errId); const input = form.querySelector(`[name="${inputName}"]`); if (message) { field?.classList.add("has-error"); if (errEl) { errEl.hidden = false; errEl.textContent = message; } input?.setAttribute("aria-invalid", "true"); } else { field?.classList.remove("has-error"); if (errEl) { errEl.hidden = true; errEl.textContent = ""; } input?.removeAttribute("aria-invalid"); } } function build() { if (overlay) return overlay; overlay = document.createElement("div"); overlay.className = "demo-modal-overlay"; overlay.setAttribute("role", "presentation"); overlay.innerHTML = ` `; document.body.appendChild(overlay); overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); overlay.querySelector(".demo-modal-close")?.addEventListener("click", close); overlay.querySelector(".talk-success-close")?.addEventListener("click", close); overlay.querySelector("[data-talk-captcha-reload]")?.addEventListener("click", () => { void loadCaptcha(); }); const form = overlay.querySelector(".demo-modal-form"); form.addEventListener("submit", async (e) => { e.preventDefault(); if (submitting || captchaLoading) return; const fd = new FormData(form); const result = validateTalk({ name: fd.get("name"), mobile: fd.get("mobile"), email: fd.get("email"), selectedPlan: fd.get("selectedPlan") || selectedPlan, message: fd.get("message"), captchaAnswer: fd.get("captchaAnswer"), }); ["name", "mobile", "email", "selectedPlan", "message", "captchaAnswer"].forEach( (key) => { const errKey = key === "captchaAnswer" ? "captcha" : key; setFieldError(form, key, result.errors[errKey] || ""); } ); const formErr = overlay.querySelector(".demo-form-error"); if (formErr) { formErr.hidden = true; formErr.textContent = ""; } if (!result.ok) { const first = ["name", "mobile", "email", "selectedPlan", "message", "captcha"].find( (k) => result.errors[k] ) || "name"; const focusName = first === "captcha" ? "captchaAnswer" : first; form.querySelector(`[name="${focusName}"]`)?.focus(); return; } if (!captchaToken) { setFieldError( form, "captchaAnswer", "Captcha is not ready. Please reload and try again." ); void loadCaptcha(); return; } const submitBtn = overlay.querySelector(".talk-submit-btn"); submitting = true; if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = "Sending…"; } try { const res = await fetch(`${API_BASE}/public/talk-to-team`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: result.data.name, mobile: result.data.mobile, email: result.data.email, selectedPlan: result.data.selectedPlan, message: result.data.message, captchaToken, captchaAnswer: result.data.captchaAnswer, }), }); let body = null; try { body = await res.json(); } catch { body = null; } if (!res.ok || !body?.success) { const msg = body?.error?.message || "Could not send your request. Please try again."; if (/captcha/i.test(msg)) { setFieldError(form, "captchaAnswer", msg); void loadCaptcha(); } else if (formErr) { formErr.hidden = false; formErr.textContent = msg; } return; } showSuccess(); } catch { if (formErr) { formErr.hidden = false; formErr.textContent = "Could not reach the server. Please try again in a moment."; } } finally { submitting = false; if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = "Talk to our team"; } } }); return overlay; } function showSuccess() { success = true; const formView = overlay.querySelector(".demo-modal-form-view"); const successView = overlay.querySelector(".demo-modal-success"); if (formView) formView.hidden = true; if (successView) successView.hidden = false; } function reset() { success = false; captchaToken = ""; const form = overlay?.querySelector(".demo-modal-form"); form?.reset(); overlay?.querySelectorAll(".demo-field-error").forEach((el) => { el.hidden = true; el.textContent = ""; }); overlay?.querySelectorAll(".demo-field").forEach((el) => { el.classList.remove("has-error"); }); const formErr = overlay?.querySelector(".demo-form-error"); if (formErr) { formErr.hidden = true; formErr.textContent = ""; } const formView = overlay?.querySelector(".demo-modal-form-view"); const successView = overlay?.querySelector(".demo-modal-success"); if (formView) formView.hidden = false; if (successView) successView.hidden = true; const planInput = overlay?.querySelector('[name="selectedPlan"]'); if (planInput) planInput.value = selectedPlan; void loadCaptcha(); } function openModal(plan) { selectedPlan = plan; const el = build(); if (!open) reset(); else { const planInput = el.querySelector('[name="selectedPlan"]'); if (planInput) planInput.value = selectedPlan; if (!captchaToken) void loadCaptcha(); } open = true; document.body.style.overflow = "hidden"; requestAnimationFrame(() => { el.classList.add("is-open"); el.querySelector(".demo-modal")?.classList.add("is-open"); if (!success) { el.querySelector('[name="name"]')?.focus(); } }); } function close() { if (!overlay) return; open = false; overlay.classList.remove("is-open"); overlay.querySelector(".demo-modal")?.classList.remove("is-open"); document.body.style.overflow = ""; } document.addEventListener("click", (e) => { const trigger = e.target.closest("[data-talk-open]"); if (!trigger) return; e.preventDefault(); const plan = (trigger.getAttribute("data-plan") || "").trim() || trigger.closest(".tier")?.querySelector("h3")?.textContent.trim() || ""; openModal(plan); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && open) close(); }); } /* ---------- Scroll reveals (GSAP ScrollTrigger when available) ---------- */ function initReveals() { const nodes = document.querySelectorAll(".reveal"); if (!nodes.length) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { return; } const revealLine = () => window.innerHeight * 0.88; const isPastRevealPoint = (el) => el.getBoundingClientRect().top < revealLine(); if (window.gsap && window.ScrollTrigger) { const gsap = window.gsap; const ScrollTrigger = window.ScrollTrigger; gsap.registerPlugin(ScrollTrigger); nodes.forEach((el) => { const y = Number(el.dataset.revealY ?? 28); const duration = Number(el.dataset.revealDuration ?? 0.7); const delay = Number(el.dataset.revealDelay ?? 0); gsap.fromTo( el, { autoAlpha: 0, y, willChange: "opacity, transform" }, { autoAlpha: 1, y: 0, duration, delay, ease: "power2.out", onComplete: () => gsap.set(el, { clearProps: "willChange" }), scrollTrigger: { trigger: el, start: "top 88%", toggleActions: "play none none none", once: true, }, } ); }); const showRestored = () => { nodes.forEach((el) => { if (isPastRevealPoint(el)) { gsap.set(el, { autoAlpha: 1, y: 0, overwrite: true, clearProps: "willChange" }); } }); ScrollTrigger.refresh(); }; requestAnimationFrame(showRestored); window.addEventListener("load", showRestored, { once: true }); window.addEventListener("pageshow", showRestored); return; } // Fallback without GSAP const show = (el) => { el.style.opacity = "1"; el.style.transform = "none"; }; const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { show(entry.target); io.unobserve(entry.target); } }); }, { threshold: 0.12, rootMargin: "0px 0px -12% 0px" } ); const syncFallback = () => { nodes.forEach((el) => { if (isPastRevealPoint(el)) { show(el); io.unobserve(el); } }); }; nodes.forEach((el) => { if (isPastRevealPoint(el)) { show(el); return; } el.style.opacity = "0"; el.style.transform = "translateY(28px)"; el.style.transition = "opacity .7s ease, transform .7s ease"; io.observe(el); }); window.addEventListener("load", syncFallback, { once: true }); window.addEventListener("pageshow", syncFallback); } /* ---------- Customer stories slider ---------- */ function initStoriesSlider() { const root = document.querySelector(".stories-slider"); if (!root) return; const viewport = root.querySelector(".stories-slider-viewport"); const track = root.querySelector(".stories-slider-track"); const slides = Array.from(root.querySelectorAll(".stories-slide")); const buttons = root.querySelectorAll(".stories-slider-btn"); const prevBtn = buttons[0]; const nextBtn = buttons[1]; if (!viewport || !track || slides.length < 2 || !prevBtn || !nextBtn) return; const GAP = 20; const DEFAULT_AUTOPLAY_MS = 5000; const reduceMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; let index = 0; let dragStartX = 0; let dragging = false; let autoplayTimer = null; function autoplayDelay() { const raw = root.getAttribute("data-autoplay"); if (raw == null || raw === "") return DEFAULT_AUTOPLAY_MS; if (/^(false|off|none)$/i.test(raw.trim())) return 0; const ms = Number(raw); return Number.isFinite(ms) && ms > 0 ? ms : 0; } function perView() { const w = viewport.clientWidth; if (w < 640) return 1; if (w < 900) return 2; return 3; } function maxIndex() { return Math.max(0, slides.length - perView()); } function stepWidth() { const n = perView(); return (viewport.clientWidth - GAP * (n - 1)) / n + GAP; } function goTo(next, animate) { index = Math.max(0, Math.min(next, maxIndex())); const offset = index * stepWidth(); track.style.transition = animate && !reduceMotion ? "transform .4s ease" : "none"; track.style.transform = offset ? `translateX(-${offset}px)` : "none"; prevBtn.disabled = index <= 0; nextBtn.disabled = index >= maxIndex(); } function stopAutoplay() { if (autoplayTimer) { clearInterval(autoplayTimer); autoplayTimer = null; } } function startAutoplay() { stopAutoplay(); const delay = autoplayDelay(); if (!delay || reduceMotion || maxIndex() <= 0) return; autoplayTimer = setInterval(() => { const max = maxIndex(); if (max <= 0) return; goTo(index >= max ? 0 : index + 1, true); }, delay); } function goAndRestart(next) { goTo(next, true); startAutoplay(); } prevBtn.addEventListener("click", () => goAndRestart(index - 1)); nextBtn.addEventListener("click", () => goAndRestart(index + 1)); viewport.addEventListener("pointerdown", (e) => { if (e.pointerType === "mouse" && e.button !== 0) return; dragging = true; dragStartX = e.clientX; stopAutoplay(); }); const endDrag = (e) => { if (!dragging) return; dragging = false; const dx = e.clientX - dragStartX; if (Math.abs(dx) >= 40) goTo(index + (dx < 0 ? 1 : -1), true); startAutoplay(); }; viewport.addEventListener("pointerup", endDrag); viewport.addEventListener("pointercancel", () => { dragging = false; startAutoplay(); }); root.addEventListener("mouseenter", stopAutoplay); root.addEventListener("mouseleave", startAutoplay); root.addEventListener("focusin", stopAutoplay); root.addEventListener("focusout", (e) => { if (!root.contains(e.relatedTarget)) startAutoplay(); }); document.addEventListener("visibilitychange", () => { if (document.hidden) stopAutoplay(); else startAutoplay(); }); const onResize = () => goTo(index, false); window.addEventListener("resize", onResize); if (typeof ResizeObserver !== "undefined") { new ResizeObserver(onResize).observe(viewport); } goTo(0, false); startAutoplay(); } /* ---------- Back to top ---------- */ function initBackToTop() { if (document.querySelector(".back-to-top")) return; const btn = document.createElement("button"); btn.type = "button"; btn.className = "back-to-top"; btn.setAttribute("aria-label", "Back to top"); btn.innerHTML = ''; document.body.appendChild(btn); const SHOW_AFTER = 400; const reduceMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; const sync = () => { btn.classList.toggle("is-visible", window.scrollY > SHOW_AFTER); }; window.addEventListener("scroll", sync, { passive: true }); sync(); btn.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" }); }); } /* ---------- Platform feature showcase ---------- */ function initPlatShowcase() { const root = document.querySelector(".plat-split"); if (!root) return; const cards = Array.from(root.querySelectorAll(".plat-card")); const images = Array.from(root.querySelectorAll(".plat-preview img")); const descEl = root.querySelector("[data-plat-desc]"); if (!cards.length || !images.length) return; const DEFAULT_AUTOPLAY_MS = 5000; const reduceMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; const gsap = window.gsap; const canAnimate = Boolean(gsap) && !reduceMotion; if (canAnimate) root.classList.add("is-gsap-preview"); let index = Math.max( 0, cards.findIndex((card) => card.classList.contains("is-active")) ); let autoplayTimer = null; let inView = typeof IntersectionObserver === "undefined"; function autoplayDelay() { const raw = root.getAttribute("data-autoplay"); if (raw == null || raw === "") return DEFAULT_AUTOPLAY_MS; if (/^(false|off|none)$/i.test(raw.trim())) return 0; const ms = Number(raw); return Number.isFinite(ms) && ms > 0 ? ms : 0; } function imageFor(i) { const id = cards[i]?.dataset.plat; return images.find((img) => img.dataset.plat === id) || null; } function setCards(i) { cards.forEach((card, idx) => { const on = idx === i; card.classList.toggle("is-active", on); card.setAttribute("aria-selected", on ? "true" : "false"); }); if (descEl) { descEl.textContent = cards[i]?.querySelector("p")?.textContent.trim() || ""; } } function direction(from, to) { const n = cards.length; if (from === n - 1 && to === 0) return 1; if (from === 0 && to === n - 1) return -1; return to > from ? 1 : -1; } function showInstant(nextIndex) { index = nextIndex; setCards(index); images.forEach((img) => { const on = img === imageFor(index); img.classList.toggle("is-active", on); if (gsap) { gsap.killTweensOf(img); gsap.set(img, { autoAlpha: on ? 1 : 0, x: 0, zIndex: on ? 1 : 0 }); } }); } function show(nextIndex, animate) { if (nextIndex === index) return; const prevImg = imageFor(index); const nextImg = imageFor(nextIndex); if (!nextImg) return; if (!canAnimate || !animate || !prevImg) { showInstant(nextIndex); return; } const dir = direction(index, nextIndex); index = nextIndex; setCards(index); gsap.killTweensOf(images); images.forEach((img) => { if (img !== prevImg && img !== nextImg) { img.classList.remove("is-active"); gsap.set(img, { autoAlpha: 0, x: 0, zIndex: 0 }); } }); nextImg.classList.add("is-active"); prevImg.classList.remove("is-active"); gsap.set(prevImg, { zIndex: 1, autoAlpha: 1, x: 0 }); gsap.set(nextImg, { zIndex: 2, autoAlpha: 0, x: 40 * dir }); gsap.to(prevImg, { autoAlpha: 0, x: -32 * dir, duration: 0.45, ease: "power2.inOut", onComplete: () => { gsap.set(prevImg, { x: 0, zIndex: 0 }); }, }); gsap.to(nextImg, { autoAlpha: 1, x: 0, duration: 0.5, ease: "power2.out", }); } function stopAutoplay() { if (autoplayTimer) { clearInterval(autoplayTimer); autoplayTimer = null; } } function startAutoplay() { stopAutoplay(); const delay = autoplayDelay(); if (!delay || !inView || document.hidden || cards.length < 2) return; autoplayTimer = setInterval(() => { show((index + 1) % cards.length, true); }, delay); } function goAndRestart(nextIndex) { show(nextIndex, true); startAutoplay(); } cards.forEach((card, i) => { card.addEventListener("click", () => goAndRestart(i)); }); root.addEventListener("mouseenter", stopAutoplay); root.addEventListener("mouseleave", startAutoplay); root.addEventListener("focusin", stopAutoplay); root.addEventListener("focusout", (e) => { if (!root.contains(e.relatedTarget)) startAutoplay(); }); document.addEventListener("visibilitychange", () => { if (document.hidden) stopAutoplay(); else startAutoplay(); }); if (typeof IntersectionObserver !== "undefined") { const io = new IntersectionObserver( (entries) => { inView = entries.some((entry) => entry.isIntersecting); if (inView) startAutoplay(); else stopAutoplay(); }, { threshold: 0.35 } ); io.observe(root); } showInstant(index); startAutoplay(); } document.addEventListener("DOMContentLoaded", () => { initNav(); initInPageNav(); initNavSpy(); initDemoPage(); initDemoModal(); initTalkModal(); initReveals(); initStoriesSlider(); initPlatShowcase(); initBackToTop(); }); })();