From 165e20d8a6a0bbef4776ccbd1e0719adfe84b12a Mon Sep 17 00:00:00 2001 From: Andrew Moore Date: Sun, 9 Aug 2026 18:16:59 -0700 Subject: [PATCH] Merge nucleic/upbeat-jade-ibis-kt5e into main Mirrored from corpo b66f72d. --- src/pages/index.astro | 78 +++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index 4f0964f..01e418d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -218,21 +218,28 @@ const canonical = new URL('/', Astro.site); the bar came back — and clean again once the toolbar went away. A boundary at a coordinate that moves is not a fix, it is the same bug relocated. - Nor can that band simply be held flat: those pixels are behind the chrome in one - state and are ordinary page in the other, so a flat patch there is invisible with - the toolbar out and a hard-edged band the moment it retracts. Both failures are - the same failure — an edge existing anywhere the viewport boundary can land. + Nor can that band be held flat, which was the next thing tried and the next thing + to fail. Reaching --paper at 100svh and holding it to 100lvh does put the same + paint at both places the edge can rest — but it also creates ~220px of dead colour + under a lit field, and a hard fling overscrolls far enough to expose the whole of + it at once. It read as a dark bar, correctly: it was one. Dodging a moving + boundary by flattening the region it moves through is still a patch, and a fast + scroll is all it took to find it. - So there is no edge. The field decays to --paper continuously, and is already - exactly --paper by 100svh, above the highest position the visible bottom can take; - everything from there to 100lvh is the same paint. Every boundary the viewport - could land on is gone, in either state and part way through the transition between - them. The decay is spread over ~14rem because the whole excursion is about five - steps of RGB — under a step per 40px, below anything the eye reads as an edge, and - the grain dithers the remainder. Keep that shape if this is ever edited: reach - --paper at or before 100svh, and get there gradually. Top edge likewise, though it - is simpler — the layout viewport's top does not move, so it only needs to arrive. - Static, so the layer still composites without repainting per frame. */ + So neither the value nor its slope may jump anywhere the viewport can reach. The + field decays continuously and never stops decaying: by 100svh it is within a + fraction of one RGB step of --paper, and it spends the rest of the way to 100lvh + closing that fraction. Both candidate edges match the strip beyond them to well + under the grain, there is no flat stretch for an overscroll to reveal, and there + is no point along the run where the rate visibly changes. The whole excursion is + about twelve steps of RGB spread over half the screen, so what this reads as is a + light that falls off toward the bottom — which is the intent, not a correction. + + If this is ever edited, the property to keep is that shape: monotonic, no flat + stretch below 100svh, and near-nothing of the field left by the time the edge can + land on it. Do not reintroduce a stop that arrives at --paper and sits there. Top + edge is simpler — the layout viewport's top does not move, so it only has to + arrive. Static, so the layer still composites without repainting per frame. */ .ambient::after { content: ''; position: absolute; @@ -241,8 +248,11 @@ const canonical = new URL('/', Astro.site); to bottom, var(--paper) 0, rgb(10 16 12 / 0%) 8rem, - rgb(10 16 12 / 0%) calc(100svh - 14rem), - var(--paper) 100svh, + rgb(10 16 12 / 0%) 48%, + rgb(10 16 12 / 35%) 70%, + rgb(10 16 12 / 70%) 80%, + rgb(10 16 12 / 93%) 86%, + rgb(10 16 12 / 98.5%) 92%, var(--paper) 100% ); } @@ -711,7 +721,7 @@ const canonical = new URL('/', Astro.site); come up the screen. Scroll-driven animations are still the newest thing here, so this stays behind @supports: without it the footer keeps the timed fade above, which lands on load and leaves it plainly visible. */ - @supports (animation-timeline: view()) { + @supports (animation-timeline: scroll()) { @media (max-width: 720px) { /* Where the reveal runs, the footer is transparent until it is scrolled to, so opacity is what hides it and the layout no longer has to. That buys back both @@ -725,23 +735,35 @@ const canonical = new URL('/', Astro.site); .footer { opacity: 0; - animation: footer-reveal linear both; - animation-timeline: view(); - /* "entry" opens before the footer's edge reaches the fold, not at it: parked, - it already measures 9-11% entered across phone sizes. So the start has to be - past that or the footer sits permanently part-faded at rest — 20% clears it - on every size tested with room to spare, and still begins the reveal within - the first few pixels of scroll. Ending at 45% means it reads as arrived at - roughly a third of the travel, instead of still fading while it already - looks fully on screen. */ - animation-range: entry 20% entry 45%; + animation: footer-reveal ease-out both; + /* The scroller, not view(). view() measures the footer against the fold, so + the reveal is a window inside the footer's own height — and the footer is + short. Every usable window was a fraction of an already short travel: the + fade got ~22px of the 78px the reader has, which is not a fade, it is a + snap. It also had to start past ~20% because "entry" opens before the + footer's edge reaches the fold, so the footer would otherwise sit part-faded + at rest — the offsets were fighting the geometry. + + Against the scroller the two ends of the reveal are the two ends of the + scroll. Nothing is cropped and nothing needs offsetting: exactly 0 at rest, + exactly 1 at the bottom, and every pixel of thumb travel in between spent + fading. Same distance to scroll as before, three and a half times the + distance to fade over. It holds for any phone, because the range is defined + by the scroll rather than by the footer's height relative to the screen. + + Safe against a zero-length scroll — the drop below always adds some, and + this is inside the phone breakpoint besides. */ + animation-timeline: scroll(root block); } } } + /* The lift is small on purpose. Over a travel this short a large one reads as the + footer being dragged rather than arriving, and it is the movement, not the fade, + that the eye catches as a jolt. */ @keyframes footer-reveal { from { - transform: translate3d(0, 1.15rem, 0); + transform: translate3d(0, 0.55rem, 0); opacity: 0; } to {