// Main app — wires nav, sections, footer, tweaks panel, cursor and scroll plumbing. const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "heroVariant": "manifesto", "ofertasLayout": "list", "accent": "oxblood", "bodyFont": "Inter Tight", "showCursor": true, "showSideProgress": true, "paperGrain": true }/*EDITMODE-END*/; function FuseCursor() { React.useEffect(() => { const dot = document.getElementById('fuse-cursor'); if (!dot) return; let x = window.innerWidth / 2, y = window.innerHeight / 2; let tx = x, ty = y; let raf; const tick = () => { // small easing for buttery feel x += (tx - x) * 0.28; y += (ty - y) * 0.28; dot.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); const onMove = (e) => { tx = e.clientX; ty = e.clientY; if (!dot.classList.contains('ready')) dot.classList.add('ready'); }; const onOver = (e) => { const t = e.target; if (!t) return; const isLink = t.closest && (t.closest('a, button, [role="button"], .diag-option, .offer-row, .leak-card, label, input, textarea')); const isText = t.closest && t.closest('input, textarea'); const onDark = t.closest && t.closest('.dark, .surface-dark, footer, #porque, .diag-result-card'); dot.classList.toggle('hover', !!isLink && !isText); dot.classList.toggle('text', !!isText); dot.classList.toggle('on-dark', !!onDark); }; const onLeave = () => dot.classList.remove('ready'); window.addEventListener('mousemove', onMove, { passive: true }); document.addEventListener('mouseover', onOver, true); document.addEventListener('mouseleave', onLeave); return () => { cancelAnimationFrame(raf); window.removeEventListener('mousemove', onMove); document.removeEventListener('mouseover', onOver, true); document.removeEventListener('mouseleave', onLeave); }; }, []); return null; } function ScrollProgress() { React.useEffect(() => { const el = document.getElementById('scroll-progress'); if (!el) return; let raf; const onScroll = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { const h = document.documentElement; const pct = h.scrollTop / Math.max(1, h.scrollHeight - h.clientHeight); el.style.width = `${pct * 100}%`; }); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => { window.removeEventListener('scroll', onScroll); cancelAnimationFrame(raf); }; }, []); return null; } function SideProgress() { const [lang] = useLang(); const [activeIdx, setActiveIdx] = React.useState(0); const [onDark, setOnDark] = React.useState(false); const sections = React.useMemo(() => ([ { id: 'top', es: 'Inicio', en: 'Top' }, { id: 'propuesta', es: 'Propuesta', en: 'Thesis' }, { id: 'paraquien', es: 'Para quién', en: 'Who' }, { id: 'ofertas', es: 'Ofertas', en: 'Offerings' }, { id: 'diagnostico', es: 'Diagnóstico', en: 'Diagnostic' }, { id: 'proceso', es: 'Proceso', en: 'Process' }, { id: 'porque', es: 'Por qué', en: 'Why' }, { id: 'cta', es: 'Hablar', en: 'Talk' }, ]), []); React.useEffect(() => { const update = () => { const cy = window.innerHeight / 2; let best = 0, bestDist = Infinity; sections.forEach((s, i) => { const el = document.getElementById(s.id); if (!el) return; const r = el.getBoundingClientRect(); const mid = r.top + r.height / 2; const d = Math.abs(mid - cy); if (d < bestDist) { bestDist = d; best = i; } }); setActiveIdx(best); // Dark-mode detection: which section is at midline? const probe = document.elementFromPoint(window.innerWidth - 60, window.innerHeight / 2); const darkAncestor = probe && probe.closest && probe.closest('.dark, footer'); setOnDark(!!darkAncestor); }; update(); window.addEventListener('scroll', update, { passive: true }); window.addEventListener('resize', update); return () => { window.removeEventListener('scroll', update); window.removeEventListener('resize', update); }; }, [sections]); return ( ); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // Sync accent palette to html data-attr React.useEffect(() => { document.documentElement.setAttribute('data-accent', t.accent); }, [t.accent]); // Body sans override — applied as a CSS variable so we can audition // different humanist sans against Instrument Serif without touching // the design system file. React.useEffect(() => { document.documentElement.style.setProperty( '--font-sans', `'${t.bodyFont}', 'Helvetica Neue', Helvetica, Arial, sans-serif` ); }, [t.bodyFont]); // Paper grain body class React.useEffect(() => { document.body.classList.toggle('paper-grain', !!t.paperGrain); }, [t.paperGrain]); // Scroll reveal — toggle `.in` on .reveal and .reveal-left as they enter viewport React.useEffect(() => { const io = new IntersectionObserver((entries) => { for (const e of entries) { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } } }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); const observe = () => { document.querySelectorAll('.reveal:not(.in), .reveal-left:not(.in)').forEach(el => io.observe(el)); }; observe(); // Also observe after each render burst (eg after Diagnostico mounts result) const mo = new MutationObserver(observe); mo.observe(document.body, { childList: true, subtree: true }); return () => { io.disconnect(); mo.disconnect(); }; }, []); return ( <>
{t.showCursor &&