// Changemaker Lite - Minimal Interactions document.addEventListener('DOMContentLoaded', function() { // Terminal copy functionality const terminals = document.querySelectorAll('.terminal-box'); terminals.forEach(terminal => { terminal.addEventListener('click', function() { const code = this.textContent.trim(); navigator.clipboard.writeText(code).then(() => { // Quick visual feedback this.style.background = '#0a0a0a'; setTimeout(() => { this.style.background = '#000'; }, 200); }); }); }); // Smooth scroll for anchors document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Reduced motion support if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.documentElement.style.scrollBehavior = 'auto'; const style = document.createElement('style'); style.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; }'; document.head.appendChild(style); } }); // Changemaker Lite - Smooth Grid Interactions document.addEventListener('DOMContentLoaded', function() { // Smooth scroll for anchors document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Add stagger animation to grid cards on scroll const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach((entry, index) => { if (entry.isIntersecting) { setTimeout(() => { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; }, index * 50); observer.unobserve(entry.target); } }); }, observerOptions); // Observe all grid cards (exclude site-card and stat-card which have their own observer) document.querySelectorAll('.grid-card:not(.site-card):not(.stat-card)').forEach((card, index) => { card.style.opacity = '0'; card.style.transform = 'translateY(20px)'; card.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; observer.observe(card); }); // Neon hover effect for service cards document.querySelectorAll('.service-card').forEach(card => { card.addEventListener('mouseenter', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const ripple = document.createElement('div'); ripple.style.position = 'absolute'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px'; ripple.style.width = '0'; ripple.style.height = '0'; ripple.style.borderRadius = '50%'; ripple.style.background = 'rgba(91, 206, 250, 0.3)'; ripple.style.transform = 'translate(-50%, -50%)'; ripple.style.pointerEvents = 'none'; ripple.style.transition = 'width 0.6s, height 0.6s, opacity 0.6s'; this.appendChild(ripple); setTimeout(() => { ripple.style.width = '200px'; ripple.style.height = '200px'; ripple.style.opacity = '0'; }, 10); setTimeout(() => { ripple.remove(); }, 600); }); }); // Animated counter for hero stats (the smaller stat grid) const animateValue = (element, start, end, duration) => { const range = end - start; const increment = range / (duration / 16); let current = start; const timer = setInterval(() => { current += increment; if (current >= end) { current = end; clearInterval(timer); } element.textContent = Math.round(current); }, 16); }; // Animate hero stat numbers on scroll (only for .stat-item, not .stat-card) const heroStatObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const statNumber = entry.target.querySelector('.stat-number'); if (statNumber && !statNumber.animated) { statNumber.animated = true; const value = parseInt(statNumber.textContent); if (!isNaN(value)) { statNumber.textContent = '0'; animateValue(statNumber, 0, value, 1000); } } heroStatObserver.unobserve(entry.target); } }); }, observerOptions); document.querySelectorAll('.stat-item').forEach(stat => { heroStatObserver.observe(stat); }); // Animated counter for proof stats - improved version const proofStatObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const statCard = entry.target; const counter = statCard.querySelector('.stat-counter'); const statValue = statCard.getAttribute('data-stat'); if (counter && statValue && !counter.hasAttribute('data-animated')) { counter.setAttribute('data-animated', 'true'); // Add counting class for visual effect counter.classList.add('counting'); // Special handling for different stat types if (statValue === '1') { // AI Ready - just animate the text counter.style.opacity = '0'; counter.style.transform = 'scale(0.5)'; setTimeout(() => { counter.style.transition = 'all 0.8s ease'; counter.style.opacity = '1'; counter.style.transform = 'scale(1)'; }, 200); } else if (statValue === '30' || statValue === '2') { // Time values like "30min", "2hr" const originalText = counter.textContent; counter.textContent = '0'; counter.style.opacity = '0'; setTimeout(() => { counter.style.transition = 'all 0.8s ease'; counter.style.opacity = '1'; counter.textContent = originalText; }, 200); } else { // Numeric values - animate the counting const value = parseInt(statValue); const originalText = counter.textContent; counter.textContent = '0'; // Simple counting animation let current = 0; const increment = value / 30; // 30 steps const timer = setInterval(() => { current += increment; if (current >= value) { current = value; clearInterval(timer); // Restore original formatted text setTimeout(() => { counter.textContent = originalText; }, 200); } else { counter.textContent = Math.floor(current).toLocaleString(); } }, 50); } } proofStatObserver.unobserve(entry.target); } }); }, { threshold: 0.3, rootMargin: '0px 0px -20px 0px' }); // Observe proof stat cards (only those with data-stat attribute) document.querySelectorAll('.stat-card[data-stat]').forEach(stat => { proofStatObserver.observe(stat); }); // Site card hover effects document.querySelectorAll('.site-card').forEach(card => { card.addEventListener('mouseenter', function() { const icon = this.querySelector('.site-icon'); if (icon) { icon.style.animation = 'none'; setTimeout(() => { icon.style.animation = 'site-float 3s ease-in-out infinite'; }, 10); } }); }); // Staggered animation for proof cards - improved const proofCardObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const container = entry.target.closest('.sites-grid, .stats-grid'); if (container) { const allCards = Array.from(container.children); const index = allCards.indexOf(entry.target); setTimeout(() => { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; }, index * 150); // Slightly longer delay for better effect } proofCardObserver.unobserve(entry.target); } }); }, { threshold: 0.2, rootMargin: '0px 0px -30px 0px' }); // Observe site cards and stat cards for stagger animation document.querySelectorAll('.site-card, .stat-card').forEach((card) => { // Set initial state card.style.opacity = '0'; card.style.transform = 'translateY(30px)'; card.style.transition = 'opacity 0.8s ease, transform 0.8s ease'; proofCardObserver.observe(card); }); // Add parallax effect to hero section let ticking = false; function updateParallax() { const scrolled = window.pageYOffset; const hero = document.querySelector('.hero-grid'); if (hero) { hero.style.transform = `translateY(${scrolled * 0.3}px)`; } ticking = false; } function requestTick() { if (!ticking) { window.requestAnimationFrame(updateParallax); ticking = true; } } // Only add parallax on desktop if (window.innerWidth > 768) { window.addEventListener('scroll', requestTick); } // Button ripple effect document.querySelectorAll('.btn').forEach(button => { button.addEventListener('click', function(e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const ripple = document.createElement('span'); ripple.style.position = 'absolute'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px'; ripple.className = 'btn-ripple'; this.appendChild(ripple); setTimeout(() => { ripple.remove(); }, 600); }); }); // Reduced motion support if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.documentElement.style.scrollBehavior = 'auto'; window.removeEventListener('scroll', requestTick); } }); // Add CSS for button ripple const style = document.createElement('style'); style.textContent = ` .btn-ripple { position: absolute; width: 20px; height: 20px; border-radius: 50%; background: rgba(111, 66, 193, 0.5); /* mkdocs purple */ transform: translate(-50%, -50%) scale(0); animation: ripple-animation 0.6s ease-out; pointer-events: none; } @keyframes ripple-animation { to { transform: translate(-50%, -50%) scale(10); opacity: 0; } } `; document.head.appendChild(style);