+
+
dvorakmortonxd.dev
@@ -446,7 +459,8 @@ export default function EditorialSite() {
v3.0.0 · © 2025–2026 Martin Dvořák
Follow the GPL-3.0 license (or don’t, I don’t mind).
-
+
+
>
);
diff --git a/src/app/components/editorial-ui.tsx b/src/app/components/editorial-ui.tsx
index 364879a..0f4d6fc 100644
--- a/src/app/components/editorial-ui.tsx
+++ b/src/app/components/editorial-ui.tsx
@@ -18,19 +18,10 @@ export function Reveal({
useEffect(() => {
const el = ref.current;
if (!el) return;
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
-
- // Only animate what starts below the fold. Anything already on screen stays
- // visible — otherwise the hero fades in after the section beneath it.
- const rect = el.getBoundingClientRect();
- const onScreen = rect.top < window.innerHeight && rect.bottom > 0;
- if (onScreen) return;
-
- // Hide it without a transition, then reveal when it scrolls into view.
- el.style.transition = "none";
- el.classList.add("reveal-armed");
- void el.offsetHeight;
- el.style.transition = "";
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+ el.classList.add("is-in");
+ return;
+ }
const io = new IntersectionObserver(
(entries) => {
@@ -41,11 +32,22 @@ export function Reveal({
}
}
},
- { rootMargin: "0px 0px -10% 0px", threshold: 0.05 }
+ { rootMargin: "0px 0px -12% 0px", threshold: 0.08 }
);
- io.observe(el);
+
+ const rect = el.getBoundingClientRect();
+ const onScreen = rect.top < window.innerHeight && rect.bottom > 0;
+ if (onScreen) {
+ // First screenful: stagger in document order so it fills top to bottom.
+ const all = Array.from(document.querySelectorAll
(".reveal"));
+ const order = Math.min(Math.max(0, all.indexOf(el)), 8);
+ el.style.setProperty("--delay", `${delay + order * 90}ms`);
+ requestAnimationFrame(() => el.classList.add("is-in"));
+ } else {
+ io.observe(el);
+ }
return () => io.disconnect();
- }, []);
+ }, [delay]);
return (
diff --git a/src/app/globals.css b/src/app/globals.css
index c941271..f36d4a9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -33,6 +33,8 @@
--fill: rgba(235, 219, 178, 0.035);
--fill-strong: rgba(235, 219, 178, 0.07);
+
+ --ease-cinematic: cubic-bezier(0.22, 1, 0.36, 1);
}
/* Beats page.css's :root when the editorial theme is active. */
@@ -230,19 +232,19 @@ html[data-device="phone"] .desktop-toggle {
Motion — scroll reveals only
------------------------------------------------------------------ */
-/* Visible by default: Reveal only hides elements that start below the fold, so
- above-the-fold content (the hero) is never hidden while waiting for JS. */
+/* Scroll reveal. Hidden until `.is-in` lands, and every block on the editorial
+ page is wrapped, so the page fills in from top to bottom: the first screenful
+ cascades in document order on load, everything else reveals as it scrolls in.
+ A failed JS bundle leaves the page visible via the