1// Lightweight, no-dependency animation layer. 2// Honors `prefers-reduced-motion` â bails out early if reduced motion is preferred. 3 4(() => { 5 const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 6 if (reduced) { 7 document.documentElement.classList.add('motion-reduced'); 8 return; 9 } 10 document.documentElement.classList.add('motion-ok'); 11 12 const onReady = (fn) => { 13 if (document.readyState !== 'loading') fn(); 14 else document.addEventListener('DOMContentLoaded', fn, { once: true }); 15 }; 16 17 // âââ Reveal on scroll ââââââââââââââââââââââââââââââââââââââââââââââââââ 18 const observeReveal = () => { 19 const targets = []; 20 21 document.querySelectorAll('[data-reveal]').forEach((el) => targets.push(el)); 22 23 document.querySelectorAll('[data-reveal-children]').forEach((parent) => { 24 Array.from(parent.children).forEach((child, i) => { 25 child.classList.add('reveal-child'); 26 child.style.setProperty('--stagger', i); 27 targets.push(child); 28 }); 29 }); 30 31 if (!targets.length) return; 32 33 const io = new IntersectionObserver( 34 (entries) => { 35 entries.forEach((e) => { 36 if (!e.isIntersecting) return; 37 e.target.classList.add('is-revealed'); 38 io.unobserve(e.target); 39 }); 40 }, 41 { threshold: 0.12, rootMargin: '0px 0px -8% 0px' } 42 ); 43 44 targets.forEach((el) => io.observe(el)); 45 }; 46 47 // âââ Count-up numbers in stat band ââââââââââââââââââââââââââââââââââââ 48 const countUp = (el) => { 49 const target = parseFloat(el.dataset.countTo); 50 if (Number.isNaN(target)) return; 51 const suffix = el.dataset.suffix || ''; 52 const isFloat = !Number.isInteger(target); 53 const duration = 1300; 54 const start = performance.now(); 55 const tick = (now) => { 56 const p = Math.min((now - start) / duration, 1); 57 const eased = 1 - Math.pow(1 - p, 3); 58 const v = target * eased; 59 el.textContent = (isFloat ? v.toFixed(1) : Math.round(v)) + suffix; 60 if (p < 1) requestAnimationFrame(tick); 61 }; 62 requestAnimationFrame(tick); 63 }; 64 65 const observeCounters = () => { 66 const counters = document.querySelectorAll('[data-count-to]'); 67 if (!counters.length) return; 68 const io = new IntersectionObserver( 69 (entries) => { 70 entries.forEach((e) => { 71 if (!e.isIntersecting) return; 72 countUp(e.target); 73 io.unobserve(e.target); 74 }); 75 }, 76 { threshold: 0.5 } 77 ); 78 counters.forEach((el) => { 79 // Reset to 0 so the animation has somewhere to count from. 80 const initial = el.textContent; 81 el.dataset.initial = initial; 82 el.textContent = (el.dataset.suffix || '').includes('â ') ? '0.0' + el.dataset.suffix : '0' + (el.dataset.suffix || ''); 83 io.observe(el); 84 }); 85 }; 86 87 // âââ Magnetic mouse-position spotlight on cards ââââââââââââââââââââââââ 88 const enableSpotlight = () => { 89 const cards = document.querySelectorAll('.feature, .plan-card, .loc-chip, .dl-card'); 90 cards.forEach((card) => { 91 card.classList.add('has-spotlight'); 92 card.addEventListener('mousemove', (e) => { 93 const r = card.getBoundingClientRect(); 94 const x = ((e.clientX - r.left) / r.width) * 100; 95 const y = ((e.clientY - r.top) / r.height) * 100; 96 card.style.setProperty('--mx', x + '%'); 97 card.style.setProperty('--my', y + '%'); 98 }); 99 }); 100 }; 101 102 // âââ Subtle parallax on hero phone âââââââââââââââââââââââââââââââââââââ 103 const enableHeroParallax = () => { 104 const art = document.querySelector('.hero-art'); 105 if (!art) return; 106 let raf = 0; 107 let tx = 0, ty = 0; 108 document.addEventListener( 109 'mousemove', 110 (e) => { 111 const cx = window.innerWidth / 2; 112 const cy = window.innerHeight / 2; 113 tx = ((e.clientX - cx) / cx) * 6; 114 ty = ((e.clientY - cy) / cy) * 4; 115 if (!raf) { 116 raf = requestAnimationFrame(() => { 117 art.style.setProperty('--px', tx + 'px'); 118 art.style.setProperty('--py', ty + 'px'); 119 raf = 0; 120 }); 121 } 122 }, 123 { passive: true } 124 ); 125 }; 126 127 // âââ Sticky-nav shadow on scroll âââââââââââââââââââââââââââââââââââââââ 128 const enableNavShadow = () => { 129 const nav = document.querySelector('.site-nav'); 130 if (!nav) return; 131 const onScroll = () => { 132 if (window.scrollY > 6) nav.classList.add('is-scrolled'); 133 else nav.classList.remove('is-scrolled'); 134 }; 135 window.addEventListener('scroll', onScroll, { passive: true }); 136 onScroll(); 137 }; 138 139 onReady(() => { 140 observeReveal(); 141 observeCounters(); 142 enableSpotlight(); 143 enableHeroParallax(); 144 enableNavShadow(); 145 }); 146})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.