// app.jsx — Main app: orchestrates sections + wires up Tweaks const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": ["#fafafa", "#23282a", "#7f8f78"], "fontPair": "instrument-manrope", "heroLayout": "stack", "servicesStyle": "split", "logoTreatment": "grid", "dark": false, "sectionRhythm": "editorial", "grain": false, "gridTexture": true, "motion": true }/*EDITMODE-END*/; const PALETTES = [ ["#f6f1e9", "#211f1d", "#b06a4e"], // Porcelain / Charcoal / Clay (warm A) ["#faf5ec", "#2b2723", "#c9a16b"], // Ivory / Espresso / Soft Gold (luxe warm) ["#fafafa", "#23282a", "#7f8f78"], // White / Slate / Sage (cool, airy — theme B) ["#f4efe9", "#201e1d", "#bd8a82"], // Alabaster / Ink / Dusty Rose (soft) ["#f4ede2", "#33302b", "#8a6a4a"], // Warm Sand (original) ["#fafafa", "#23282a", "#88989b"], // White / Slate / Steel Blue (cool — theme C) ]; const FONT_PAIRS = { 'instrument-manrope': { label: 'Instrument · Manrope', serif: "'Instrument Serif', 'EB Garamond', Georgia, serif", sans: "'Manrope', ui-sans-serif, system-ui, sans-serif", }, 'instrument-geist': { label: 'Instrument · Geist', serif: "'Instrument Serif', 'EB Garamond', Georgia, serif", sans: "'Geist', ui-sans-serif, system-ui, sans-serif", }, 'cormorant-jost': { label: 'Cormorant · Jost', serif: "'Cormorant Garamond', 'EB Garamond', Georgia, serif", sans: "'Jost', ui-sans-serif, system-ui, sans-serif", }, 'bodoni-manrope': { label: 'Bodoni Moda · Manrope', serif: "'Bodoni Moda', 'DM Serif Display', Georgia, serif", sans: "'Manrope', ui-sans-serif, system-ui, sans-serif", }, 'marcellus-space': { label: 'Marcellus · Space Grotesk', serif: "'Marcellus', Georgia, serif", sans: "'Space Grotesk', ui-sans-serif, system-ui, sans-serif", }, 'dm-manrope': { label: 'DM Serif · Manrope', serif: "'DM Serif Display', Georgia, serif", sans: "'Manrope', ui-sans-serif, system-ui, sans-serif", }, }; // ── Scroll-reveal: tag elements ─────────────────────── // Tags content children as `.reveal` so the CSS scroll-driven animation // (animation-timeline: view()) fades + lifts them in as they enter the // viewport. No JS animation logic needed — the browser drives it from // the scroll position. function ScrollReveal() { React.useEffect(() => { const sweep = () => { const containers = document.querySelectorAll( '.sec, .lens, .footer, .brands-marquee, .brands-grid, .marquee, .statement' ); containers.forEach((c) => { const hd = c.querySelector('.sec-hd'); if (hd) { [...hd.children].forEach((el, i) => { if (!el.classList.contains('reveal')) { el.classList.add('reveal'); el.setAttribute('data-delay', String(i + 1)); } }); } const direct = c.querySelectorAll( ':scope > .reel-grid > *, :scope > .svc-list > *, :scope > .svc-grid > *, :scope > .svc-hlist > *, :scope > .about > *, :scope > .lens > *, :scope > .footer-grid > *, :scope > .inquire > *, :scope > .brands-grid > *, :scope > .statement-inner > *, :scope .about-body > p, :scope .about-stats > .stat, :scope .lens-body, :scope .lens-cta-row' ); direct.forEach((el, i) => { if (!el.classList.contains('reveal')) { el.classList.add('reveal'); el.setAttribute('data-delay', String((i % 6) + 1)); } }); }); }; sweep(); }, []); return null; } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // Apply palette + dark mode to :root React.useEffect(() => { const root = document.documentElement; const [bg, ink, accent] = t.dark ? [t.palette[1], t.palette[0], t.palette[2]] : t.palette; root.style.setProperty('--bg', bg); root.style.setProperty('--ink', ink); root.style.setProperty('--accent', accent); // Stable copies used by rhythm dark-section inversion root.style.setProperty('--bg-root', bg); root.style.setProperty('--ink-root', ink); root.setAttribute('data-dark', t.dark ? 'true' : 'false'); }, [t.palette, t.dark]); // Apply font pair React.useEffect(() => { const root = document.documentElement; const pair = FONT_PAIRS[t.fontPair] || FONT_PAIRS['instrument-geist']; root.style.setProperty('--serif', pair.serif); root.style.setProperty('--sans', pair.sans); }, [t.fontPair]); // Apply grain React.useEffect(() => { document.documentElement.style.setProperty('--grain-opacity', t.grain ? '1' : '0'); }, [t.grain]); // Apply faded grid texture React.useEffect(() => { document.documentElement.style.setProperty('--grid-opacity', t.gridTexture ? '0.3' : '0'); }, [t.gridTexture]); // Motion master switch React.useEffect(() => { document.body.setAttribute('data-motion', t.motion ? 'on' : 'off'); }, [t.motion]); // Theme cycle: A (warm porcelain) → B (white/sage) → C (white/steel blue) → A const THEME_A = PALETTES[0]; const THEME_B = PALETTES[2]; const THEME_C = PALETTES[5]; const themeKey = (() => { const accent = t.palette && t.palette[2]; if (accent === THEME_C[2]) return 'c'; if (accent === THEME_B[2]) return 'b'; return 'a'; })(); const toggleTheme = () => { if (themeKey === 'a') setTweak('palette', THEME_B); else if (themeKey === 'b') setTweak('palette', THEME_C); else setTweak('palette', THEME_A); }; const showControls = false; // Cross-page unify: hydrate theme/font/dark from localStorage on mount, // then mirror changes back so the Services page shares the same look. React.useEffect(() => { try { const raw = localStorage.getItem('ll.ui'); if (raw) { const v = JSON.parse(raw); const edits = {}; if (Array.isArray(v.palette)) edits.palette = v.palette; if (typeof v.dark === 'boolean') edits.dark = v.dark; if (v.fontPair) edits.fontPair = v.fontPair; if (Object.keys(edits).length) setTweak(edits); } } catch (e) {} }, []); const firstWrite = React.useRef(true); React.useEffect(() => { if (firstWrite.current) { firstWrite.current = false; return; } try { localStorage.setItem('ll.ui', JSON.stringify({ palette: t.palette, dark: t.dark, fontPair: t.fontPair })); } catch (e) {} }, [t.palette, t.dark, t.fontPair]); // Scroll to hash after React mounts — browser fires this before the DOM exists. React.useEffect(() => { const hash = window.location.hash; if (!hash) return; const el = document.querySelector(hash); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, []); return ( <>