.content is the app's one scroll pane and it outlives the view swap, so its
scrollTop was inherited by whatever loaded next: leave one artist 900px down
their discography, open another, and you arrived 900px down the new one —
mid-grid, hero already squeezed into the bar, with no hint the page had a top.
Reset it on catalog.artistOpenSeq, a counter bumped once per openCatalogArtist
call, rather than on the artist's id. The id cannot distinguish a re-open of the
artist already on screen — clicking their name from Now Playing while parked
deep in their own page, where nothing moved and the click read as dead — from no
navigation at all. Nor can this key on the page object: the cold path replaces
it stage by stage under the same id, which would haul a reader back to the top
mid-read. The counter bumps once per open and never per stage.
The bump sits after openCatalogArtist's invalid-id bail-out, so a request that
never opens anything cannot scroll the page the reader is still on.
Verified live: three artist-to-artist hops parked at 1222 each land at 0 and
stay 0 through every cold stage; re-opening the artist already on screen from
1070 lands at 0; mounting from another view with the pane genuinely parked at
1500 mounts at 0; and a reader parked at 600 on the artist they are already on
holds at 600 through six seconds of stage churn.
The pane could scroll ~850px (1971px at a tall window) past the last content
pixel: at max scroll ZERO of 11 release rows were on screen, the whole viewport
empty page.
The sheet is 300vh of absolutely positioned decoration, and an abs-positioned
descendant extends its scroller's scrollable overflow. .content's scrollHeight
tracked the sheet's box — pin flow origin + the sheet's live translate + 300vh —
instead of the page. Because that translate is scroll-driven, the range also
breathed with scroll position and depended on scroll history: the same scrollTop
measured 4167, 4329 and 4368 depending on the route taken. Hiding the sheet gave
exactly 1797, the page plus the pane's padding-top.
Wrap the sheet in a .sheetclip box with overflow: clip, sized to the pane's
client box, so the sheet paints exactly the region the pane can show and
contributes nothing to the scroll range.
- The clip box is ABSOLUTE and the pin stays height: 0. Sizing the pin itself
and cancelling with a negative margin-bottom leaves +24px behind — the margin
collapses against the neighbouring height-0 pins and walks the hero zone from
24 to 48. Out of flow there is no margin to collapse, and the sticky
constraint is untouched.
- overflow-clip-margin is not an option for the sheet's 28px bleed: it
re-expands the overflow region right back to 2440. The bleed moves onto the
clip box instead.
- The sheet's offsets are now clip-box relative. --band-from-pin is
pin-relative and the box sits --pin-origin above the pin, so the two cancel:
--band-from-pin + --pin-origin is --band-top.
- --frame-h (measure()'s scroller.clientHeight) becomes unconditional. It fed
the scroll-driven-only .fade-frame; .sheetclip needs it on both branches.
Setting it on the fallback path is inert — .fade-frame's rule lives inside
the @supports block and never matches there.
Verified live against the pre-change rendering at identical scrollTop, each
comparison bracketed by two baseline captures so the hero's 20s rotation could
not be mistaken for a difference: 0 differing pixels at four of five stops (the
fifth is one chip's hover ring, 83px), hero zone and scrollbody unmoved at every
stop. Scroll range is now content-exact with the clip box flush to the pane at
1280x800, 1600x1100, 1100x620 and 1920x1400, and across artists with 4, 10, 11
and 22 rows. At max scroll all 11 rows are on screen instead of none.
Scroll-driven animation conversion
- apply() writes nothing per frame on the supported path; the hero
squeeze, fade line and header curves all ride scroll-driven animations
on the compositor.
- Section boundaries flip via IntersectionObserver sentinels; the scroll
listener is gone from the hot path.
- The scrollbar thumb rides its own scroll-driven animation.
- @supports-not fallback keeps the previous rAF --p path verbatim, so
unsupported engines render identically.
- Curves verified exact to 1e-6 against the old path, 0.000px continuity
across boundary flips, correct retarget on resize.
Perf fixes
- Forced-layout reads pulled out of both scroll handlers.
- Resize settle-reflow: a second reflow 200ms after resize quiesces, and
the pane is observed too. This was the root cause of the fullscreen
fade-wedge and of the long-standing intermittent where rows stayed
hidden after a resize.
Glass
- backdrop-filter removed from chips and preview surfaces.
- Bar glass blur 20 -> 12.
- Choose-hero pill removed from the UI; pickArtistHero/clearArtistHero
backend is untouched and the control can be re-homed later.
Hero design (final)
- Crossfade twins are permanent. Twin blur is baked via bakeBlur()
(clamp-to-edge pad -> blur -> crop) so the frames stay pixel-aligned
with no edge fringe and no geometry scaling.
- Bloom strip retired everywhere; HeroGpuCanvas's internal bloom canvases
are hidden by one scoped rule and the component is unchanged.
- Seam is a pure mirror: the artwork's top strip flipped inside the glass
band, clipped at the bar's top line, riding the squeeze with the hero
and dissolving in lockstep on rotation.
- GPU twin overlay crossfades on rotation instead of snapping — the
incoming baked frame fades in on hero-banner-in with a 2-frame cap.
- ~930 lines of experimental variants and the perf-flag system deleted;
the surviving path is a pixel no-op against the approved configuration.
Debug overlay
- PERF/SEAM rows dropped; the overlay keeps stats plus an FPS/drops/worst
counter (Ctrl+Shift+D).
The content fade mask on .scrollbody lives in the scrolling element's local
box, so its cut stop (--flow-cut) must ride scroll to stay at a fixed screen
line. Driving that from heroScroll's rAF lagged the native compositor scroll
by a frame on fast flicks, so the cut slid up and rows briefly showed above
where they should clip (user-reported).
Move --flow-cut to a CSS scroll-driven animation (@property + @keyframes +
animation-timeline: scroll(nearest block), @supports-gated), so it's a pure
function of scroll position evaluated on the compositor and can't lag. The
keyframe delta and animation-range share one oversized constant (100000px)
so the value is base + scrollTop for ANY scroll distance — necessary because
the artist page changes its own content height as it scrolls, making the
total range non-constant. apply() keeps the per-frame path only as the
unsupported-engine fallback.
Verified live: screen cut constant at 217px across scrollTop 300..2500,
--flow-cut == base+scrollTop exactly.
Working:
- PreviewOverlay.svelte mounted at app root; opens over a fixture manifest
(window.__previewFixture(n) in dev), inert background, player bar hidden,
focus trap, keyboard nav, F3 rail, T3 swaps, R1b capsule, accent extraction.
- previewSession.svelte.js: store + service wiring (production + fixture).
- Root integration in App.svelte; revision-gateway bumps + notePlaybackGeneration
threaded through state.svelte.js player entry points; api.win guarded so plain
vite browser mounts.
- Launcher on the artist page builds a real artist manifest and opens the modal
with a play-full starter.
- 70 JS tests still green.
Two bugs found and fixed during bring-up:
- effect_update_depth_exceeded: bleed-crossfade and bump effects read AND wrote
the same $state; wrapped writes in untrack(). This loop was breaking Svelte's
scheduler globally (even ui.* stopped reacting).
- accentFromImage now sets crossOrigin for remote covers.
KNOWN BROKEN (handoff): reel vertical centering. The active card stays centred
at index 0 but drifts by one stride per step. Root cause isolated — see
TODO-Feature-Docs/scrollable-track-previews/HANDOFF-PHASE2C.md.
The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.