tune overview anchors + stack-title-bar placement

- SCENE_ANCHOR_OFFSET replaced with getSceneAnchorOffset() so the
  bifrost scene can compute its offset per-viewport instead of using
  a fixed px count. bifrost lands at offsetTop + 0.85 * vh so the arc
  and sub-headline are already drawn in; stack-scene drops from 2100
  to 1800 so the anchor lands mid-stack rather than on the 4th card's
  final beat.
- .stack-title-bar top drops from clamp(3.75rem, 7vh, 5.25rem) to
  clamp(1.25rem, 2.8vh, 1.85rem) so the title floats at the same
  vertical baseline as the fixed .site-mark wordmark in the top-left,
  instead of sitting below it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arlind Ukshini 2026-04-24 09:53:40 +02:00
parent 9b2c166b6c
commit 5f466e68a9
2 changed files with 29 additions and 23 deletions

View file

@ -1232,24 +1232,30 @@
* see sceneOrder[] inside init(). * see sceneOrder[] inside init().
* Special value "hero" scrolls to top (0). * Special value "hero" scrolls to top (0).
*/ */
// Per-scene scroll offsets in pixels. Added to the scene's offsetTop // Per-scene scroll offsets. Added to the scene's offsetTop when a
// when a dot-nav button anchors to it, so the reader lands AFTER the // dot-nav button anchors to it, so the reader lands AFTER the scene's
// scene's initial reveal rather than at an empty frame where the // initial reveal rather than at an empty pre-scrub frame.
// scrub hasn't advanced yet.
// //
// stack-scene — the pin is 5000px long; Phase A (cards falling in) // stack-scene — pin is 5000px long; Phase A (cards falling in) is
// completes at ~0.42 of that (~2100px). Landing at +2100 puts // complete at ~0.42 of that (~2100px). 1800 lands just before the
// the reader on the fully stacked state, just before the grid // 4th card fully settles, so the reveal still has one tick to go.
// rearrange begins in Phase B.
// //
// hero, bifrost, bifrost-join — short reveal tweens; offsetTop is // bifrost — section is 200vh with a scrubbed reveal that runs from
// already the correct landing spot so offset is 0. // top-top to bottom-bottom (100vh scroll range). The sub-headline
const SCENE_ANCHOR_OFFSET = { // fades in at ~0.83 of that. Offset is computed per viewport as
'hero': 0, // 85% of vh so the reader arrives on the fully-drawn arc +
'stack-scene': 2100, // wordmark, regardless of display size.
'bifrost': 0, //
'bifrost-join': 0, // hero, bifrost-join — short reveal tweens; offsetTop is already
}; // the correct landing spot so offset is 0.
function getSceneAnchorOffset(sceneId) {
const vh = window.innerHeight;
switch (sceneId) {
case 'stack-scene': return 1800;
case 'bifrost': return Math.round(vh * 0.85);
default: return 0;
}
}
function scrollTo(sceneId) { function scrollTo(sceneId) {
if (!scrollerEl) return; // init() hasn't run yet — ignore if (!scrollerEl) return; // init() hasn't run yet — ignore
@ -1260,7 +1266,7 @@
// the scene element directly works in most cases but produces a tiny // the scene element directly works in most cases but produces a tiny
// non-zero offset (padding / border) — hard-code 0 for hero. // non-zero offset (padding / border) — hard-code 0 for hero.
const base = sceneId === 'hero' ? 0 : target.offsetTop; const base = sceneId === 'hero' ? 0 : target.offsetTop;
const scrollY = base + (SCENE_ANCHOR_OFFSET[sceneId] || 0); const scrollY = base + getSceneAnchorOffset(sceneId);
if (lenisInstance && typeof lenisInstance.scrollTo === 'function') { if (lenisInstance && typeof lenisInstance.scrollTo === 'function') {
// Lenis does the smooth animation. `immediate: false` uses the // Lenis does the smooth animation. `immediate: false` uses the

View file

@ -1076,13 +1076,13 @@ html {
.sc-current token is updated from bifrost.js's ScrollTrigger onUpdate .sc-current token is updated from bifrost.js's ScrollTrigger onUpdate
— 1/4 → 4/4 — one tick per landing card. — 1/4 → 4/4 — one tick per landing card.
Vertical offset clears the fixed .site-mark (top:28px, width:118px) Vertical top is aligned with the fixed .site-mark (top:28px) so
so the title sits below the wordmark instead of crashing into it. the title rides beside the wordmark at the very top of the scene,
Horizontal padding on the left is bumped to push past the wordmark's floating horizontally at the same baseline. Horizontal padding on
right edge; the right side uses the same edge token as the theatre. */ the left is sized to clear the wordmark's right edge (~146px). */
.stack-title-bar { .stack-title-bar {
position: absolute; position: absolute;
top: clamp(3.75rem, 7vh, 5.25rem); top: clamp(1.25rem, 2.8vh, 1.85rem);
left: 0; left: 0;
right: 0; right: 0;
z-index: 20; z-index: 20;
@ -1090,7 +1090,7 @@ html {
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
gap: 1.5rem; gap: 1.5rem;
padding-left: clamp(10.5rem, 12vw, 12.5rem); padding-left: clamp(10rem, 12vw, 12rem);
padding-right: clamp(0.75rem, 2vw, 2.25rem); padding-right: clamp(0.75rem, 2vw, 2.25rem);
pointer-events: none; pointer-events: none;
} }