Merge nucleic/upbeat-jade-ibis-kt5e into main

Mirrored from corpo b66f72d.
This commit is contained in:
2026-08-09 18:16:59 -07:00
parent 369dbd077b
commit 165e20d8a6
+50 -28
View File
@@ -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 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. 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 Nor can that band be held flat, which was the next thing tried and the next thing
state and are ordinary page in the other, so a flat patch there is invisible with to fail. Reaching --paper at 100svh and holding it to 100lvh does put the same
the toolbar out and a hard-edged band the moment it retracts. Both failures are paint at both places the edge can rest — but it also creates ~220px of dead colour
the same failure — an edge existing anywhere the viewport boundary can land. 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 So neither the value nor its slope may jump anywhere the viewport can reach. The
exactly --paper by 100svh, above the highest position the visible bottom can take; field decays continuously and never stops decaying: by 100svh it is within a
everything from there to 100lvh is the same paint. Every boundary the viewport fraction of one RGB step of --paper, and it spends the rest of the way to 100lvh
could land on is gone, in either state and part way through the transition between closing that fraction. Both candidate edges match the strip beyond them to well
them. The decay is spread over ~14rem because the whole excursion is about five under the grain, there is no flat stretch for an overscroll to reveal, and there
steps of RGB — under a step per 40px, below anything the eye reads as an edge, and is no point along the run where the rate visibly changes. The whole excursion is
the grain dithers the remainder. Keep that shape if this is ever edited: reach about twelve steps of RGB spread over half the screen, so what this reads as is a
--paper at or before 100svh, and get there gradually. Top edge likewise, though it light that falls off toward the bottom — which is the intent, not a correction.
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. */ 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 { .ambient::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -241,8 +248,11 @@ const canonical = new URL('/', Astro.site);
to bottom, to bottom,
var(--paper) 0, var(--paper) 0,
rgb(10 16 12 / 0%) 8rem, rgb(10 16 12 / 0%) 8rem,
rgb(10 16 12 / 0%) calc(100svh - 14rem), rgb(10 16 12 / 0%) 48%,
var(--paper) 100svh, 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% 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 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, this stays behind @supports: without it the footer keeps the timed fade above,
which lands on load and leaves it plainly visible. */ which lands on load and leaves it plainly visible. */
@supports (animation-timeline: view()) { @supports (animation-timeline: scroll()) {
@media (max-width: 720px) { @media (max-width: 720px) {
/* Where the reveal runs, the footer is transparent until it is scrolled to, so /* 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 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 { .footer {
opacity: 0; opacity: 0;
animation: footer-reveal linear both; animation: footer-reveal ease-out both;
animation-timeline: view(); /* The scroller, not view(). view() measures the footer against the fold, so
/* "entry" opens before the footer's edge reaches the fold, not at it: parked, the reveal is a window inside the footer's own height — and the footer is
it already measures 9-11% entered across phone sizes. So the start has to be short. Every usable window was a fraction of an already short travel: the
past that or the footer sits permanently part-faded at rest — 20% clears it fade got ~22px of the 78px the reader has, which is not a fade, it is a
on every size tested with room to spare, and still begins the reveal within snap. It also had to start past ~20% because "entry" opens before the
the first few pixels of scroll. Ending at 45% means it reads as arrived at footer's edge reaches the fold, so the footer would otherwise sit part-faded
roughly a third of the travel, instead of still fading while it already at rest — the offsets were fighting the geometry.
looks fully on screen. */
animation-range: entry 20% entry 45%; 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 { @keyframes footer-reveal {
from { from {
transform: translate3d(0, 1.15rem, 0); transform: translate3d(0, 0.55rem, 0);
opacity: 0; opacity: 0;
} }
to { to {