/* 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 (
<>
>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();