(null);
+ const [hostVisible, setHostVisible] = useState(true);
+ const [documentVisible, setDocumentVisible] = useState(
+ () => typeof document === "undefined" || !document.hidden,
+ );
+ const [reducedMotion, setReducedMotion] = useState(
+ () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches,
+ );
+ const [ready, setReady] = useState(false);
+
+ useEffect(() => {
+ const host = hostRef.current;
+ if (!host || typeof IntersectionObserver === "undefined") return;
+ const observer = new IntersectionObserver(([entry]) => setHostVisible(entry?.isIntersecting ?? true));
+ observer.observe(host);
+ return () => observer.disconnect();
+ }, []);
+
+ useEffect(() => {
+ if (typeof document === "undefined") return;
+ const update = () => setDocumentVisible(!document.hidden);
+ document.addEventListener("visibilitychange", update);
+ return () => document.removeEventListener("visibilitychange", update);
+ }, []);
+
+ useEffect(() => {
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)");
+ const update = () => setReducedMotion(media.matches);
+ media.addEventListener("change", update);
+ return () => media.removeEventListener("change", update);
+ }, []);
+
+ const source = useMemo(
+ () => buildSceneDocument(reducedMotion, assetBaseUrl),
+ [assetBaseUrl, reducedMotion],
+ );
+ const mounted = hostVisible && documentVisible;
+
+ useEffect(() => setReady(false), [mounted, reducedMotion, assetBaseUrl]);
+
+ return (
+
+ {mounted ? (
+
+ );
+}
diff --git a/src/shaders/sylva-living-world/sources/inner-green-assets/lexend-latin.woff2 b/src/shaders/sylva-living-world/sources/inner-green-assets/lexend-latin.woff2
new file mode 100644
index 0000000..0968c15
Binary files /dev/null and b/src/shaders/sylva-living-world/sources/inner-green-assets/lexend-latin.woff2 differ