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

Mirrored from corpo f8f4b82.
This commit is contained in:
2026-08-09 17:46:09 -07:00
parent 4dff1238dd
commit 369dbd077b
+73 -25
View File
@@ -15,10 +15,12 @@ const canonical = new URL('/', Astro.site);
hand those 120px to the page, so no element and no viewport unit reaches them — hand those 120px to the page, so no element and no viewport unit reaches them —
see the note on .ambient for what actually resolves it. --> see the note on .ambient for what actually resolves it. -->
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<!-- Not --paper. Safari paints its overscroll regions with this flat colour, but <!-- --paper, and it has to stay --paper: this is one of the surfaces Safari fills
the page underneath is the ambient field, which lifts the edges well off paper. around the page, alongside the canvas, and the field is now brought to --paper
Measured off rendered pixels at the top and bottom edges. --> at the viewport edges so all of them agree. A "better matching" value here is
<meta name="theme-color" content="#0f1813" /> precisely the bug — it reintroduces a colour that differs from the page edge,
which surfaces as a bar whenever Safari switches to it during a scroll. -->
<meta name="theme-color" content="#0a100c" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
@@ -175,10 +177,10 @@ const canonical = new URL('/', Astro.site);
right: 0; right: 0;
left: 0; left: 0;
/* Deliberately not inset: 0. A fixed box pinned to the bottom stops at the small /* Deliberately not inset: 0. A fixed box pinned to the bottom stops at the small
viewport, but iOS Safari keeps rendering the page behind its translucent viewport, leaving the strip between svh and lvh unpainted by this layer — which
toolbar — so the strip between svh and lvh fell back to flat paper and read as matters because the toolbar retracts and hands those pixels back to the page.
a grey bar under the chrome. Sized to the large viewport, the field paints Sized to the large viewport, the layer owns them in both states; what it puts
through the toolbar instead. */ there is the ::after note's business. */
height: 100%; height: 100%;
height: 100lvh; height: 100lvh;
overflow: hidden; overflow: hidden;
@@ -194,23 +196,55 @@ const canonical = new URL('/', Astro.site);
and no viewport-fit=cover reaches them; earlier attempts only moved the seam. and no viewport-fit=cover reaches them; earlier attempts only moved the seam.
So the canvas is the single point, and there is exactly one thing to get right: So the canvas is the single point, and there is exactly one thing to get right:
the canvas colour and the page's own edge colour have to be equal. The canvas can every surface Safari fills around the page, and the page's own colour where it
only be flat --paper, and the field is a moving light whose edge tone drifts, so meets them, must be the same paint. Those surfaces can only be flat colours, and
matching by picking a constant is a value that is wrong again as soon as the bands the field is a moving light whose edge tone drifts, so matching them by picking a
move. Instead the field is brought to zero at the bottom of the viewport — then constant is a value that is wrong again as soon as the bands move — and wrong in a
page edge, canvas and theme-color are all --paper by construction, whatever the different way in each scroll state, which is what made the bar come and go.
animation is doing, and the seam cannot come back.
Costs nothing to look at: this falls inside the strip Safari's toolbar already The field is therefore brought to zero at both edges of the viewport. --paper is
covers. Static, so the layer still composites without repainting per frame. */ then the only colour in play: canvas, theme-color, overscroll, the chrome strips,
and the page itself where it touches any of them. There is no second value left
for Safari to swap in, so no scroll state, toolbar collapse or fixed-layer
reposition has anything to expose. That is the property to preserve — if this ever
needs editing, keep the edges at --paper rather than tuning a colour to match.
The one thing that has to be right is where the bottom reaches --paper, and it is
not a number to taste. This layer is fixed and 100lvh tall, but the bottom edge of
what the reader can see is only at 100lvh with the toolbar collapsed; expanded, it
is at 100svh, ~100px higher, and Safari slides between the two mid-scroll. Ending
the fade at the element's own bottom therefore left the visible edge sitting part
way up the fade whenever the toolbar was out — lighter than the strip below it, so
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.
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. */
.ambient::after { .ambient::after {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; inset: 0;
bottom: 0; background: linear-gradient(
left: 0; to bottom,
height: 5rem; var(--paper) 0,
background: linear-gradient(to top, var(--paper), rgb(10 16 12 / 0%)); rgb(10 16 12 / 0%) 8rem,
rgb(10 16 12 / 0%) calc(100svh - 14rem),
var(--paper) 100svh,
var(--paper) 100%
);
} }
/* Oversized so the bands can travel a long way without an edge ever entering the /* Oversized so the bands can travel a long way without an edge ever entering the
@@ -679,14 +713,28 @@ const canonical = new URL('/', Astro.site);
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: view()) {
@media (max-width: 720px) { @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
halves of the drop the fallback needs: the toolbar term goes, because a footer
behind Safari's bar cannot show through it at zero alpha, and the clearance
shrinks to the little that keeps the rule from being mid-fade at rest. All of
it was scroll the reader had to spend before anything began to happen. */
.page-shell {
--footer-drop: clamp(4.75rem, 9.3vh, 5.1rem);
}
.footer { .footer {
opacity: 0; opacity: 0;
animation: footer-reveal linear both; animation: footer-reveal linear both;
animation-timeline: view(); animation-timeline: view();
/* Finishes at 88% rather than 100%: the last stretch of "entry" is the /* "entry" opens before the footer's edge reaches the fold, not at it: parked,
element clearing Safari's translucent toolbar, and waiting that long it already measures 9-11% entered across phone sizes. So the start has to be
means it is still fading while it looks fully on screen. */ past that or the footer sits permanently part-faded at rest — 20% clears it
animation-range: entry 12% entry 88%; 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%;
} }
} }
} }