Merge nucleic/olive-north-lynx-rstf into main
Mirrored from corpo 9e9a38a.
This commit is contained in:
+48
-150
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user