1/** 2 * Task360 Scroll Animations 3 * - Fade-in/slide-up on viewport entry (IntersectionObserver) 4 * - Stat counter animation (counts from 0 to target) 5 */ 6(function() { 7 'use strict'; 8 9 // ââ Scroll-triggered fade-in ââ 10 var animEls = document.querySelectorAll('.animate-on-scroll'); 11 if (animEls.length && 'IntersectionObserver' in window) { 12 var observer = new IntersectionObserver(function(entries) { 13 entries.forEach(function(entry) { 14 if (entry.isIntersecting) { 15 entry.target.classList.add('is-visible'); 16 observer.unobserve(entry.target); 17 } 18 }); 19 }, { threshold: 0.15 }); 20 21 animEls.forEach(function(el) { observer.observe(el); }); 22 } else { 23 // Fallback: show everything immediately 24 animEls.forEach(function(el) { el.classList.add('is-visible'); }); 25 } 26 27 // ââ Stat counter animation ââ 28 var statEls = document.querySelectorAll('.stat-number[data-target]'); 29 if (statEls.length && 'IntersectionObserver' in window) { 30 var counted = false; 31 var statObserver = new IntersectionObserver(function(entries) { 32 entries.forEach(function(entry) { 33 if (entry.isIntersecting && !counted) { 34 counted = true; 35 animateCounters(); 36 statObserver.disconnect(); 37 } 38 }); 39 }, { threshold: 0.3 }); 40 41 statEls.forEach(function(el) { statObserver.observe(el); }); 42 } 43 44 function animateCounters() { 45 statEls.forEach(function(el) { 46 var target = parseFloat(el.getAttribute('data-target')); 47 var suffix = el.getAttribute('data-suffix') || ''; 48 var duration = 1600; 49 var start = 0; 50 var startTime = null; 51 52 function step(timestamp) { 53 if (!startTime) startTime = timestamp; 54 var progress = Math.min((timestamp - startTime) / duration, 1); 55 // Ease-out cubic 56 var eased = 1 - Math.pow(1 - progress, 3); 57 var current = Math.round(eased * target); 58 el.textContent = current + suffix; 59 if (progress < 1) { 60 requestAnimationFrame(step); 61 } else { 62 el.textContent = target + suffix; 63 } 64 } 65 66 requestAnimationFrame(step); 67 }); 68 } 69 70 // ââ FAQ Accordion ââ 71 document.querySelectorAll('.faq-question').forEach(function(btn) { 72 btn.addEventListener('click', function() { 73 var item = btn.closest('.faq-item'); 74 var isOpen = item.classList.contains('open'); 75 item.classList.toggle('open'); 76 btn.setAttribute('aria-expanded', !isOpen); 77 }); 78 }); 79 80 // ââ Download Gates ââ 81 document.querySelectorAll('.download-gate-form').forEach(function(form) { 82 form.addEventListener('submit', function(e) { 83 e.preventDefault(); 84 var gate = form.closest('.download-gate'); 85 var emailInput = form.querySelector('input[type="email"]'); 86 if (!emailInput.value || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailInput.value)) { 87 emailInput.style.borderColor = '#e53e3e'; 88 return; 89 } 90 var btn = form.querySelector('button[type="submit"]'); 91 btn.disabled = true; 92 btn.textContent = '...'; 93 var data = new FormData(form); 94 fetch(form.action, { method: 'POST', body: data, headers: { 'Accept': 'application/json' } }) 95 .then(function() { 96 gate.querySelector('.download-gate-locked').style.display = 'none'; 97 gate.querySelector('.download-gate-unlocked').style.display = 'block'; 98 }) 99 .catch(function() { 100 gate.querySelector('.download-gate-locked').style.display = 'none'; 101 gate.querySelector('.download-gate-unlocked').style.display = 'block'; 102 }); 103 }); 104 }); 105})();
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.