The axis that flipped itself back and forth
Around 900px the horizontal axis sometimes turned itself on and off, endlessly: the frame's own scrollbar took a few pixels off the very width the axis reads to decide whether it is horizontal. Found from a screenshot the 6-in-1 sent at 902-905px.
What the screenshot showed
The 6-in-1 sent a screenshot of the timeline at roughly 902-905px wide: the decade labels and the mark circles sat on top of each other, as if the axis had been drawn twice - once horizontal, once vertical - into the same pixels.
What it turned out not to be
The first guess was a collision with the view transition (document.startViewTransition, which drives the zoom) during a live window drag: the old frame freezes as a snapshot while the window has already redrawn at the new width. The 6-in-1 confirmed dragging the window at the time - but also said a plain reload at the same width broke the axis the same way, no drag or zoom involved. That rules the view transition out: it never runs on a page load.
The second guess was the component rendering twice (two <main class="density-app"> or two .density-marks, say, from a stale chunk in the service worker’s cache). Checked straight in the console on live production (document.querySelectorAll('.density-app').length and .density-marks): both 1. The DOM is not duplicated.
What it turned out to be
.density-frame (the box the axis is drawn into) is overflow: auto. Its own measured width (ResizeObserver, size.width in Timeline.tsx) decides whether the axis is horizontal (>= 900). But at a width where the horizontal drawing needs a little more height than the frame gives it - narrow columns at 903px wrap the decade labels one line further - the frame grows its own vertical scrollbar, and that scrollbar is what takes a few pixels off the next width reading.
Measured live on production (console, document.querySelector('.density-frame')): innerWidth 903, the frame’s clientWidth - 894. Nine pixels, what the thin scrollbar (scrollbar-width: thin in the CSS) costs. 894 is under 900 - the axis should turn vertical. Vertical needs less height - the scrollbar goes - the width is 903 again - the axis turns horizontal again - which needs the scrollbar again - 894 again. A loop that feeds itself, with no pause for anything on screen to settle: .mark-dot carries transition: transform 0.12s, and on every turn the marks travel to entirely different coordinates (the horizontal and vertical formulas swap x and y), so what shows in between is a mix of both readings at once.
The fix
One number the switch itself can cross both ways is exactly what a loop is. In place of a single 900px threshold, two, depending on which side the axis already stood on: 900 to ENTER horizontal from vertical, 880 to LEAVE it again. The scrollbar’s nine pixels no longer cross the lower threshold while the axis is already horizontal, so the loop never starts.
src/ui/Timeline.tsx: wasHorizontal, a ref that remembers the last reading and is updated in a useLayoutEffect after each commit; horizontal reads it rather than a bare size.width >= 900.
What was checked
Only one file changed, so npm run dev in the shared working copy would not have shown anything new - checked instead in a worktree of its own, in the session’s scratchpad (node_modules a junction to the shared one, to skip npm ci; a first attempt through astro dev failed on Astro’s own dev-server lock and on an @fs/ import through the junction, so it was checked on a built npm run build plus npm run preview instead, with its own ATLAS_PREVIEW_PORT).
At a live 903px: the frame’s clientWidth still 894, the axis still reading horizontal - ten readings in a row, 200ms apart, no flip. The screenshot is clean, the labels and marks each where they belong. npm run verify:code passed in full (format, lint, types, tests, data, the public contract, the budgets - unchanged, since the change is JS only). The full atlas.spec.ts in chromium, 99 tests, including every panel-width check, no regressions.
Entry written September 29, 2026
Commits this entry accounts for
7de44db