Every token, primitive, and app pattern in one place. Light mode only. The chrome is neutral — every grey is chroma ≤ 2, so nothing but the data carries colour — and Trackstage blue #2F5CE0 is permitted in exactly five places: primary buttons, links, focus rings, the active nav item, and --chart-1. Controls are 40px (--control-h), compact controls 36px, table rows 44px; 8px radii on controls, 12px on cards. Components are shadcn on Base UI — always extend these rather than hand-rolling new ones.
The logomark is an abstract agenda: a time rail plus three session blocks, four rounded rectangles and nothing else. Geometry lives in src/components/brand/assets.ts, so this page, the app chrome, and every download below are generated from one source. Everything here is produced client-side — no asset files to keep in sync.
Logo variants — on every surface, with downloads
Mark — boxed (app icon)
Mark — plain (mono, currentColor)
Full lockup — mark + wordmark
Wordmark only
Inverse assets are the same geometry painted white — the mark is drawn with currentColor, so it is monochrome-safe on any surface.
Social profile picture — 1:1
Favicon & app icon
Shipped as /favicon.svg plus /icon-192.png and /icon-512.png in the manifest.
Open Graph / social banner — 1200×630

Wired in the root head as og:image and twitter:image.
Clearspace & minimum size
Do / don't
Boxed mark on light surfaces
Plain mark inherits the text color
Never recolor the mark off-brand
Never stretch or squash it
All colors come from CSS custom properties in src/styles.css. Never hardcode a hex in a component. The policy, borrowed verbatim from Stripe: colour is reserved for status signals. Chrome is neutral — every grey below is chroma ≤ 2 — and the accent is permitted in exactly five places: primary buttons, links, focus rings, the active nav item, and --chart-1. If you are reaching for a colour anywhere else, the answer is a hairline or a muted label.
Surfaces
Brand & text
Status
Categorical tags
Inter Variable, deliberately — the boring font. It is the most legible, least fashionable interface typeface there is, which is exactly what a tool for non-technical event producers needs. Character comes from tighter tracking on headings, not from a second family.
Specimen
Trackstage
Inter Variable · headings at font-heading tracking-tight
Tabular figures — every number in a table
Use tabular-nums anywhere numbers stack: agenda times, counts, scores.
Type scale
One width system for the whole product. Five tokens in src/styles.css, four container utilities, no page-level max-w-* anywhere else. A page-level wrapper picks exactly one container; anything inside it that needs a cap uses the token directly so it does not pick up a second gutter.
The tokens
.container-app--container-app100% − gutterOrganizer app content and its top bar. Full-bleed inside the shell so tables get every pixel.
.container-page--container-page72remEvery public surface: marketing bands, event pages, speaker portal, review queue.
.container-narrow--container-narrow42remSingle-column forms — the public submit flow, create-form screens.
.container-card--container-card25remCentred entry cards: sign in, portal magic-link, invalid-link states.
.container-reading--container-reading65chProse measure. Width only — no gutter, no centring — so it caps a paragraph in place.
Relative widths — the same viewport, four containers
Illustrative proportions at a wide viewport. Every container centres itself and carries the page gutter — 1rem, stepping up to 1.5rem at sm. The token values are content widths: the gutter is added on top, so the readable column is identical on every route.
Do — one container per page wrapper
<main className="container-page py-8"> <h1>…</h1> <p className="container-reading">…</p> </main>
Width, centring, and gutter come from one class. Prose caps itself with the reading measure, in place.
Don't — bespoke widths per route
<main className="mx-auto w-full max-w-5xl px-4 sm:px-6"> <main className="mx-auto max-w-6xl px-4"> <main className="mx-auto max-w-7xl px-6">
Three routes, three widths, three gutters — and because some put the padding on the container and some on the parent, the content column differed even where the max-width matched.
Exceptions — what the system does not govern
Component-internal widths stay with their component: dialog and drawer sizes (sm:max-w-lg), table-cell truncation caps, toolbar search fields, and chart columns. They size to their content, not to the page.
8px on controls, 12px on cards, full round on pills and avatars. One shadow level — cards use a hairline ring plus shadow-xs.
rounded-md — 8px
rounded-lg — 10px
rounded-xl — 12px
rounded-full
Labels sit above inputs, required fields get a red asterisk, helper text lives under the label. Structured values always use a real picker.
Text, select, textarea
Shown on the public agenda.
Toggles & choices
Date picker (never a raw text field)
Search toolbar (DataToolbar)
Identical wording organizer-side and speaker-side. Green = will speak, amber = undecided, red = declined, gray = draft/withdrawn — and Active/Scheduled are neutral, because blue means clickable now. The DEFAULT is variant="dot": a coloured dot plus a plain ink label, which is what tables and detail panes render. variant="pill" is the filled version, kept for the places where emphasis is the message — queue banners and drawer headers — where queue states also carry a ring because they are staged, not committed.
Submission pipeline
Generic lifecycle
Small size / no dot
variant="pill" — emphasis only
Tables are compact with a sticky header, hover highlight, status pill column, and a row action menu.
Table
| Status | Title | Track | Speakers | |
|---|---|---|---|---|
| Accepted | Agents in production | AI Engineering | Ada Lovelace | |
| Pending | Cutting CFP busywork | Product | Grace Hopper | |
| Decline Queue | Yet another framework | Infrastructure | Alan Turing |
Badges
Avatars
Progress
Skeleton
Tabs
Breadcrumb
Accordion
Alerts explain what happened and what to do next. Toasts confirm background work without stealing focus.
Alert — default
Alert — destructive
Toast
Tooltip
Create and detail flows use the right slide-over (~480px) so the table underneath never loses its state. Dialogs are reserved for short confirmations.
Drawer (DrawerShell)
Dialog
Popover (inline status edit)
Alert dialog
Composed primitives that every organizer screen reuses.
PageHeader — banner (organizer default)
PageHeader — plain
EmptyState
No submissions yet
Submissions are talks people apply with through your call for speakers form. Share your form link and they'll land here.
WizardShell
Step content goes here. The rail shows checkmarks for completed steps and a dark card for the active one.
Sidebar navigation item states
The interior.dev micro-interaction library, adopted end to end and restyled onto our tokens — same motion, our design language. Every tile below is live; the caption says where it belongs in the product. Import from @/components/interactions; the full map lives in docs/memory/INTERACTIONS.md.
45 of interior.dev’s 54 registry items, restyled onto our tokens. The nine a11y-critical overlays (modal, popover, drawer, dropdown, context-menu, tabs, accordion, tooltip-group, pagination) were not adopted — our shadcn-on-Base-UI versions stay canonical. Import everything from @/components/interactions.
Action feedback
copy-button
Public CFP link, portal link, evaluator magic links, API keys.
loading-button
Every async submit: save form, send invite, commit a decision queue.
hold-to-confirm
Irreversible organizer actions — commit accept/decline queue, delete an event.
like-burst
Evaluator shortlisting — a one-tap 'I want this talk' with optimistic count.
ripple
Touch feedback on large tap targets: agenda slots, mobile portal rows.
icon-morph
Sidebar collapse, drawer close, expand/collapse toggles in the agenda.
press-depth
Raw mechanic. Prefer PepButton below, which wears our button variants.
PepButton (ours)
Opt-in wrapper: press-depth on shadcn buttonVariants. Hero CTAs only.
Input
floating-label
Compact forms where a separate label would cost a row — drawers, filters.
inline-validation
Public CFP fields — the error appears without shoving the rest of the form.
password-strength
Better Auth sign-up and password reset.
otp-input
Speaker-portal magic-link codes and evaluator access codes.
tag-input
Form-builder select options, session tags, track keywords.
expanding-search
DataToolbar on Abstracts / Sessions / Speakers, and the app top bar.
Async
skeleton-swap
Every Convex-backed list on first paint — reserves height, so no jump.
progress-bar
Airtable sync, bulk email send, CSV import — indeterminate → determinate.
load-more
Paginated Abstracts / Speakers lists — loads before the user hits bottom.
streaming-text
AI-ish moments: generated session summaries, evaluator note drafts.
task-steps
Speaker onboarding checklist and long organizer jobs (bulk decisions).
Notification
live-activity
Background organizer jobs — bulk email, Airtable sync — as a small dock.
collapsible-banner
'Your CFP closes in 3 days' / setup nudges — folds to its title, then goes.
presence-avatars
Speakers on a session, evaluators in a plan, org members in settings.
typing-indicator
Evaluator discussion threads and organizer↔speaker message threads.
new-items-pill
Live Convex subscriptions — new abstracts arrive without stealing scroll.
Navigation
command-palette
⌘K across the organizer app — jump to any event, screen, or record.
segmented-control
Agenda view switcher (List / Day / Week / Rooms / Conflicts).
wizard-steps
Form builder's 7-step rail and the public CFP's 5-step tracker.
tree-view
Agenda by day → track → room, and the settings navigator.
Scroll
sticky-header
Every organizer page header — condenses as the table scrolls under it.
reading-progress
Long speaker-facing docs: CFP guidelines, code of conduct, event handbook.
scroll-spy
Landing page section nav and the long Settings page rail.
snap-carousel
Landing screenshot gallery and mobile agenda day-by-day paging.
hide-on-scroll
DataToolbar on long tables — the filter bar yields to the rows.
Data
sortable-table
Abstracts / Sessions / Speakers tables — rows travel to their new order.
filter-grid
Track / status filter chips over the submissions and speakers grids.
value-flash
Dashboard metrics on live Convex subscriptions — marks what just changed.
poll-results
Evaluation round outcomes and audience-vote style track breakdowns.
Gesture
slider-detents
Evaluator scoring (1–5 with felt stops) and agenda slot-length pickers.
swipe-deck
Evaluator triage — decide a stack of abstracts one card at a time.
reorder-list
Form-builder question order and agenda running order.
long-press
Touch equivalent of right-click: bulk-select rows, agenda slot actions.
lightbox
Speaker headshots and sponsor artwork — zoom that returns where it started.
Content
text-reveal
Landing hero headline and section intros — words arrive in reading order.
logo-marquee
Landing 'built on' / sponsor strip — stops when you look at it.
blur-up-image
Speaker headshots and landing screenshots — placeholder resolves in.
show-more
Long abstract descriptions in tables, drawers, and the speaker portal.
Candidate E, “De-blued”, is SHIPPED (Marko, 2026-08-11): neutral chrome everywhere, Trackstage blue #2F5CE0 kept and confined to primary buttons, links, focus rings, the active nav item and --chart-1. The teal/petrol accent family was reviewed and rejected; type stays on Inter for now. The other candidates and their live panels have been removed — this section is the record of the decision, not a chooser.
Today's structure with the blue taken out of the chrome. Page headers are a neutral hairline instead of a lavender panel, the sidebar and its active item are grey, secondary surfaces lose their tint, and Active/Scheduled stop reading as links. Trackstage blue #2F5CE0 stays, and it now appears in exactly five places: primary buttons, links, the active nav item, the selected-row indicator, and --chart-1. Everything below the fold of this decision is recorded in docs/memory/DESIGN-REVAMP.md.
Submissions
128
+12 this week
Accepted
34
27% of total
| Title | Track | Speaker | Status |
|---|---|---|---|
| Scaling a design system to 40 teams | Design | Amara Osei | Accepted |
| What we learned running 300 sessions | Community | Jonas Lindqvist | Pending |
| The programme committee playbook | Platform | Rio Tanaka | Active |