/* App root — composes everything */ const App = () => { const [serviceFromCard, setServiceFromCard] = React.useState(null); const goBook = () => { document.getElementById('book')?.scrollIntoView({ behavior: 'smooth' }); }; const handleServiceSelect = (id) => { setServiceFromCard({ id, ts: Date.now() }); goBook(); }; // Scroll-reveal observer React.useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-visible'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); const tag = () => { document.querySelectorAll('.section-head:not(.section-head-feature)').forEach(el => el.classList.add('reveal')); document.querySelectorAll('.how-flow, .why-grid, .services-grid, .gallery-grid, .about-stats, .hero-trust, .footer-top').forEach(el => el.classList.add('reveal-stagger')); document.querySelectorAll('.about-img-stack, .about-copy, .ba-slider, .final-cta-card, .faq-list, .testimonial-carousel').forEach(el => el.classList.add('reveal')); document.querySelectorAll('.reveal, .reveal-stagger').forEach(el => io.observe(el)); }; const id = requestAnimationFrame(tag); return () => { cancelAnimationFrame(id); io.disconnect(); }; }, []); // Animated number counters in the About section React.useEffect(() => { const parseTarget = (raw) => { const m = raw.match(/([\d.]+)/); if (!m) return null; const num = parseFloat(m[1]); const prefix = raw.slice(0, m.index); const suffix = raw.slice(m.index + m[1].length); return { num, prefix, suffix }; }; const animate = (el) => { const original = el.dataset.original || el.textContent.trim(); el.dataset.original = original; const parsed = parseTarget(original); if (!parsed) return; const { num, prefix, suffix } = parsed; const duration = 1400; const start = performance.now(); const decimals = (original.match(/\.(\d+)/) || [,''])[1].length; const ease = (t) => 1 - Math.pow(1 - t, 3); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const v = num * ease(t); el.textContent = prefix + (decimals ? v.toFixed(decimals) : Math.round(v)) + suffix; if (t < 1) requestAnimationFrame(tick); else el.classList.add('counter-pulse'); }; requestAnimationFrame(tick); }; const counterIO = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.querySelectorAll('.serif').forEach(animate); counterIO.unobserve(e.target); } }); }, { threshold: 0.4 }); const id = requestAnimationFrame(() => { document.querySelectorAll('.about-stats').forEach(el => counterIO.observe(el)); }); return () => { cancelAnimationFrame(id); counterIO.disconnect(); }; }, []); // Mouse-tracking glow on service cards & primary buttons React.useEffect(() => { const onMove = (e) => { const targets = document.querySelectorAll('.service-card, .btn-primary'); targets.forEach(el => { const r = el.getBoundingClientRect(); if (e.clientX < r.left - 40 || e.clientX > r.right + 40 || e.clientY < r.top - 40 || e.clientY > r.bottom + 40) return; const x = ((e.clientX - r.left) / r.width) * 100; const y = ((e.clientY - r.top) / r.height) * 100; el.style.setProperty('--mx', x + '%'); el.style.setProperty('--my', y + '%'); }); }; window.addEventListener('mousemove', onMove); return () => window.removeEventListener('mousemove', onMove); }, []); return ( <>