Adds three things to the /roadmap horizontal route:
- a scroll-proximity focus effect (centre milestone scales/brightens,
edges recede) so the track feels alive as you move it;
- eased, accumulating wheel/trackpad scrolling instead of instant jumps;
- a trailing tail past the last milestone (opt-in tailLength) so the line
keeps going and the final item can scroll toward the centre.
Also enlarges the milestone card text for readability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a fifth roadmap status, `planned`, for items that are committed and
scheduled but not yet started — sitting between `in_beta` and `exploring`
in the progression. Rendered with the design system's indigo pigment
(#5a6d83) on the route, carousel, legend, and admin pill.
Migration 0008 widens the status CHECK constraint via a table rebuild
(SQLite can't alter it in place), preserving rows and attributions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first route item now starts where the dispatch banner's left edge
sits (the page's content-max column), instead of 60px from the
viewport edge. Looks intentional now — the route and the dispatch
banner share a vertical anchor.
- computeRouteLayout now accepts optional paddingLeft / paddingRight
that override the symmetric paddingX. Existing call sites and
tests are unchanged.
- RoadmapRoute SSR + client recompute set paddingLeft = max(60,
(vw - 1152) / 2), so on viewports ≤ 1152px nothing moves (degrades
gracefully) and on wider screens the start migrates inward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The actual cause of the persistent top/bottom card clipping wasn't the
track height or the padding — it's that the CSS spec forces
overflow-y: visible to compute as auto whenever overflow-x is auto.
Browsers clip the scroll container on both axes regardless of how we
declare overflow-y. Every previous fix was band-aiding the same
underlying problem.
Geometric fix: flip cardSide so cards hang toward the centreline
instead of away from it.
- i=0 (dot on centreline) → card below (default, no clip risk)
- i=1 (dot above-centre, odd) → card below (grows toward midY)
- i=2 (dot below-centre, even >0) → card above (grows toward midY)
- …alternating thereafter
Cards now always grow into the track, never out of it. Both axes are
naturally bounded by the track's height. Hover-expanded cards stay
inside the scroll container's clip box, so the browser-forced clipping
has nothing to remove.
Tests updated to expect the new pattern. The 7-item case carries an
extra spot-check that every card's side is opposite to its dot's
offset from the centreline — i.e. the geometric invariant the fix
relies on.
Visual rhythm: cards still alternate above/below as the path swings
up and down; the wave reads the same. What changes is which milestones
have cards above vs below — and only at the visual top of the page
where it improves things by stopping the clipping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled fixes to the route's geometry:
- computeRouteLayout's width calculation flipped to Math.max(viewport
* 0.80, itemCount * minSpacing + padding * 2). On a wide screen with
few items the 80% target wins and the path stretches across the
page; once item count makes the data-driven width exceed 80% (the
carousel case), the data-driven value wins and the track extends
past the viewport unchanged.
- .rr-scroll horizontal padding 140 → 60 each side. The previous 140
was overcompensating; with the new 80% target the milestones already
sit inside their own breathing room. 60 is card-half + 30px buffer,
enough for a 220px card centred under a dot 60px from the edge.
scroll-padding kept in sync at 60 for snap-stop landings.
- trackHeight default 580 → 420; midY 290 → 210. The 580 was bandaging
the vertical-clipping issue — that fix lands in the next commit. With
the clip properly addressed, 420 fits the path's amplitude 120 swing
cleanly with no wasted vertical space.
Tests rewritten to match the new width semantics: 1 item @ 1000 →
920px (0.8 * 1000 + 120); 3 items @ 1400 → 1240px; 20 items @ 800 →
6200px (data-driven wins). midY assertion 290 → 210.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled changes that all serve the same goal — less furniture
above the route, more honest information below it.
Progress dots gone. At 5 pills × ~400px per pill the strip was too
coarse to feel meaningful; the arrows + edge fades already
communicate scroll position. .rr-progress markup, the script logic
that updated the .active class, and the .rr-progress / .rr-progress-dot
styles are all deleted.
Legend moves from beside 'The route' in the section header to below
the track, centred. Reading order is now title → walk the path → key,
which is the order it makes sense in. The header collapses to just
the title on the left and the two arrow buttons on the right.
Path amplitude is no longer constant. computeRouteLayout multiplies
the base amplitude (120) by a per-item factor that ramps 0.78 (first
off-axis item) → 1.18 (last item), so closer-in items swing tighter
and further-out items swing wider. The visual effect is subtle but
the path now feels hand-planned instead of strictly sinusoidal.
Test updated to verify the multiplier — |itemY[2] - midY| now exceeds
|itemY[1] - midY| in the 3-item case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The route was clipping at three places: top and bottom of hovered
cards (the track was only 460 tall) and at the left/right viewport
edges (first card half-off-screen at scrollLeft 0, last card off the
right at scrollEnd).
Track height: default trackHeight in roadmap-layout 460 → 580; .rr-track
inline-style and the SVG height matched. midY now 290. Path centreline
stays in the visual centre and gains 60px breathing room above + 60px
below — which is exactly the room a hovered card needs to expand into.
Scroll-container padding: .rr-scroll gains 140px of horizontal padding
plus matching scroll-padding-left/right so snap-stops land cleanly.
The 140 figure is 220px card-width / 2 + 30px buffer, so the first and
last cards have a full card-width of clear space inside the viewport
at the scroll extremes.
Layout helper test verifies midY === 290.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure math, no DOM. computeRouteLayout(opts) takes itemCount + viewport
width and returns trackWidth, pathD, itemX, itemY, cardSide, midY:
- itemX evenly distributes items between padding and viewport-padding,
expanding the canvas beyond the viewport when itemCount * minSpacingX
exceeds the available width. Single-item case centres the dot.
- itemY puts the first item on the centreline; subsequent items
alternate +amplitude / -amplitude so the path snakes gently up and
down. The route reads as a river rather than a saw-tooth because the
cubic-bezier control points use the segment midpoint x — that holds
the tangent flat at each milestone.
- cardSide alternates 'below' / 'above' starting from 'below' on item 0.
Cards hang from their dot via a thin vertical connector in the
consuming component.
Also adds travelledStopFor(statuses) — the stop position on the path
stroke gradient where 'travelled' fades into 'ahead'. Clamps to 0.98
even when every item is shipping so the fade is always visible.
9 unit tests cover itemCount 1/2/3/7/20 plus the travelledStop edge
cases (no shipping → 0; all shipping → ≤ 0.98; mixed → exact midpoint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>