TrackstageDesign system

Trackstage design system

Every token, primitive, and app pattern in one place. Authored light-first — light is the default everywhere, dark is an organizer opt-in scoped to the app. 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.

Brand & assets

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)

Trackstage

Full lockup — mark + wordmark

Trackstage

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

16px32px48px

Shipped as /favicon.svg plus /icon-192.png and /icon-512.png in the manifest.

Open Graph / social banner — 1200×630

Trackstage — open-source speaker and program management

Wired in the root head as og:image and twitter:image.

Clearspace & minimum size

Clearspace = ½ the mark height on all sides16px minimum — below that, drop the wordmark

Do / don't

Boxed mark on light surfaces

Trackstage

Plain mark inherits the text color

Never recolor the mark off-brand

Never stretch or squash it

Color

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

Page background--background · #FAFAFA
Card--card · #FFFFFF
Sidebar--sidebar · #FAFAFA
Muted--muted · #F7F7F8
Accent (hover/selected)--accent · #F4F4F5
Border--border · #EAEAEC

Brand & text

Primary--primary · #2F5CE0
Primary hover--primary-hover · #2950C0
Text (ink)--foreground · #17171A
Muted text--muted-foreground · #6E6E76
Accent text--accent-foreground · #17171A
Destructive--destructive · #DC2626

Status

Green fill--status-green-bg · #D1FAE5
Green text--status-green-fg · #065F46
Amber fill--status-amber-bg · #FEF3C7
Amber text--status-amber-fg · #92400E
Red fill--status-red-bg · #FEE2E2
Red text--status-red-fg · #991B1B
Gray fill--status-gray-bg · #F4F4F5
Neutral fill--status-blue-bg · #F4F4F5

Categorical tags

Tag · blue--tag-blue-bg · #E6F0FB
Tag · green--tag-green-bg · #E7F4EC
Tag · amber--tag-amber-bg · #FBF0DC
Tag · purple--tag-purple-bg · #F3EAFB
Tag · gray--tag-gray-bg · #F4F4F5

Dark mode

Organizers choose Light / Dark / System in /app/account — light unless they choose otherwise. Dark is scoped to the organizer app — every public and marketing surface stays light for everyone. The rules mirror the light palette: chrome stays neutral, elevation still reads upward (the page is the darkest surface, cards step up), colour still carries data, and the accent is lifted rather than changed — #2F5CE0 #3D6BE5, which keeps white button text at AA.

Page background--background · #0B0D12
Card--card · #12141A
Muted--muted · #171A20
Accent (hover/selected)--accent · #1F232B
Border--border · #23262E
Text (ink)--foreground · #ECEDEF
Muted text--muted-foreground · #9A9DA6
Primary (lifted)--primary · #3D6BE5
Destructive--destructive · #E5484D
Green fill--status-green-bg · #0F2E22
Amber fill--status-amber-bg · #33240B
Red fill--status-red-bg · #3A1516

Typography

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

The quick brown fox jumps over 12 lazy speakers400 Regular — body copy
The quick brown fox jumps over 12 lazy speakers500 Medium — labels, nav
The quick brown fox jumps over 12 lazy speakers600 Semibold — headings
The quick brown fox jumps over 12 lazy speakers700 Bold — rare emphasis

Tabular figures — every number in a table

09:00 – 09:45Room A128 seats
11:15 – 12:00Room B94 seats
14:30 – 15:15Main Hall1,024 seats

Use tabular-nums anywhere numbers stack: agenda times, counts, scores.

Type scale

Page title20px / 600
Section title16px / 600
Body14px / 400
Label14px / 500
Helper text14px / muted
Meta / pill12px / muted
Group label11px / 600 / uppercase

Layout & width

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% − gutter

Organizer app content and its top bar. Full-bleed inside the shell so tables get every pixel.

.container-page--container-page72rem

Every public surface: marketing bands, event pages, speaker portal, review queue.

.container-narrow--container-narrow42rem

Single-column forms — the public submit flow, create-form screens.

.container-card--container-card25rem

Centred entry cards: sign in, portal magic-link, invalid-link states.

.container-reading--container-reading65ch

Prose measure. Width only — no gutter, no centring — so it caps a paragraph in place.

Relative widths — the same viewport, four containers

container-app
container-page
container-narrow
container-card

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.

Shape & elevation

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

Buttons

Primary blue for the one action that matters on a screen; outline for secondary; ghost inside tables and toolbars.

Variants

Sizes

With icons & states

Form controls

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)

August 2026

Search toolbar (DataToolbar)

Status pills

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

DraftPendingAccept QueueDecline QueueAcceptedDeclinedWithdrawn

Generic lifecycle

OpenClosedActiveScheduledSentFailedCompleteIncomplete

Small size / no dot

AcceptedPendingDeclinedAccept Queue

variant="pill" — emphasis only

DraftPendingAccept QueueDecline QueueAcceptedDeclinedWithdrawn

Data display

Tables are compact with a sticky header, hover highlight, status pill column, and a row action menu.

Table

StatusTitleTrackSpeakers
AcceptedAgents in productionAI EngineeringAda Lovelace
PendingCutting CFP busyworkProductGrace Hopper
Decline QueueYet another frameworkInfrastructureAlan Turing

Badges

DefaultSecondaryOutlineDestructive

Avatars

ALGHAT

Progress

x

Skeleton

TabsCounts use TabsCount — “Label N”, never “Label (N)”.

Every submission, whatever its status.

Breadcrumb

Accordion

Feedback

Alerts explain what happened and what to do next. Toasts confirm background work without stealing focus.

Alert — default

Alert — destructive

Toast

Tooltip

Overlays

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

App patterns

Composed primitives that every organizer screen reuses.

PageHeader — banner (organizer default)

Submissions

Everything submitted to this event, in one table.

PageHeader — plain

Speaker profile

What the speaker sees in their portal.

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

Welcome screen

Step content goes here. The rail shows checkmarks for completed steps and a dark card for the active one.

All changes saved

CopyLinkButtonPublic links are never hidden behind a menu. Falls back to a toast carrying the URL when the Clipboard API is unavailable.

SubscribeMenuThe live .ics feed, offered three ways because calendar apps disagree about which one they accept.

PersonPickerOne person graph per event, made visible: pick someone who already exists (portal, tasks and profile carry over) or type a new address. Idle here — with an eventId it searches the event's people.

Search by name, company or email.

Sidebar navigation item states

Program

SubmissionsFormsAgendaSpeakersSettings

Interactions

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.