feat(pulse): events box lighter + bundled coming-up, unboxed Fenja, horizontal roadmap, bigger council cards

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>
This commit is contained in:
Jonathan Hvid 2026-05-12 09:47:44 +02:00
parent ca3686de29
commit 637055a73e
2 changed files with 257 additions and 433 deletions

Binary file not shown.

View file

@ -3,14 +3,14 @@ import AppLayout from '../layouts/AppLayout.astro';
import Avatar from '../components/Avatar.astro';
import AvatarPile from '../components/AvatarPile.astro';
import {
getUpcomingEvents, getPastEvents, getEventBySlug, getEventAttendees,
getEventRsvpCount, getUserRsvp, setEventRsvp, recordActivity,
getUpcomingEvents, getEventBySlug, getEventAttendees,
getUserRsvp, setEventRsvp, recordActivity,
getAllRoadmapItems, getLatestPublishedDispatches, getAllCabMembers,
} from '../lib/db';
import {
pulseDateLabel, timeOfDay, tenureSince, pigmentForId, relativeTime,
eventKindLabel, defaultActionLabel,
dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchExcerptParas,
pulseDateLabel, timeOfDay, tenureSince, relativeTime,
eventKindLabel,
dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchLongPreview,
} from '../lib/format';
const user = Astro.locals.user;
@ -36,7 +36,7 @@ if (Astro.request.method === 'POST') {
// ── Greeting ───────────────────────────────────────────────────────
const firstName = user.name.split(' ')[0];
const greeting = `Good ${timeOfDay()}, ${firstName}.`;
const dateLabel = pulseDateLabel();
// (date label dropped per the v3 eyebrow-removal pass; tenure line stays inline)
const tenureAnchor = user.role === 'cab' && user.cab_joined_date
? user.cab_joined_date
@ -47,7 +47,6 @@ const tenure = tenureSince(tenureAnchor);
const upcoming = getUpcomingEvents(20);
const hero = upcoming.find(e => e.kind !== 'office_hours') ?? upcoming[0] ?? null;
const comingUp = upcoming.filter(e => e.id !== hero?.id).slice(0, 4);
const past = getPastEvents(4);
function parseUtc(s: string): Date {
if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s);
@ -64,12 +63,12 @@ const timeStr = (iso: string) => fmt({ hour: '2-digit', minute: '2-digit', ho
const heroAttendees = hero ? getEventAttendees(hero.slug, 'yes') : [];
const heroConfirmedCount = heroAttendees.length;
const heroMyRsvp = hero ? getUserRsvp(user.id, hero.slug) : null;
const heroAudience = hero?.audience ?? 'Members only';
// ── Latest from Fenja (single most recent dispatch) ────────────────
// ── Latest from Fenja ──────────────────────────────────────────────
const [latestDispatch] = getLatestPublishedDispatches(1);
const latestPreview = latestDispatch ? dispatchLongPreview(latestDispatch, 520) : '';
// ── Roadmap preview (3 most-recently-updated items) ────────────────
// ── Roadmap preview (3 most-recently-updated items, horizontal) ────
const roadmapPreview = getAllRoadmapItems()
.sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1))
.slice(0, 3);
@ -90,7 +89,7 @@ function roadmapStatusBlurb(item: { status: 'shipping' | 'beta' | 'exploring'; t
}
}
// ── Members strip — all cab users in member-number order ───────────
// ── Council members ─────────────────────────────────────────────────
const members = getAllCabMembers();
---
<AppLayout title="Pulse" user={user}>
@ -98,44 +97,36 @@ const members = getAllCabMembers();
<!-- ── Greeting ─────────────────────────────────────────────── -->
<section class="cascade greeting">
<p class="label-sm date-label">{dateLabel}</p>
<h1 class="greeting-line">
<span class="greeting-italic">{greeting}</span>
</h1>
<h1 class="greeting-line">{greeting}</h1>
<p class="greeting-sub body-md">
You've been a member for <em>{tenure}</em>. The team is reading every note you leave.
You've been a member for {tenure}. The team is reading every note you leave.
</p>
</section>
<!-- ── Events (top, --ink card with hero + coming up + past) ── -->
<!-- ── Events (--ink card with hero + bundled coming-up + see all) -->
{hero ? (
<section class="cascade events-card" aria-label="Events">
<!-- Hero -->
<header class="events-hero-top">
<span class="events-eyebrow">Next up · {heroAudience}</span>
<span class="events-eyebrow">Invitation by hand</span>
</header>
<div class="events-hero-body">
<div class="events-date">
<span class="events-weekday">{weekday(hero.starts_at)}</span>
<span class="events-day">{dayNum(hero.starts_at)}</span>
<span class="events-month">{monthShort(hero.starts_at)}</span>
<div class="hero-body">
<div class="hero-date">
<span class="hero-weekday">{weekday(hero.starts_at)}</span>
<span class="hero-day">{dayNum(hero.starts_at)}</span>
<span class="hero-month">{monthShort(hero.starts_at)}</span>
</div>
<div class="events-detail">
<h2 class="events-title">{hero.title}</h2>
<p class="events-desc">{hero.description}</p>
<p class="events-meta">
<div class="hero-detail">
<h2 class="hero-title">{hero.title}</h2>
<p class="hero-desc">{hero.description}</p>
<p class="hero-meta">
{hero.location}{hero.location && ' · '}{timeStr(hero.starts_at)}{hero.duration_label ? ` · ${hero.duration_label}` : ''}
</p>
</div>
</div>
<footer class="events-hero-foot">
<div class="events-foot-left">
<span class="events-foot-stat">
<footer class="hero-foot">
<div class="hero-foot-left">
<span class="hero-foot-stat">
{hero.capacity ? `${hero.capacity} seats · ` : ''}{heroConfirmedCount} confirmed
</span>
{heroAttendees.length > 0 && (
@ -143,168 +134,106 @@ const members = getAllCabMembers();
)}
</div>
<form method="POST" class="events-foot-right">
<form method="POST" class="hero-foot-right">
<input type="hidden" name="action" value="rsvp" />
<input type="hidden" name="event_slug" value={hero.slug} />
{heroMyRsvp === 'yes' ? (
<>
<span class="events-confirmed">You're confirmed ✓</span>
<button type="submit" name="status" value="no" class="events-change">Change</button>
<span class="hero-confirmed">You're confirmed ✓</span>
<button type="submit" name="status" value="no" class="hero-change">Change</button>
</>
) : (
<button type="submit" name="status" value="yes" class="events-cta">Save your seat →</button>
<button type="submit" name="status" value="yes" class="hero-cta">Save your seat →</button>
)}
</form>
</footer>
<!-- Coming up (less prominent, same blue card) -->
<!-- Bundled coming-up sub-cards (no RSVP buttons) -->
{comingUp.length > 0 && (
<>
<hr class="events-divider" />
<p class="events-sub-eyebrow">Also coming up</p>
<ul class="events-list">
{comingUp.map(ev => (
<li class="events-list-row">
<span class="events-list-date">
<span class="events-list-day">{dayNum(ev.starts_at)}</span>
<span class="events-list-month">{monthShort(ev.starts_at)}</span>
</span>
<span class="events-list-body">
<span class="events-list-title">{ev.title}</span>
<span class="events-list-meta">{[ev.duration_label, ev.audience, eventKindLabel(ev.kind)].filter(Boolean).join(' · ')}</span>
</span>
<form method="POST" class="events-list-action-form">
<input type="hidden" name="action" value="rsvp" />
<input type="hidden" name="event_slug" value={ev.slug} />
<button type="submit" name="status" value="yes" class="events-list-action">
{ev.action_label ?? defaultActionLabel(ev.kind)}
</button>
</form>
</li>
))}
</ul>
</>
)}
<!-- Past gatherings (least prominent, same blue card) -->
{past.length > 0 && (
<>
<hr class="events-divider" />
<header class="events-past-head">
<p class="events-sub-eyebrow">Past gatherings</p>
<a href="/events/past" class="events-past-all">View all →</a>
</header>
<ul class="events-list events-list--past">
{past.map(ev => {
const attended = getEventRsvpCount(ev.slug).going;
const hasNotes = !!ev.notes_url;
return (
<li class="events-list-row">
<span class="events-list-date">
<span class="events-list-day">{dayNum(ev.starts_at)}</span>
<span class="events-list-month">{monthShort(ev.starts_at)}</span>
</span>
<span class="events-list-body">
<span class="events-list-title">{ev.title}</span>
<span class="events-list-meta">{attended} attended · {hasNotes ? 'Notes shared' : 'No notes'}</span>
</span>
{hasNotes && (
<a href={ev.notes_url!} class="events-list-action">Notes ↗</a>
)}
</li>
);
})}
</ul>
</>
)}
</section>
) : (
<section class="cascade events-card events-card--empty">
<p class="events-empty-line"><em>Nothing scheduled yet — when we have something, you'll be the first to know.</em></p>
</section>
)}
<!-- ── Roadmap + Latest from Fenja ──────────────────────────── -->
<section class="cascade combined-card">
{latestDispatch && (
<div class="sub-section">
<header class="sub-head">
<p class="label-sm sub-eyebrow">Latest from Fenja</p>
<a href="/dispatches" class="sub-all">All updates →</a>
</header>
<article class="latest-dispatch">
<header class="latest-byline">
<Avatar id={latestDispatch.author_id} name={latestDispatch.author_name} size={26} />
<span class="latest-byline-name">{latestDispatch.author_name}</span>
{latestDispatch.author_title && <span class="latest-byline-title">· {latestDispatch.author_title}</span>}
<span class="latest-byline-time label-sm">
{relativeTime(latestDispatch.published_at ?? latestDispatch.created_at)}
</span>
<span class="latest-kind-pill" style={`--pill: ${dispatchKindPigment(latestDispatch.kind)}`}>
{dispatchKindLabel(latestDispatch.kind)}
</span>
</header>
<h3 class="latest-title">{latestDispatch.title}</h3>
<p class="latest-excerpt">{dispatchExcerptParas(latestDispatch).lead}</p>
<a href={`/dispatches/${dispatchSlug(latestDispatch)}`} class="latest-read">Read the full dispatch →</a>
</article>
</div>
)}
{latestDispatch && roadmapPreview.length > 0 && <hr class="sub-divider" />}
<div class="sub-section">
<header class="sub-head">
<p class="label-sm sub-eyebrow">From the roadmap</p>
<a href="/roadmap" class="sub-all">See the full roadmap →</a>
</header>
{roadmapPreview.length === 0 ? (
<p class="body-sm muted">No roadmap items yet.</p>
) : (
<ul class="roadmap-list">
{roadmapPreview.map(item => (
<li class="roadmap-row">
<span
class:list={['status-dot', { breathing: item.status === 'shipping' }]}
style={`background:${roadmapStatusDot(item.status)}`}
aria-hidden="true"
></span>
<div class="roadmap-row-text">
<p class="roadmap-row-title">{item.title}</p>
<p class="roadmap-row-blurb label-sm">{roadmapStatusBlurb(item)}</p>
<ul class="coming-up-grid">
{comingUp.map(ev => (
<li class="coming-up-card">
<div class="cu-date">
<span class="cu-day">{dayNum(ev.starts_at)}</span>
<span class="cu-month">{monthShort(ev.starts_at)}</span>
</div>
<div class="cu-body">
<h3 class="cu-title">{ev.title}</h3>
<p class="cu-meta">{[ev.duration_label, ev.audience, eventKindLabel(ev.kind)].filter(Boolean).join(' · ')}</p>
</div>
</li>
))}
</ul>
)}
</div>
</section>
<!-- ── Members strip ────────────────────────────────────────── -->
{members.length > 0 && (
<section class="cascade members-card">
<header class="members-head-row">
<p class="label-sm sub-eyebrow">The council</p>
<a href="/members" class="sub-all">See who our council is made up of →</a>
<a href="/events" class="section-link section-link--ink hero-see-all">See all events →</a>
</section>
) : (
<section class="cascade events-card events-card--empty">
<p class="events-empty-line">Nothing scheduled yet — when we have something, you'll be the first to know.</p>
<a href="/events" class="section-link section-link--ink">See all events →</a>
</section>
)}
<!-- ── Latest from Fenja (unboxed, longer excerpt) ──────────── -->
{latestDispatch && (
<article class="cascade latest-article">
<header class="latest-byline">
<Avatar id={latestDispatch.author_id} name={latestDispatch.author_name} size={28} />
<span class="latest-byline-name">{latestDispatch.author_name}</span>
{latestDispatch.author_title && <span class="latest-byline-title">· {latestDispatch.author_title}</span>}
<span class="latest-byline-time">{relativeTime(latestDispatch.published_at ?? latestDispatch.created_at)}</span>
<span class="latest-kind-pill" style={`--pill: ${dispatchKindPigment(latestDispatch.kind)}`}>
{dispatchKindLabel(latestDispatch.kind)}
</span>
</header>
<ul class="members-strip">
{members.map(m => (
<li class="member-pill">
<Avatar id={m.id} name={m.name} size={32} />
<span class="member-pill-text">
<span class="member-pill-name">{m.name}</span>
{m.title && <span class="member-pill-title label-sm">{m.title}</span>}
</span>
<h2 class="latest-title">{latestDispatch.title}</h2>
<p class="latest-body">{latestPreview}</p>
<a href={`/dispatches/${dispatchSlug(latestDispatch)}`} class="section-link">Read the full dispatch →</a>
</article>
)}
<!-- ── Roadmap — horizontal cards ───────────────────────────── -->
{roadmapPreview.length > 0 && (
<section class="cascade roadmap-section" aria-label="From the roadmap">
<ul class="roadmap-grid">
{roadmapPreview.map(item => (
<li class="roadmap-card">
<span
class:list={['status-dot', { breathing: item.status === 'shipping' }]}
style={`background:${roadmapStatusDot(item.status)}`}
aria-hidden="true"
></span>
<div class="roadmap-card-text">
<h3 class="roadmap-card-title">{item.title}</h3>
<p class="roadmap-card-blurb">{roadmapStatusBlurb(item)}</p>
</div>
</li>
))}
</ul>
<a href="/roadmap" class="section-link">See the full roadmap →</a>
</section>
)}
<!-- ── Council members — larger cards with company ──────────── -->
{members.length > 0 && (
<section class="cascade council-section" aria-label="The council">
<ul class="council-grid">
{members.map(m => (
<li class="council-card">
<Avatar id={m.id} name={m.name} size={56} />
<div class="council-card-text">
<span class="council-card-name">{m.name}</span>
{m.title && <span class="council-card-title">{m.title}</span>}
<span class="council-card-org">{m.organisation}</span>
</div>
</li>
))}
</ul>
<a href="/members" class="section-link">See who our council is made up of →</a>
</section>
)}
@ -318,7 +247,7 @@ const members = getAllCabMembers();
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-8);
gap: var(--space-10);
}
/* ── Cascade entry (first paint only) ─────────────────────────── */
@ -331,20 +260,14 @@ const members = getAllCabMembers();
.cascade:nth-child(2) { animation-delay: 100ms; }
.cascade:nth-child(3) { animation-delay: 200ms; }
.cascade:nth-child(4) { animation-delay: 300ms; }
@keyframes cascade-in {
to { opacity: 1; transform: translateY(0); }
}
.cascade:nth-child(5) { animation-delay: 400ms; }
@keyframes cascade-in { to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
.cascade { opacity: 1; transform: none; animation: none; }
}
/* ── Greeting ─────────────────────────────────────────────────── */
.greeting { display: flex; flex-direction: column; gap: var(--space-3); }
.date-label {
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-muted);
}
.greeting-line {
font-family: var(--font-serif);
font-weight: 400;
@ -354,105 +277,90 @@ const members = getAllCabMembers();
color: var(--on-surface);
margin: 0;
}
.greeting-italic { font-style: italic; }
.greeting-sub {
color: var(--on-surface-variant);
max-width: 48rem;
margin: 0;
}
.greeting-sub em { font-style: italic; color: var(--on-surface); }
/* ── Events card (top, --ink) ─────────────────────────────────── */
/* ── Events card (--ink) ──────────────────────────────────────── */
.events-card {
background: var(--ink);
color: var(--ink-text);
border-radius: var(--radius-lg);
padding: 1.75rem;
padding: var(--space-7) var(--space-8);
display: flex;
flex-direction: column;
gap: var(--space-5);
gap: var(--space-6);
}
.events-card--empty {
align-items: stretch;
justify-content: center;
text-align: center;
min-height: 200px;
align-items: flex-start;
text-align: left;
min-height: 160px;
justify-content: space-between;
}
.events-empty-line {
color: var(--ink-text);
font-family: var(--font-serif);
font-size: 1.25rem;
margin: auto;
margin: 0;
max-width: 32rem;
}
.events-empty-line em { font-style: italic; }
.events-hero-top {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--ink-muted);
}
.events-eyebrow {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
opacity: 0.92;
}
.events-hero-body {
/* Hero (lighter, fewer italics) */
.hero-body {
display: grid;
grid-template-columns: 100px 1fr;
gap: var(--space-6);
padding: var(--space-4) 0;
grid-template-columns: 110px 1fr;
gap: var(--space-7);
position: relative;
}
.events-hero-body::after {
.hero-body::after {
content: '';
position: absolute;
left: 100px;
left: 110px;
top: 0; bottom: 0;
width: 0.5px;
background: rgba(232, 224, 208, 0.2);
background: rgba(232, 224, 208, 0.18);
}
.events-date { display: flex; flex-direction: column; gap: 2px; }
.events-weekday, .events-month {
.hero-date { display: flex; flex-direction: column; gap: 4px; }
.hero-weekday, .hero-month {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-text);
color: rgba(232, 224, 208, 0.75);
}
.events-day {
.hero-day {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: 2.75rem;
line-height: 1;
color: var(--ink-text);
}
.events-detail { padding-left: var(--space-5); }
.events-title {
.hero-detail { padding-left: var(--space-6); }
.hero-title {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: 1.75rem;
line-height: 1.2;
color: var(--ink-text);
margin: 0 0 var(--space-3);
}
.events-desc {
color: rgba(232, 224, 208, 0.85);
.hero-desc {
color: rgba(232, 224, 208, 0.92);
margin: 0 0 var(--space-3);
max-width: 40rem;
max-width: 50rem;
}
.events-meta {
color: var(--ink-muted);
.hero-meta {
color: rgba(232, 224, 208, 0.7);
font-size: var(--text-body-sm);
margin: 0;
}
.events-hero-foot {
border-top: 0.5px solid rgba(232, 224, 208, 0.2);
/* Hero foot */
.hero-foot {
border-top: 0.5px solid rgba(232, 224, 208, 0.18);
padding-top: var(--space-4);
display: flex;
justify-content: space-between;
@ -460,17 +368,16 @@ const members = getAllCabMembers();
gap: var(--space-4);
flex-wrap: wrap;
}
.events-foot-left { display: flex; align-items: center; gap: var(--space-4); }
.events-foot-stat {
color: var(--ink-muted);
.hero-foot-left { display: flex; align-items: center; gap: var(--space-4); }
.hero-foot-stat {
color: rgba(232, 224, 208, 0.7);
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
}
.events-foot-right { display: flex; align-items: center; gap: var(--space-3); }
.events-cta {
.hero-foot-right { display: flex; align-items: center; gap: var(--space-3); }
.hero-cta {
background: var(--ink-text);
color: var(--ink);
border: none;
@ -484,8 +391,8 @@ const members = getAllCabMembers();
cursor: pointer;
transition: opacity var(--duration-fast) var(--ease-standard);
}
.events-cta:hover { opacity: 0.85; }
.events-confirmed {
.hero-cta:hover { opacity: 0.85; }
.hero-confirmed {
color: var(--ink-text);
font-family: var(--font-sans);
font-size: var(--text-label-md);
@ -496,10 +403,10 @@ const members = getAllCabMembers();
border: 0.5px solid rgba(232, 224, 208, 0.4);
border-radius: 999px;
}
.events-change {
.hero-change {
background: transparent;
border: none;
color: var(--ink-muted);
color: rgba(232, 224, 208, 0.75);
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
@ -508,152 +415,62 @@ const members = getAllCabMembers();
text-decoration: underline;
}
/* Sub-sections (Coming up + Past gatherings on the same blue card) */
.events-divider {
border: none;
height: 0.5px;
background: rgba(232, 224, 208, 0.2);
margin: var(--space-3) 0 var(--space-2);
}
.events-sub-eyebrow {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-muted);
margin: 0 0 var(--space-2);
}
.events-past-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3);
}
.events-past-all {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--ink-text);
text-decoration: none;
border-bottom: none;
opacity: 0.85;
}
.events-past-all:hover { opacity: 1; border-bottom: none; }
.events-list {
/* Bundled coming-up sub-cards (no RSVP buttons) */
.coming-up-grid {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
}
.events-list-row {
display: grid;
grid-template-columns: 60px 1fr auto;
gap: var(--space-4);
align-items: center;
padding: var(--space-3) 0;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-3);
}
.events-list-row + .events-list-row { border-top: 0.5px solid rgba(232, 224, 208, 0.1); }
.events-list-date { display: flex; flex-direction: column; gap: 2px; }
.events-list-day {
.coming-up-card {
background: rgba(232, 224, 208, 0.06);
border: 0.5px solid rgba(232, 224, 208, 0.14);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
display: flex;
gap: var(--space-4);
align-items: flex-start;
}
.cu-date { display: flex; flex-direction: column; gap: 2px; min-width: 36px; }
.cu-day {
font-family: var(--font-serif);
font-style: italic;
font-size: 1.25rem;
font-size: 1.5rem;
line-height: 1;
color: var(--ink-text);
}
.events-list-month {
.cu-month {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-muted);
color: rgba(232, 224, 208, 0.7);
}
.events-list-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.events-list-title {
.cu-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.cu-title {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: 1rem;
line-height: 1.25;
color: var(--ink-text);
margin: 0;
}
.events-list-meta {
.cu-meta {
font-size: 0.75rem;
color: var(--ink-muted);
}
.events-list-action-form { justify-self: end; }
.events-list-action {
background: none;
border: none;
color: var(--ink-text);
font-family: var(--font-sans);
font-size: var(--text-label-md);
font-weight: 500;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
cursor: pointer;
padding: 0;
text-decoration: none;
border-bottom: none;
opacity: 0.85;
}
.events-list-action:hover { opacity: 1; border-bottom: none; }
/* ── Combined card (Roadmap + Latest from Fenja) ──────────────── */
.combined-card {
background: var(--surface-card);
border: 0.5px solid var(--surface-card-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.sub-section {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.sub-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3);
}
.sub-eyebrow {
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--on-surface-variant);
color: rgba(232, 224, 208, 0.65);
margin: 0;
}
.sub-all {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--pigment-terracotta);
text-decoration: none;
border-bottom: none;
}
.sub-all:hover { opacity: 0.85; border-bottom: none; }
.hero-see-all { align-self: flex-start; }
.sub-divider {
height: 1px;
background: var(--surface-card-border);
border: none;
margin: var(--space-2) 0;
/* ── Latest from Fenja (unboxed) ──────────────────────────────── */
.latest-article {
display: flex;
flex-direction: column;
gap: var(--space-3);
max-width: 56rem;
}
/* Latest from Fenja */
.latest-dispatch { display: flex; flex-direction: column; gap: var(--space-3); }
.latest-byline {
display: flex;
align-items: center;
@ -665,6 +482,7 @@ const members = getAllCabMembers();
.latest-byline-title { color: var(--on-surface-variant); }
.latest-byline-time {
color: var(--on-surface-muted);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
margin-left: auto;
}
@ -680,120 +498,126 @@ const members = getAllCabMembers();
}
.latest-title {
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: 1.25rem;
line-height: 1.3;
font-size: 1.625rem;
line-height: 1.25;
color: var(--on-surface);
margin: 0;
letter-spacing: var(--tracking-snug);
}
.latest-excerpt {
margin: 0;
.latest-body {
color: var(--on-surface);
line-height: var(--leading-relaxed);
margin: 0;
font-size: var(--text-body-lg);
}
.latest-read {
align-self: flex-start;
font-family: var(--font-sans);
font-size: var(--text-label-md);
font-weight: 500;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--pigment-terracotta);
text-decoration: none;
border-bottom: none;
}
.latest-read:hover { opacity: 0.85; border-bottom: none; }
/* Roadmap rows */
.roadmap-list {
/* ── Roadmap horizontal cards ─────────────────────────────────── */
.roadmap-section {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.roadmap-grid {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-4);
}
.roadmap-card {
background: var(--surface-card);
border: 0.5px solid var(--surface-card-border);
border-radius: var(--radius-md);
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-3);
min-height: 130px;
}
.roadmap-row {
display: flex;
align-items: flex-start;
gap: var(--space-4);
padding: var(--space-3) 0;
}
.roadmap-row + .roadmap-row { border-top: 0.5px solid var(--surface-card-border); }
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 0.4em;
}
@keyframes breathe {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.5; }
}
.status-dot.breathing { animation: breathe 2.4s ease-in-out infinite; }
.roadmap-row-text { flex: 1; display: flex; flex-direction: column; gap: var(--space-1); }
.roadmap-row-title { margin: 0; font-weight: 500; color: var(--on-surface); }
.roadmap-row-blurb {
.roadmap-card-text { display: flex; flex-direction: column; gap: var(--space-1); }
.roadmap-card-title {
font-family: var(--font-serif);
font-weight: 400;
font-size: 1.0625rem;
line-height: 1.3;
color: var(--on-surface);
margin: 0;
}
.roadmap-card-blurb {
color: var(--on-surface-muted);
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
margin: 0;
}
/* ── Members strip ────────────────────────────────────────────── */
.members-card {
background: var(--surface-card);
border: 0.5px solid var(--surface-card-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
/* ── Council cards — larger, with company ─────────────────────── */
.council-section {
display: flex;
flex-direction: column;
gap: var(--space-4);
gap: var(--space-5);
}
.members-head-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3);
}
.members-strip {
.council-grid {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--space-4);
}
.council-card {
background: var(--surface-card);
border: 0.5px solid var(--surface-card-border);
border-radius: var(--radius-lg);
padding: var(--space-6) var(--space-6) var(--space-7);
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
}
.member-pill {
display: inline-flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-4) var(--space-2) var(--space-2);
background: var(--surface);
border-radius: var(--radius-full);
gap: var(--space-5);
}
.member-pill-text { display: inline-flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.member-pill-name { font-size: var(--text-body-sm); color: var(--on-surface); }
.member-pill-title {
color: var(--on-surface-muted);
.council-card-text {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.council-card-name {
font-family: var(--font-serif);
font-weight: 400;
font-size: 1.125rem;
line-height: 1.2;
color: var(--on-surface);
}
.council-card-title {
font-family: var(--font-sans);
font-size: var(--text-body-sm);
color: var(--on-surface-variant);
}
.council-card-org {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 14rem;
color: var(--on-surface-muted);
}
/* ── Responsive: hero column collapse ─────────────────────────── */
@media (max-width: 720px) {
.events-hero-body { grid-template-columns: 1fr; }
.events-hero-body::after { display: none; }
.events-list-row { grid-template-columns: 50px 1fr; }
.events-list-action-form,
.events-list-row > a { grid-column: 1 / -1; justify-self: start; padding-top: var(--space-2); }
/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 880px) {
.roadmap-grid { grid-template-columns: 1fr; }
.hero-body { grid-template-columns: 1fr; }
.hero-body::after { display: none; }
.hero-detail { padding-left: 0; }
}
</style>