TrackstageDesign system

Trackstage design system

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.

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

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

Tabs

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

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.

Explorations — decided

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.

E

De-blued — SHIPPED

Decided 2026-08-11

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

Everything submitted to Frontend Summit 2026, in one table.

Submissions

128

+12 this week

Accepted

34

27% of total

TitleTrackSpeakerStatus
Scaling a design system to 40 teamsDesignAmara OseiAccepted
What we learned running 300 sessionsCommunityJonas LindqvistPending
The programme committee playbookPlatformRio TanakaActive

What else was on the table

FAttio
Rejected as a whole, harvested in part. Its neutral system is what E now ships — near-white sidebar, hairline borders, no tinted banners, status as a dot plus a label. Its density was never on the table (RULES.md #22) and its warm near-white ramp went further than the brief needed.
GJuicebox-soft
Rejected. Warm greys and 16px radii read friendlier but cost vertical space on the densest screens, and it is the furthest from the restraint the brief asked for.
Teal / petrol accent
Rejected on sight, after the maths said yes. Petrol #0F6E70 won every measured axis — contrast, distance from the status greens, distinctiveness in a field where every competitor is blue — and Marko still preferred the blue we had. The blue was never the problem; the amount of chrome wearing it was.
A–DType pairings
Deferred, not rejected. Inter stays for now; D (Bricolage Grotesque headings · Instrument Sans body) is the one to revisit when type gets its own pass.