1. Markdown preview in the admin edit panel now re-renders from the
textarea's current value on every toggle (dynamic-imports marked on
the client). Previously the panel showed the server-rendered seed
value forever, so new dispatches always previewed empty.
2. Pulse sub-form drops the opens_at field (opens on dispatch publish
automatically) and changes closes_at to a date input — the chosen
day is treated as end-of-day in the DB.
3. /dispatches/[slug] reading width widened 50% (720 → 1080px).
4. Roadmap display_order cascades on insert / update / delete:
inserting at N bumps N..end up by 1, deleting N pulls N+1..end
down by 1, moving from A to B shifts the intermediate range by 1
in the appropriate direction. Order stays dense — no gaps, no
collisions. All three transitions run in a transaction.
5. /roadmap always anchors at scrollLeft=0 on mount so the first
milestone aligns with the content-column left edge. Previously
the page jumped to the last-shipping milestone, which felt random
once items past the viewport landed.
6. Events admin list shows the actual date (fmtDateTime) instead of
"in 3 days" — easier to scan when planning across months.
7. duration_label is auto-computed from starts_at + ends_at on save
(minutes < 90, hours < 4, "Half day", "Full day", "N days").
The manual field is gone from the admin form; the column on the
member-facing event pages keeps reading the stored value as before.
8. Pulse hero still skips office hours per the existing logic — no
change. Confirmed via the test note's clarification.
9. Pulse "also coming up" strip relabeled to Previous + Upcoming.
Previous = most recent past non-office-hours event. Upcoming =
next non-office-hours event after the hero. Each card now carries
a small terracotta eyebrow with the label.
Typecheck clean, build clean, 147/147 tests pass.
Co-Authored-By: Claude Opus 4.7 (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>
Replaces the snap-based scroll-by-arrow model with one that handles
three input modalities feeding the same scroll state. Result: the
track glides continuously instead of clicking into milestones.
Stripped:
- scroll-snap-type: x mandatory on .rr-scroll
- scroll-behavior: smooth on .rr-scroll
- scroll-snap-align: center on .rr-dot
- The fades-only updateNav stub from commit 2
Added on .rr-scroll:
- cursor: grab → grabbing on .rr-dragging
- touch-action: pan-y so vertical page-scroll passes through on mobile
while horizontal drags activate the route's own drag handler
- user-select: none stops text selection mid-drag
- .rr-dragging .rr-card { pointer-events: none } so a hover-reveal
can't pop open while the track is being dragged
Script (vanilla, ~140 lines):
- animateScrollTo(target, durationMs): cubic-ease-out via RAF.
Cancels any existing momentum or animation before starting.
- Wheel handler: vertical deltaY translates to horizontal scrollLeft
when |deltaX| < |deltaY|; horizontal trackpad gestures pass through
1:1 unscaled. preventDefault on this scroll element only — vertical
wheel elsewhere on the page scrolls the page as normal.
- Pointer-drag: pointerdown captures the start position + scrollLeft;
pointermove updates scrollLeft and tracks velocity in px/ms over
the most recent sample. setPointerCapture for cross-element drag.
- Momentum on release: signed velocity × 16ms decays at 0.93 per
frame, stops below 0.4 px/frame. Direction inverted because
dragging right moves scrollLeft left.
- Click vs drag discrimination at 5px total movement: under 5px,
the synthetic click passes through (card navigates); over 5px,
a capturing-phase click suppressor on the scroll element eats
the next click so a drag-then-release-over-a-card doesn't
accidentally navigate.
- Advance arrow click now runs animateScrollTo(scrollLeft + 60% of
viewport, 480ms) instead of the placeholder native scrollBy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the prev/next button pair removed last commit with a single
viewport-anchored circular control.
- 48px diameter, 1px terracotta border, terracotta chevron on cream.
- Hover / focus-visible fills to terracotta with cream chevron and a
1.06× scale-up.
- Anchored absolute inside .rr-wrap (already position: relative):
right: 32px / top: 50% / translateY(-50%).
- Toggles .rr-at-end (opacity 0.25, pointer-events: none) when the
scroll container reaches its right edge.
- First-load hint: .rr-hint class added 100ms after mount fires a
rr-advance-pulse keyframe three times (iteration-count: 3) — soft
8px shadow ring in 15% terracotta pulses out and back. Animation
stops naturally; no JS cleanup needed.
No left arrow on purpose — the path reads past → future, and the
user's instinct at any milestone is 'what's next?' The right arrow
earns its keep by hinting the existence of more track beyond the
visible window. A symmetric left arrow would just be noise.
Click handler today: scrollBy({behavior: 'smooth'}) by 60% of viewport
width. Step 4 replaces this with a custom-animated glide and adds the
drag + wheel scroll modalities.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Page-level rebuild. /roadmap.astro renders a centred header block that
reads as one calm vertical stack:
Roadmap ← 11px tracked uppercase eyebrow (--on-surface-variant)
Roadmap ← 48px serif h1, single word
A live picture of the work… ← 14px subtitle, 480px max-width
The eyebrow + h1 read 'Roadmap → Roadmap' on purpose — the tracked
uppercase eyebrow primes the eye for the serif headline, and the
repetition feels confident rather than redundant. If it starts to grate
in practice, the eyebrow's the easy drop.
'What we are building.' is gone. The earlier 'The route' sub-header
inside <RoadmapRoute> is gone. The two prev/next arrow buttons in that
sub-header are gone (the single right-edge advance arrow lands in the
next commit).
Legend moves out of <RoadmapRoute> and into /roadmap.astro as the page's
final block. With the route still .rr-fullbleed, this lets the legend
return to centred content-column width — exactly the spec's
'header-centred / route-wide / legend-centred' rhythm.
Mid-state in this commit: the advance arrow doesn't exist yet, so
there's no in-page scroll affordance beyond the still-active scroll-snap
behaviour. Step 3 adds the arrow; step 4 strips the snap and adds drag
+ wheel + animated glide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coupled changes so the route actually uses the page width:
.rr-wrap.rr-desktop gains the .rr-fullbleed class which uses the canonical
calc(50% - 50vw) margin trick to break out of the parent .page max-width.
The headline, dispatch banner, route section header, and legend all stay
inside the content column at 72rem; only the route itself widens to the
viewport. Visually reads as a magazine spread — the section header lands
centred, then the path spreads outward beneath.
Viewport-aware layout: SSR still uses the 1100 default (we can't know
the client viewport server-side), but a new mount script on
RoadmapRoute recomputes the layout against window.innerWidth and
updates:
- .rr-track width (via inline style)
- .rr-path-svg width attribute
- .rr-path-d d attribute (rebuilt from the same cubic-bezier
formula the SSR helper uses, with the live itemX values; itemY
comes from per-milestone data-y attributes since amplitude
doesn't change with viewport)
- .rr-milestone left positions
Resize: 120ms debounced handler runs the same recompute + refreshes
the arrow/fade nav state. Each milestone keeps its same data-y, so
only the horizontal spread changes — the river's vertical shape is
preserved on resize.
Initial-scroll into shipping rewired to read the .rr-current
milestone's live `style.left` after recompute, not the SSR-computed
data-initial-x value (which is now stale once the client redoes the
math).
.rr-scroll horizontal padding 60 → 80 + scroll-padding-{left,right}
60 → 80 so first/last cards have breathing room inside the now-
viewport-wide container.
Smoke as Lars: rr-fullbleed class on the wrap, data-y attributes on
each milestone, rr-path-svg id present. The SVG width and itemX
positions land at viewport-derived values after mount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root cause of the hover-clipping we kept band-aiding with larger
track-heights: a scroll container with overflow-x: auto implicitly
clips on the perpendicular axis too. Explicit overflow-y: visible
lets cards expand above and below the track freely.
Implementation matches the spec's belt-and-braces pattern. New layered
markup:
.rr-wrap → position: relative, anchors fades
.rr-scroll → overflow-x: auto + overflow-y: visible,
padding 60/60/80, scroll-padding 60/60 sides
.rr-scroll-inner → structural, no layout effect
.rr-track → positioned at the inner wrapper
The padding-top: 60px / padding-bottom: 80px on .rr-scroll gives cards
room to grow above and below the track without ever hitting a clip
boundary, even on browsers that mis-handle the overflow-x/y mix.
Edge fades reposition: top: 60px / bottom: 80px (was 0 / 16) so they
only cover the track itself, not the overflow padding zones above and
below where hover-expanded cards now live.
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 previous .rr-current was a 1.15× scale on top of an animated pulsing
::after ring — subtle, and the pulse was easy to miss against the cream
ground. Replaced with a static box-shadow ring at 6px offset in 45%
terracotta, plus a 1.3× scale on the dot itself. The pulse is gone;
the ring is now visible at rest, which is what the marker needs to do.
Hover/focus on a milestone card now scales its sibling dot via :has():
- any card hover/focus → its dot 1.15
- the current-shipping card hover/focus → its dot 1.4
The dot acknowledges that you've engaged with its card. Cleaner than
tying scroll position or click state.
:has() ships in every evergreen browser since 2023; older Firefox just
won't grow the dot, which degrades to no harm.
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>
The treasure-map metaphor is a desktop affordance — on phones it
becomes a vertical timeline instead. Markup was rendered alongside the
desktop track in commit RM.5; this commit adds the styles to actually
show it and hides the desktop bits.
Per-row layout — 32px / 1fr grid:
- Left column holds the 12px round status-coloured dot and a 1px
rgba(0,0,0,0.18) vertical line continuing down to the next dot. The
last row has no line (rendered conditionally in the markup), so the
trail ends cleanly at the final milestone.
- Right column holds the same eyebrow / serif title / description /
trailing line — but always visible. No hover, no reveal. Reading is
the only interaction.
Arrow buttons and progress dots both hidden at this breakpoint. The
mobile timeline needs no JS — pure markup + CSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vanilla TS script at the bottom of RoadmapRoute.astro. No library.
- Arrows scrollBy ±72% of the scroll-container's clientWidth, smooth
behaviour. Disabled at scroll start/end.
- Edge fades (.rr-fade-left / -right) flip opacity 0↔1 at scroll start /
end so the affordance disappears when there's nowhere further to go.
- Progress dots track scrollLeft/(scrollWidth-clientWidth) percentage,
bucketing into dots.length slots. Active dot gets .active (themed in
CSS as --on-surface).
- On mount, the script reads section.data-initial-x — the SVG x position
of the most recent shipping milestone (computed server-side from the
layout helper) — and scrolls so that x sits ~25% from the left edge
of the viewport. Clamped to [0, scrollWidth-clientWidth]. Member opens
/roadmap and immediately sees one shipped item + several ahead-of-them
items, not the very start of history.
- setTimeout(update, 50) re-measures after first paint settles
(especially relevant when SVG fonts or other late-arriving assets
shift the trackWidth by a couple of px).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The treasure-map. Static render only; nav script lands in the next commit.
Section header: serif 'The route' + tracked-uppercase legend
(Shipping / In beta / Exploring / Considering) on the left; two 32px
round arrow buttons on the right (matching the /pulse RoadmapCarousel
chrome).
Body — desktop layout (.rr-desktop):
- Outer .rr-wrap holds an overflow-x: auto .rr-scroll with snap-x.
- Track is sized to layout.trackWidth × 460. Cubic-bezier SVG path
rendered behind milestones, stroked with a horizontal gradient that
fades from #2a2520 / 0.55 alpha through to #2a2520 / 0.15 at the
travelled-stop position (computed by travelledStopFor in step 3).
- Each milestone is a 14px round dot in its status colour, with a 5px
cream halo cutting the path beneath. The 'you are here' marker (most
recent shipping item) gets a 1.15× scale + a quiet 2.4s pulse ring.
- Cards hang from each dot via a 1px / 30px vertical connector, on the
alternating cardSide returned by layout. .rr-card is the anchor target;
hover and :focus-visible both reveal the description + trailing line
via max-height + opacity transitions, so keyboard tab is a first-class
interaction (no mouse required).
- Trailing line: item.metadata_text if set, else 'Shaped by {first
names}' if attributed_members non-empty, else nothing.
- Edge fades on both sides for scroll affordance (left fade hidden when
at scrollLeft 0; right fade hides when at scrollEnd — the JS in step 6
will toggle their opacity).
Progress dots row underneath — count = max(2, min(6, ceil(items/2))).
First dot starts active; nav script will move it.
Mobile vertical fallback (.rr-mobile) markup is included now but kept
display:none on desktop. Step 7 turns it on at the (max-width: 767px)
breakpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>