// === App root + global effects (ink cursor, easter egg) === const InkCursor = () => { React.useEffect(() => { let last = 0; const onMove = (e) => { const t = performance.now(); if (t - last < 36) return; last = t; const d = document.createElement("div"); d.className = "ink-drop"; const sz = 6 + Math.random() * 8; d.style.width = sz + "px"; d.style.height = sz + "px"; d.style.left = e.clientX + "px"; d.style.top = e.clientY + "px"; document.body.appendChild(d); setTimeout(() => d.remove(), 850); }; const onClick = (e) => { const s = document.createElement("div"); s.className = "ink-splatter"; s.style.left = e.clientX + "px"; s.style.top = e.clientY + "px"; s.innerHTML = ``; document.body.appendChild(s); setTimeout(() => s.remove(), 750); }; window.addEventListener("mousemove", onMove); window.addEventListener("click", onClick); return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("click", onClick); }; }, []); return null; }; const EasterEgg = () => { React.useEffect(() => { let buf = ""; let timer = null; const triggers = ["nihao", "你好"]; const fire = () => { const chars = ["印", "好", "赞", "推", "学", "卷", "躺", "搭", "六", "包"]; for (let i = 0; i < 28; i++) { const s = document.createElement("div"); s.className = "falling-seal"; s.textContent = chars[Math.floor(Math.random() * chars.length)]; s.style.left = Math.random() * 100 + "vw"; s.style.animationDelay = (Math.random() * 1.2) + "s"; s.style.animationDuration = (2.8 + Math.random() * 2) + "s"; s.style.transform = `rotate(${(Math.random() * 30 - 15)}deg)`; document.body.appendChild(s); setTimeout(() => s.remove(), 6000); } }; const onKey = (e) => { if (e.key && e.key.length === 1) buf += e.key.toLowerCase(); if (e.key === "你" || e.key === "好") buf += e.key; if (buf.length > 12) buf = buf.slice(-12); if (triggers.some((t) => buf.includes(t))) { fire(); buf = ""; } clearTimeout(timer); timer = setTimeout(() => (buf = ""), 2500); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); return null; }; const ParallaxChar = () => { // Make the hero background character drift on scroll React.useEffect(() => { const el = document.querySelector(".hero-bg-char"); if (!el) return; const onScroll = () => { const y = window.scrollY; el.style.transform = `translateY(${y * -0.25}px) scale(${1 + y * 0.0006})`; }; window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return null; }; // JS fallback for scroll-progress where animation-timeline unsupported const ScrollProgressHook = () => { React.useEffect(() => { if (CSS.supports && CSS.supports("animation-timeline", "scroll()")) return; const bar = document.querySelector(".scroll-progress"); if (!bar) return; const onScroll = () => { const h = document.documentElement; const max = h.scrollHeight - h.clientHeight; const p = max > 0 ? h.scrollTop / max : 0; bar.style.setProperty("--p", p.toFixed(4)); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return null; }; // Magnetic CTA — pulls the button toward the cursor within range. const MagneticHooks = () => { React.useEffect(() => { const targets = Array.from(document.querySelectorAll(".btn-seal")); targets.forEach((el) => el.classList.add("magnetic")); const R = 90; const onMove = (e) => { targets.forEach((el) => { const r = el.getBoundingClientRect(); const cx = r.left + r.width / 2; const cy = r.top + r.height / 2; const dx = e.clientX - cx; const dy = e.clientY - cy; const d = Math.hypot(dx, dy); if (d < R) { el.style.setProperty("--mx", `${dx * 0.28}px`); el.style.setProperty("--my", `${dy * 0.28}px`); el.classList.add("pulled"); } else { el.style.setProperty("--mx", "0px"); el.style.setProperty("--my", "0px"); el.classList.remove("pulled"); } }); }; window.addEventListener("pointermove", onMove); return () => window.removeEventListener("pointermove", onMove); }, []); return null; }; // View-timeline fallback: tag cards as reveal-up; IO toggles .in on non-supporting browsers const RevealUpHooks = () => { React.useEffect(() => { document.querySelectorAll(".entry-card, .cat-tile, .testi-card, .contrib-card, .price-card") .forEach((el) => el.classList.add("reveal-up")); if (CSS.supports && CSS.supports("animation-timeline", "view()")) return; const obs = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add("in"); }); }, { threshold: 0.15 }); document.querySelectorAll(".reveal-up").forEach((el) => obs.observe(el)); return () => obs.disconnect(); }, []); return null; }; const App = () => (