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 {
|
||||
position: sticky;
|
||||
top: 50%;
|
||||
top: var(--hero-copy-pin-top, 0px);
|
||||
align-self: flex-start;
|
||||
transform: translateY(-50%);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
position: sticky;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
top: var(--technology-stage-pin-top, 0px);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.technology-panel {
|
||||
@@ -907,6 +907,19 @@ const canonical = new URL('/', Astro.site);
|
||||
--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 {
|
||||
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) {
|
||||
*,
|
||||
*::before,
|
||||
@@ -1044,18 +1073,93 @@ const canonical = new URL('/', Astro.site);
|
||||
const hero = heroLead?.closest<HTMLElement>('.hero');
|
||||
const heroCopy = heroLead?.querySelector<HTMLElement>('.hero-copy');
|
||||
const scrollRoot = technologyStack.closest<HTMLElement>('main');
|
||||
const technologyStage = technologyStack.querySelector<HTMLElement>('.technology-stage');
|
||||
const cards = [...technologyStack.querySelectorAll<HTMLAnchorElement>('[data-technology-card]')];
|
||||
const steps = [...technologyStack.querySelectorAll<HTMLElement>('[data-technology-step]')];
|
||||
const progressCurrent = technologyStack.querySelector<HTMLElement>('[data-technology-progress-current]');
|
||||
const mobileStack = window.matchMedia('(max-width: 720px)');
|
||||
|
||||
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);
|
||||
}
|
||||
scrollRoot?.classList.add('has-technology-scroll');
|
||||
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) => {
|
||||
cards.forEach((card, cardIndex) => {
|
||||
const isActive = cardIndex === index;
|
||||
@@ -1069,12 +1173,40 @@ const canonical = new URL('/', Astro.site);
|
||||
|
||||
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(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
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' },
|
||||
|
||||
Reference in New Issue
Block a user