document.addEventListener('DOMContentLoaded', () => { const body = document.body; // loading const loader = document.querySelector('.page-loader'); body.classList.add('is-loading'); // menu const menuBtn = document.querySelector('.menu-btn'); const mobileMenu = document.querySelector('.mobile-menu'); const tabletMenu = document.querySelector('.tablet-menu'); menuBtn?.addEventListener('click', () => { body.classList.toggle('menu-open'); const isOpen = body.classList.contains('menu-open'); mobileMenu?.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); tabletMenu?.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); }); document.querySelectorAll('.mobile-menu a').forEach((link) => { link.addEventListener('click', () => { body.classList.remove('menu-open'); mobileMenu?.setAttribute('aria-hidden', 'true'); }); }); document.querySelectorAll('.tablet-menu a').forEach((link) => { link.addEventListener('click', () => { body.classList.remove('menu-open'); tabletMenu?.setAttribute('aria-hidden', 'true'); }); }); const productDropdown = document.querySelector('.product-dropdown'); productDropdown?.addEventListener('mouseenter', () => { body.classList.remove('menu-open'); mobileMenu?.setAttribute('aria-hidden', 'true'); }); // hero/header/product tabs const header = document.querySelector('.site-header'); const productTabs = document.querySelector('.product-tabs'); const hero = document.querySelector('.hero'); const channelSection = document.querySelector('#channel'); let lastScrollY = window.scrollY; function updateHeaderState() { if (!hero) return; const currentScrollY = window.scrollY; const isScrollingUp = currentScrollY < lastScrollY; const passedHero = currentScrollY >= hero.offsetTop; header?.classList.toggle('is-scrolled', passedHero); productTabs?.classList.toggle('is-compact', passedHero); if (channelSection && productTabs) { const rect = channelSection.getBoundingClientRect(); const passedChannelHalf = rect.top + rect.height / 2 <= window.innerHeight / 2; productTabs.classList.toggle( 'is-hidden', passedChannelHalf && !isScrollingUp ); } lastScrollY = currentScrollY; } window.addEventListener('scroll', updateHeaderState); window.addEventListener('resize', updateHeaderState); updateHeaderState(); // accordion document.querySelectorAll('.accordion').forEach((item) => { const btn = item.querySelector('button'); const icon = item.querySelector('.accordion-icon'); btn?.addEventListener('click', () => { const isOpen = item.classList.toggle('is-open'); if (!icon) return; icon.src = isOpen ? icon.dataset.open : icon.dataset.close; }); }); // modal const openModal = (id) => { const modal = document.getElementById(id); if (!modal) return; modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); body.style.overflow = 'hidden'; }; const closeModal = (modal) => { if (!modal) return; modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); body.style.overflow = ''; }; document.querySelectorAll('[data-modal-open]').forEach((btn) => { btn.addEventListener('click', () => { openModal(btn.dataset.modalOpen); }); }); document.querySelectorAll('[data-modal-close]').forEach((btn) => { btn.addEventListener('click', () => { closeModal(btn.closest('.modal')); }); }); document.addEventListener('keydown', (e) => { if (e.key !== 'Escape') return; document.querySelectorAll('.modal.is-open').forEach(closeModal); }); // review slider document.querySelectorAll('[data-review-slider]').forEach((slider) => { const track = slider.querySelector('.review-track'); const viewport = slider.querySelector('.review-viewport'); const cards = [...slider.querySelectorAll('.review-card')]; const prev = slider.querySelector('.prev'); const next = slider.querySelector('.next'); const controls = slider.querySelector('.review-controls'); const currentText = slider.querySelector('.review-count__current'); const totalText = slider.querySelector('.review-count__total'); if (!track || !viewport || !cards.length || !controls) return; let currentIndex = 0; let resizeTimer; const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0; const getVisibleCount = () => window.innerWidth <= 600 ? 1 : 2; const getMaxIndex = () => Math.max(0, cards.length - 1); const getMoveX = () => { const gap = parseFloat(getComputedStyle(track).gap) || 0; return cards[0].offsetWidth + gap; }; const syncCardHeight = () => { cards.forEach((card) => { card.style.height = 'auto'; }); let maxHeight = 0; cards.forEach((card) => { maxHeight = Math.max(maxHeight, card.offsetHeight); }); cards.forEach((card) => { card.style.height = `${maxHeight}px`; }); }; const updateControls = () => { controls.style.display = cards.length <= getVisibleCount() ? 'none' : ''; if (totalText) { totalText.textContent = cards.length; } }; const update = (nextIndex) => { const maxIndex = getMaxIndex(); currentIndex = Math.max(0, Math.min(nextIndex, maxIndex)); cards.forEach((card) => { card.hidden = false; }); track.style.transition = 'transform .45s ease'; track.style.transform = `translateX(${-currentIndex * getMoveX()}px)`; if (currentText) { currentText.textContent = currentIndex + 1; } if (prev) prev.disabled = currentIndex === 0; if (next) next.disabled = currentIndex === maxIndex; }; prev?.addEventListener('click', () => update(currentIndex - 1)); next?.addEventListener('click', () => update(currentIndex + 1)); // swipe let startX = 0; let currentX = 0; let isDragging = false; viewport.addEventListener('touchstart', (e) => { if (!isTouchDevice()) return; startX = e.touches[0].clientX; currentX = startX; isDragging = true; track.style.transition = 'none'; }); viewport.addEventListener('touchmove', (e) => { if (!isDragging || !isTouchDevice()) return; currentX = e.touches[0].clientX; const diffX = currentX - startX; const baseX = -currentIndex * getMoveX(); track.style.transform = `translateX(${baseX + diffX}px)`; }); viewport.addEventListener('touchend', () => { if (!isDragging || !isTouchDevice()) return; const diffX = currentX - startX; const threshold = 50; isDragging = false; if (diffX < -threshold) { update(currentIndex + 1); } else if (diffX > threshold) { update(currentIndex - 1); } else { update(currentIndex); } }); window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { currentIndex = Math.min(currentIndex, getMaxIndex()); updateControls(); syncCardHeight(); update(currentIndex); }, 150); }); updateControls(); syncCardHeight(); update(currentIndex); }); // text split document.querySelectorAll('.js-text-reveal').forEach((el) => { const lines = el.querySelectorAll('.reveal-line'); let index = 0; if (lines.length) { lines.forEach((line, lineIndex) => { const text = line.textContent.trim(); line.innerHTML = [...text].map((char) => { return `${char}`; }).join(''); }); } else { const inner = el.querySelector('.text-reveal__inner') || el; const text = inner.textContent.trim(); inner.innerHTML = [...text].map((char) => { return `${char}`; }).join(''); } el.classList.add('is-ready'); }); // observers const revealObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add('is-active'); revealObserver.unobserve(entry.target); }); }, { threshold: 0.4 }); const upgradeDecoObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add('is-active'); upgradeDecoObserver.unobserve(entry.target); }); }, { threshold: 0.35 }); const reviewSection = document.querySelector('.reviews'); const reviewHex = document.querySelector('.js-review-hex'); const reviewHexObserver = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) return; reviewHex?.classList.add('is-active'); reviewHexObserver.unobserve(entry.target); }, { threshold: 0.35 }); const footer = document.querySelector('.site-footer'); const footerObserver = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) return; footer?.classList.add('is-active'); footerObserver.unobserve(entry.target); }, { threshold: 0.2 }); const sectionBg = document.querySelector('.section-bg'); const sectionBgObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; sectionBg?.classList.add('is-active'); sectionBgObserver.unobserve(entry.target); }); }, { threshold: 0.2 }); function initRevealAnimations() { document.querySelectorAll('.js-text-reveal').forEach((el) => { revealObserver.observe(el); }); document.querySelectorAll('.js-underline-reveal').forEach((el) => { revealObserver.observe(el); }); document.querySelectorAll('.js-upgrade-deco').forEach((block) => { upgradeDecoObserver.observe(block); }); if (reviewSection && reviewHex) { reviewHexObserver.observe(reviewSection); } if (footer) { footerObserver.observe(footer); } if (sectionBg) { sectionBgObserver.observe(sectionBg); } } // start animations after loading window.addEventListener('load', () => { setTimeout(() => { loader?.classList.add('is-hide'); body.classList.remove('is-loading'); initRevealAnimations(); }, 600); }); }); // menu active const navLinks = document.querySelectorAll( '.desktop-nav a, .tablet-menu__nav a, .mobile-menu__nav a' ); const sections = [ '#feature', '#timing', '#quality', '#review', '#faq', '#channel' ] .map((selector) => document.querySelector(selector)) .filter(Boolean); function updateActiveNav() { const offset = window.innerHeight * 0.35; let currentId = sections[0]?.id; sections.forEach((section) => { const rect = section.getBoundingClientRect(); if (rect.top <= offset) { currentId = section.id; } }); navLinks.forEach((link) => { link.classList.toggle( 'active', link.getAttribute('href') === `#${currentId}` ); }); } window.addEventListener('scroll', updateActiveNav); window.addEventListener('resize', updateActiveNav); window.addEventListener('load', updateActiveNav); document.querySelectorAll( '.desktop-nav a, .tablet-menu__nav a, .mobile-menu__nav a' ).forEach((link) => { link.addEventListener('click', () => { const id = link.getAttribute('href')?.replace('#', ''); navLinks.forEach((item) => { item.classList.toggle( 'active', item.getAttribute('href') === `#${id}` ); }); setTimeout(updateActiveNav, 500); }); }); updateActiveNav();