From 3240e0f052798c0ad8deec794d676f7025eac4b8 Mon Sep 17 00:00:00 2001 From: Jonathan Hvid Date: Mon, 11 May 2026 17:12:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(pulse):=20simplify=20home=20=E2=80=94=20ev?= =?UTF-8?q?ents=20on=20top,=20merged=20roadmap+fenja,=20members=20strip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/layouts/AppLayout.astro | 2 - src/pages/pulse.astro | 933 ++++++++++++++++++++++-------------- 2 files changed, 580 insertions(+), 355 deletions(-) diff --git a/src/layouts/AppLayout.astro b/src/layouts/AppLayout.astro index 968220f..a019b9a 100644 --- a/src/layouts/AppLayout.astro +++ b/src/layouts/AppLayout.astro @@ -12,8 +12,6 @@ const { title, user } = Astro.props; const navLinks = [ { href: '/pulse', label: 'Pulse' }, { href: '/roadmap', label: 'Roadmap' }, - { href: '/members', label: 'Members' }, - { href: '/events', label: 'Events' }, ]; const footerLinks = [ diff --git a/src/pages/pulse.astro b/src/pages/pulse.astro index 43a1dfe..ac05c56 100644 --- a/src/pages/pulse.astro +++ b/src/pages/pulse.astro @@ -1,31 +1,32 @@ --- import AppLayout from '../layouts/AppLayout.astro'; -import MembershipCard from '../components/MembershipCard.astro'; -import DispatchesSection from '../components/DispatchesSection.astro'; -import RecentlyFromTheCouncil from '../components/RecentlyFromTheCouncil.astro'; +import Avatar from '../components/Avatar.astro'; +import AvatarPile from '../components/AvatarPile.astro'; import { - getOpenPulse, getPulseWithCounts, castVote, recordActivity, - getPulseById, getAllRoadmapItems, getUpcomingEvents, - countCabMembers, getUserVote, + getUpcomingEvents, getPastEvents, getEventBySlug, getEventAttendees, + getEventRsvpCount, getUserRsvp, setEventRsvp, recordActivity, + getAllRoadmapItems, getLatestPublishedDispatches, getAllCabMembers, } from '../lib/db'; -import { pulseDateLabel, timeOfDay, tenureSince, voteCountSentence } from '../lib/format'; +import { + pulseDateLabel, timeOfDay, tenureSince, pigmentForId, relativeTime, + eventKindLabel, defaultActionLabel, + dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchExcerptParas, +} from '../lib/format'; const user = Astro.locals.user; -// ── POST: cast vote ──────────────────────────────────────────────── +// ── POST: RSVP from the hero card ────────────────────────────────── if (Astro.request.method === 'POST') { const data = await Astro.request.formData(); const action = String(data.get('action') ?? ''); - if (action === 'vote') { - const pulseId = Number(data.get('pulse_id')); - const optionIndex = Number(data.get('option_index')); - const target = getPulseById(pulseId); - if (target && target.status === 'open' && Number.isInteger(optionIndex) - && optionIndex >= 0 && optionIndex < target.options.length) { - const existing = getUserVote(pulseId, user.id); - if (existing === null) { - castVote(pulseId, user.id, optionIndex); - recordActivity(user.id, 'voted', 'pulse', pulseId); + if (action === 'rsvp') { + const slug = String(data.get('event_slug') ?? ''); + const status = String(data.get('status') ?? '') as 'yes' | 'no' | 'interested'; + if (slug && ['yes', 'no', 'interested'].includes(status)) { + const ev = getEventBySlug(slug); + if (ev) { + setEventRsvp(user.id, slug, status); + recordActivity(user.id, 'rsvped', 'event', ev.id); } } return Astro.redirect('/pulse'); @@ -42,31 +43,31 @@ const tenureAnchor = user.role === 'cab' && user.cab_joined_date : user.created_at; const tenure = tenureSince(tenureAnchor); -// ── This week's Pulse ────────────────────────────────────────────── -const openPulseRaw = getOpenPulse(); -const totalMembers = countCabMembers(); -const openPulse = openPulseRaw ? getPulseWithCounts(openPulseRaw.id, user.id) : null; +// ── Events ───────────────────────────────────────────────────────── +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); -// Time-left label: "32 seconds" / "3 hours" / "2 days" — soft countdown -function timeLeftLabel(closesAt: string): string { - const ms = new Date(closesAt).getTime() - Date.now(); - if (ms <= 0) return 'closing now'; - const d = Math.floor(ms / 86400000); - if (d >= 1) return `${d} day${d === 1 ? '' : 's'}`; - const h = Math.floor(ms / 3600000); - if (h >= 1) return `${h} hour${h === 1 ? '' : 's'}`; - const m = Math.floor(ms / 60000); - if (m >= 1) return `${m} minute${m === 1 ? '' : 's'}`; - const s = Math.floor(ms / 1000); - return `${s} seconds`; +function parseUtc(s: string): Date { + if (/T.*[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return new Date(s); + return new Date(s.replace(' ', 'T') + 'Z'); } +function fmt(part: Intl.DateTimeFormatOptions, iso: string): string { + return new Intl.DateTimeFormat('en-GB', { ...part, timeZone: 'Europe/Copenhagen' }).format(parseUtc(iso)); +} +const dayNum = (iso: string) => fmt({ day: 'numeric' }, iso); +const weekday = (iso: string) => fmt({ weekday: 'short' }, iso).toUpperCase(); +const monthShort = (iso: string) => fmt({ month: 'short' }, iso).toUpperCase(); +const timeStr = (iso: string) => fmt({ hour: '2-digit', minute: '2-digit', hour12: false }, iso); -function closeDayLabel(closesAt: string): string { - const d = new Date(closesAt); - return new Intl.DateTimeFormat('en-GB', { - weekday: 'long', timeZone: 'Europe/Copenhagen', - }).format(d); -} +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) ──────────────── +const [latestDispatch] = getLatestPublishedDispatches(1); // ── Roadmap preview (3 most-recently-updated items) ──────────────── const roadmapPreview = getAllRoadmapItems() @@ -80,7 +81,7 @@ function roadmapStatusDot(status: 'shipping' | 'beta' | 'exploring'): string { exploring: 'var(--on-surface-muted)', })[status]; } -function roadmapStatusBlurb(item: { status: 'shipping' | 'beta' | 'exploring'; target: string | null; attributed: unknown[] }): string { +function roadmapStatusBlurb(item: { status: 'shipping' | 'beta' | 'exploring'; target: string | null }): string { const target = item.target ? ` · ${item.target}` : ''; switch (item.status) { case 'shipping': return `Shipping${target}`; @@ -89,16 +90,8 @@ function roadmapStatusBlurb(item: { status: 'shipping' | 'beta' | 'exploring'; t } } -// ── Events row ───────────────────────────────────────────────────── -const upcoming = getUpcomingEvents(20); -const nextExclusive = upcoming.find(e => e.kind === 'dinner' || e.kind === 'summit') ?? null; -const nextOfficeHours = upcoming.find(e => e.kind === 'office_hours') ?? null; - -function formatEventDate(iso: string): string { - return new Intl.DateTimeFormat('en-GB', { - day: 'numeric', month: 'long', timeZone: 'Europe/Copenhagen', - }).format(new Date(iso)).toUpperCase(); -} +// ── Members strip — all cab users in member-number order ─────────── +const members = getAllCabMembers(); ---
@@ -114,63 +107,163 @@ function formatEventDate(iso: string): string {

- -
- {openPulse ? ( - <> -
- - This week's pulse · closes in {timeLeftLabel(openPulse.closes_at)} + + {hero ? ( +
+ + +
+ Next up · {heroAudience} + Invitation by hand +
+ +
+
+ {weekday(hero.starts_at)} + {dayNum(hero.starts_at)} + {monthShort(hero.starts_at)}
-

{openPulse.question}

- {openPulse.context &&

{openPulse.context}

} -
- - - {openPulse.options.map((opt, i) => { - const chosen = openPulse.my_vote === i; - const count = openPulse.votes_by_option[i] ?? 0; - const pct = openPulse.votes_total > 0 ? (count / openPulse.votes_total) * 100 : 0; - const locked = openPulse.my_vote !== null; - const letter = String.fromCharCode(65 + i); // A/B/C/D - return ( - - ); - })} +
+

{hero.title}

+

{hero.description}

+

+ {hero.location}{hero.location && ' · '}{timeStr(hero.starts_at)}{hero.duration_label ? ` · ${hero.duration_label}` : ''} +

+
+
+ +
+
+ + {hero.capacity ? `${hero.capacity} seats · ` : ''}{heroConfirmedCount} confirmed + + {heroAttendees.length > 0 && ( + + )} +
+ + + + + {heroMyRsvp === 'yes' ? ( + <> + You're confirmed ✓ + + + ) : ( + + )} +
-

- {voteCountSentence(openPulse.votes_total, totalMembers)} Closes {closeDayLabel(openPulse.closes_at)}. -

- - ) : ( -
- This week's pulse -

No pulse is open right now. The next one drops soon.

+ + {comingUp.length > 0 && ( + <> +
+

Also coming up

+
    + {comingUp.map(ev => ( +
  • + + {dayNum(ev.starts_at)} + {monthShort(ev.starts_at)} + + + {ev.title} + {[ev.duration_label, ev.audience, eventKindLabel(ev.kind)].filter(Boolean).join(' · ')} + +
    + + + +
    +
  • + ))} +
+ + )} + + + {past.length > 0 && ( + <> +
+
+

Past gatherings

+ View all → +
+
    + {past.map(ev => { + const attended = getEventRsvpCount(ev.slug).going; + const hasNotes = !!ev.notes_url; + return ( +
  • + + {dayNum(ev.starts_at)} + {monthShort(ev.starts_at)} + + + {ev.title} + {attended} attended · {hasNotes ? 'Notes shared' : 'No notes'} + + {hasNotes && ( + Notes ↗ + )} +
  • + ); + })} +
+ + )} +
+ ) : ( +
+

Nothing scheduled yet — when we have something, you'll be the first to know.

+
+ )} + + +
+ + {latestDispatch && ( +
+
+

Latest from Fenja

+ All updates → +
+ +
+ + +

{latestDispatch.title}

+ +

{dispatchExcerptParas(latestDispatch).lead}

+ + Read the full dispatch → +
)} -
- -
-
-

From the roadmap

+ {latestDispatch && roadmapPreview.length > 0 &&
} + +
+
+

From the roadmap

+ See the full roadmap → +
+ {roadmapPreview.length === 0 ? (

No roadmap items yet.

) : ( @@ -190,48 +283,28 @@ function formatEventDate(iso: string): string { ))} )} - See the full roadmap →
- -
- -
- -
+ + {members.length > 0 && ( +
+
+

The council

+ See who our council is made up of → +
- -
- -
- - - {(nextExclusive || nextOfficeHours) && ( -
- {nextExclusive && ( -
-

- Members only · {formatEventDate(nextExclusive.starts_at)} -

-

{nextExclusive.title}

-

{nextExclusive.description}

- {nextExclusive.capacity && ( -

{nextExclusive.capacity} seats · invitation by hand

- )} -
- )} - {nextOfficeHours && ( -
-

- Studio hours · {formatEventDate(nextOfficeHours.starts_at)} -

-

{nextOfficeHours.title}

-

{nextOfficeHours.description}

-
- )} +
    + {members.map(m => ( +
  • + + + {m.name} + {m.title && {m.title}} + +
  • + ))} +
)} @@ -245,7 +318,7 @@ function formatEventDate(iso: string): string { margin: 0 auto; display: flex; flex-direction: column; - gap: var(--space-10); + gap: var(--space-8); } /* ── Cascade entry (first paint only) ─────────────────────────── */ @@ -258,8 +331,6 @@ function formatEventDate(iso: string): string { .cascade:nth-child(2) { animation-delay: 100ms; } .cascade:nth-child(3) { animation-delay: 200ms; } .cascade:nth-child(4) { animation-delay: 300ms; } - .cascade:nth-child(5) { animation-delay: 400ms; } - .cascade:nth-child(6) { animation-delay: 500ms; } @keyframes cascade-in { to { opacity: 1; transform: translateY(0); } } @@ -269,13 +340,11 @@ function formatEventDate(iso: string): string { /* ── 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; @@ -286,7 +355,6 @@ function formatEventDate(iso: string): string { margin: 0; } .greeting-italic { font-style: italic; } - .greeting-sub { color: var(--on-surface-variant); max-width: 48rem; @@ -294,192 +362,364 @@ function formatEventDate(iso: string): string { } .greeting-sub em { font-style: italic; color: var(--on-surface); } - /* ── Pulse card ───────────────────────────────────────────────── */ - .pulse-card { + /* ── Events card (top, --ink) ─────────────────────────────────── */ + .events-card { + background: var(--ink); + color: var(--ink-text); + border-radius: var(--radius-lg); + padding: 1.75rem; + display: flex; + flex-direction: column; + gap: var(--space-5); + } + .events-card--empty { + align-items: stretch; + justify-content: center; + text-align: center; + min-height: 200px; + } + .events-empty-line { + color: var(--ink-text); + font-family: var(--font-serif); + font-size: 1.25rem; + margin: auto; + 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; + } + + .events-hero-body { + display: grid; + grid-template-columns: 100px 1fr; + gap: var(--space-6); + padding: var(--space-4) 0; + position: relative; + } + .events-hero-body::after { + content: ''; + position: absolute; + left: 100px; + top: 0; bottom: 0; + width: 0.5px; + background: rgba(232, 224, 208, 0.2); + } + .events-date { display: flex; flex-direction: column; gap: 2px; } + .events-weekday, .events-month { + font-family: var(--font-sans); + font-size: var(--text-label-sm); + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--ink-text); + } + .events-day { + font-family: var(--font-serif); + font-style: italic; + font-size: 2.75rem; + line-height: 1; + color: var(--ink-text); + } + .events-detail { padding-left: var(--space-5); } + .events-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); + margin: 0 0 var(--space-3); + max-width: 40rem; + } + .events-meta { + color: var(--ink-muted); + font-size: var(--text-body-sm); + margin: 0; + } + + .events-hero-foot { + border-top: 0.5px solid rgba(232, 224, 208, 0.2); + padding-top: var(--space-4); + display: flex; + justify-content: space-between; + align-items: center; + 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); + 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 { + background: var(--ink-text); + color: var(--ink); + border: none; + padding: 10px 20px; + border-radius: 999px; + font-family: var(--font-sans); + font-size: var(--text-label-md); + font-weight: 600; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + cursor: pointer; + transition: opacity var(--duration-fast) var(--ease-standard); + } + .events-cta:hover { opacity: 0.85; } + .events-confirmed { + color: var(--ink-text); + font-family: var(--font-sans); + font-size: var(--text-label-md); + font-weight: 600; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + padding: 10px 16px; + border: 0.5px solid rgba(232, 224, 208, 0.4); + border-radius: 999px; + } + .events-change { + background: transparent; + border: none; + color: var(--ink-muted); + font-family: var(--font-sans); + font-size: var(--text-label-sm); + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + cursor: pointer; + 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 { + 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; + } + .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 { + font-family: var(--font-serif); + font-style: italic; + font-size: 1.25rem; + line-height: 1; + color: var(--ink-text); + } + .events-list-month { + font-family: var(--font-sans); + font-size: var(--text-label-sm); + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--ink-muted); + } + + .events-list-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; } + .events-list-title { + font-family: var(--font-serif); + font-style: italic; + font-size: 1rem; + color: var(--ink-text); + } + .events-list-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-7) var(--space-8); + padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-4); } - .pulse-meta { + .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; + } + + .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; } + + .sub-divider { + height: 1px; + background: var(--surface-card-border); + border: none; + margin: var(--space-2) 0; + } + + /* Latest from Fenja */ + .latest-dispatch { display: flex; flex-direction: column; gap: var(--space-3); } + .latest-byline { display: flex; align-items: center; - gap: var(--space-3); + gap: var(--space-2); + flex-wrap: wrap; + font-size: var(--text-body-sm); } - - .live-dot { - width: 8px; - height: 8px; - background: var(--pigment-terracotta); - border-radius: 50%; - animation: breathe 2.4s ease-in-out infinite; - } - - @keyframes breathe { - 0%, 100% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.4); opacity: 0.5; } - } - - .pulse-label { - letter-spacing: var(--tracking-wider); - text-transform: uppercase; - color: var(--on-surface-variant); - font-weight: 500; - } - - .pulse-label-muted { - letter-spacing: var(--tracking-wider); - text-transform: uppercase; + .latest-byline-name { font-weight: 600; color: var(--on-surface); } + .latest-byline-title { color: var(--on-surface-variant); } + .latest-byline-time { color: var(--on-surface-muted); + letter-spacing: var(--tracking-wide); + margin-left: auto; } - - .pulse-question { - font-family: var(--font-serif); - font-style: italic; - font-size: 1.375rem; - line-height: var(--leading-snug); - color: var(--on-surface); - margin: 0; - max-width: 50rem; - } - - .pulse-context { - color: var(--on-surface-variant); - margin: 0; - max-width: 50rem; - } - - .pulse-options { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-3); - margin-top: var(--space-2); - } - - .pulse-option { - position: relative; - display: flex; - align-items: flex-start; - gap: var(--space-3); - padding: var(--space-4) var(--space-5); - background: var(--background); - border: var(--ghost-border); - border-radius: var(--radius-md); + .latest-kind-pill { + background: color-mix(in oklab, var(--pill) 14%, transparent); + color: var(--pill); + padding: 2px 9px; + border-radius: 999px; font-family: var(--font-sans); - font-size: var(--text-body-md); - color: var(--on-surface); - text-align: left; - cursor: pointer; - transition: transform 300ms var(--ease-standard), - border-color 300ms var(--ease-standard), - background var(--duration-fast) var(--ease-standard); - overflow: hidden; - } - .pulse-option:hover:not(.locked) { - transform: translateY(-2px); - border-color: var(--outline); - } - .pulse-option.chosen { - border-color: var(--pigment-terracotta); - background: color-mix(in oklab, var(--pigment-terracotta) 6%, var(--surface-card)); - } - .pulse-option.locked:not(.chosen) { - cursor: default; - color: var(--on-surface-variant); - } - .pulse-option:disabled { opacity: 0.8; } - - .pulse-option-letter { + font-size: var(--text-label-sm); + letter-spacing: var(--tracking-wide); font-weight: 600; - color: var(--on-surface-muted); - flex-shrink: 0; } - .pulse-option.chosen .pulse-option-letter { color: var(--pigment-terracotta); } - - .pulse-option-text { flex: 1; } - - .pulse-option-bar { - position: absolute; - left: 0; right: 0; bottom: 0; - height: 2px; - background: var(--surface-container); - } - .pulse-option-bar-fill { - display: block; - height: 100%; - background: var(--pigment-terracotta); - opacity: 0.6; - transition: width 600ms var(--ease-standard); - } - - .pulse-count { - color: var(--on-surface-variant); - margin: 0; - } - .pulse-count strong { color: var(--on-surface); font-weight: 600; } - - .pulse-empty { - display: flex; - flex-direction: column; - gap: var(--space-3); - } - .pulse-empty-line { + .latest-title { font-family: var(--font-serif); font-style: italic; + font-weight: 400; font-size: 1.25rem; - color: var(--on-surface-variant); + line-height: 1.3; + color: var(--on-surface); margin: 0; } - - /* ── Roadmap preview + Membership card ──────────────────────── */ - .preview-row { - display: grid; - grid-template-columns: 2fr 1fr; - gap: var(--space-6); - align-items: stretch; + .latest-excerpt { + margin: 0; + color: var(--on-surface); + line-height: var(--leading-relaxed); } - - .section-eyebrow { - letter-spacing: var(--tracking-wider); + .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(--on-surface-variant); - margin-bottom: var(--space-4); + color: var(--pigment-terracotta); + text-decoration: none; + border-bottom: none; } + .latest-read:hover { opacity: 0.85; border-bottom: none; } - .roadmap-preview { - display: flex; - flex-direction: column; - gap: var(--space-3); - background: var(--surface-card); - border: 0.5px solid var(--surface-card-border); - border-radius: var(--radius-lg); - padding: var(--space-6); - } - - .membership-slot { display: flex; } - .membership-slot > * { flex: 1; } - + /* Roadmap rows */ .roadmap-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; - gap: 0; } - .roadmap-row { display: flex; align-items: flex-start; gap: var(--space-4); - padding: var(--space-4) 0; - border-top: var(--ghost-border); + padding: var(--space-3) 0; } - .roadmap-row:last-child { border-bottom: var(--ghost-border); } + .roadmap-row + .roadmap-row { border-top: 0.5px solid var(--surface-card-border); } .status-dot { width: 10px; @@ -488,6 +728,10 @@ function formatEventDate(iso: string): string { 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); } @@ -499,74 +743,57 @@ function formatEventDate(iso: string): string { margin: 0; } - .see-all { - color: var(--on-surface-variant); - text-decoration: none; - border-bottom: none; - letter-spacing: var(--tracking-wide); - text-transform: uppercase; - margin-top: var(--space-3); - align-self: flex-start; - transition: color var(--duration-fast) var(--ease-standard); - } - .see-all:hover { color: var(--on-surface); border-bottom: none; } - - /* ── Events row ──────────────────────────────────────────────── */ - .event-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-6); - } - - .event-card { - padding: var(--space-8); - border-radius: var(--radius-md); - display: flex; - flex-direction: column; - gap: var(--space-3); - transition: transform 300ms var(--ease-standard); - } - .event-card:hover { transform: translateY(-2px); } - - .event-card--dark { - background: var(--ink); - color: var(--ink-text); - } - .event-card--dark .event-title, - .event-card--dark .event-desc, - .event-card--dark .event-scarcity { - color: var(--ink-text); - } - - .event-card--light { + /* ── 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); + display: flex; + flex-direction: column; + gap: var(--space-4); + } + .members-head-row { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: var(--space-3); } - .event-eyebrow { - letter-spacing: var(--tracking-wider); - text-transform: uppercase; - color: var(--on-surface-muted); - } - .event-eyebrow--light { color: var(--ink-muted); } - - .event-title { - font-family: var(--font-serif); - font-size: 1.5rem; - line-height: var(--leading-snug); + .members-strip { + list-style: none; + padding: 0; margin: 0; + display: flex; + flex-wrap: wrap; + gap: var(--space-3); } - - .event-desc { margin: 0; } - - .event-scarcity { + .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); + } + .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); letter-spacing: var(--tracking-wide); - margin: 0; + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 14rem; } - /* ── Responsive: collapse 2-col rows on narrow widths ────────── */ - @media (max-width: 880px) { - .preview-row, .event-row, .pulse-options { grid-template-columns: 1fr; } + /* ── 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); } }