Confirmed before any code:
- .rr-scroll: padding 0/140/8, scroll-padding-{left,right} 140,
overflow-x: auto only — overflow-y is implicit (the spec's known
flaky combination). paddingX default in computeRouteLayout is 60.
- <RoadmapInMotion> exists at src/components/RoadmapInMotion.astro
and is mounted at /roadmap.astro:29. firstSentenceOf helper is
local to that file (not exported, no other callers — safe to delete
outright).
- <LatestDispatchBanner> uses the three-column row grid (auto 1fr auto)
from the prior pass — that whole layout is what step 5 rebuilds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous seed conflated status='shipping' with 'about to ship' —
'Audit log export' was status=shipping target='Next week' which is
actually a queued release, not a live one. Refined so:
status='shipping' → live in production now
status='in_beta' → not yet live, GA target set
status='exploring' → on the long horizon
status='considering' → not committed
Updated distribution: 2 shipping / 2 in_beta / 3 exploring / 2
considering. travelledStop = (1 + 0.5) / 9 ≈ 0.17, so the gradient
visibly transitions from travelled to ahead right at the 'you are
here' marker — the visual story matches the data.
Targets rewritten to read in this new register:
- Traceability layer Live since March
- Document ingestion Live since late May ← .rr-current
- Audit log export GA next week (now in_beta)
- Agentic query mode July
- Contextual memory Q3 2026
- Multi-organisation graphs Q3 2026
- Multi-tenant isolation Q4 2026
- Federated learning hooks 2027 (considering)
- Open evaluation framework 2027 (considering)
Descriptions rewritten so the In motion strip pulls a meaningful first
sentence from item #2 — 'Indexing PDF, Word, and plain text with
proper chunking.'
shipped_at backdated on items 1-2 only (60 days / 7 days ago), so the
.rr-current marker lands on the most recently-shipped item (Document
ingestion), not the about-to-GA in_beta item.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New <RoadmapInMotion> component renders between the dispatch banner
and the route's section header. Pulls the most-recent shipping item
(same selection rule the .rr-current marker uses) and prints the
first sentence of its description as a 18px serif italic line
preceded by an 'IN MOTION RIGHT NOW' tracked eyebrow.
A member who only spends 5 seconds on /roadmap now still walks away
with a sentence about what just shipped — no scroll, no hover.
firstSentenceOf() is the obvious regex against the first
[.!?](?=\s|$). Bails to the 200-char slice if no sentence boundary
fits (covers 'Dr.' / 'e.g.' confusables). Returns '' on null. The
strip hides itself entirely when there's no shipping item, or when
the shipping item has no description text.
Page subtitle: 'Hover any milestone for the full story.' →
'Tap or hover any milestone for the full story.' — touch devices
don't have hover, and the kind of detail that says we're paying
attention.
Admin description-field gains a helper note: 'For shipping items:
the first sentence appears on /roadmap as the "In motion right now"
line. Make it count.' Nudges good first-sentence writing without
adding a new field to maintain.
Banner margin under the dispatch banner reduces 56 → 40px because
the in-motion strip carries its own 36px bottom margin to the route.
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>
Three changes to <LatestDispatchBanner>:
- Padding: 22/26 → 28/26 — extra top weight stops the eyebrow + title
feeling stuck to the card edge.
- Excerpt: the previous nowrap + text-overflow: ellipsis clamp was
truncating mid-sentence. Replaced with -webkit-line-clamp: 2 so a
real sentence (and a half) renders. line-height 1.55, font-size
bumped 12 → 13 to match the longer reading rhythm.
- Kind pill next to the title, same pattern as the home-page dispatch
byline pill: 9px tracked uppercase, 2/8 padding, 3px radius, kind-
pigment-tinted background at 10% opacity. Wraps below the title on
narrow widths via flex-wrap on .b-title-row.
Banner margin-bottom (set on /roadmap.astro) stays at 56px for now;
the spec's reduction to 40px is conditional on the In motion strip
landing, which is the commit after next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirmed before changing anything:
- trackHeight default = 460 in roadmap-layout.ts (midY 230). .rr-track
inline-styled at 460px. .rr-scroll has only padding-bottom: 8px — no
horizontal padding, no scroll-padding-{left,right}.
- .rr-current class is being applied to the correct dot:
class:list={['rr-dot', { 'rr-current': i === lastShippingIndex }]}.
lastShippingIndex computed via the spec's pop-of-shipping-indices.
- Progress dots row is rendered as <div class="rr-progress"> with
Math.max(2, Math.min(6, ceil(items.length/2))) .rr-progress-dot
children. Nav script toggles .active.
- Card eyebrow reads `${item.target.toUpperCase()} · ${STATUS_LABEL[
item.status]}` (or the bare label when target is null) — matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 7-item roadmap seed with the 9-item layout from the v5
spec. Distribution: 3 shipping → 1 in_beta → 3 exploring → 2 considering.
travelledStop computes to (2 + 0.5) / 9 ≈ 0.28, so the route gradient
visibly reads as 'travelled-then-ahead' rather than one solid tone.
Each item gets a target string and either a metadata_text (8 of 9) or
a fresh attribution (the one without metadata_text, 'Multi-tenant
isolation', attributed to Camilla — so the route card surfaces the
'Shaped by Camilla' trailing line via the fallback path).
metadata_text varies across the spec'd cues — 'Shaped by Lars in our
March session' / 'Pilot-tested with Mette's team' / 'Builds on
traceability layer' / 'Request beta access →' / '2 council requests' /
'Open question on key custody' / 'Council input wanted' / 'Long-term
direction'.
Attribution coverage now spans 6 of the 7 cab members so multiple
'Shaped by ...' trailers exist if metadata_text were ever cleared.
The first three shipping items get realistic shipped_at backdates
(-21 / -7 / -1 days) so the 'most recent shipping' detection lands on
'Audit log export' — which becomes the pulsed 'you are here' dot on
the route.
Smoke as Lars: /roadmap header reads 'What we are building.',
LatestDispatchBanner shows the deprioritising-public-cloud decision,
all nine route titles render, metadata_text trailing lines present in
the DOM, .rr-current marker on the most recent shipping milestone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Body content of /roadmap is fully replaced. The previous implementation
parsed content/roadmap.md with a hand-written regex into three flat
columns (IN PROGRESS / NEXT / LATER) — gone in its entirety, including
the parseSections helper and horizonColors map (page-local, not exported,
so nothing else broke).
New layout:
1. Page header — tracked 'ROADMAP' eyebrow + 48px serif 'What we
are building.' + a 14px sub line up to ~540px wide that explains
the hover affordance. 36px margin below.
2. <LatestDispatchBanner /> — renders nothing if zero dispatches.
56px below before the route's section header.
3. <RoadmapRoute items={items} /> — pulls all roadmap_items ordered
by display_order asc, falls back to id asc on ties.
Page padding 40/36/80 desktop, 32/24/64 mobile. h1 drops to 36px on
phones; banner gap collapses to 36px.
content/roadmap.md is no longer read; admin manages everything via
/admin?tab=roadmap. The markdown file stays in the repo as the seed
source for fresh databases (still consumed by scripts/seed-roadmap.js)
but the live page is database-driven.
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>
White card with 0.5px border, 12px radius, 22px/26px padding. Three-
column grid (auto / 1fr / auto):
- Left: 30px deterministic-pigment avatar + tracked 'LATEST DISPATCH ·
{relative}' eyebrow + 11px '{first_name} · {title || team}' line.
- Middle: 19px serif title + 12px single-line ellipsis excerpt.
- Right: 11px terracotta 'READ DISPATCH' link (1px terracotta bottom
border) + 10px muted 'ALL DISPATCHES →' below it.
The whole card is one <a> targeting the dispatch slug — hover lifts it
1px and tints the surface; entire surface is the click target.
Hidden entirely when getLatestPublishedDispatches(1) returns []. The
banner doesn't render an empty-state placeholder — the /roadmap page
just starts with the route in that case.
At <768px the grid collapses to a single column and the excerpt wraps.
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>
Migration 0007 (spec said 0006 but 0006 was already roadmap_considering)
adds a single nullable metadata_text column to roadmap_items — a short
admin-set narrative cue shown on hover in the route cards. ~60 chars
suggested in admin helper text. Hidden in the UI when NULL.
db.ts: RoadmapItem type gains the field. createRoadmapItem + updateRoadmapItem
accept an optional metadata_text parameter. moveRoadmapItem passes it through
when swapping display_order between siblings so the helper preserves it.
Admin: /admin?tab=roadmap edit form gets a new 'Hover note' input under
the description, with the helper text and a 120-char hard cap. Empty
string saves as NULL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirmed before writing code:
- Current /roadmap reads content/roadmap.md via a hand-written
parseSections regex into three flat columns (IN PROGRESS / NEXT /
LATER) — it doesn't query roadmap_items at all. Full body replace
in step 8; AppLayout + new editorial page header stay.
- roadmap_items.status enum already covers 'considering' from 0006.
No need to fold the enum extension into this migration.
- getLatestPublishedDispatches(limit: number) is in db.ts (line 1177);
pass 1 for the banner. No new helper.
- No tests reference /roadmap markup. The route layout helper is the
only new unit-test surface.
Divergence: spec asks for migration 0006_roadmap_metadata.sql but that
number's already used by roadmap_considering. Going with 0007.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The italic 'B' on 'Bifrost' was being clipped at the top because
background-clip: text only paints inside the inline element's content
box — which at the previous tight line-height didn't include the
ascender flourish.
Two cooperating fixes:
- .wordmark-project gets line-height: 1.5 so the parent line-box has
enough vertical room for the italic ascender to live in.
- .wordmark-bifrost becomes display: inline-block with 4px top / 2px
bottom padding. That extends the element's content box vertically so
the gradient-clip mask covers the full italic glyph including the
serif curl above the cap-height.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for the previous baseline-align attempt:
- 'Bifrost' was getting its italic ascender clipped because line-height
was pinned at 1. Switched to line-height: 1.3 on .wordmark-project so
the swash on the italic 'B' / 'f' has room to render.
- The lockup was reading low overall because align-items: baseline +
translateY(3px) on the SVG together shifted the SVG below the text
baseline. Reverted to align-items: center for reliable cross-browser
centring, dropped the SVG transform, and pulled the SVG height from
22 → 20 so the visible glyph height of the wordmark matches the cap-
height of Newsreader at 18px more closely. The '·' bullet keeps a
-2px nudge because Newsreader's bullet sits typographically above its
baseline at this size.
- 2px padding-top on .wordmark-project compensates for the small bit
of descender slack at the bottom of the Fenja SVG — caps now sit on
the same visible cap-line as the wordmark glyphs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small adjustments so the lockup reads as one mark, not a logo
next to a label:
- align-items: baseline on .wordmark-link so the typographic baseline
drives vertical alignment (instead of bounding-box centres).
- 'Project' + 'Bifrost' bumped from text-body-md (16px) to 18px with
line-height: 1 and tracking-snug. The bigger weight matches the
visible glyph height of the Fenja SVG and pulls the bullet into
conversation with the wordmark instead of floating between them.
- The Fenja SVG gets a translateY(3px) nudge to compensate for the
descender slack in its 265×101 viewBox — the SVG's actual baseline
sits a couple of px above its bounding-box bottom, so the flex
baseline-align would otherwise place 'fenja' above the text baseline.
- The '·' separator drops 1px and grows to 18px so it sits between the
two marks rather than floating above them.
The exact numbers are empirical — eyeballed in dev — but the rationale
each pin to a baseline relationship rather than 'magic px offset'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.wordmark-project was Manrope 500 — visually a sans-serif label next to
a serif-italic gradient noun. Switches to Newsreader 400 so the
wordmark reads as one continuous editorial mark, with the italic +
gradient on 'Bifrost' doing the visual lift instead of a family contrast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Council section gets a proper carousel — a horizontal marquee that
moves continuously across the page, listing every cab member in turn
rather than a fixed-size grid.
Implementation:
- Members rendered twice in a single flex track; CSS keyframe translates
from translateX(0) to translateX(-50%) over 40s+ (duration scales with
member count via the --marquee-duration inline custom prop, capped at
6 sec per member or 28 sec minimum). At -50% the first copy is fully
offscreen and the second copy occupies the visible window seamlessly;
the loop resets without a visible jump.
- aria-hidden on the duplicated copies so screen readers don't double-
announce.
- mask-image fades both edges so members slide in and out softly rather
than clipping at the container edge.
- Paused on hover so a reader can stop and parse a tile.
- prefers-reduced-motion: animation off and the strip becomes a quietly
scrollable horizontal list — keyboard / trackpad users can pan
manually instead of relying on the animation.
Seed adds 3 more cab members for a total of 7 (Mads Lindberg, Camilla
Storm, Frederik Lund) with backdated cab_joined_date so member_numbers
allocate 5/6/7. Each gets title + pull_quote + focus_tags consistent
with the existing four. Tenure spread is now 3 → 24 weeks across the
seven members so /members renders meaningfully varied 'member since'
dates.
The previous 4-tile grid + 5th-tile-as-link case is gone; the marquee
loops the full set so no truncation is needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pulse vote widget on /pulse and on /dispatches/[slug] now behaves
the way the v5 spec asks:
- Eyebrow shortens: 'This week's pulse' → 'This week's'.
- Status line copy changes shape depending on whether the viewer has
voted yet. Pre-vote: 'Vote to see how the council weighs in · Closes
TUESDAY' — sets expectations that the percentages reveal after a
vote. Post-vote: '2 of 7 voted · Closes TUESDAY · Click to change' —
tells the viewer they can change their mind.
- Each option now renders a right-aligned tabular-nums percentage badge,
but only after the viewer has voted. Pre-vote there's no percentage
on screen at all — voting is a commitment, not a peek.
- Options stay clickable after voting (no `disabled`). Re-clicking a
different option changes the vote.
DB: new helper castOrChangeVote(pulseId, userId, optionIndex) does an
UPSERT — INSERT on first vote, UPDATE option_index + voted_at on
subsequent. Returns true if this was the brand-new vote, so the caller
can write the 'voted' activity row exactly once and not double-count
changes-of-mind in the feed. castVote(...) (INSERT OR IGNORE) stays in
db.ts for callers that explicitly want first-vote-wins semantics.
Status-class rename: .locked → .closed on both pulse-option and
inline-poll-option. The class now reflects what it actually represents
(the pulse is closed) rather than the false invariant 'the user has
voted and can't change'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greeting (1, 2): the tracked date label above the greeting and the
milestone tenure line below it both come out. The greeting is now just
'Good morning, Jonathan.' alone on the left, with the MEMBER · NNN /
'Founding circle' stamp on the right. tenureMilestone helper + test stay
in place — they're still useful for future surfaces but no longer
rendered on /pulse.
Section titles (3): the noun in each section title gets serif italic +
a trailing period — 'On the <em>roadmap.</em>' and 'The <em>council.</em>'.
The two-word framing reads better visually than the previous flat
sans-serif title.
Spacing (6): bumped the major section transitions. Greeting → hero now
80px (was 56), also-coming-up → editorial 96px, editorial → roadmap
96px, roadmap → council 96px. The hero → also-coming-up gap stays at
the deliberate 18px because they're a pair, and the dispatch column's
internal margin-top 48px to its 'Earlier' list stays unchanged because
they belong to the same story.
Footer (7): 'Council manifesto' link removed from the footer for all
pages. The standalone /council-manifesto route stays in the codebase
(orphaned, not linked) so it isn't a 404 when someone has the URL.
Active nav link (8): the previous 4px terracotta dot below the active
link is gone. The active link now reads in a different typographic
register entirely:
- terracotta colour
- serif italic Newsreader (vs sans uppercase for inactive)
- sentence case (text-transform: none — the label appears as 'Pulse'
instead of 'PULSE')
- leading '· ' prefix dot for an additional 'you are here' marker
Four cues at once, no positioned overlay, no floating elements. The
typography shift alone tells you which page you're on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spacing — explicit per-section margins on /pulse rather than a single
gap. Page is padding: 40px 36px 80px now. Transitions match the spec:
greeting ─ 48px ─ below nav
greeting ─ 56px ─ hero
hero ─ 18px ─ also coming up (intentionally tight; related)
also ─ 72px ─ editorial row
editorial ─ 72px ─ roadmap
roadmap ─ 72px ─ council
The hero, editorial, roadmap and council transitions all sit at 72px so
the page reads as four distinct registers rather than a slab stack. The
hero → also-coming-up gap stays deliberately tight at 18px because the
two are a pair (the strip is the lighter outro to the indigo card).
Council section restructured to match the roadmap carousel framing:
- Outer card chrome dropped — no more single white surface wrapping the
grid. Section is just a header row + a 4-column grid of tiles.
- Header row: 22px serif 'The council' on the left, 11px terracotta
tracked uppercase 'See who our council is made up of →' on the right.
Same pattern as the roadmap header.
- Tiles: 38px avatar (down from 56), 15px serif name, 11px title,
10px tracked organisation. No background, no border. 24px grid gap.
- First 4 members render; if more, a 5th tile replaces the would-be
fifth member with a right-aligned 'See all N council members →' link.
With the current 4-member seed this case isn't exercised but the
branch is in place for when the council grows.
- 2-up on tablets, 1-up below 520px.
Seed update: roadmap now has 7 items spanning all four statuses (2
shipping / 1 in_beta / 2 exploring / 2 considering) ordered by
display_order 1..7. Traceability layer carries the 'Shaped by Lars'
attribution; Agentic query mode is attributed to Anna; Contextual memory
to Henriette. The rest are unattributed so the attribution trailer's
hidden case is exercised too. With 7 items the carousel arrows engage
and the right-edge fade is visible at start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 3-column grid on /pulse with a CSS scroll-snap carousel.
3 cards per view on desktop (flex 0 0 calc((100% - 2px) / 3)), one full
card with a slice of the next at 88% on mobile.
Header row: serif 22px 'On the roadmap' title on the left; right side has
the 'See the full roadmap →' section link plus two 30px round arrow
buttons. Arrows disable (opacity 0.25) when at start/end of scroll. Hidden
on mobile — touch swipe is the affordance.
Carousel-scroll has 0.5px top + bottom borders in rgba(0,0,0,0.08).
Scrollbar hidden cross-browser. Each card has a 0.5px right border (last
card excluded), 24/26 padding, background --background so it sits on the
cream rather than introducing a white surface.
Card contents:
- 6px status dot in the status colour + tracked 10px label
'{STATUS_LABEL} · {TARGET}' in the same family colour. The
considering tier uses the lighter #d4d2c8 dot with the #b4b2a9 label
to distinguish it from exploring.
- 19px serif title, line-height 1.2.
- 12px description, line-height 1.55, --on-surface-variant. If the
item has attributions, an attributionLine() trailer appends 'Shaped
by Lars.' / 'Shaped by Lars and Anna.' / etc. in --on-surface-muted.
Right-edge fade gradient (80px) fades to opacity 0 at scrollEnd via a
small vanilla script. The script also handles arrow disabled state and
scrollBy ±cardWidth on click. No library.
Items are loaded with the full set ordered by display_order ASC (then
id ASC tiebreak) — admin orders chronologically nearest-to-furthest and
the carousel just consumes that.
If items.length < 4 the arrows + fade are hidden; the cards still flex
naturally and don't actually need to scroll.
/pulse: dropped the old .roadmap-section/.roadmap-grid/.roadmap-card +
status-dot/breathe styles. Carousel does its own.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Below the indigo hero, two new sections replace the previous fenja-section
two-box layout.
ALSO-COMING-UP STRIP — plain text on cream, not a card. Flex
justify-between, 4px vertical padding. Up to 2 upcoming events on the
left, each rendered as a 22px serif day number beside a stacked
column: 9px tracked 'MONTH · KIND' label on top, 13px event title on
bottom. A 1px × 28px vertical divider in rgba(0,0,0,0.08) between the
two items. Right side carries the lone 'All gatherings →' link in 11px
terracotta tracked uppercase.
EDITORIAL ROW — 1.7fr / 1fr grid with a 56px column gap. Asymmetric
on purpose; the dispatch column carries the weight.
Dispatch column (left, wider, no card chrome):
- Byline row: 26px deterministic-pigment avatar + 12px author first
name + 11px relative time + kind pill (9px tracked, 2/8 padding,
3px radius, pigment-tinted background, margin-left: auto).
- 32px serif headline, line-height 1.15.
- 14px lead paragraph, line-height 1.7 (via dispatchLongPreview).
- 'Read the full dispatch' link — 11px terracotta tracked uppercase
with a hard 1px terracotta underline (not the global .section-link
italic treatment — this one's a button-ish affordance).
- Margin-top 48px, padding-top 28px, 0.5px on-surface-border line,
then the EARLIER list. Rows are 15px serif title left, 10px tracked
'{KIND} · {N}D' right. Day count is rounded UTC delta.
Pulse column (right, narrower, no card chrome):
- 10px tracked 'this week's pulse' eyebrow with a 5px terracotta dot
prefix. The only return of an eyebrow on /pulse.
- 22px serif question.
- 11px tracked status line: 'CLOSES {DAY} · {N} OF {TOTAL} VOTED'.
- Options stack with 6px gap. Each option has no top/right/bottom
border, only a 2px left border. Default left-border is rgba(0,0,0,
0.1); selected option flips to terracotta and gains a 5% terracotta
background tint. Letter prefix (A/B/C/D) is tracked, 10px, muted
by default and terracotta when chosen.
- Vote action POST handler unchanged — same /pulse?action=vote path
introduced in v2.
If the featured dispatch has no attached poll, the right column
doesn't render and the dispatch column takes the full row (the spec's
single-column collapse case).
Stale CSS for the old events-card / hero-* / coming-up-* / fenja-* /
fenja-poll-* class names all deleted in the same pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained component for the next gathering. Three-column grid:
- Left (140px): 10px tracked weekday · 88px serif day numeral (zero-
padded, line-height 0.85) · 11px tracked MONTH · HH:MM line. All on
the bleached --on-ink cream.
- Middle (1fr): 10px tracked NEXT GATHERING · KIND eyebrow · 26px serif
title · 13px description (--on-ink-body @ 85%) · 12px location line
(--on-ink-muted @ 65%).
- Right (auto, min 140px): tracked duration label right-aligned, then
up to 3 confirmed attendees as 'name + 18px avatar' rows, with a
+N overflow chip beneath if more.
Bottom strip after a 0.5px --ink-divider top border, padding-top 22px:
- Left: tracked '{capacity} SEATS · {n} CONFIRMED | CLOSES {DAY}' status
line, with a low-opacity '|' divider character between the halves.
- Right: form actions — 'Can't make it' link + cream pill 'Save your
seat →'. If already confirmed, swaps to outlined 'You're confirmed
✓' + a small 'Change' link.
Empty state collapses the action strip and renders a serif italic
'Nothing scheduled yet — when we have something, you'll be the first to
know.' (the only italic on the card, mirroring the spec).
/pulse swap: the previous inline events-card markup (hero body + bundled
coming-up grid + see-all link) is replaced by a single <EventHeroCard>
slot. The 'Also coming up' strip lives outside the indigo card now and
will be built in step 6; for this commit a temporary comment marker
holds its slot. AvatarPile import is dropped from /pulse since
EventHeroCard renders the attendees list inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single-column greeting block with a 1fr / 1fr row. The left
side holds the date label (MONDAY, 11 MAY in tracked uppercase 11px,
swapped from the bullet separator to comma per spec), the serif 36px
greeting line ('Good morning, Jonathan.') with the first name italic,
and a milestone-aware tenure line. The right side (only rendered when
the user has a member_number — i.e. cab members) shows 'MEMBER · NNN'
zero-padded above 'Founding circle' in serif.
tenureMilestone(days) is the new helper that produces the milestone copy
through seven buckets: 0 / 1–6 / 7–20 / 21–55 / 56–180 / 181–364 / 365+.
The 1–6 bucket renders 'Day 2.', 'Day 3.' etc. (day count is days-since-
join; day one is the first 24 hours after joining). The months bucket
uses Math.floor(days/30) with a min of 2 to avoid a one-day-after-21
reading '1 months in.'. Years pluralise normally.
daysSince(iso) — small wrapper that handles SQL date strings ('YYYY-MM-DD
HH:MM:SS' or pure dates) and returns whole-day UTC delta. Same UTC
coercion the rest of the page uses.
Tests: 7 cases for tenureMilestone at the boundary days 0/1/7/22/60/200/
400 (plus an extra 730 to exercise the year plural). 43/43 passing.
This block replaces the in-line <MembershipCard> idea that was floating
around in earlier passes; the dark indigo card stays in src/components
for /members/:slug, but on /pulse the two-line stamp is enough.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Active link no longer carries a background fill or pill. Instead, a
4px terracotta dot is centred 22px below the link via ::after. Quieter,
more confident, and survives the cream/white surface change without
needing a hover-area redesign.
- Vertical divider before the user's name shifts from the existing ghost-
border colour to a solid rgba(0,0,0,0.15) line, 18px tall with 18px
padding either side (replacing the prior --space-2 / 8px). Still a
scaled-1px element rather than a pipe character.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 0006 (the spec said 0005 but that number was already taken by
polls_on_dispatches from the previous session): rebuilds the
roadmap_items CHECK to ('shipping','in_beta','exploring','considering')
and renames any existing 'beta' rows to 'in_beta' in-place. FKs from
roadmap_attributions are preserved across the DROP/RENAME by toggling
PRAGMA foreign_keys off around the rebuild — attribution count unchanged
after migrate (verified 4 rows survive on the demo DB).
Tokens (src/styles/tokens.css): adds --on-ink, --on-ink-body,
--on-ink-muted, --ink-divider. The bleached #fffcf7 cream replaces the
warm #e8e0d0 --ink-text wherever it sits on indigo. Legacy --ink-text /
--ink-muted stay in tokens.css for now — if any later commit references
them they remain defined; the migration of existing call sites is
covered here.
Migrated to the new tokens in this pass:
- src/components/MembershipCard.astro (members/:slug card)
- src/pages/events.astro (hero invitation card)
Both render with cleaner whites on indigo as a side effect.
Code updates for the new status enum:
- db.ts: RoadmapStatus = shipping | in_beta | exploring | considering
- admin/RoadmapTab.astro: Status select gains Considering + In beta;
grouped section iteration covers all four
- admin/index.astro: validation list updated
- scripts/seed-roadmap.js: 'In progress' markdown bucket → 'in_beta'
- pulse.astro: roadmapStatusDot + roadmapStatusBlurb temporarily widened
(full rewrite of that section lands in step 7)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirmed before writing code:
- /pulse render order: greeting · events-card · fenja-section (article +
inline poll) · roadmap-section (3-col grid) · council-section.
- <MembershipCard> already absent from /pulse (removed in v2). The
component stays in src/components for /members/:slug use.
- events.kind: ('dinner','office_hours','summit','virtual','working_session')
- roadmap_items.status: ('shipping','beta','exploring') — needs widening.
- tokens.css: --background #faf6ee, --ink #2c3a52, --ink-text #e8e0d0,
--ink-muted #b8a989. No --on-ink* tokens yet.
Divergences resolved with the user before step 2:
1. Migration is 0006_roadmap_considering.sql, not 0005 — that number is
already taken by polls_on_dispatches from the previous session.
2. roadmap_items.status will rename existing 'beta' rows to 'in_beta'
(matching the spec's canonical name) in the same migration.
3. --on-ink, --on-ink-muted, --on-ink-body, --ink-divider added; existing
--ink-text / --ink-muted references on indigo surfaces (MembershipCard,
/events hero, /pulse hero) migrate in the same pass so old tokens
aren't left half-used.
4. Eyebrow uppercase labels return on /pulse only (NEXT GATHERING ·
KIND, EARLIER, THIS WEEK'S PULSE, MEMBER · NNN). /members /events
/dispatches keep the eyebrowless treatment.
5. Pulse column shows the featured dispatch's attached poll. No attached
poll ⇒ editorial row collapses to one column. Aligns with the
polls-attached-to-articles model from the previous pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Layout (per the v4 follow-up spec):
1b. Latest from Fenja is now a two-box layout when there's an attached
poll: article on the left (wider), poll widget on the right. Without
a poll, the article box takes the full row. Both boxes are surfaced
on --surface-card with the same generous padding so they read as
sibling pieces.
1c. Featured excerpt is extended to ~720 chars (was ~520) via a wider
threshold on dispatchLongPreview. Below the article+poll row, the
next two most-recent published dispatches render as minimalist rows
— just title + kind + relative time, separated by ghost borders.
2. Hero event: date column is now 150px wide (was 110px); grid uses
align-items: center so the date+detail columns are vertically aligned
rather than top-stuck. Day number scaled up to 3.5rem (was 2.75).
Outer card padding bumped from --space-7 to --space-10. Hero title
bumped to 2rem.
3. More air: page-level section gap --space-10 → --space-12. Each
on-page card has been re-padded; outer page horizontal padding goes
down to --space-16 from --space-20 to match the narrower canvas.
6. Council members no longer have individual card chrome. One outer
--surface-card wraps the whole grid; each member cell is just an
avatar + name + title + company stack with no background or border.
Cells use a larger 6/8 grid gap so they don't crowd each other.
Inline poll widget on /dispatches/[slug]: when a dispatch has an
attached pulse, the article body is followed by a compact poll card
matching the /pulse-side widget. Vote POST handled inline; the page
re-renders with the locked + result-bar state.
scripts/seed-demo.js: the existing 'Which milestone should we anchor Q3
around?' pulse now attaches to the decision dispatch ('We are
deprioritising public-cloud parity for Q3') via pulse_id. Other
dispatches stay poll-free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema (migration 0005): dispatches gains a nullable pulse_id FK to
pulses(id) ON DELETE SET NULL. Partial index on the populated rows.
The pulses + votes tables themselves are unchanged — vote uniqueness,
status derivation, and the existing tests still hold; only the entity
relationship changes.
db.ts:
- Dispatch type gains pulse_id. New DispatchWithPoll = DispatchWithAuthor
+ a hydrated poll (pulse + counts + viewer's vote).
- createDispatch accepts an optional poll input — if provided, creates the
pulse first in the same transaction and stamps dispatches.pulse_id.
- updateDispatch grows two new arguments: poll (input or null) and a
pollExplicit flag. The flag distinguishes "leave the existing poll
alone" (undefined) from "the admin actively chose to detach / replace
it" (true). The detach path nulls pulse_id; the replace path mutates
the existing pulse in place via updatePulse so vote history survives.
- publishDispatch / archiveDispatch are now wrappers that also publishPulse
/ closePulse on the attached pulse. Dispatch state drives poll state.
- getDispatchWithPoll(dispatchId, viewerId) — single call for the page
renderers.
Admin:
- The Pulses tab is removed from the admin tab nav. The route + POST
handlers stay in place so existing draft pulses aren't orphaned, but
the entity is no longer a place admins go to think.
- DispatchesTab form gains a poll fieldset: question + 4 option inputs
(first two required if any are filled) + opens_at + closes_at. A
hidden poll_explicit flag tells the server the form intentionally
asserted the poll state (so leaving the fields blank during edit
detaches rather than no-ops). On edit, fields prefill from the
attached pulse if present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- --content-max: 83rem → 72rem (back to the original width — the wider
canvas tested less well in practice; sections felt under-furnished).
- .section-link: italic serif retained, but now black (--on-surface)
instead of terracotta, underlined with a fine 0.5px stroke and 4px
text-underline-offset, font-size bumped from text-body-md to
text-title-lg (1.125rem). Hover slightly thickens the underline rather
than swapping colour, so the link still reads as a link at rest.
- .section-link--ink modifier swaps to cream on the dark events card.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies the v3 rules globally: italics reserved for the Bifrost wordmark
and section-links; uppercase tracked eyebrows removed from page heads
and content sections.
Pages updated:
- /members: drops the 'MEMBERS' eyebrow + italic on h1 + italic on member
name + italic on pull-quote.
- /events: drops the head eyebrow + 'Next up · Members only' /
'Invitation by hand' hero eyebrows + 'Also coming up' / 'Past
gatherings' sub-section eyebrows + italics on hero day, hero title,
also-row titles, past-card titles. The past-gatherings header-row
'View all →' link migrates to the bottom of the section as a
section-link.
- /events/past: drops the eyebrow + italic h1; back-link uses
.section-link.
- /dispatches/: drops the 'DISPATCHES' eyebrow + italic h1 + dispatch-row
title italic + date column italic.
- /dispatches/[slug]: drops italic on the article title + h2/h3 inside
rendered markdown + blockquote italic + adjacent prev/next title
italic. Back-link migrates to .section-link.
- /roadmap: drops the 'ROADMAP' eyebrow and the .lead class on the
subtitle.
Orphaned eyebrow class rules left in place; harmless and the next
visual pass can sweep them with the rest of the unused CSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Events card (--ink):
- 'Next up · Members only' and 'Invitation by hand' eyebrows removed.
- All ink-card text uses cream tones (rgba(232,224,208,...) at 92/75/70/65%)
instead of the warm tan --ink-muted; the previous low-contrast labels
read 'dark' on the indigo and now read uniformly light.
- Italic font removed everywhere on the card (hero day number, hero title,
coming-up titles, etc.) — italic is reserved for the Bifrost wordmark
and section-links only.
- Past gatherings dropped from /pulse entirely; the listing lives on
/events and /events/past.
- 'Also coming up' is now a grid of small bundled sub-cards inside the
blue surface (auto-fit minmax 220px). Each card shows date + title +
meta only — no RSVP action, no per-row submit form.
- 'See all events →' section-link replaces the old past-gatherings
'View all →' as the sole bottom-of-block link to /events.
Latest from Fenja (unboxed):
- Card surface dropped. Article sits on the cream page background.
- Excerpt extended via new dispatchLongPreview(d, 520) helper —
sentence-boundary cut at ~520 chars (was ~200). Title in serif regular,
not italic.
- 'Read the full dispatch →' section-link at the bottom.
Roadmap (horizontal):
- Three roadmap items become a 3-column grid of small white cards instead
of a vertical list. Each card has status dot + title + status blurb
with consistent min-height.
- 'See the full roadmap →' section-link at the bottom.
Council members (larger cards):
- Was a flowing pill row, now an auto-fit grid (minmax 260px) of larger
white cards. Each card has a 56px avatar + name + title + company,
with generous padding for whitespace. Company name is the new field.
- 'See who our council is made up of →' section-link at the bottom.
General (eyebrows + italics): all uppercase tracked eyebrow labels gone
from /pulse — date label, 'Latest from Fenja', 'From the roadmap', 'The
council', etc. Italic body text removed throughout — greeting, titles,
member names, dispatch title, roadmap titles. The Bifrost wordmark in the
header and the .section-link utility class are the only remaining italics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Width: --content-max 72rem → 83rem (15% wider, per the v3 follow-up
spec). Every page that uses var(--content-max) gets the new bound.
- Wordmark in the top-left nav: Fenja logo · "Project Bifrost". 'Bifrost'
is serif italic with a horizontal pigment-rainbow gradient
(terracotta → ochre → copper → indigo → heather), background-clip:text.
The bullet separator uses --on-surface-muted at 1rem.
- Global .section-link utility class: serif italic, terracotta, no
underline, no all-caps. Modifier --ink for use on the dark events card.
This becomes the only italic body text on the site (along with the
Bifrost wordmark); everywhere else loses italics in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swaps real-looking organisation domains in seed fixtures for the
virkN.dk placeholder pattern, so demos and screenshots can't be misread
as implying real-world relationships with the originals.
- mette@ssi.dk → mette@virk1.dk
- lars@rigspolitiet.dk → lars@virk2.dk
- jonathan@fenja.ai → jonathan@studio.test (separate fake domain for the
team account, kept distinct from the council virkN namespace)
- anna@kommune.dk → anna@virk3.dk
- soren@energinet.dk → soren@virk4.dk
- henriette@dnv.dk → henriette@virk5.dk
Organisation strings get the same treatment ('Virksomhed 1' …).
Also fixes two latent bugs surfaced while re-seeding:
- seed.js's INSERT didn't populate the slug column added in migration
0003. After a re-seed the three base users had NULL slugs. Add a
kebab-from-name fallback in the INSERT so slugs round-trip.
- seed.js's DELETE chain pre-dated the Phase 1/2 schema additions and
failed FK constraints (pulses/dispatches/events/votes/activity/
join_requests/roadmap_attributions). Extend the wipe order so all
user-referencing tables clear before users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures /pulse around three blocks instead of seven, per the
follow-up simplification spec.
Nav: Events and Members drop out of the top bar. Becomes
Pulse · Roadmap · [Admin]. Members and Events remain reachable via the
two new on-page links ('See who our council is made up of →' and
'View all →' under past gatherings).
/pulse render order:
1. Greeting (unchanged)
2. Events card (--ink). One blue card now holds all three sub-sections:
- Hero NEXT UP / INVITATION BY HAND treatment for the soonest event,
full date+title+desc+capacity+RSVP CTA. AvatarPile of confirmed.
- 0.5px ink-muted divider, then ALSO COMING UP — compact list of other
upcoming events with their action-label fallback. Less visual weight,
same dark surface.
- Divider, then PAST GATHERINGS — compact list with notes / no-notes
indicator, plus a 'View all →' link to /events/past.
- Empty state retains the visual weight of the card if nothing is up.
3. Combined Roadmap + Latest from Fenja (--surface-card). One white card,
two stacked sub-sections separated by a 1px divider. Top is the single
most recent published dispatch (was 'Latest from the studio', now
labeled 'LATEST FROM FENJA'; 'All updates →' link to /dispatches). Bottom
is the three most-recently-updated roadmap items + 'See the full roadmap →'.
4. Members strip (--surface-card). Every cab user as a pill (avatar + name
+ title) flowing horizontally. Header has the 'See who our council is
made up of →' link to /members.
Removed from /pulse:
- This-week's-pulse voting block (deferred → todo.md, idea is to fold
poll-shaped dispatches into the Latest from Fenja stream)
- MembershipCard (the COUNCIL · NNN identity card)
- RecentlyFromTheCouncil (deferred → todo.md)
- Bottom event-row with the two small dinner + studio hours cards (events
moved to the top hero card, so these were duplicates)
POST handler is now RSVP-only — vote handling went with the pulse block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two things lifted off /pulse in the follow-up pass need somewhere to live
so they aren't forgotten:
- The 'Recently from the council' feed (RecentlyFromTheCouncil reading
from the contributions table). Component and underlying table stay in
place; only the /pulse embed is gone. Records the options for re-
introducing it: inline in the Latest-from-Fenja card, a dedicated
Voices section further down, or stop surfacing on the home page.
- 'This week's pulse' voting card. pulses/votes schema + admin Pulses tab
stay. Records the merge-into-dispatches idea — fold poll-shaped
dispatches into the Latest from Fenja stream rather than rebuilding a
parallel block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Studio hours rename pass (one commit, grep-driven):
- /pulse right-card eyebrow: 'Office hours' → 'Studio hours'
- scripts/seed-demo.js: event title + description match the spec
('Studio hours with Jonathan' · '30-minute slots. Open agenda. Drop in
when you've got something to talk through.')
- No code-level enum changes — the kind value office_hours is preserved
for back-compat; display labels switch wherever surfaced (admin form
select, /pulse eyebrow, /events list and meta)
scripts/seed-demo.js — Phase 2 demo state. Destructive in scope (wipes the
data tables it owns then re-inserts), idempotent on re-run:
- 4 cab members: Lars (existing) + Anna Kjær / Søren Vedel / Henriette
Rask. cab_joined_date staggered 24/6/4/2 weeks ago so tenures vary.
title, pull_quote, focus_tags populated per spec. member_number
backfilled via the same SQL pattern as migration 0004 (deterministic).
- 1 active pulse with 2 of 4 council members voted. Vote count on /pulse
now reads '2 of 4 council members have weighed in.' — the line voted
test was designed to lock down.
- 4 roadmap items: Traceability layer (shipping, attributed to Lars),
Document ingestion (beta, attributed to Anna + Søren), Contextual
memory (exploring, attributed to Henriette), Agentic query mode
(exploring, unattributed).
- 3 contributions, most recent ('inline annotations' idea by Søren) has
3 reactions — populates the RecentlyFromTheCouncil card.
- 4 published dispatches at 2/5/9/12 days ago covering all four kinds
(decision / behind_the_scenes / update / note). Real-ish prose so the
excerpt cutter has actual sentence boundaries to find.
- Events: hero dinner 5w out, Studio hours 2w out, a working_session 3w
out (exercising the new kind), April roundtable 3w ago with a
notes_url, March launch dinner 7.5w ago without notes (exercises both
past-card thumb modes). Hero dinner has 1 confirmed RSVP (Lars) to
drive the avatar pile at small scale.
- Activity rows for the (now-hidden but still-written) feed so admin's
Activity tab has something to display.
Smoke (curl as Lars): /pulse renders 'Good afternoon, Lars.' · COUNCIL · 001
· '2 of 4' · Latest from the studio · Recently from the council · Studio
hours. /members shows all four members with pull quotes + focus pills.
/events shows the dinner hero, 'Save your seat →', Studio hours +
working session in 'also coming up', April and March in 'past gatherings'.
/dispatches lists all four; /dispatches/{slug} renders body + adjacent
prev/next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Dispatches tab — full CRUD matching the existing pattern (?tab=
querystring, plain HTML POST, hidden action field, redirect-with-?msg=).
Author select is restricted to Fenja-role users (defaults to the current
admin). Create form has a status toggle (draft / publish on save).
publish_dispatch stamps published_at via the existing helper; archive
preserves it. Body is a monospace textarea so admins can see markdown
without proportional kerning confusion.
Participants tab gains a per-row Edit link. When ?tab=participants&edit=ID
is set, the table is replaced by <UserEditTab>: title input, comma-
separated focus_tags input (parsed server-side via parseFocusTags), a
pull_quote textarea with a 200-char live counter, and a read-only
member_number display (set on role transition to cab). The inline role
dropdown + deactivate stay on the table.
EventsTab — adds audience, duration_label, action_label, notes_url
inputs. Kind <select> now labels office_hours as 'Studio hours' and
exposes working_session as the new fifth option.
Admin action-link / action-cell styles were missing on admin/index.astro
(they were defined only inside per-tab components); added to the page
stylesheet so the new Participants Edit link inherits the same look.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/dispatches: editorial header (DISPATCHES · 'Notes from the studio.' ·
'Decisions, half-built ideas, and things we've changed our mind about.')
+ a vertical list of every published dispatch. Each row is a single link
target with a three-column grid: 180px byline (avatar + author name + title
or role label) / 1fr body (serif italic title + kind pill, then a single-
paragraph excerpt) / 130px date column. 0.5px bottom borders, hover tint.
/dispatches/[slug]: 720px single-column read view. Header is kind pill +
publish date, serif italic title at 2rem, author byline with 32px avatar.
Body uses the existing renderMd() (marked) with serif italic h2s, copper
blockquotes, mono code blocks. Footer is a 0.5px divider then two adj-card
links (prev / next in published order) on opposite ends — the missing side
renders an empty grid slot so layout is preserved.
Canonical-slug redirect: if /dispatches/12-old-title is hit but the title
has since changed, the page issues a 302 to /dispatches/12-new-title. id
is the authority, kebab title is for readability.
format.ts: adds roleLabel (pilot/cab/fenja → 'Pilot' / 'Council' /
'Fenja team') for the byline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/events:
- Header: EVENTS · 'Where the council gathers.' · one-line subtitle
- Hero invitation card on --ink for the soonest non-office_hours event:
NEXT UP · MEMBERS ONLY / INVITATION BY HAND eyebrow strip, two-column
date/detail body separated by a 0.5px vertical line, foot strip with
'{capacity} seats · {confirmed} confirmed' + AvatarPile of confirmed
attendees and the RSVP CTA. The RSVP button toggles between cream-on-ink
'Save your seat →' and outlined 'You're confirmed ✓ Change'. Empty-state
card retains the visual weight when no upcoming non-office_hours event.
- ALSO COMING UP — every other upcoming event including office_hours.
Three-column rows; the right column uses event.action_label or falls back
to defaultActionLabel(kind). Studio hours surfaces with 'Book a slot →'.
- PAST GATHERINGS — two-column grid. Each card has a 56px thumb: photo_url
if set, else a copper-tinted notes square when notes_url is present, else
a deterministic two-pigment gradient block. View all → links to /events/past.
/events/past — same card component, full list of starts_at < now() events.
No boolean past flag column; filter is purely date-based.
AvatarPile (src/components/AvatarPile.astro) — reusable. Overlapping circle
slots with a 1.5px border in a caller-provided colour (defaults to surface,
the hero card overrides to --ink so circles read on dark). Stacks z-index
so leftmost is on top; +N overflow chip at the end.
format.ts: adds eventKindLabel (office_hours → 'Studio hours') and
defaultActionLabel per kind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header: MEMBERS eyebrow, serif italic 'The council.', subtitle pitched at
'an invited circle of operators' — no number, no seat count anywhere.
One row per cab user, ordered by member_number asc. Three-column grid
(60px / 1fr / 130px, 18px gap) with 0.5px bottom borders. Avatar uses the
deterministic id-pigment palette; the same id always yields the same
colour across the portal.
Body column: serif italic name; if the row belongs to the signed-in user,
a small uppercase tracked 'You' tag in terracotta. One metadata line —
title · organisation · Member since {month year} — title is dropped if
null. The pull-quote block is only rendered when pull_quote is non-null;
its 2px left border is the user's pigment at 40% opacity (50% on the
signed-in user's own row).
Right column: up to 3 focus-tag pills, vertical right-aligned, tinted in
the same pigment family (15% bg / 100% colour). Empty focus_tags renders
nothing — no placeholder pills.
Signed-in user's row gets a 4% pigment tint and rounded corners; no border
between rows in that case.
No filters, no search, no sort, no header count, no empty-seat row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nav (AppLayout.astro): brand stays left; everything else is a single
right-aligned flex group — Pulse · Roadmap · Members · Events · [Admin
if fenja] · 0.5px vertical divider · name · Sign out. The .nav-user
wrapper is gone; the name and logout button now belong to the same flex
flow as the link list.
/pulse:
- ActivityTicker render removed. The component file gets a one-line
deprecation comment; the activity table and write hooks stay in place
for later use.
- 'X others online now' chip strip removed — including all its supporting
helpers and styles in the page.
- CouncilMark replaced with <MembershipCard> in the right column of the
preview row. The roadmap preview is now a white --surface-card with
0.5px border; pulse-card switches to the same white surface and
--radius-lg. The .chosen pulse option uses --pigment-terracotta border
and a 6% terracotta tint via color-mix.
- <DispatchesSection limit={4} /> and <RecentlyFromTheCouncil /> stacked
below the preview row, in the position the online-now strip vacated.
- Vote-count denominator pulls from countCabMembers() and renders via
voteCountSentence(votes, total) — a new helper covering 0/1/5+ cases.
- Event row: dark dinner card now uses --ink/--ink-text; light card uses
--surface-card with 0.5px border.
Tests: 3 new cases for voteCountSentence (0/1/5). 36/36 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
White-card surface. Header row with secondary 'Recently from the council'
eyebrow and a terracotta 'All contributions →' link. Body is one card:
22px id-pigment avatar, full name, relative time, contribution-type pill
(idea→copper, inspiration→ochre, question→indigo), then the body rendered
as a serif italic pull-quote with a 40%-opacity terracotta left border,
clamped to 3 lines via -webkit-line-clamp with markdown stripped before
trimming. Footer shows the reaction count.
Hidden entirely when the contributions table has no visible rows — no
empty-state placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DispatchesSection (white-card, used on /pulse):
- Header row: LATEST FROM THE STUDIO + All dispatches → link
- Featured: 26px avatar byline (name · title · relative time + kind pill),
serif italic title, two-paragraph excerpt (lead in body tone, trail in
--secondary with ellipsis) cut on the nearest sentence boundary, then
the terracotta uppercase 'Read the full dispatch →' link
- Earlier: 1px divider + EARLIER label + up to 3 rows with 22px avatar,
serif italic title (single-line ellipsis), relative time
- Hidden entirely when zero published dispatches; divider + earlier list
omitted when exactly one published dispatch exists
Avatar component (src/components/Avatar.astro) — pure presentational,
takes id + name + size, paints a deterministic-pigment circle with serif
italic initials. Reused by DispatchesSection now and by /members, /events,
and RecentlyFromTheCouncil in the next commits.
format.ts: adds dispatchKindPigment (decision→terracotta, update→indigo,
behind_the_scenes→ochre, note→heather) for pill backgrounds.
Tests: 9 cases covering create-as-draft vs published, publishDispatch
idempotency (re-publish preserves published_at), archive preserves
published_at, the published feed excludes drafts/archived, adjacent
prev/next, and slug round-trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the CouncilMark slot on /pulse. Dark --ink surface, --ink-text
foreground, --ink-muted for labels. Top row: 22px cream circle with the
N monogram, COUNCIL · NNN (zero-padded to 3) on the right; defensive
fallback COUNCIL · MEMBER when member_number is null. First and last name
on separate lines in serif italic, split on the last space so compound
surnames hang together. MEMBER SINCE / month-year block sits above the
focus-tag pill row. Pills render only when focus_tags is non-empty —
no placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>