- End Condition: Your landing page only ============================================================ */ (function () { 'use strict';function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }ready(function () {var nums = document.querySelectorAll('.s3-stat__num[data-target]');if (!nums.length) return; /* exit if section not on page */function animCount(el) { var target = parseInt(el.getAttribute('data-target'), 10); var suffix = el.getAttribute('data-suffix') || ''; var start = null; var dur = 1400;el.textContent = '0' + suffix;function step(ts) { if (!start) start = ts; var prog = Math.min((ts - start) / dur, 1); var ease = 1 - Math.pow(1 - prog, 3); /* ease-out-cubic */ el.textContent = Math.floor(ease * target) + suffix; if (prog < 1) requestAnimationFrame(step); else el.textContent = target + suffix; }requestAnimationFrame(step); }if ('IntersectionObserver' in window) { var obs = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { animCount(entry.target); obs.unobserve(entry.target); } }); }, { threshold: 0.5 });nums.forEach(function (el) { obs.observe(el); });} else { /* Fallback for older browsers — show final number immediately */ nums.forEach(function (el) { el.textContent = el.getAttribute('data-target') + (el.getAttribute('data-suffix') || ''); }); }});})(); - End Condition : Your landing page only ============================================================ */ (function () { 'use strict';function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }ready(function () {var btns = document.querySelectorAll('.s4-scroll-btn');if (!btns.length) return; /* exit if section not on this page */btns.forEach(function (btn) { btn.addEventListener('click', function (e) { var target = document.querySelector( btn.getAttribute('data-target') || btn.getAttribute('href') ); if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); });});})(); - End Condition : Your landing page only Priority : 1KEY CHANGES FROM PREVIOUS VERSION: 1. Hover NO longer pauses the slider — it keeps scrolling always 2. Infinite seamless loop — never jumps back visibly to the start. Works by cloning all cards and appending them to the track. When the slider reaches the cloned section it instantly snaps back to the real cards — the view is identical so no jump is seen. ============================================================ */(function () { 'use strict';function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); }ready(function () {/* ── GET ELEMENTS ──────────────────────────────────────── */ var track = document.getElementById('s9Track'); var slider = document.getElementById('s9Slider'); var prevBtn = document.getElementById('s9Prev'); var nextBtn = document.getElementById('s9Next'); var dotsEl = document.getElementById('s9Dots');if (!track || !slider) return; /* exit if section not on this page *//* ── CLONE CARDS FOR INFINITE LOOP ────────────────────── How the infinite loop works: Original cards: [1][2][3][4][5] After cloning: [1][2][3][4][5][1c][2c][3c][4c][5c]When the slider reaches the cloned section (looks identical to the originals), we instantly snap back to the real cards with no animation. The view is the same so no jump is visible.To add more cards: add more .s9-card elements in your HTML. This script will automatically clone them. */ var origCards = Array.from(track.querySelectorAll('.s9-card')); var origCount = origCards.length;origCards.forEach(function (card) { var clone = card.cloneNode(true); /* deep clone including children */ clone.setAttribute('aria-hidden', 'true'); /* hide clones from screen readers */ track.appendChild(clone); });/* All cards including clones */ var allCards = track.querySelectorAll('.s9-card');/* ── VARIABLES ─────────────────────────────────────────── */ var currentIndex = 0; /* current slide position */ var autoTimer = null; /* stores setInterval ID */ var isDragging = false; var startX = 0; var dragDelta = 0; var isSnapping = false; /* true while invisible snap-back is happening *//* ── HOW MANY CARDS VISIBLE AT ONCE ───────────────────── Desktop ≥ 950px → 3 cards Tablet ≥ 600px → 2 cards Mobile < 600px → 1 card Change these numbers to adjust breakpoints. */ function visibleCount() { var w = slider.offsetWidth; if (w < 600) return 1; if (w < 950) return 2; return 3; }/* Last valid index for REAL cards (ignoring clones) */ function maxRealIndex() { return Math.max(0, origCount - visibleCount()); }/* ── CARD WIDTH ────────────────────────────────────────── (slider width - total gap space) / number of visible cards gap value must match the gap in section-09-testimonials.css */ function cardWidth() { var gap = 20; var vis = visibleCount(); return (slider.offsetWidth - gap * (vis - 1)) / vis; }/* ── APPLY WIDTHS TO ALL CARDS ─────────────────────────── Sets the same calculated width on every card (real + clones). Called on init and on window resize. */ function resizeCards() { var w = cardWidth(); Array.from(allCards).forEach(function (c) { c.style.minWidth = w + 'px'; c.style.width = w + 'px'; }); track.style.gap = '20px'; goTo(Math.min(currentIndex, maxRealIndex()), false); }/* ── GO TO A SLIDE ─────────────────────────────────────── Moves the track to show the card at position idx. animate = true → smooth CSS transition animate = false → instant, no animation (used for snap-back) */ function goTo(idx, animate) { if (typeof animate === 'undefined') animate = true; currentIndex = Math.max(0, idx);var offset = currentIndex * (cardWidth() + 20);track.style.transition = animate ? 'transform .65s cubic-bezier(.4, 0, .2, 1)' /* smooth ease */ : 'none';track.style.transform = 'translateX(-' + offset + 'px)';updateDots(); updateButtons(); }/* ── ADVANCE ONE SLIDE (INFINITE LOOP LOGIC) ───────────── This is the core of the infinite loop.Normal case (not at end): just go to next index.At the last REAL slide: 1. Slide forward into the CLONE section with animation. The first clone looks identical to the first real card, so it appears to slide to a new card seamlessly. 2. After the animation finishes, instantly snap back to real index 0 with NO animation. Because the view is identical (clone = real card), no jump is visible. 3. The loop continues from index 0.Snap-back timing: must be longer than the transition duration (.65s = 650ms). Set to 680ms as safe margin. */ function advance() { if (isSnapping) return; /* don't double-advance during snap */if (currentIndex >= maxRealIndex()) { /* Step 1: slide into clone territory (looks seamless) */ goTo(currentIndex + 1, true);isSnapping = true;/* Step 2: after animation ends, silently snap to index 0 */ setTimeout(function () { goTo(0, false); isSnapping = false; }, 680); /* 680ms > 650ms transition — adjust if you change transition speed */} else { /* Normal advance */ goTo(currentIndex + 1, true); } }/* ── AUTO-PLAY ─────────────────────────────────────────── Advances one slide every 4 seconds automatically. Change 4000 to 3000 (faster) or 6000 (slower).NOTE: mouseenter/mouseleave handlers are intentionally NOT added here. The slider NEVER pauses on hover. */ function startAuto() { stopAuto(); autoTimer = setInterval(advance, 4000); }function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }function resetAuto() { stopAuto(); startAuto(); }/* ── DOTS ──────────────────────────────────────────────── One dot per REAL slide position (not for clones). Active dot reflects current real position. */ function buildDots() { dotsEl.innerHTML = ''; var count = maxRealIndex() + 1; for (var i = 0; i < count; i++) { (function (i) { var dot = document.createElement('button'); dot.className = 's9-dot' + (i === 0 ? ' s9-dot--active' : ''); dot.setAttribute('aria-label', 'Go to slide ' + (i + 1)); dot.addEventListener('click', function () { if (!isSnapping) { goTo(i, true); resetAuto(); } }); dotsEl.appendChild(dot); })(i); } }/* Keep active dot in sync — maps clone positions back to real */ function updateDots() { var realIdx = currentIndex % (maxRealIndex() + 1); var dots = dotsEl.querySelectorAll('.s9-dot'); dots.forEach(function (d, i) { d.classList.toggle('s9-dot--active', i === realIdx); }); }/* Arrows never disabled — infinite loop means no dead ends */ function updateButtons() { if (prevBtn) prevBtn.disabled = false; if (nextBtn) nextBtn.disabled = false; }/* ── ARROW BUTTONS ─────────────────────────────────────── Prev: go back one slide. If at 0, jump to last real slide. Next: advance normally using the infinite loop logic. */ if (prevBtn) { prevBtn.addEventListener('click', function () { if (!isSnapping) { if (currentIndex <= 0) { goTo(maxRealIndex(), true); /* wrap to end */ } else { goTo(currentIndex - 1, true); } resetAuto(); } }); }if (nextBtn) { nextBtn.addEventListener('click', function () { if (!isSnapping) { advance(); resetAuto(); } }); }/* ── KEYBOARD NAVIGATION ───────────────────────────────── ← and → keys work when slider is focused (tabbed to) */ slider.setAttribute('tabindex', '0'); slider.addEventListener('keydown', function (e) { if (isSnapping) return; if (e.key === 'ArrowLeft') { if (currentIndex <= 0) goTo(maxRealIndex(), true); else goTo(currentIndex - 1, true); resetAuto(); } if (e.key === 'ArrowRight') { advance(); resetAuto(); } });/* ── TOUCH + MOUSE DRAG ────────────────────────────────── Swipe on mobile / drag on desktop. Threshold: drag must exceed 25% of card width to change slide. Change .25 to .15 for easier swipe, .35 for harder. */ function onDragStart(x) { if (isSnapping) return; isDragging = true; startX = x; dragDelta = 0; stopAuto(); track.style.transition = 'none'; /* disable animation during drag */ }function onDragMove(x) { if (!isDragging) return; dragDelta = x - startX; var offset = currentIndex * (cardWidth() + 20) - dragDelta; track.style.transform = 'translateX(-' + offset + 'px)'; }function onDragEnd() { if (!isDragging) return; isDragging = false;var threshold = cardWidth() * 0.25;if (dragDelta < -threshold) { advance(); /* swiped left → next */ } else if (dragDelta > threshold) { if (currentIndex <= 0) goTo(maxRealIndex(), true); else goTo(currentIndex - 1, true); /* swiped right → prev */ } else { goTo(currentIndex, true); /* not far enough → snap back */ }startAuto(); }/* Mouse events */ slider.addEventListener('mousedown', function (e) { onDragStart(e.clientX); }); window.addEventListener('mousemove', function (e) { onDragMove(e.clientX); }); window.addEventListener('mouseup', function () { onDragEnd(); });/* Touch events */ slider.addEventListener('touchstart', function (e) { onDragStart(e.touches[0].clientX); }, { passive: true }); slider.addEventListener('touchmove', function (e) { onDragMove(e.touches[0].clientX); }, { passive: true }); slider.addEventListener('touchend', function () { onDragEnd(); });/* ── NO mouseenter/mouseleave ──────────────────────────── Previous version had: slider.addEventListener('mouseenter', stopAuto); slider.addEventListener('mouseleave', startAuto); These lines are intentionally REMOVED so the slider never pauses when the mouse moves over it. *//* ── RESIZE HANDLER ───────────────────────────────────── Recalculates widths and rebuilds dots after resize. Debounced to avoid firing too many times. */ var resizeTimer; window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { buildDots(); resizeCards(); }, 120); });/* ── INIT ──────────────────────────────────────────────── Order matters — resize first, then build dots, then start auto-play. */ resizeCards(); buildDots(); updateButtons(); startAuto();});})();