diff --git a/protected/bifrost.js b/protected/bifrost.js index d9f0e55..a5f6a29 100644 --- a/protected/bifrost.js +++ b/protected/bifrost.js @@ -1232,24 +1232,30 @@ * — see sceneOrder[] inside init(). * Special value "hero" scrolls to top (0). */ - // Per-scene scroll offsets in pixels. Added to the scene's offsetTop - // when a dot-nav button anchors to it, so the reader lands AFTER the - // scene's initial reveal rather than at an empty frame where the - // scrub hasn't advanced yet. + // Per-scene scroll offsets. Added to the scene's offsetTop when a + // dot-nav button anchors to it, so the reader lands AFTER the scene's + // initial reveal rather than at an empty pre-scrub frame. // - // stack-scene — the pin is 5000px long; Phase A (cards falling in) - // completes at ~0.42 of that (~2100px). Landing at +2100 puts - // the reader on the fully stacked state, just before the grid - // rearrange begins in Phase B. + // stack-scene — pin is 5000px long; Phase A (cards falling in) is + // complete at ~0.42 of that (~2100px). 1800 lands just before the + // 4th card fully settles, so the reveal still has one tick to go. // - // hero, bifrost, bifrost-join — short reveal tweens; offsetTop is - // already the correct landing spot so offset is 0. - const SCENE_ANCHOR_OFFSET = { - 'hero': 0, - 'stack-scene': 2100, - 'bifrost': 0, - 'bifrost-join': 0, - }; + // bifrost — section is 200vh with a scrubbed reveal that runs from + // top-top to bottom-bottom (100vh scroll range). The sub-headline + // fades in at ~0.83 of that. Offset is computed per viewport as + // 85% of vh so the reader arrives on the fully-drawn arc + + // wordmark, regardless of display size. + // + // 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) { 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 // non-zero offset (padding / border) — hard-code 0 for hero. 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') { // Lenis does the smooth animation. `immediate: false` uses the diff --git a/protected/index.html b/protected/index.html index 3e70b6d..042617a 100644 --- a/protected/index.html +++ b/protected/index.html @@ -1076,13 +1076,13 @@ html { .sc-current token is updated from bifrost.js's ScrollTrigger onUpdate — 1/4 → 4/4 — one tick per landing card. - Vertical offset clears the fixed .site-mark (top:28px, width:118px) - so the title sits below the wordmark instead of crashing into it. - Horizontal padding on the left is bumped to push past the wordmark's - right edge; the right side uses the same edge token as the theatre. */ + Vertical top is aligned with the fixed .site-mark (top:28px) so + the title rides beside the wordmark at the very top of the scene, + floating horizontally at the same baseline. Horizontal padding on + the left is sized to clear the wordmark's right edge (~146px). */ .stack-title-bar { position: absolute; - top: clamp(3.75rem, 7vh, 5.25rem); + top: clamp(1.25rem, 2.8vh, 1.85rem); left: 0; right: 0; z-index: 20; @@ -1090,7 +1090,7 @@ html { justify-content: space-between; align-items: baseline; 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); pointer-events: none; }