Merge nucleic/warm-velvet-dingo-rmhb into main
Mirrored from corpo 464f4e7.
This commit is contained in:
+141
-9
@@ -518,9 +518,9 @@ const canonical = new URL('/', Astro.site);
|
|||||||
|
|
||||||
.has-scroll-stack .hero-copy {
|
.has-scroll-stack .hero-copy {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 50%;
|
top: var(--hero-copy-pin-top, 0px);
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
transform: translateY(-50%);
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enlarged type and middle-width windows can make the flex line wrap before the
|
/* Enlarged type and middle-width windows can make the flex line wrap before the
|
||||||
@@ -694,8 +694,8 @@ const canonical = new URL('/', Astro.site);
|
|||||||
|
|
||||||
.is-scroll-stack .technology-stage {
|
.is-scroll-stack .technology-stage {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 50%;
|
top: var(--technology-stage-pin-top, 0px);
|
||||||
transform: translateY(-50%);
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.technology-panel {
|
.technology-panel {
|
||||||
@@ -907,6 +907,19 @@ const canonical = new URL('/', Astro.site);
|
|||||||
--edge: clamp(1.15rem, 5vw, 1.85rem);
|
--edge: clamp(1.15rem, 5vw, 1.85rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The first gesture clears the hero and settles on Nucleic by itself. The two
|
||||||
|
technology rows then become the remaining stops, so even a quick mobile fling
|
||||||
|
cannot skip straight from the opening composition to the second product. */
|
||||||
|
main.has-technology-scroll {
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-technology-scroll .hero-copy,
|
||||||
|
.has-technology-scroll .technology-steps > span {
|
||||||
|
scroll-snap-align: center;
|
||||||
|
scroll-snap-stop: always;
|
||||||
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
padding-top: clamp(2.5rem, 8vh, 5rem);
|
padding-top: clamp(2.5rem, 8vh, 5rem);
|
||||||
}
|
}
|
||||||
@@ -1010,6 +1023,22 @@ const canonical = new URL('/', Astro.site);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep the desktop composition on one flex line through the narrow-desktop range.
|
||||||
|
Without this optical scale and permission for the card to use its actual four
|
||||||
|
columns, their minimum widths add up to just over the available measure at
|
||||||
|
roughly 721–999px. The flex line then wraps and enters its stacked fallback,
|
||||||
|
where the copy naturally leaves before the card story finishes. The curve meets
|
||||||
|
the phone size at 721px and approaches the normal desktop size at 1000px. */
|
||||||
|
@media (min-width: 721px) and (max-width: 999px) {
|
||||||
|
.statement {
|
||||||
|
font-size: clamp(3.2rem, calc(12.5vw - 2.4rem), 5.45rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology-stack {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
@@ -1044,18 +1073,93 @@ const canonical = new URL('/', Astro.site);
|
|||||||
const hero = heroLead?.closest<HTMLElement>('.hero');
|
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 scrollRoot = technologyStack.closest<HTMLElement>('main');
|
||||||
|
const technologyStage = technologyStack.querySelector<HTMLElement>('.technology-stage');
|
||||||
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 progressCurrent = technologyStack.querySelector<HTMLElement>('[data-technology-progress-current]');
|
const progressCurrent = technologyStack.querySelector<HTMLElement>('[data-technology-progress-current]');
|
||||||
|
const mobileStack = window.matchMedia('(max-width: 720px)');
|
||||||
|
|
||||||
heroLead?.classList.add('has-scroll-stack');
|
heroLead?.classList.add('has-scroll-stack');
|
||||||
hero?.classList.add('has-scroll-story');
|
hero?.classList.add('has-scroll-story');
|
||||||
if (heroLead && heroCopy) {
|
scrollRoot?.classList.add('has-technology-scroll');
|
||||||
const stackWrappedBelow = technologyStack.offsetLeft <= heroCopy.offsetLeft + 1;
|
|
||||||
heroLead.classList.toggle('is-stacked-stack', stackWrappedBelow);
|
|
||||||
}
|
|
||||||
technologyStack.classList.add('is-scroll-stack');
|
technologyStack.classList.add('is-scroll-stack');
|
||||||
|
|
||||||
|
let stackWrappedBelow = false;
|
||||||
|
let mobileEntryScrollTop = 0;
|
||||||
|
let mobileStackEntered = false;
|
||||||
|
|
||||||
|
const syncPinnedInsets = () => {
|
||||||
|
if (!scrollRoot || !heroCopy || !technologyStage || stackWrappedBelow) return;
|
||||||
|
|
||||||
|
const setCenteredInset = (element: HTMLElement, property: string) => {
|
||||||
|
const inset = `${Math.max(0, (scrollRoot.clientHeight - element.offsetHeight) / 2)}px`;
|
||||||
|
if (element.style.getPropertyValue(property) !== inset) {
|
||||||
|
element.style.setProperty(property, inset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setCenteredInset(heroCopy, '--hero-copy-pin-top');
|
||||||
|
setCenteredInset(technologyStage, '--technology-stage-pin-top');
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncMobileEntry = () => {
|
||||||
|
if (!mobileStack.matches || !scrollRoot || !heroCopy || !technologyStage) return;
|
||||||
|
|
||||||
|
const rootRect = scrollRoot.getBoundingClientRect();
|
||||||
|
const copyRect = heroCopy.getBoundingClientRect();
|
||||||
|
const stackRect = technologyStack.getBoundingClientRect();
|
||||||
|
const stageInset = Number.parseFloat(getComputedStyle(technologyStage).top) || 0;
|
||||||
|
const currentScroll = scrollRoot.scrollTop;
|
||||||
|
|
||||||
|
/* These resolve to document-space thresholds even while the stage is sticky.
|
||||||
|
The handoff waits for both conditions: the hero is above the scrollport and
|
||||||
|
the product card has reached its pinned position. */
|
||||||
|
const heroCleared = currentScroll + copyRect.bottom - rootRect.top + 16;
|
||||||
|
const stagePinned = currentScroll + stackRect.top - rootRect.top - stageInset;
|
||||||
|
mobileEntryScrollTop = Math.max(0, Math.ceil(heroCleared), Math.ceil(stagePinned));
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncEnhancedLayout = () => {
|
||||||
|
if (!heroLead || !heroCopy) return;
|
||||||
|
|
||||||
|
/* Remove the fallback before measuring so resizing from a stacked viewport
|
||||||
|
back to two columns can recover instead of measuring its own forced block. */
|
||||||
|
heroLead.classList.remove('is-stacked-stack');
|
||||||
|
stackWrappedBelow = technologyStack.offsetLeft <= heroCopy.offsetLeft + 1;
|
||||||
|
heroLead.classList.toggle('is-stacked-stack', stackWrappedBelow);
|
||||||
|
|
||||||
|
if (stackWrappedBelow) {
|
||||||
|
heroCopy.style.removeProperty('--hero-copy-pin-top');
|
||||||
|
technologyStage?.style.removeProperty('--technology-stage-pin-top');
|
||||||
|
} else {
|
||||||
|
syncPinnedInsets();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncMobileEntry();
|
||||||
|
};
|
||||||
|
|
||||||
|
syncEnhancedLayout();
|
||||||
|
|
||||||
|
if ('ResizeObserver' in window && scrollRoot && heroCopy && technologyStage) {
|
||||||
|
const pinObserver = new ResizeObserver(() => {
|
||||||
|
syncPinnedInsets();
|
||||||
|
syncMobileEntry();
|
||||||
|
});
|
||||||
|
pinObserver.observe(scrollRoot);
|
||||||
|
pinObserver.observe(heroCopy);
|
||||||
|
pinObserver.observe(technologyStage);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizeFrame = 0;
|
||||||
|
window.addEventListener(
|
||||||
|
'resize',
|
||||||
|
() => {
|
||||||
|
cancelAnimationFrame(resizeFrame);
|
||||||
|
resizeFrame = requestAnimationFrame(syncEnhancedLayout);
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
|
||||||
const activateTechnology = (index: number) => {
|
const activateTechnology = (index: number) => {
|
||||||
cards.forEach((card, cardIndex) => {
|
cards.forEach((card, cardIndex) => {
|
||||||
const isActive = cardIndex === index;
|
const isActive = cardIndex === index;
|
||||||
@@ -1069,12 +1173,40 @@ const canonical = new URL('/', Astro.site);
|
|||||||
|
|
||||||
activateTechnology(0);
|
activateTechnology(0);
|
||||||
|
|
||||||
|
let mobileEntryTimer = 0;
|
||||||
|
const settleMobileEntry = () => {
|
||||||
|
if (!mobileStack.matches || !scrollRoot || mobileStackEntered) return;
|
||||||
|
if (scrollRoot.scrollTop + 1 < mobileEntryScrollTop) return;
|
||||||
|
|
||||||
|
/* A single fast fling may cross every observer step. Return it to the required
|
||||||
|
Nucleic-only frame; only a later gesture is allowed to advance the stack. */
|
||||||
|
mobileStackEntered = true;
|
||||||
|
activateTechnology(0);
|
||||||
|
scrollRoot.scrollTo({ top: mobileEntryScrollTop, behavior: 'auto' });
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollRoot?.addEventListener(
|
||||||
|
'scroll',
|
||||||
|
() => {
|
||||||
|
if (mobileStack.matches && scrollRoot.scrollTop <= mobileEntryScrollTop * 0.25) {
|
||||||
|
mobileStackEntered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.clearTimeout(mobileEntryTimer);
|
||||||
|
mobileEntryTimer = window.setTimeout(settleMobileEntry, 120);
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
scrollRoot?.addEventListener('scrollend', settleMobileEntry, { passive: true });
|
||||||
|
|
||||||
const stepObserver = new IntersectionObserver(
|
const stepObserver = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (!entry.isIntersecting) return;
|
if (!entry.isIntersecting) return;
|
||||||
const index = Number((entry.target as HTMLElement).dataset.technologyStep);
|
const index = Number((entry.target as HTMLElement).dataset.technologyStep);
|
||||||
if (Number.isInteger(index)) activateTechnology(index);
|
const canActivate = index === 0 || !mobileStack.matches || mobileStackEntered;
|
||||||
|
if (Number.isInteger(index) && canActivate) activateTechnology(index);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ root: scrollRoot, rootMargin: '-45% 0px -45% 0px' },
|
{ root: scrollRoot, rootMargin: '-45% 0px -45% 0px' },
|
||||||
|
|||||||
Reference in New Issue
Block a user