feat(pulse): spacing pass + council section header + 7-item roadmap seed

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>
This commit is contained in:
Jonathan Hvid 2026-05-12 10:58:34 +02:00
parent 7bd3997564
commit cde98f9454
2 changed files with 103 additions and 40 deletions

View file

@ -169,12 +169,15 @@ db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES
db.prepare('INSERT INTO votes (pulse_id, user_id, option_index, voted_at) VALUES (?,?,?,?)')
.run(decisionPulseId, cabs[1].id, 1, nowIso(-30 * 60));
// ── Roadmap: 1 shipping / 1 beta / 2 exploring, attributions ───────
// ── Roadmap: 7 items spanning shipping → considering, admin-ordered ──
const roadmap = [
{ title: 'Traceability layer', description: 'Every response cites its sources with structured provenance.', status: 'shipping', target: 'Live now', display_order: 10, shipped_at: nowIso(-2 * 24 * 3600), attributed: [cabs[0].id] },
{ title: 'Document ingestion pipeline', description: 'Upload PDF, Word, plain text. Chunked, indexed, retrievable.', status: 'beta', target: null, display_order: 10, shipped_at: null, attributed: [cabs[1].id, cabs[2].id] },
{ title: 'Contextual memory', description: 'The system learns the regulatory and organisational context over time.', status: 'exploring', target: 'Q3 2026', display_order: 10, shipped_at: null, attributed: [cabs[3].id] },
{ title: 'Agentic query mode', description: 'Multi-step retrieval and synthesis with full provenance.', status: 'exploring', target: 'Q4 2026', display_order: 20, shipped_at: null, attributed: [] },
{ title: 'Traceability layer', description: 'Every response cites its sources with structured provenance.', status: 'shipping', target: 'Live now', display_order: 1, shipped_at: nowIso(-2 * 24 * 3600), attributed: [cabs[0].id] },
{ title: 'Audit log export', description: 'One-click export of every model call, source, and reviewer action.', status: 'shipping', target: 'Next week', display_order: 2, shipped_at: nowIso(-1 * 24 * 3600), attributed: [] },
{ title: 'Agentic query mode', description: 'Multi-step retrieval and synthesis with full provenance.', status: 'in_beta', target: 'July', display_order: 3, shipped_at: null, attributed: [cabs[1].id] },
{ title: 'Contextual memory', description: 'The system learns the regulatory and organisational context over time.', status: 'exploring', target: 'Q3 2026', display_order: 4, shipped_at: null, attributed: [cabs[3].id] },
{ title: 'Multi-tenant isolation', description: 'Strict per-organisation data boundaries for shared deployments.', status: 'exploring', target: 'Q4 2026', display_order: 5, shipped_at: null, attributed: [] },
{ title: 'Federated learning hooks', description: 'Train shared models across council members without moving data.', status: 'considering', target: '2027', display_order: 6, shipped_at: null, attributed: [] },
{ title: 'Open evaluation framework', description: 'A public benchmark suite for sovereign AI deployments.', status: 'considering', target: '2027', display_order: 7, shipped_at: null, attributed: [] },
];
const insertRoad = db.prepare(`
@ -341,7 +344,7 @@ insertActivity.run(cabs[1].id,'voted', 'pulse', decisionPulseId, no
insertActivity.run(cabs[0].id,'rsvped', 'event', db.prepare("SELECT id FROM events WHERE slug = ?").get(dinnerSlug).id, nowIso(-8 * 3600));
console.log(' pulse #' + decisionPulseId + ' open, 2 of 4 voted');
console.log(' roadmap: 1 shipping / 1 beta / 2 exploring');
console.log(' roadmap: 7 items (2 shipping / 1 in_beta / 2 exploring / 2 considering)');
console.log(' contributions: 3 (most recent has 3 reactions)');
console.log(' dispatches: 4 published (2/5/9/12 days ago)');
console.log(' events: dinner + studio hours + working session, 2 past');

View file

@ -227,27 +227,38 @@ const members = getAllCabMembers();
<!-- ── Roadmap — horizontal snap-scrolling carousel ─────────── -->
{roadmapItems.length > 0 && (
<div class="cascade">
<div class="cascade roadmap-wrap">
<RoadmapCarousel items={roadmapItems} />
</div>
)}
<!-- ── Council members — single background, no per-member boxes - -->
<!-- ── Council — matches roadmap section framing ─────────────── -->
{members.length > 0 && (
<section class="cascade council-section" aria-label="The council">
<header class="council-header">
<h2 class="council-title">The council</h2>
<a href="/members" class="council-all">See who our council is made up of →</a>
</header>
<ul class="council-grid">
{members.map(m => (
<li class="council-cell">
<Avatar id={m.id} name={m.name} size={56} />
<div class="council-cell-text">
<span class="council-cell-name">{m.name}</span>
{m.title && <span class="council-cell-title">{m.title}</span>}
<span class="council-cell-org">{m.organisation}</span>
{members.slice(0, 4).map(m => (
<li class="council-tile">
<Avatar id={m.id} name={m.name} size={38} />
<div class="council-tile-text">
<span class="council-tile-name">{m.name}</span>
{m.title && <span class="council-tile-title">{m.title}</span>}
<span class="council-tile-org">{m.organisation}</span>
</div>
</li>
))}
{members.length > 4 && (
<li class="council-tile council-tile--more">
<a href="/members" class="council-tile-link">
See all {members.length} council members →
</a>
</li>
)}
</ul>
<a href="/members" class="section-link">See who our council is made up of</a>
</section>
)}
@ -256,14 +267,22 @@ const members = getAllCabMembers();
<style>
.page {
padding: var(--space-12) var(--space-16) var(--space-16);
padding: 40px 36px 80px;
max-width: var(--content-max);
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-12);
}
/* Per-section spacing table (no shared gap — each transition is tuned).
Read top-down: greeting → hero → also → editorial → roadmap → council. */
.greeting { margin-top: 48px; } /* below nav */
.hero-slot { margin-top: 56px; } /* greeting → hero */
.also-coming-up { margin-top: 18px; } /* hero → also (tight; related) */
.editorial-row { margin-top: 72px; } /* also → editorial */
.roadmap-wrap { margin-top: 72px; } /* editorial → roadmap */
.council-section{ margin-top: 72px; } /* roadmap → council */
/* ── Cascade entry (first paint only) ─────────────────────────── */
.cascade {
opacity: 0;
@ -603,58 +622,99 @@ const members = getAllCabMembers();
.pulse-option.chosen .pulse-option-letter { color: var(--pigment-terracotta); }
.pulse-option-text { flex: 1; }
/* ── Council — one outer surface, no per-member boxes ─────────── */
/* ── Council — section framing matches the roadmap carousel ──── */
.council-section {
background: var(--surface-card);
border: 0.5px solid var(--surface-card-border);
border-radius: var(--radius-lg);
padding: var(--space-8) var(--space-8) var(--space-7);
display: flex;
flex-direction: column;
gap: var(--space-6);
gap: 24px;
}
.council-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.council-title {
font-family: var(--font-serif);
font-weight: 400;
font-size: 22px;
line-height: 1.2;
color: var(--on-surface);
margin: 0;
}
.council-all {
font-family: var(--font-sans);
font-size: 11px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--pigment-terracotta);
text-decoration: none;
border-bottom: none;
}
.council-all:hover { opacity: 0.8; border-bottom: none; }
.council-grid {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-6) var(--space-8);
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.council-cell {
.council-tile {
display: flex;
gap: var(--space-4);
align-items: center;
gap: 12px;
min-width: 0;
}
.council-cell-text {
.council-tile-text {
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
min-width: 0;
}
.council-cell-name {
.council-tile-name {
font-family: var(--font-serif);
font-weight: 400;
font-size: 1.125rem;
line-height: 1.2;
font-size: 15px;
line-height: 1.15;
color: var(--on-surface);
}
.council-cell-title {
.council-tile-title {
font-family: var(--font-sans);
font-size: var(--text-body-sm);
font-size: 11px;
color: var(--on-surface-variant);
line-height: 1.35;
}
.council-tile-org {
font-family: var(--font-sans);
font-size: 10px;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--on-surface-variant);
}
.council-cell-org {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
color: var(--on-surface-muted);
.council-tile--more {
align-items: center;
justify-content: flex-end;
}
.council-tile-link {
font-family: var(--font-sans);
font-size: 11px;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--pigment-terracotta);
text-decoration: none;
border-bottom: none;
text-align: right;
}
.council-tile-link:hover { opacity: 0.8; border-bottom: none; }
/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 880px) {
.editorial-row { grid-template-columns: 1fr; gap: var(--space-8); }
.also-coming-up { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
.council-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.council-grid { grid-template-columns: 1fr; }
}
</style>