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