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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="technology-index" aria-label="Choose a technology">
|
<div class="technology-index" role="status" aria-live="polite" aria-atomic="true">
|
||||||
<button type="button" class="is-active" data-technology-jump="0" aria-label="Show Nucleic" aria-pressed="true">01</button>
|
<span class="technology-index-current" data-technology-progress-current>01</span>
|
||||||
<span aria-hidden="true">/</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,32 +192,21 @@ const canonical = new URL('/', Astro.site);
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
height: 100%;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
|
overflow: hidden;
|
||||||
/* Longhand on purpose, here and on body: the background-color value is the one
|
/* 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
|
thing Safari reads for its fills, and the shorthand form is how it silently
|
||||||
goes missing (background: resets background-color when edited carelessly). */
|
goes missing (background: resets background-color when edited carelessly). */
|
||||||
background-color: var(--paper);
|
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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
|
height: 100svh;
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
/* Safari 26 paints the rubber-band overscroll gap from body's computed
|
/* 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
|
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;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
width: min(100%, 1600px);
|
width: min(100%, 1600px);
|
||||||
min-height: 100svh;
|
height: 100svh;
|
||||||
|
min-height: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
padding: var(--shell-y) var(--edge);
|
padding: var(--shell-y) var(--edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +457,18 @@ const canonical = new URL('/', Astro.site);
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
display: grid;
|
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 {
|
.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);
|
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
|
/* 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
|
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
|
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 {
|
.has-scroll-stack .hero-copy {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 50svh;
|
top: 50%;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
@@ -540,7 +547,7 @@ const canonical = new URL('/', Astro.site);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.is-stacked-stack .technology-stage {
|
.is-stacked-stack .technology-stage {
|
||||||
top: clamp(8.5rem, 28svh, 14rem);
|
top: clamp(8.5rem, 28%, 14rem);
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,15 +618,15 @@ const canonical = new URL('/', Astro.site);
|
|||||||
animation: drift-up 11s 1.5s ease-in-out backwards;
|
animation: drift-up 11s 1.5s ease-in-out backwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The swipe is a soft pale-green blade crossing only “runs.” once as the
|
/* The swipe crosses “that runs.” once as the second line arrives. It paces
|
||||||
second line arrives. The pseudo-element duplicates the verb, clips its
|
across “that”, then significantly accelerates through “runs.” The pseudo
|
||||||
gradient to those glyphs, and is intentionally held by .runs so “that”
|
duplicates the whole phrase and clips its gradient to the glyphs; its
|
||||||
stays static. Its transparent parked positions also leave reduced-motion
|
transparent parked positions leave reduced-motion rendering unchanged. */
|
||||||
rendering unchanged. */
|
.line-runs .word::after {
|
||||||
.line-runs .runs::after {
|
content: 'that runs.';
|
||||||
content: 'runs.';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
padding-right: 0.06em;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
100deg,
|
100deg,
|
||||||
@@ -653,8 +660,6 @@ const canonical = new URL('/', Astro.site);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.line-runs .runs {
|
.line-runs .runs {
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
color: transparent;
|
color: transparent;
|
||||||
background: linear-gradient(100deg, #cdead9 8%, #2f8f5c 82%);
|
background: linear-gradient(100deg, #cdead9 8%, #2f8f5c 82%);
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -689,7 +694,7 @@ const canonical = new URL('/', Astro.site);
|
|||||||
|
|
||||||
.is-scroll-stack .technology-stage {
|
.is-scroll-stack .technology-stage {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 50svh;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,29 +797,10 @@ const canonical = new URL('/', Astro.site);
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.technology-index button {
|
.technology-index-current {
|
||||||
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 {
|
|
||||||
color: var(--pine);
|
color: var(--pine);
|
||||||
}
|
}
|
||||||
|
|
||||||
.technology-index button:focus-visible {
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.technology-steps {
|
.technology-steps {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -893,15 +879,12 @@ const canonical = new URL('/', Astro.site);
|
|||||||
from { transform: translate3d(0, 0, 0); }
|
from { transform: translate3d(0, 0, 0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The whole animation is the sweep — one iteration, no dwell keyframe — so
|
/* The first segment advances at a measured linear pace until the light has
|
||||||
the page pays for paint during a single ~0.65s window at load and never
|
cleared “that”; the shorter final segment covers the rest of its distance
|
||||||
again. The sweep is linear on purpose: an eased segment read as the light
|
in one third of the time, so “runs.” gets a deliberate 3× burst. */
|
||||||
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. */
|
|
||||||
@keyframes sheen-pass {
|
@keyframes sheen-pass {
|
||||||
from { background-position: -115% 0; }
|
from { background-position: -115% 0; }
|
||||||
|
66.667% { background-position: 18% 0; }
|
||||||
to { background-position: 235% 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. */
|
the text off the bezel rather than what the desktop composition wants. */
|
||||||
.page-shell {
|
.page-shell {
|
||||||
--edge: clamp(1.15rem, 5vw, 1.85rem);
|
--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 {
|
.hero {
|
||||||
@@ -992,7 +956,7 @@ const canonical = new URL('/', Astro.site);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.is-scroll-stack .technology-stage {
|
.is-scroll-stack .technology-stage {
|
||||||
top: clamp(8.5rem, 28svh, 14rem);
|
top: clamp(8.5rem, 28%, 14rem);
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,63 +970,6 @@ const canonical = new URL('/', Astro.site);
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.8rem;
|
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) {
|
if (technologyStack && 'IntersectionObserver' in window && !reducedMotion.matches) {
|
||||||
const heroLead = technologyStack.closest<HTMLElement>('.hero-lead');
|
const heroLead = technologyStack.closest<HTMLElement>('.hero-lead');
|
||||||
|
const hero = heroLead?.closest<HTMLElement>('.hero');
|
||||||
const heroCopy = heroLead?.querySelector<HTMLElement>('.hero-copy');
|
const heroCopy = heroLead?.querySelector<HTMLElement>('.hero-copy');
|
||||||
|
const scrollRoot = technologyStack.closest<HTMLElement>('main');
|
||||||
const cards = [...technologyStack.querySelectorAll<HTMLAnchorElement>('[data-technology-card]')];
|
const cards = [...technologyStack.querySelectorAll<HTMLAnchorElement>('[data-technology-card]')];
|
||||||
const steps = [...technologyStack.querySelectorAll<HTMLElement>('[data-technology-step]')];
|
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');
|
heroLead?.classList.add('has-scroll-stack');
|
||||||
|
hero?.classList.add('has-scroll-story');
|
||||||
if (heroLead && heroCopy) {
|
if (heroLead && heroCopy) {
|
||||||
const stackWrappedBelow = technologyStack.offsetLeft <= heroCopy.offsetLeft + 1;
|
const stackWrappedBelow = technologyStack.offsetLeft <= heroCopy.offsetLeft + 1;
|
||||||
heroLead.classList.toggle('is-stacked-stack', stackWrappedBelow);
|
heroLead.classList.toggle('is-stacked-stack', stackWrappedBelow);
|
||||||
@@ -1154,11 +1064,7 @@ const canonical = new URL('/', Astro.site);
|
|||||||
card.setAttribute('aria-hidden', String(!isActive));
|
card.setAttribute('aria-hidden', String(!isActive));
|
||||||
});
|
});
|
||||||
|
|
||||||
jumps.forEach((jump, jumpIndex) => {
|
if (progressCurrent) progressCurrent.textContent = String(index + 1).padStart(2, '0');
|
||||||
const isActive = jumpIndex === index;
|
|
||||||
jump.classList.toggle('is-active', isActive);
|
|
||||||
jump.setAttribute('aria-pressed', String(isActive));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
activateTechnology(0);
|
activateTechnology(0);
|
||||||
@@ -1171,17 +1077,9 @@ const canonical = new URL('/', Astro.site);
|
|||||||
if (Number.isInteger(index)) activateTechnology(index);
|
if (Number.isInteger(index)) activateTechnology(index);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ rootMargin: '-45% 0px -45% 0px' },
|
{ root: scrollRoot, rootMargin: '-45% 0px -45% 0px' },
|
||||||
);
|
);
|
||||||
|
|
||||||
steps.forEach((step) => stepObserver.observe(step));
|
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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user