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 —
see the note on .ambient for what actually resolves it. -->
<meta name="viewport" content="width=device-width" />
<!-- Not --paper. Safari paints its overscroll regions with this flat colour, but
the page underneath is the ambient field, which lifts the edges well off paper.
Measured off rendered pixels at the top and bottom edges. -->
<meta name="theme-color" content="#0f1813" />
<!-- --paper, and it has to stay --paper: this is one of the surfaces Safari fills
around the page, alongside the canvas, and the field is now brought to --paper
at the viewport edges so all of them agree. A "better matching" value here is
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} />
<title>{title}</title>
<meta name="description" content={description} />
@@ -175,10 +177,10 @@ const canonical = new URL('/', Astro.site);
right: 0;
left: 0;
/* 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
toolbar — so the strip between svh and lvh fell back to flat paper and read as
a grey bar under the chrome. Sized to the large viewport, the field paints
through the toolbar instead. */
viewport, leaving the strip between svh and lvh unpainted by this layer — which
matters because the toolbar retracts and hands those pixels back to the page.
Sized to the large viewport, the layer owns them in both states; what it puts
there is the ::after note's business. */
height: 100%;
height: 100lvh;
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.
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
only be flat --paper, and the field is a moving light whose edge tone drifts, so
matching by picking a constant is a value that is wrong again as soon as the bands
move. Instead the field is brought to zero at the bottom of the viewport — then
page edge, canvas and theme-color are all --paper by construction, whatever the
animation is doing, and the seam cannot come back.
every surface Safari fills around the page, and the page's own colour where it
meets them, must be the same paint. Those surfaces can only be flat colours, and
the field is a moving light whose edge tone drifts, so matching them by picking a
constant is a value that is wrong again as soon as the bands move — and wrong in a
different way in each scroll state, which is what made the bar come and go.
Costs nothing to look at: this falls inside the strip Safari's toolbar already
covers. Static, so the layer still composites without repainting per frame. */
The field is therefore brought to zero at both edges of the viewport. --paper is
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 {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 5rem;
background: linear-gradient(to top, var(--paper), rgb(10 16 12 / 0%));
inset: 0;
background: linear-gradient(
to bottom,
var(--paper) 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
@@ -679,14 +713,28 @@ const canonical = new URL('/', Astro.site);
which lands on load and leaves it plainly visible. */
@supports (animation-timeline: view()) {
@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 {
opacity: 0;
animation: footer-reveal linear both;
animation-timeline: view();
/* Finishes at 88% rather than 100%: the last stretch of "entry" is the
element clearing Safari's translucent toolbar, and waiting that long
means it is still fading while it looks fully on screen. */
animation-range: entry 12% entry 88%;
/* "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%;
}
}
}