The same system, all the way to the menu bar.
A duplicate of the website design system (design-system-v3.html) — the source of truth for color, type, the rendered mockups, and the real provider iconography — extended with the surfaces that live only in the macOS app and the browser extension: the menu-bar glyph, brand aggregation, the extension companion, a cross-surface matrix, and a running list of gaps to resolve. The website file is untouched; this copy is where the app layer grows.
Three principles
Palette
Ink navy + paper cream, with a cool blue accent lifted from the widget bars. Every swatch below comes from the Figma frame.
Ink & Accent
Paper & Cream
Widget gradients
Type system
Hedvig Letters Serif sets headlines and pull-quotes with a gentle humanist warmth. DM Sans handles UI, body, and numerics with a grotesque clarity. Everything else — the macOS app and widgets — uses SF.
Rhythm
An 8pt base scale. Widgets use 16px outer padding and 14px vertical gaps — the system keeps the rest of the site on the same grid.
Spacing scale
Corner radius
Radius nesting
Children always have a smaller radius than their parent: 20 → 14 → 10. This keeps concentric corners visually parallel rather than gapping. The same rule applies to widget chrome (22) → row content (14) → pill (999).
Soft physics
Three elevation tiers. Default animation is 220ms with a gentle ease-out. Widgets lift slightly on hover to hint interactivity.
Motion rules
Default: 220ms cubic-bezier(.2,.7,.2,1). Each animatable behavior below has its own tuned curve. Honor prefers-reduced-motion — collapse all transitions to 0ms when set.
| Element | Property | Duration | Easing |
|---|---|---|---|
| Buttons (hover / active) | transform, filter, shadow | 120ms · fast | cubic-bezier(.2,.7,.2,1) |
| Cards (scroll-enter) | opacity 0→1, translateY 16→0 | 420ms · slow | ease-out, 80ms stagger |
| Bars (scroll-enter) | width 0→final | 500ms | cubic-bezier(.25,.46,.45,.94) |
| Rings (scroll-enter) | stroke-dashoffset 0→filled | 600ms | cubic-bezier(.25,.46,.45,.94) |
| Pace dot (after bar fills) | ±4px sinusoidal drift | 2.5s loop, 400ms delay | ease-in-out, infinite |
| FAQ expand | max-height (only) | 300ms | cubic-bezier(.25,.46,.45,.94) |
| Theme toggle | background, color, border, shadow | 220ms · default | cubic-bezier(.2,.7,.2,1) |
Building blocks
Buttons
Button states
Hover lifts 1px and adds the medium shadow. Active settles back to baseline with a slight darken. All transitions: 120ms cubic-bezier(.2,.7,.2,1). Honors prefers-reduced-motion.
Footer pairing
A window footer pairs a recoverable action (left) with a forward action (right). The right slot's weight should match what the left slot is doing — otherwise one side dominates. Both pills together = balanced & bold. A bare text-link Back next to a primary pill reads as "you must press this" and is forbidden.
| Left | Right | Use case |
|---|---|---|
.btn-primary | .btn-secondary | Two genuine alternatives, both worth pursuing. Both bold, balanced. |
.btn-secondary | .btn-primary | One forward CTA + one alternative. Same balance, reversed by emphasis. |
Text-link ← Back | .btn-secondary | The forward action is real but not the screen's main event (chooser, install previews). Asymmetry is intentional — secondary is the only window-level affordance. |
Text-link ← Back | .btn-ghost or text-link | The forward action is optional/dismissive ("Skip", "Maybe later"). Both sides feel light; user isn't pushed. |
Text-link ← Back | .btn-primary | Don't. Use Secondary on the right, or upgrade Back to a Ghost button. |
Progress bars
Marketing-page bars match the in-app widget bars: 4px height, pill radius, optional pace dot. Pace dot is only rendered when 0.01 < pace < 0.99 (i.e. between 1% and 99%) — at the extremes it merges with the cap and adds visual noise. This rule comes from WidgetProgressBar in TokenomicsWidgetEntryView.swift.
Pills & badges
Form field
Cards
One ring to rule them
Menu-bar rings aggregate every provider into one glance.
Quiet polling
Exponential backoff on 429s keeps the APIs happy.
Zero-friction auth
Reads existing CLI credentials. Nothing to sign in twice.
Provider chips
A single provider's identity at-a-glance — used in marketing copy ("supports Claude, Codex, Gemini…") and in compact lists. 48px tall, 14px radius (md), 20px icon.
FAQ
Does Tokenomics phone home?
How does it stay updated?
Which macOS versions are supported?
Text button (naked link)
A tertiary action with no background or border — just accent-colored text. Used for "Open the guided setup", "Back", "Read more", inline help links, and tertiary CTAs that shouldn't compete with primary buttons. Hover underlines (3px offset); when the button has a directional arrow, the arrow nudges 2px toward its direction on hover.
Variants: .btn-text (default — accent), .is-back (muted, used for window/back navigation; text-muted by default, text-primary on hover), .is-danger (uses --danger for destructive tertiary actions like "Disconnect"). Pair with an inline SVG .arrow for directional CTAs.
When to choose what:
.btn-primary— the one main action on a screen.btn-secondary— alternate path of equal-ish weight ("Cancel", "Skip").btn-ghost— sits next to primaries but lower weight; has a hover background.btn-text— tertiary; no padding/border/bg; reads as a link but is a button<a>— inline within prose (e.g. "Install Homebrew first")
Button in a field
A small action button that sits inside a code field, input, or pill row — used for Copy, Clear, Edit-in-line, etc. Smaller padding (5×10), 6px corner radius, --text-muted by default, fills with --surface-2 on hover (matches .btn-secondary / .btn-ghost for a unified subtle hover language). Less visual weight than a regular .btn because it's already nested inside an interactive surface.
Variants: .btn-in-field (default), .is-icon (square 5px padding for icon-only), .is-success (green text + 30% green border for confirmation states).
Segmented control
A two-or-three-option toggle on a tinted track with the active option elevated on a white pill. Two rounding variants ship today:
Rule of thumb: Use .seg-rounded for primary navigation/install-flow choices that sit inline with body content. Use .seg-pill for inline plan/tier pickers nested inside a row or list. Both reuse the same opt button — only the corner radius and padding differ.
The product, on the page
These are HTML/CSS reproductions of the Figma widgets (node 421:440) — used on marketing pages so the app and site feel continuous. Colors come straight from the palette above.
Desktop frame new in v2
The marketing-page presentation of the widgets: a dark gradient platter with subtle radial glow that holds all three sizes in a fixed mosaic. Dimensions follow the WidgetKit proportional rule — 2 × small + gap = medium width and 2 × medium height + gap = large height, with medium and large sharing the same width. With small = 200 and gap = 32: medium 432×200 and large 432×432. Total mosaic width 1128px; height 432px. The frame scrolls horizontally on narrow viewports rather than scaling (inner content is fixed-pixel and would break).
The popover that drops from the menu bar new in v2
The actual popover users see when they click the menu-bar item. Glass surface (translucent fill + 24px backdrop-blur), 16px radius, 420px max width. The active provider tab expands into a pill with icon + name; inactive tabs are icon-only. Bars are 6px (not 4px like widgets — this is a fuller-resolution surface). The whole component sits on a wallpaper-tinted "scene" so the marketing page can show it in context.
The annotated dual-ring new in v2
Marketing-page-only component used to teach the pace-dot concept. Same ring math as the small widget (outer:inner ratio 0.305:0.23) but scaled up ~3× — outer r=100, inner r=75, stroke 20 — so callout lines and labels can sit cleanly outside the rings. Pace dots are sized to match the thicker stroke (r=9 outer, r=8 inner).
Two paths, one decision replaces v1
This replaces the underlined-tab install block from v1. The real component is a two-column layout: a segmented pill control on the left (Download / Homebrew, with the active tab elevated on a white pill via shadow-sm) and a panel on the right that swaps between a visual drag-to-install flow and a $ brew install row with a labeled Copy button. The segmented pill matches macOS Settings panes; the drag-flow visual mirrors the DMG mount window users see after downloading.
Install
Install once. Stay current.
Sparkle handles silent background updates. No re-downloading from GitHub every time there's a new version.
macOS 14 Sonoma or later · Developer ID signed & notarized
Install Homebrew first, then run this command.
Download panel — the alternate state of the right column when "Download" is the active tab. A drag-to-install affordance that mirrors what users see in the mounted DMG. Assets live in assets/brand/: Tokenomics-appicon.svg, install-arrow.svg, app-folder-icon.svg.
Drag to Install…
Success, warning, danger new in v3
Status colors lifted from the in-app onboarding flow (guided-onboarding-mockup.html). The blue accent stays for ambient/informational use; these three carry meaning. Each pair is tuned for cream surfaces (light) and ink-blue surfaces (dark) — the dark variants are softer to keep the cream-style calmness even on a deep background.
Light theme
Dark theme
Note on warning
The warning color is a burnt amber, not a saturated yellow. Pure yellow on cream washes out and fights the calm editorial feel. Amber preserves the universal "caution" semantic while harmonizing with navy/cream. If a stronger differentiator from danger is ever needed, alternatives to test: #A87B14 (deep mustard, light) / #D9B463 (warm gold, dark). For now, the values above are the source of truth — they ship in onboarding.
Badge variants
Each variant uses color-mix at 12% bg / 25% border / 100% text — gives a soft tint that reads on cream and ink alike.
Onboarding & in-app patterns new in v3
These components live inside the macOS app — they don't appear on trytokenomics.com but they share the same tokens, fonts, and tone. Pulled from guided-onboarding-mockup.html so the design system covers both the marketing site and the product.
Window chrome
Every onboarding screen sits inside a chromed window with traffic lights and a centered title. 12px corner radius, navy-tinted shadow, cream body.
Window body — content goes here. Padding is 32px top, 40px sides, 28px bottom.
Stepper
Labeled stepper for guided flows. Each .step has fixed slot width (110px) and each .step-line is fixed length (36px), so marks always sit at the same horizontal positions regardless of label length or font weight. States: pending · done · active · error. Active state adds a soft 4px accent ring; error state recolors the mark and ring red.
Error state — when a step fails:
Done-check + connected state
Used at the end of a connection flow. The check sits on a soft success-tinted disc (16% mix), 64×64.
OpenAI is connected.
Tokenomics is now reading your Codex CLI usage. Want to add another, or jump to the menu bar?
Error block
Inline failure surface — used inside the window body when a specific step fails. Border at 30% danger, fill at 8%, headline in danger, body in --text-muted using monospace for paths.
Rule: every error block ends with a single concrete next-step action button (e.g. "Use per-user install instead"). Never two CTAs of equal weight.
Provider chooser row
List item used in the provider chooser screen (image #7). 32px squircle icon (8px radius), name + scope, and an action chip on the right — either a Connected badge or a Quick setup pill button.
Detect / check row
Used in the "Checking tools" step — shows what the app found on the system. Status icon left, label + sublabel center, optional right-side meta.
Toggle (iOS-style)
Used in the Providers list (image #5) to enable/disable a connected provider. 40×24 capsule, 20px thumb with 1px shadow, 220ms transition.
Plan segment (Free / Standard / Enterprise)
Inline plan selector for Gemini-style providers with multiple tiers (image #5). Reuses the same segmented-pill pattern as the install block but at smaller size.
Help banner
Surfaces at the top of the Providers popover (image #5). Calls attention to the guided onboarding without dominating. Uses 8% accent tint.
Brands that contain pools app addendum
Newer than v3: providers group under a brand (Claude, ChatGPT, Gemini), and a brand can hold several pools — e.g. ChatGPT = the ChatGPT web app + the Codex CLI. The popover collapses tabs to icons at 4+ brands and adds a pool section header per pool; the large widget renders a brand header with indented pool sub-rows. From feat/popover-brand-collapse and feat/widgets-brand-surfaces.
Popover · multi-pool brand
Pool section header: 10 pt semibold, 0.5 letter-spacing, --pop-subtle, label from pinTrackerLabel. The tab strip collapses inactive tabs to icon-only at ≥ 4 brands; drag-reorder is removed on the brand strip (still present on the legacy provider strip).
Large widget · brand-grouped
BrandGroupRow: brand header icon 13 px, name 9 pt semibold at shortColor @ 0.7; pool sub-rows indent 22 px. A single-pool brand collapses to a plain CompactProviderRow (no header). Data-row caps: medium 3, large 6 (brand headers don’t count).
Chrome & Safari companion app addendum
The web-companion popup mirrors the menu-bar popover almost line for line — header, tabs, usage bars, sync footer — but renders in the browser with a widget palette and the system font (not DM Sans), so it reads as the same object as the desktop widget. The options page steps up to the full theme tokens. Verdict: on-brand, by design.
Popup · widget palette + system font · mirrors the menu-bar popover
Options page · full theme tokens (a document surface, so it uses --surface / --accent with real token toggles).
One real gap: the extension’s tokens.css stops at --r-md / --shadow-md — backport --r-lg/--r-xl/--shadow-lg/--shadow-ring for options-page parity.
One recipe, many surfaces app addendum
The components that appear on more than one surface, and where each bends. Rows that are ✓ across many columns are the spine that makes the product feel like one thing; the “~” rows are where a token version and a native version coexist — the highest-leverage targets for unification.
| Component | Web | Ext. | Popover | Widget | Onboard | Settings |
|---|---|---|---|---|---|---|
| Usage bar + pace dot | ✓ | ✓ | ✓ | ✓ | – | – |
| Tab strip (icon-collapse) | – | ✓ | ✓ | – | – | – |
| Segmented control | ✓ | – | – | – | ~ | ✓ |
| Pill / badge | ✓ | ✓ | ✓ | – | ✓ | ✓ |
| Icon button | – | ✓ | ✓ | – | – | ~ |
| Card (surface) | ✓ | ✓ | – | – | ✓ | ✓ |
| Toggle | ✓ | ✓ | – | – | – | ~ |
| Provider icon | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Dual ring | ✓ | – | – | ✓ | ✓ | – |
✓ present & aligned · ~ present but native/variant · – n/a on this surface
Named, for review app addendum
Every item here is a proposal flagged for us to decide together — surfaced by reading each surface against the token spec. Grouped into token additions, component names, drift to resolve, and open questions only you can answer. Nothing here is decided.
Token additions
Provider-icon squircles use 8 px — between --r-xs (6) and --r-sm (10). Name it, or snap to --r-sm.
The popover pool-section header is a real 10 pt semibold role with no token. Add it to Typography.App.
Add --r-lg / --r-xl / --shadow-lg / --shadow-ring to the extension’s tokens.css for options-page parity.
Tints recur at 6 / 8 / 12 / 14 / 18 / 22 / 30% — name the ladder instead of re-deriving per component.
Component names
Popover multi-pool label row — currently inlined in PopoverView.
The brand-aware empty-popover state; already token-clean — promote it as the model for the other states.
One card + one tinted-badge recipe, re-built across chooser / multi-select / permission steps — extract once.
Widget brand header + indented sub-rows (icon 13, name 9/semibold@0.7, indent 22) — pin the values.
Drift to resolve
Error/auth icons use system .orange → warning; tab selected-bg uses white@10% → surface-2; tab radius 8 → --r-xs.
design-system.md says --text; Tokens.swift + mockup say brand-200 (cyan). Cyan is correct — the MD table is stale.
Adopt token toggle + segmented in Notifications / visibility / Gemini modal — unless we keep Settings deliberately OS-native (see D-2).
The worktree added a utilization arg the body ignores — implement utilization-driven color, or revert the signature.
Open questions
Memory says 680×580; code measures 720×560. Which is authoritative?
Is the SF-Pro-native feel intentional, or unfinished? Decides whether the drift above is a fix.
Shared component names across Swift + extension + web, or per-runtime?
This file is now the full web system (duplicated from v3) plus app surfaces. Keep it as the one combined doc, or trim back to app-only and cross-reference v3?
CSS custom properties
Copy-paste this table into styles.css — it's already wired into the page above.
| Token | Value | Role |
|---|---|---|
--ink-900 | #051928 | Deepest navy · gradient end |
--ink-800 | #0E334D | Primary ink · gradient start |
--ink-700 | #286195 | Bar track base (light) |
--brand-600 | #2F84BF | Short bar / labels (light) |
--brand-500 | #3389C7 | Long bar (dark mode) |
--brand-400 | #4BA6D2 | Bar track base (dark) |
--brand-300 | #56A2D6 | Long bar (light mode) |
--brand-200 | #75CBF5 | Short bar / labels (dark) |
--cream-50 | #F3EFE5 | Page background |
--cream-100 | #E6E0D4 | Secondary surface · gradient end |
--font-serif | 'Hedvig Letters Serif' | Display, H1–H4, pull-quotes |
--font-sans | 'DM Sans' | Body, UI, numerics, eyebrow |
--r-md | 14px | Cards, inputs |
--r-lg | 20px | Section cards |
--r-xl | 28px | Hero widgets (app uses 22px) |
--shadow-lg | 28/60 navy @ 10% | Hero widgets, featured art |
--dur / --ease | 220ms / cubic-bezier(.2,.7,.2,1) | Default transition |
--success | #2F8F4F · #6FD18A | Connected badges, done-checks |
--warning | #C26A1F · #E2A765 | Soft alerts (amber, not yellow) |
--danger | #B33A3A · #E27777 | Errors, failed steps, destructive |
Usage notes
- App vs site: The macOS app uses SF (system). The website uses Hedvig Letters Serif + DM Sans. Widget mockups shown here intentionally keep SF stack to read as product, not marketing.
- Serif discipline: Hedvig only for H1–H4 and pull-quotes. Never for CTAs, labels, or numerics.
- Numerics: Always
font-variant-numeric: tabular-nums;for percentages and counters so rows don't shift. - Widget parity: The light/dark widget gradients on the site must match the app's widget extension exactly — they're the same
#0E334D → #051928/#F3EFE5 → #E6E0D4stops. - Contrast: Label color at 50% opacity (dark) / 67% (light) mirrors the Figma spec; preserve this or text loses its editorial calm.