/** * Home hero carousel — autoplay + dot navigation + mobile swipe + layout sync * * Layout contract (extensible per banner): * data-hero-layout-reference — slide that defines carousel height * data-hero-layout-anchor — element whose bottom anchors dot position * --hero-coach-dots-gap — gap between anchor bottom and dots top (default 35px) * Mobile: .is-banner-active fixes dots to viewport bottom while banner is visible */ (function () { var AUTOPLAY_MS = 5000; var SWIPE_THRESHOLD = 48; var MOBILE_MQ = '(max-width: 991.98px)'; var DESKTOP_MQ = '(min-width: 992px)'; function readPx(styles, name, fallback) { var value = parseFloat(styles.getPropertyValue(name)); return Number.isFinite(value) ? value : fallback; } function getLayoutScope(root) { return root.closest('#homepage') || root; } function clearMeasuredLayout(scope, root) { [ '--hero-reference-content-bottom', '--hero-reference-viewport-height', '--hero-reference-dots-top', '--hero-mobile-dots-reserve', '--hero-dots-y', ].forEach(function (name) { scope.style.removeProperty(name); }); if (root) { var dots = root.querySelector('.home-hero-carousel__dots'); if (dots) { dots.removeAttribute('data-hero-dots-positioned'); } } } function syncHeroLayout(root) { var inner = root.querySelector('.home-hero-carousel__inner'); var viewport = root.querySelector('.home-hero-carousel__viewport'); if (!inner || !viewport) return; var referenceSlide = root.querySelector('[data-hero-layout-reference]') || root.querySelector('[data-home-hero-slide]'); if (!referenceSlide) return; var anchor = referenceSlide.querySelector('[data-hero-layout-anchor]') || referenceSlide.querySelector('.home-hero-carousel__ai-coach') || referenceSlide.querySelector('.home-hero-carousel__actions :last-child'); if (!anchor) return; var scope = getLayoutScope(root); var styles = getComputedStyle(scope); var coachDotsGap = readPx(styles, '--hero-coach-dots-gap', 35); var dotsHeight = readPx(styles, '--hero-dots-height', 10); var innerPaddingBottom = readPx(styles, '--hero-inner-padding-bottom', 40); var isDesktop = window.matchMedia(DESKTOP_MQ).matches; if (isDesktop) { var innerRect = inner.getBoundingClientRect(); var viewportRect = viewport.getBoundingClientRect(); var anchorRect = anchor.getBoundingClientRect(); var contentBottom = Math.ceil(anchorRect.bottom - viewportRect.top); var viewportHeight = contentBottom + coachDotsGap; var dotsTop = Math.ceil(anchorRect.bottom - innerRect.top + coachDotsGap); var dotsBottomY = dotsTop + dotsHeight + innerPaddingBottom; scope.style.setProperty('--hero-reference-content-bottom', contentBottom + 'px'); scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px'); scope.style.setProperty('--hero-reference-dots-top', dotsTop + 'px'); scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px'); root.setAttribute('data-hero-layout-synced', 'desktop'); var dots = root.querySelector('.home-hero-carousel__dots'); if (dots) { dots.setAttribute('data-hero-dots-positioned', 'true'); } root.classList.remove('is-banner-active'); return; } clearMeasuredLayout(scope, root); var viewportRect = viewport.getBoundingClientRect(); var slideGrid = referenceSlide.querySelector('.home-hero-carousel__slide-grid'); var visual = referenceSlide.querySelector('.home-hero-carousel__visual'); var mobileFixedDotsBottom = readPx(styles, '--hero-mobile-fixed-dots-bottom', 16); var imageScale = readPx(styles, '--hero-visual-image-scale', 1.06); if (!slideGrid) return; var visualBottom = visual ? visual.getBoundingClientRect().bottom - viewportRect.top : slideGrid.getBoundingClientRect().bottom - viewportRect.top; if (visual) { var img = visual.querySelector('img'); if (img && imageScale > 1) { var imgHeight = img.getBoundingClientRect().height; visualBottom += Math.ceil((imgHeight * imageScale - imgHeight) / 2); } } var viewportHeight = Math.ceil(visualBottom); var dotsReserve = dotsHeight + mobileFixedDotsBottom; var homepageRect = scope.getBoundingClientRect(); var dotsBottomY = Math.ceil( viewportRect.top - homepageRect.top + viewportHeight + dotsReserve + innerPaddingBottom ); scope.style.setProperty('--hero-reference-viewport-height', viewportHeight + 'px'); scope.style.setProperty('--hero-mobile-dots-reserve', dotsReserve + 'px'); scope.style.setProperty('--hero-dots-y', dotsBottomY + 'px'); root.setAttribute('data-hero-layout-synced', 'mobile'); } function bindMobileFixedDots(root) { if (typeof IntersectionObserver === 'undefined') return; var mobileMq = window.matchMedia(MOBILE_MQ); var observer = new IntersectionObserver( function (entries) { if (!mobileMq.matches) { root.classList.remove('is-banner-active'); return; } root.classList.toggle('is-banner-active', entries[0].isIntersecting); }, { threshold: 0, rootMargin: '0px 0px -8% 0px', } ); observer.observe(root); mobileMq.addEventListener('change', function () { if (!mobileMq.matches) { root.classList.remove('is-banner-active'); } }); } function bindHeroLayoutSync(root) { var sync = function () { syncHeroLayout(root); }; sync(); if (typeof ResizeObserver !== 'undefined') { var observer = new ResizeObserver(sync); observer.observe(root); var inner = root.querySelector('.home-hero-carousel__inner'); var referenceSlide = root.querySelector('[data-hero-layout-reference]') || root.querySelector('[data-home-hero-slide]'); if (inner) observer.observe(inner); if (referenceSlide) observer.observe(referenceSlide); var visual = referenceSlide.querySelector('.home-hero-carousel__visual'); if (visual) observer.observe(visual); } var resizeTimer; window.addEventListener('resize', function () { window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(sync, 100); }); if (document.fonts && document.fonts.ready) { document.fonts.ready.then(sync); } } function initHomeHeroCarousel(root) { if (root.getAttribute('data-carousel-ready') === 'true') return; root.setAttribute('data-carousel-ready', 'true'); var slides = root.querySelectorAll('[data-home-hero-slide]'); var dots = root.querySelectorAll('[data-home-hero-dot]'); var viewport = root.querySelector('.home-hero-carousel__viewport'); if (!slides.length) return; var index = 0; var timer = null; var touchStartX = 0; var touchStartY = 0; var touchTracking = false; function setSlide(nextIndex) { index = (nextIndex + slides.length) % slides.length; slides.forEach(function (slide, i) { var active = i === index; slide.classList.toggle('is-active', active); slide.setAttribute('aria-hidden', active ? 'false' : 'true'); }); dots.forEach(function (dot, i) { var active = i === index; dot.classList.toggle('is-active', active); dot.setAttribute('aria-selected', active ? 'true' : 'false'); }); } function startAutoplay() { stopAutoplay(); timer = window.setInterval(function () { setSlide(index + 1); }, AUTOPLAY_MS); } function stopAutoplay() { if (timer) { window.clearInterval(timer); timer = null; } } function isMobileSwipeEnabled() { return window.matchMedia(MOBILE_MQ).matches; } function bindTouchSwipe() { if (!viewport || viewport.getAttribute('data-swipe-ready') === 'true') return; viewport.setAttribute('data-swipe-ready', 'true'); viewport.addEventListener( 'touchstart', function (e) { if (!isMobileSwipeEnabled() || !e.touches.length) return; touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; touchTracking = true; stopAutoplay(); }, { passive: true } ); viewport.addEventListener( 'touchend', function (e) { if (!touchTracking) return; touchTracking = false; if (!isMobileSwipeEnabled()) { startAutoplay(); return; } var touch = e.changedTouches[0]; if (!touch) { startAutoplay(); return; } var dx = touch.clientX - touchStartX; var dy = touch.clientY - touchStartY; if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dx) <= Math.abs(dy)) { startAutoplay(); return; } if (dx < 0) { setSlide(index + 1); } else { setSlide(index - 1); } startAutoplay(); }, { passive: true } ); viewport.addEventListener( 'touchcancel', function () { touchTracking = false; startAutoplay(); }, { passive: true } ); } dots.forEach(function (dot) { dot.addEventListener('click', function () { var target = Number(dot.getAttribute('data-home-hero-dot')); if (Number.isNaN(target)) return; setSlide(target); startAutoplay(); }); }); root.addEventListener('mouseenter', stopAutoplay); root.addEventListener('mouseleave', startAutoplay); root.addEventListener('focusin', stopAutoplay); root.addEventListener('focusout', function (e) { if (!root.contains(e.relatedTarget)) startAutoplay(); }); bindTouchSwipe(); bindHeroLayoutSync(root); bindMobileFixedDots(root); setSlide(0); startAutoplay(); } function boot() { document.querySelectorAll('[data-home-hero-carousel]').forEach(initHomeHeroCarousel); } window.AifoHomeHeroCarouselInit = initHomeHeroCarousel; window.AifoHomeHeroCarouselSyncLayout = function (root) { if (root) { syncHeroLayout(root); return; } document.querySelectorAll('[data-home-hero-carousel]').forEach(syncHeroLayout); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } document.addEventListener('aifo:home-hero-carousel-reload', boot); })();