// ─── SENES MEDIA · App entry · routing + tweaks ─────────────────── const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#ED3225", "workLayout": "default", "grain": true, "atmosphere": "ember" }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [page, setPage] = React.useState(() => { const h = window.location.hash.replace("#", ""); return ["home", "work", "about", "services", "contact"].includes(h) ? h : "home"; }); const [mobileNavOpen, setMobileNavOpen] = React.useState(false); React.useEffect(() => { window.location.hash = page; window.scrollTo({ top: 0, behavior: "instant" }); }, [page]); React.useEffect(() => { const onHash = () => { const h = window.location.hash.replace("#", ""); if (["home", "work", "about", "services", "contact"].includes(h)) setPage(h); }; window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); // apply accent React.useEffect(() => { document.documentElement.style.setProperty("--red", t.accent); document.documentElement.style.setProperty("--red-bright", lighten(t.accent, 0.1)); document.documentElement.style.setProperty("--red-deep", darken(t.accent, 0.15)); }, [t.accent]); // atmosphere palette → CSS vars used by hero-atmosphere React.useEffect(() => { const palette = ATMOS[t.atmosphere] || ATMOS.ember; document.documentElement.style.setProperty("--atm-a", palette.a); document.documentElement.style.setProperty("--atm-b", palette.b); document.documentElement.style.setProperty("--atm-c", palette.c); }, [t.atmosphere]); // local clock — South Africa (SAST, UTC+2) React.useEffect(() => { const fmt = () => { const d = new Date(); const opt = { timeZone: "Africa/Johannesburg", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }; const txt = new Intl.DateTimeFormat("en-GB", opt).format(d) + " SAST"; const el = document.getElementById("local-clock"); if (el) el.textContent = txt; }; fmt(); const id = setInterval(fmt, 1000); return () => clearInterval(id); }, []); // ─── Auto-reveal: site-wide fade-up that re-triggers on scroll ── React.useEffect(() => { const setup = setTimeout(() => { const selectors = [ "main h1", "main h2", "main h3", "main .display", "main .manifesto-text", "main .philosophy", "main .work-card", "main .review-card", "main .pricing-card", "main .philosophy-card", "main .tools-card", "main .info-cell", "main .timeline-row", "main .process-row", "main .service-row", "main .laptop-wrap", "main .stat", "main .pd-figure", ]; const targets = document.querySelectorAll(selectors.join(",")); const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("ar-in"); } else { // Re-arm only if scrolled clearly out of view const r = e.boundingClientRect; if (r.top > window.innerHeight || r.bottom < 0) { e.target.classList.remove("ar-in"); } } }); }, { threshold: 0.08, rootMargin: "0px 0px -6% 0px" }); let groupIndex = new Map(); targets.forEach((el) => { // Skip elements already wrapped in a Reveal, or inside a hero if (el.closest(".reveal, .page-hero, .home-hero, .pd-hero")) return; // Stagger within the same parent const parent = el.parentElement; const idx = (groupIndex.get(parent) || 0); groupIndex.set(parent, idx + 1); el.style.setProperty("--ar-delay", `${Math.min(idx, 6) * 60}ms`); el.classList.add("auto-reveal"); obs.observe(el); }); window.__senesObs = obs; }, 60); return () => { clearTimeout(setup); if (window.__senesObs) window.__senesObs.disconnect(); }; }, [page]); const [activeProject, setActiveProject] = React.useState(null); const openProject = (project) => setActiveProject(project); const closeProject = (action, nextProject) => { if (action === "open" && nextProject) { setActiveProject(nextProject); return; } setActiveProject(null); }; const renderPage = () => { const props = { navigate: setPage, layout: t.workLayout, openProject }; if (page === "home") return ; if (page === "work") return ; if (page === "about") return ; if (page === "services") return ; if (page === "contact") return ; return ; }; return ( {t.grain &&
}