Merge nucleic/olive-north-lynx-rstf into main

Mirrored from corpo 9e9a38a.
This commit is contained in:
2026-08-10 03:10:41 -07:00
parent 9a316b7dad
commit c8be2779aa
+48 -150
View File
@@ -127,10 +127,10 @@ const canonical = new URL('/', Astro.site);
</a>
</div>
<div class="technology-index" aria-label="Choose a technology">
<button type="button" class="is-active" data-technology-jump="0" aria-label="Show Nucleic" aria-pressed="true">01</button>
<div class="technology-index" role="status" aria-live="polite" aria-atomic="true">
<span class="technology-index-current" data-technology-progress-current>01</span>
<span aria-hidden="true">/</span>
<button type="button" data-technology-jump="1" aria-label="Show narOS and nash" aria-pressed="false">02</button>
<span>02</span>
</div>
</div>
</div>
@@ -192,32 +192,21 @@ const canonical = new URL('/', Astro.site);
}
html {
height: 100%;
min-width: 320px;
overflow: hidden;
/* Longhand on purpose, here and on body: the background-color value is the one
thing Safari reads for its fills, and the shorthand form is how it silently
goes missing (background: resets background-color when edited carelessly). */
background-color: var(--paper);
}
/* Desktop keeps no rubber band, and this went back and forth twice — don't move it
again without new information. Unlocked, macOS Safari still banded even with
every page-side pixel owned (body gap fill, ambient apron): parts of its bounce
are chrome-side material — the render-time tint sample, the refresh spinner
well — that no page CSS reaches, and the owner judged the occasional band worse
than the lost bounce. Phones keep the native feel: the page actually scrolls
there, iOS fills its gap from body correctly, and none would also cost
pull-to-refresh. */
@media (min-width: 721px) {
html {
overscroll-behavior: none;
}
}
body {
margin: 0;
min-width: 320px;
height: 100svh;
min-height: 100svh;
overflow-x: hidden;
overflow: hidden;
color: var(--ink);
/* Safari 26 paints the rubber-band overscroll gap from body's computed
background-color and from nowhere else — not theme-color (ignored now), not the
@@ -408,8 +397,10 @@ const canonical = new URL('/', Astro.site);
display: grid;
grid-template-rows: auto 1fr auto;
width: min(100%, 1600px);
min-height: 100svh;
height: 100svh;
min-height: 0;
margin: 0 auto;
overflow: hidden;
padding: var(--shell-y) var(--edge);
}
@@ -466,6 +457,18 @@ const canonical = new URL('/', Astro.site);
main {
display: grid;
min-height: 0;
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
}
main::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.hero {
@@ -482,6 +485,10 @@ const canonical = new URL('/', Astro.site);
padding: clamp(1.75rem, 4.5vh, 4.5rem) 0 clamp(1.5rem, 3vh, 3rem);
}
.hero.has-scroll-story {
align-content: start;
}
/* Row one of the hero, as a flex line rather than two grid areas. The two bases
add up to exactly the grid width, so at normal text size this renders where the
8/4 column split always put it — but when text alone is scaled up (Firefox's
@@ -511,7 +518,7 @@ const canonical = new URL('/', Astro.site);
.has-scroll-stack .hero-copy {
position: sticky;
top: 50svh;
top: 50%;
align-self: flex-start;
transform: translateY(-50%);
}
@@ -540,7 +547,7 @@ const canonical = new URL('/', Astro.site);
}
.is-stacked-stack .technology-stage {
top: clamp(8.5rem, 28svh, 14rem);
top: clamp(8.5rem, 28%, 14rem);
transform: none;
}
@@ -611,15 +618,15 @@ const canonical = new URL('/', Astro.site);
animation: drift-up 11s 1.5s ease-in-out backwards;
}
/* The swipe is a soft pale-green blade crossing only “runs.” once as the
second line arrives. The pseudo-element duplicates the verb, clips its
gradient to those glyphs, and is intentionally held by .runs so “that”
stays static. Its transparent parked positions also leave reduced-motion
rendering unchanged. */
.line-runs .runs::after {
content: 'runs.';
/* The swipe crosses “that runs.” once as the second line arrives. It paces
across “that”, then significantly accelerates through “runs.” The pseudo
duplicates the whole phrase and clips its gradient to the glyphs; its
transparent parked positions leave reduced-motion rendering unchanged. */
.line-runs .word::after {
content: 'that runs.';
position: absolute;
inset: 0;
padding-right: 0.06em;
color: transparent;
background-image: linear-gradient(
100deg,
@@ -653,8 +660,6 @@ const canonical = new URL('/', Astro.site);
}
.line-runs .runs {
position: relative;
display: inline-block;
color: transparent;
background: linear-gradient(100deg, #cdead9 8%, #2f8f5c 82%);
background-clip: text;
@@ -689,7 +694,7 @@ const canonical = new URL('/', Astro.site);
.is-scroll-stack .technology-stage {
position: sticky;
top: 50svh;
top: 50%;
transform: translateY(-50%);
}
@@ -792,29 +797,10 @@ const canonical = new URL('/', Astro.site);
display: flex;
}
.technology-index button {
appearance: none;
border: 0;
padding: 0.3rem;
color: inherit;
background: transparent;
font: inherit;
letter-spacing: inherit;
line-height: inherit;
cursor: pointer;
transition: color 220ms ease;
}
.technology-index button:hover,
.technology-index button:focus-visible,
.technology-index button.is-active {
.technology-index-current {
color: var(--pine);
}
.technology-index button:focus-visible {
outline-offset: 1px;
}
.technology-steps {
position: absolute;
inset: 0;
@@ -893,15 +879,12 @@ const canonical = new URL('/', Astro.site);
from { transform: translate3d(0, 0, 0); }
}
/* The whole animation is the sweep — one iteration, no dwell keyframe — so
the page pays for paint during a single ~0.65s window at load and never
again. The sweep is linear on purpose: an eased segment read as the light
accelerating through the middle of the line (it was — ease-in-out is
fastest at its midpoint, which lands mid-word), and a sprinter at full
speed holds a pace. The softness at entry and exit comes from the band's
own gradient profile, not from the curve. */
/* The first segment advances at a measured linear pace until the light has
cleared “that”; the shorter final segment covers the rest of its distance
in one third of the time, so “runs.” gets a deliberate 3× burst. */
@keyframes sheen-pass {
from { background-position: -115% 0; }
66.667% { background-position: 18% 0; }
to { background-position: 235% 0; }
}
@@ -922,25 +905,6 @@ const canonical = new URL('/', Astro.site);
the text off the bezel rather than what the desktop composition wants. */
.page-shell {
--edge: clamp(1.15rem, 5vw, 1.85rem);
/* The footer is parked past the fold: on a phone its rule is a third horizontal
line competing with the masthead's and the card's, and the page reads calmer
without it. The drop has to pay for the footer's own height and the shell's
bottom padding before it buys any clearance, hence a value this large.
The (100lvh - 100svh) term is the toolbar: main is sized to the small
viewport so the hero centres in what you can actually see, but Safari's
bottom bar is translucent and the page shows through it, so clearing svh
only put the footer's rule behind the controls rather than out of sight.
Both terms collapse to the clamp anywhere the two viewports agree.
The clamp is sized to park the footer just out of sight, not to bury it: it
pays for the footer's own height and the shell's bottom padding first, and
what is left over is the clearance below the fold — about 20-27px, enough
that the rule cannot peek at rest, and no more. Every pixel beyond that is
scroll the reader has to spend before the footer starts arriving. */
--footer-drop: calc(100lvh - 100svh + clamp(5.25rem, 10.5vh, 6rem));
min-height: calc(100svh + var(--footer-drop));
}
.hero {
@@ -992,7 +956,7 @@ const canonical = new URL('/', Astro.site);
}
.is-scroll-stack .technology-stage {
top: clamp(8.5rem, 28svh, 14rem);
top: clamp(8.5rem, 28%, 14rem);
transform: none;
}
@@ -1006,63 +970,6 @@ const canonical = new URL('/', Astro.site);
flex-direction: column;
align-items: flex-start;
gap: 0.8rem;
margin-top: var(--footer-drop);
}
}
/* The phone footer is below the fold, so a timed entrance would have played out
unseen. Driven off the scroll position instead, it and its rule arrive as they
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: 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
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 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, 0.55rem, 0);
opacity: 0;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@@ -1134,12 +1041,15 @@ const canonical = new URL('/', Astro.site);
if (technologyStack && 'IntersectionObserver' in window && !reducedMotion.matches) {
const heroLead = technologyStack.closest<HTMLElement>('.hero-lead');
const hero = heroLead?.closest<HTMLElement>('.hero');
const heroCopy = heroLead?.querySelector<HTMLElement>('.hero-copy');
const scrollRoot = technologyStack.closest<HTMLElement>('main');
const cards = [...technologyStack.querySelectorAll<HTMLAnchorElement>('[data-technology-card]')];
const steps = [...technologyStack.querySelectorAll<HTMLElement>('[data-technology-step]')];
const jumps = [...technologyStack.querySelectorAll<HTMLButtonElement>('[data-technology-jump]')];
const progressCurrent = technologyStack.querySelector<HTMLElement>('[data-technology-progress-current]');
heroLead?.classList.add('has-scroll-stack');
hero?.classList.add('has-scroll-story');
if (heroLead && heroCopy) {
const stackWrappedBelow = technologyStack.offsetLeft <= heroCopy.offsetLeft + 1;
heroLead.classList.toggle('is-stacked-stack', stackWrappedBelow);
@@ -1154,11 +1064,7 @@ const canonical = new URL('/', Astro.site);
card.setAttribute('aria-hidden', String(!isActive));
});
jumps.forEach((jump, jumpIndex) => {
const isActive = jumpIndex === index;
jump.classList.toggle('is-active', isActive);
jump.setAttribute('aria-pressed', String(isActive));
});
if (progressCurrent) progressCurrent.textContent = String(index + 1).padStart(2, '0');
};
activateTechnology(0);
@@ -1171,17 +1077,9 @@ const canonical = new URL('/', Astro.site);
if (Number.isInteger(index)) activateTechnology(index);
});
},
{ rootMargin: '-45% 0px -45% 0px' },
{ root: scrollRoot, rootMargin: '-45% 0px -45% 0px' },
);
steps.forEach((step) => stepObserver.observe(step));
jumps.forEach((jump) => {
jump.addEventListener('click', () => {
const index = Number(jump.dataset.technologyJump);
if (!Number.isInteger(index) || !steps[index]) return;
activateTechnology(index);
steps[index].scrollIntoView({ behavior: 'smooth', block: 'center' });
});
});
}
</script>