The single source of truth for building Freestyle UI. Read this before adding any screen, component, or marketing surface. Values here are lifted directly from
apps/electron/src/renderer/src/globals.cssand the shipped settings pages — match them exactly. When in doubt, copy an existing page's recipe rather than inventing a new one.
Freestyle is voice dictation for your desktop — hold a key, speak, and it types itself anywhere. The interface is editorial, calm, and warm: it reads like a well-set magazine, not a SaaS dashboard.
Three principles govern every decision:
- Warm paper, not cold white. The whole product sits on a cream substrate
(
#F4F0E4). Never use pure white (#FFF) or pure black (#000). - Serif for voice, mono for machinery. A large Instrument Serif headline with one italic, olive-accented word is the signature. Monospace, uppercase, widely-tracked micro-labels do the structural/metadata work.
- Restraint. Olive is a seasoning, not a flood. One accent word, one active state, one meter — never a page of green. Lots of air; hairline borders; no drop shadows except on things that truly float (modals, popovers).
Avoid: gradients, glassmorphism, emoji, neon, heavy shadows, rounded-pill buttons everywhere, icon soup, and "data slop" (decorative stats/badges that don't inform a decision).
Tokens are CSS variables on :root (light) and .dark. Always consume the
semantic token, never a raw hex, in product UI. Raw hex is listed only so
you can reproduce the palette in a standalone HTML artifact.
| Token | Hex | Use |
|---|---|---|
--background |
#F4F0E4 |
App canvas (warm paper) |
--foreground |
#16140F |
Primary text (near-black ink) |
--card |
#FBF8EE |
Card / panel / popover surface |
--card-foreground |
#16140F |
Text on cards |
--primary |
#6B8F12 |
Olive accent — italic headline word, active state, meters, primary CTA fill |
--primary-foreground |
#FBF8EE |
Text/icon on olive |
--secondary |
#ECE7D6 |
Subtle fills — segmented tracks, sidebar, hover |
--secondary-foreground |
#34302A |
Text on secondary |
--muted |
#ECE7D6 |
Muted fill |
--muted-foreground |
#7B7461 |
Secondary text, eyebrows, metadata |
--accent |
#E8EFC9 |
Pale olive wash — on-device / positive highlight |
--accent-foreground |
#2E3F05 |
Text on accent wash |
--destructive |
#DD6E4E |
Terracotta — destructive actions, errors, warnings |
--border |
#D6CDB8 |
Hairline borders, dividers |
--input |
#E3DCC8 |
Input borders |
--ring |
#6B8F12 |
Focus ring (olive) |
--plum (--chart-3) |
#5E4E78 |
Tertiary data accent only — never a UI accent |
| Token | Hex |
|---|---|
--background |
#16140F |
--foreground |
#ECE7D6 |
--card |
#1E1C16 |
--primary |
#8AB62A (brighter olive for contrast) |
--primary-foreground |
#16140F |
--secondary / --muted |
#2A2720 |
--muted-foreground |
#9E977F |
--accent |
#2E3F05 |
--accent-foreground |
#E8EFC9 |
--destructive |
#E0805F |
--border / --input |
#3A362D |
--ring |
#8AB62A |
- Olive is rationed. A typical screen has olive in exactly 2–3 places: the italic headline word, the active/selected state, and a single CTA or meter.
- On-device =
--accentwash + olive. Privacy/local affordances use the pale olive (--accent/--accent-foreground). Cloud/neutral uses--secondary. - Selection tint: active rows use
rgba(107,143,18,0.06)(olive at 6%), not a solid fill. - Text selection is olive at 25%:
::selection { background: rgba(107,143,18,0.25) }.
Three families, three jobs. Load weights 300–800 for DM Sans; 400 for the others.
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:opsz,wght@9..40,300..800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">| Role | Family | Where |
|---|---|---|
| Display | Instrument Serif (.serif, .serif-italic) |
Page titles, hero numbers, the model name in a "current" slot |
| Body / UI | DM Sans |
Everything functional — labels, paragraphs, buttons, inputs. Base 14px / line-height 1.5 |
| Mono / micro-label | JetBrains Mono (.mono) |
Eyebrows, metadata, badges, keycaps, status. Always uppercase + tracked when used as a label |
Helper classes (from globals.css):
.serif { font-family:"Instrument Serif",serif; letter-spacing:-0.01em; line-height:0.95; }
.serif-italic { font-family:"Instrument Serif",serif; font-style:italic; letter-spacing:-0.005em; line-height:0.95; }
.mono { font-family:"JetBrains Mono",ui-monospace,monospace; }Every top-level page uses this exact pattern — an italic, olive accent word followed by an upright period:
<h1 class="serif text-foreground m-0 text-[48px] font-normal leading-[0.95] tracking-[-0.025em]">
<span class="serif-italic text-primary">Models</span><span>.</span>
</h1>Optionally followed by one muted sentence (text-muted-foreground text-[14px] leading-[1.5] max-w-[580px]).
Mono, 10px, uppercase, tracking 0.14em–0.18em, muted (or text-primary when
it labels the required/primary thing):
<span class="mono text-muted-foreground text-[10px] uppercase tracking-[0.16em]">Providers & keys</span>| Use | Size / class |
|---|---|
| Page title | 48px serif |
| "Current value" serif (e.g. selected model) | 30–34px serif |
| Section heading | mono 10px eyebrow (structure comes from labels, not big H2s) |
| Body | 14px |
| Secondary / metadata | 11–13px |
| Badge / keycap | 9–10px mono |
Tracking convention: the smaller the mono label, the wider the tracking
(9–10px → 0.14–0.18em; 11px → 0.04em).
- Spacing: Tailwind 4px scale. Common rhythm — card padding
p-6(24px), gapsgap-2.5 / gap-3 / gap-6, section spacingmt-7(28px). - Radius:
--radius: 0.625rem(10px) is the base. In practice:- Cards / panels / modals →
rounded-[14px] - Buttons, inputs, small controls →
7–8px - Pills, badges, toggles, chips →
rounded-full
- Cards / panels / modals →
- Borders: always
1px solid var(--border). Hairlines everywhere — they do the work shadows would in other systems. Empty states use the same borderborder-dashed. - Shadows: only on floating layers.
- Popover/picker:
0 24px 50px -34px rgba(20,12,4,0.4) - Modal:
0_24px_60px_-16px_rgba(20,12,4,0.4) - Resting cards: no shadow.
- Popover/picker:
- Library:
lucide-react. (In standalone HTML, hand-draw matching 24×24 paths atstroke-width:1.7, round caps/joins.) - Size: 12–16px inline; default color
--muted-foreground, shifting to--primaryonly when active/selected or signalling on-device/privacy. - Vocabulary in use:
Mic(voice),Sparkles(LLM cleanup),HardDrive/Cpu(local / hardware),Download,Check,Search,RefreshCw,AlertTriangle(errors),Key(API keys),Trash2,Plus,X,ChevronRight. - Keep icons functional. Don't decorate headings with icons.
<section class="border-border bg-card rounded-[14px] border p-6"> … </section>Grid that stacks under 820px, divided by a hairline:
<section class="border-border bg-card grid grid-cols-1 gap-6 rounded-[14px] border p-6 min-[820px]:grid-cols-2"> … </section><span class="mono bg-primary text-primary-foreground rounded-full px-1.5 py-[2px] text-[9px] tracking-[0.14em]">ACTIVE</span>Variants: olive (bg-primary) for active; bg-accent text-accent-foreground
for on-device/positive; bg-primary/10 text-primary for "FASTER"-type hints.
Rounded-full, 1px border; selected = olive fill + olive border + primary-fg text; idle = transparent + border + muted text.
- Primary:
bg-foreground text-background(near-black ink on paper), hoverbg-foreground/90. Radius 7–8px. (Olive fill is reserved for selection states/CTAs where the ink button would compete.) - Secondary/ghost:
border-border border bg-transparent, hoverbg-secondary. - Label: DM Sans
12.5px,font-medium/600.
22px tall, 40px wide, rounded-full. On = bg-primary border-primary/80 with a
primary-foreground knob; off = bg-secondary border-border with a muted knob.
<div class="border-border bg-background flex items-center gap-2 rounded-md border px-2.5 py-1">
<Search class="text-muted-foreground h-3.5 w-3.5" />
<input class="flex-1 border-none bg-transparent text-[12.5px] outline-none placeholder:text-muted-foreground/70" />
</div>Hairline-divided rows inside a rounded-[14px] card; selected row tinted
bg-primary/5 with a trailing Check in --primary. Use CSS grid with
explicit columns + gap, never inline-flow siblings.
Five 5px dots, filled to value in --primary, empty in --border.
Track bg-secondary, fill bg-primary, height 1.5 (6px), rounded-full.
Pair with mono 10px byte/percent readout.
<div class="border-border bg-card rounded-[14px] border border-dashed px-9 py-[52px] text-center"> … </div>Centered, bg-card w-full max-w-md rounded-[14px] border p-7 with the modal
shadow above. Dim the backdrop with warm ink, not pure black.
- The app is an Electron desktop window. The top 36–38px is a draggable
titlebar (
WebkitAppRegion: drag); interactive content setsno-drag. - Page scroll area uses
.responsive-page-scroll—padding-inline: 3rem(→ 2rem ≤1080px → 1rem ≤820px),padding-bottom: 3rem. - Content max-width ~760px, centered, for settings/editorial pages — keep measure comfortable; don't run full-bleed text.
- Page rhythm:
Title→ one muted sentence → primary card → supporting sections separated bymt-7and a mono eyebrow each. - Left nav (settings):
bg-secondaryrail, active item lifts tobg-cardwith a 1px shadow and olive icon.
- Subtle and quick:
transition~0.15s easeon color, background, border, opacity, and toggle/knobtransform. - Disabled/secondary content dims via
opacity(e.g. optional pane at0.55–0.6when its toggle is off) rather than disappearing. - No bounces, no long eases, no parallax. The pill/recording states may pulse, nothing else.
- Voice: plain, confident, second person. "Choose how Freestyle listens." "Audio never leaves your Mac." Short sentences. No marketing fluff, no exclamation marks.
- Labels: mono eyebrows are terse and categorical —
VOICE · REQUIRED,LLM CLEANUP · OPTIONAL,PROVIDERS & KEYS. - Wordmark: lowercase
freestylewith an olive period —freestyle.Never capitalize it mid-sentence; never drop the period in the logo lockup. - Numbers/metadata (sizes, RAM, $/hr, percentages) are mono.
- Sentence case for everything except mono labels (which are UPPERCASE).
Do
- Start from an existing page's structure and swap the content.
- Keep one olive accent word per title; keep one primary action per view.
- Use hairline borders + generous whitespace to separate, before reaching for fills or shadows.
- Make every badge/stat earn its place by informing a choice.
Don't
- Introduce new hues, gradients, or a second accent color.
- Use pure white/black, emoji, or drop shadows on resting elements.
- Build big
<h2>section headers — structure with mono eyebrows. - Lay out rows of controls as bare inline siblings — use flex/grid +
gap. - Add placeholder/filler sections to fill space.
When prototyping outside the app, set the tokens on :root and use the three
font families. Minimum viable shell:
<style>
:root{
--background:#F4F0E4; --foreground:#16140F; --card:#FBF8EE;
--primary:#6B8F12; --primary-foreground:#FBF8EE;
--secondary:#ECE7D6; --muted-foreground:#7B7461;
--accent:#E8EFC9; --accent-foreground:#2E3F05;
--destructive:#DD6E4E; --border:#D6CDB8; --radius:10px;
}
body{ background:var(--background); color:var(--foreground);
font-family:"DM Sans",system-ui,sans-serif; font-size:14px; line-height:1.5; }
.serif{ font-family:"Instrument Serif",serif; }
.serif-italic{ font-family:"Instrument Serif",serif; font-style:italic; }
.mono{ font-family:"JetBrains Mono",ui-monospace,monospace; }
</style>Then reach for the recipes in §6. If a new pattern is genuinely needed, design
it from these tokens (use oklch to derive harmonious neighbours of the olive
rather than inventing a fresh color), and add it back to this file.