App Guidelines · Tokens 0.5.4

Random Fact

Two chromes, one control height, one radius, nine type styles and one set of tokens. Every app is built from the same small system.

Every control, corner, type style and color below is drawn with the values @randomfact/tokens 0.5.4 ships, at their specified sizes, in the real webfonts. Contrast ratios are computed live from the hexes. Accepted 2026-09-23

Render in

00

The palette

The six brand anchors are the same hex in both themes and pair within a layer. The surface system and the data tokens (success, danger, warning) take a value per theme. Brand ink, --rf-brand, is blue in light and sky in dark: links, marketing headings, the focus ring and any brand-colored mark. Blue itself does not flip, and there is no lifted blue. Shown so the app layer can be judged over the ground it sits on.

light

Anchors, paired by layer

gold #f7b600
blue #3f7ca5
cream #fce9a8
sky #c0e5f9
bronze #915e00
navy #071f39

Surfaces and semantic colors

--bg (page)
#faf9f7
--surface (card)
#ffffff
--border
#e5e7eb
--muted
#f1f0ee
--secondary / --accent
#c0e5f9
--primary
#f7b600
on it: #071f39 9.21:1 AA
--destructive
#ef4444
white on it 3.76:1 fails
--ring
#3f7ca5

Brand ink and the data tokens

A link, and a marketing heading
--rf-brand #3f7ca5 (blue) · on card 4.53:1 AA · page 4.30:1 fails, large text only · also --ring
+4.2%
--rf-success #1a7a4a · on card 5.35:1 AA
−1.8%
--rf-danger #c2410c · on card 5.18:1 AA
↑ Price increase
--rf-warning #9a6700 · on card 4.87:1 AA · page 4.63:1 AA
Delete
--destructive #ef4444 · an action, same in both themes
dark

Anchors, paired by layer

gold #f7b600
blue #3f7ca5
cream #fce9a8
sky #c0e5f9
bronze #915e00
navy #071f39

Surfaces and semantic colors

--bg (page)
#000000
--surface (card)
#0a1929
--border
#334155
--muted
#0d1f30
--secondary / --accent
#14293f
--primary
#3f7ca5
on it: #f1f5f9 4.13:1 fails
--destructive
#ef4444
white on it 3.76:1 fails
--ring
#c0e5f9

Brand ink and the data tokens

A link, and a marketing heading
--rf-brand #c0e5f9 (sky) · on card 13.36:1 AA · page 15.82:1 AA · also --ring
+4.2%
--rf-success #4ade80 · on card 10.18:1 AA
−1.8%
--rf-danger #fb923c · on card 7.84:1 AA
↑ Price increase
--rf-warning #fbbf24 · on card 10.62:1 AA · page 12.58:1 AA
Delete
--destructive #ef4444 · an action, same in both themes

01

Two chromes

Shell (Can, Convo): sidebar + a 56px page-header bar, mobile-first. Workbench (Investment Analyzer, Transaction Analyzer): a 56px top bar with wordmark, tabs, workspace trigger, icon actions and ⋮. Both ship from @randomfact/ui; nothing else is allowed.

light

Shell · AppShell

Platform

New ticket

PLAT-58 App design system

Both chromes in @randomfact/ui, one control scale, the extended palette.

PLAT-53 Overview capture on release

Regenerate the screenshots as part of pnpm release.

Page title in the bar is the Section heading style (Exo 600 16px). Sidebar toggle and search are 36-square IconButtons; New ticket is the default Button.

Workbench · WorkbenchShell

Transaction Analyzerby Random Fact
OverviewLedgerReports
Acme Ltd
Ledger

September 2026

Scroll inside this frame: the header is sticky, its surface is the page at .84 (light) / .80 (dark) under a 4px blur, with the hairline in --rf-line.

Uncategorized

14 transactions waiting for a category.

Rules

3 rules ran this month.

Exports

Last export 12 Sep 2026.

Team

2 members.

Header row min-h 56, 36px controls, 16px glyphs, 2px progress overlay that never grows the bar. The header is one row at every width: the tabs scroll inside their own strip, and actions the row can't fit go into ⋮, as here. On a phone the wordmark's words hide, leaving the logo, and the workspace name truncates. The top bar draws the wordmark itself; the app's logo, which sits to its left, is left out here.

dark

Shell · AppShell

Platform

New ticket

PLAT-58 App design system

Both chromes in @randomfact/ui, one control scale, the extended palette.

PLAT-53 Overview capture on release

Regenerate the screenshots as part of pnpm release.

Page title in the bar is the Section heading style (Exo 600 16px). Sidebar toggle and search are 36-square IconButtons; New ticket is the default Button.

Workbench · WorkbenchShell

Transaction Analyzerby Random Fact
OverviewLedgerReports
Acme Ltd
Ledger

September 2026

Scroll inside this frame: the header is sticky, its surface is the page at .84 (light) / .80 (dark) under a 4px blur, with the hairline in --rf-line.

Uncategorized

14 transactions waiting for a category.

Rules

3 rules ran this month.

Exports

Last export 12 Sep 2026.

Team

2 members.

Header row min-h 56, 36px controls, 16px glyphs, 2px progress overlay that never grows the bar. The header is one row at every width: the tabs scroll inside their own strip, and actions the row can't fit go into ⋮, as here. On a phone the wordmark's words hide, leaving the logo, and the workspace name truncates. The top bar draws the wordmark itself; the app's logo, which sits to its left, is left out here.

02

One control height

Every control that sits in a row is 36px, control text 14px, every glyph 16px. Button has no sm/lg; icon-only is a 36-square IconButton. The one exception is RowAction, 28px, inside a table row or list item only. Chips and badges are one size too: 20px, 12/500, rounded-sm, and never align with a control.

light
36
SaveCancelAssignGhostDeleteSearch transactions…Northwind TradersGroceriesOverviewLedgerAcme Ltd
28
RowAction, 28 square, 16px glyph. Never in a control row beside a 36.
Rule “Groceries” ran on 14 transactions.
Two payees need a category.

Alert, Progress, Slider and Sheet are not row controls. An alert is as tall as its text (14px, 16px glyph), with its tone on the border and glyph and its words on the two text tiers. A progress bar is an 8px rounded-full track. A slider is a 6px track in --muted with a brand-ink range and a 16px thumb edged in brand ink. A sheet is a side panel docked to a viewport edge, with no radius, closed by the 36px IconButton. Command and the floating selection bar are app-local, in Transaction Analyzer.

PayeeCategoryAmount
Northwind TradersGroceries−1,204.95
Contoso EnergyUtilities−88.20
SalaryIncome4,250.00
Total2,956.85

Rows: Montserrat 400 14px, numbers tabular-nums right-aligned, total row 600. Chips are the data-color pair at one 20px size, rounded-sm. Input text is 16px below md (iOS zoom), 14px from md; shown at 14 here.

dark
36
SaveCancelAssignGhostDeleteSearch transactions…Northwind TradersGroceriesOverviewLedgerAcme Ltd
28
RowAction, 28 square, 16px glyph. Never in a control row beside a 36.
Rule “Groceries” ran on 14 transactions.
Two payees need a category.

Alert, Progress, Slider and Sheet are not row controls. An alert is as tall as its text (14px, 16px glyph), with its tone on the border and glyph and its words on the two text tiers. A progress bar is an 8px rounded-full track. A slider is a 6px track in --muted with a brand-ink range and a 16px thumb edged in brand ink. A sheet is a side panel docked to a viewport edge, with no radius, closed by the 36px IconButton. Command and the floating selection bar are app-local, in Transaction Analyzer.

PayeeCategoryAmount
Northwind TradersGroceries−1,204.95
Contoso EnergyUtilities−88.20
SalaryIncome4,250.00
Total2,956.85

Rows: Montserrat 400 14px, numbers tabular-nums right-aligned, total row 600. Chips are the data-color pair at one 20px size, rounded-sm. Input text is 16px below md (iOS zoom), 14px from md; shown at 14 here.

03

One radius

--radius: 0.625rem (10px) and the shadcn derivation, published as aliases. Every app draws these four radii and none declares its own. Each tile shows its corner at 3× zoom.

light
sm · 6px
chips, badges, kbd
Design
3×
md · 8px
every control, menus, menu items
Save changes
3×
lg · 10px
popovers, dialogs, sheets, alerts
3×
xl · 14px
cards and panels

Q3 invoices

12 open, 3 overdue

3×

Write the alias, never bare rounded: bare rounded reads --radius itself (10px), larger than rounded-md.

dark
sm · 6px
chips, badges, kbd
Design
3×
md · 8px
every control, menus, menu items
Save changes
3×
lg · 10px
popovers, dialogs, sheets, alerts
3×
xl · 14px
cards and panels

Q3 invoices

12 open, 3 overdue

3×

Write the alias, never bare rounded: bare rounded reads --radius itself (10px), larger than rounded-md.

04

Type styles

Exo speaks (titles, figures), Montserrat reads. Nine styles, no fractional sizes, 12px is the floor, no italic. Display figure is for the one number a page is about; any other number is a Figure. fonts.css loads exactly Exo 600/700 and Montserrat 400/500/600.

light
Accounts
Page titleExo 700 · 24/32 · tracking-tight
1,284,310
Display figureExo 700 · 40/40 · tracking −0.02em · tabular-nums · one per page
1,204,395.20
FigureExo 700 · 24/32 · tabular-nums · a KPI or stat
Recent activity
Section headingExo 600 · 16/24 · tracking −0.01em
Q3 invoices
Card titleMontserrat 600 · 14/20
Save changes
Control labelMontserrat 500 · 14/20 · buttons, tabs, triggers
Paid to Northwind Traders on 14 September 2026, matched to rule “Groceries”.
BodyMontserrat 400 · 14/20 · inputs, menu items, cells
Workspace
OverlineMontserrat 600 · 12/16 · uppercase, tracking-wide, --text-2
Updated 2 minutes ago
Caption / metaMontserrat 400 · 12/16 · --text-2

The weights the apps load

Exo 600 — Section headingloaded
Exo 700 — Page title, Display figure, Figureloaded
Montserrat 400 — Body, Captionloaded
Montserrat 500 — Control labelloaded
Montserrat 600 — Card title, Overlineloaded
Montserrat italic — not loadedsynthesised oblique

The apps load only these five. A weight they leave out renders as the nearest loaded one: an Exo element left at 400 renders 600, a Montserrat element at 700 renders 600. (This site loads the full variable fonts, so that fallback can't be shown live here.)

Numbers in tables

Groceries1,204.95
Utilities88.20
Transport312.00
Total1,605.15

Amounts are right-aligned and every digit is the same width, so a column lines up by place value. A single headline number, like a balance, uses the Figure style instead.

The brand sets amounts like these in its monospace, DM Mono, but the apps' shared fonts don't include it yet, so they use Montserrat's same-width digits. Investment Analyzer already uses DM Mono for typing numbers in.

The app wordmark

Investment Analyzerby Random Fact

The app's name in Exo 700 at 16px, over “by Random Fact” in Montserrat 400 at 12px, in sentence case. The app's logo sits to its left (left out here). The top bar draws the wordmark, so an app never sets it itself.

dark
Accounts
Page titleExo 700 · 24/32 · tracking-tight
1,284,310
Display figureExo 700 · 40/40 · tracking −0.02em · tabular-nums · one per page
1,204,395.20
FigureExo 700 · 24/32 · tabular-nums · a KPI or stat
Recent activity
Section headingExo 600 · 16/24 · tracking −0.01em
Q3 invoices
Card titleMontserrat 600 · 14/20
Save changes
Control labelMontserrat 500 · 14/20 · buttons, tabs, triggers
Paid to Northwind Traders on 14 September 2026, matched to rule “Groceries”.
BodyMontserrat 400 · 14/20 · inputs, menu items, cells
Workspace
OverlineMontserrat 600 · 12/16 · uppercase, tracking-wide, --text-2
Updated 2 minutes ago
Caption / metaMontserrat 400 · 12/16 · --text-2

The weights the apps load

Exo 600 — Section headingloaded
Exo 700 — Page title, Display figure, Figureloaded
Montserrat 400 — Body, Captionloaded
Montserrat 500 — Control labelloaded
Montserrat 600 — Card title, Overlineloaded
Montserrat italic — not loadedsynthesised oblique

The apps load only these five. A weight they leave out renders as the nearest loaded one: an Exo element left at 400 renders 600, a Montserrat element at 700 renders 600. (This site loads the full variable fonts, so that fallback can't be shown live here.)

Numbers in tables

Groceries1,204.95
Utilities88.20
Transport312.00
Total1,605.15

Amounts are right-aligned and every digit is the same width, so a column lines up by place value. A single headline number, like a balance, uses the Figure style instead.

The brand sets amounts like these in its monospace, DM Mono, but the apps' shared fonts don't include it yet, so they use Montserrat's same-width digits. Investment Analyzer already uses DM Mono for typing numbers in.

The app wordmark

Investment Analyzerby Random Fact

The app's name in Exo 700 at 16px, over “by Random Fact” in Montserrat 400 at 12px, in sentence case. The app's logo sits to its left (left out here). The top bar draws the wordmark, so an app never sets it itself.

05

Two text tiers

--text-1 and --text-2, no opacity modifiers, no third tier. Brand ink, --rf-brand, is the one other ink, for links and brand marks. Ratios are measured against the page and the card.

light
Paid to Northwind Traders--text-1 on page 13.95:1 AA
Matched by rule “Groceries”, 2 minutes ago--text-2 on page 4.59:1 AA
Disabled control text is the only faded textopacity-50, disabled only
Paid to Northwind Traders--text-1 on card 14.68:1 AA
Matched by rule “Groceries”, 2 minutes ago--text-2 on card 4.83:1 AA
No /70: an opacity modifier is a third tier by another name—
dark
Paid to Northwind Traders--text-1 on page 19.17:1 AA
Matched by rule “Groceries”, 2 minutes ago--text-2 on page 8.19:1 AA
Disabled control text is the only faded textopacity-50, disabled only
Paid to Northwind Traders--text-1 on card 16.19:1 AA
Matched by rule “Groceries”, 2 minutes ago--text-2 on card 6.92:1 AA
No /70: an opacity modifier is a third tier by another name—

06

Workbench chrome

Three tokens: --rf-header-bg, --rf-line, --rf-sand. Tab ink is --text-2 inactive and --text-1 on hover and active; the hover fill is sand at 60%. Frame 1640px with a 16px gutter, shared by the bar and the page.

light
Reportsinactive · --text-2 4.59:1 AA
Reportshover · sand 60%, --text-1
Reportsactive · sand, --text-1 13.13:1 AA
Reportswhy not --text-2 on sand: 4.32:1 fails
--rf-header-bg
rgba(250, 249, 247, 0.84)
over the page: --bg at .84
--rf-line
rgba(7, 31, 57, 0.12)
navy hairline
--rf-sand
#e8f4fc
active-tab fill
--rf-sand 60%
color-mix(in srgb, #e8f4fc 60%, transparent)
tab hover fill
Ledger · September 2026Uncategorized · 14 transactionsRules · 3 ran this month
header surface over content, backdrop-blur-sm
dark
Reportsinactive · --text-2 8.19:1 AA
Reportshover · sand 60%, --text-1
Reportsactive · sand, --text-1 15.26:1 AA
Reportswhy not --text-2 on sand: 6.52:1 AA
--rf-header-bg
rgba(0, 0, 0, 0.8)
over the page: --bg at .8
--rf-line
rgba(192, 229, 249, 0.16)
sky hairline
--rf-sand
#0d1f30
= the dark --muted
--rf-sand 60%
color-mix(in srgb, #0d1f30 60%, transparent)
tab hover fill
Ledger · September 2026Uncategorized · 14 transactionsRules · 3 ran this month
header surface over content, backdrop-blur-sm

07

Data color

Twelve categorical hues in four families, plus a three-step neutral ramp whose first step is “Other”. A chart hue clears 3:1 on the page; a chip's ink clears 4.5:1 on its ground over the card. The chart order is the one that keeps adjacent series apart under color-blindness. There is no red or pink hue. State is a separate pair: success, warning and danger each have an ink on an opaque ground, also 4.5:1.

light

Chart order, hue on the page

1. blue-2
#2971a7
on page 4.97:1 ✓
2. gold-1
#af7c00
on page 3.50:1 ✓
3. teal-3
#006d4b
on page 6.07:1 ✓
4. purple-1
#97549b
on page 4.92:1 ✓
5. teal-2
#33854a
on page 4.34:1 ✓
6. purple-3
#4d478c
on page 7.69:1 ✓
7. teal-1
#008e82
on page 3.84:1 ✓
8. purple-2
#6f57ab
on page 5.50:1 ✓
9. gold-2
#a65d00
on page 4.77:1 ✓
10. blue-3
#31559a
on page 6.89:1 ✓
11. gold-3
#9d4616
on page 6.01:1 ✓
12. blue-1
#1a8ead
on page 3.62:1 ✓

A chart never cycles the order: a thirteenth series folds into Other. A dot that names something outside a chart, in a list or a rail, may take the order once more as a hollow ring with a 2px stroke; past 24 it folds into Other too.

The neutral ramp

neutral-1 · Other · fill
#657383
on page 4.61:1 ✓
neutral-2 · fill
#394655
on page 9.15:1 ✓
neutral-3 · rules and text only
#121e2b
on page 16.01:1 ✓

Same cool tint, each step further from the page: darker in light, lighter in dark. A neutral flow (a draw, a sink) fills with neutral-1 and neutral-2 only. neutral-3 is for axes, dividers and text: near-black in light, and in dark too close to the sky brand ink to fill a mark.

Chips, ink on ground over the card, by family

blue-16.90:1 AAblue-27.24:1 AAblue-37.43:1 AAteal-16.74:1 AAteal-27.03:1 AAteal-36.77:1 AAgold-17.36:1 AAgold-27.50:1 AAgold-37.57:1 AApurple-17.65:1 AApurple-27.59:1 AApurple-37.51:1 AAneutral-17.27:1 AAneutral-28.63:1 AAneutral-310.12:1 AA

Status chips, ink on an opaque ground over the card

success6.25:1 AAwarning6.09:1 AAdanger6.40:1 AA

State takes a status variant (Badge variant="success|warning|danger"); identity takes a Chip hue. A status is never painted in a categorical hue, nor a category in a status pair.

dark

Chart order, hue on the page

1. blue-2
#4a8fc7
on page 6.04:1 ✓
2. gold-1
#bb881a
on page 6.65:1 ✓
3. teal-3
#0f825e
on page 4.37:1 ✓
4. purple-1
#b06cb4
on page 5.65:1 ✓
5. teal-2
#50a064
on page 6.55:1 ✓
6. purple-3
#635ea5
on page 3.67:1 ✓
7. teal-1
#179a8e
on page 6.05:1 ✓
8. purple-2
#8c74cc
on page 5.49:1 ✓
9. gold-2
#b96f1c
on page 5.35:1 ✓
10. blue-3
#4a70b7
on page 4.29:1 ✓
11. gold-3
#b85e31
on page 4.68:1 ✓
12. blue-1
#1a8ead
on page 5.52:1 ✓

A chart never cycles the order: a thirteenth series folds into Other. A dot that names something outside a chart, in a list or a rail, may take the order once more as a hollow ring with a 2px stroke; past 24 it folds into Other too.

The neutral ramp

neutral-1 · Other · fill
#798898
on page 5.79:1 ✓
neutral-2 · fill
#aab9ca
on page 10.50:1 ✓
neutral-3 · rules and text only
#e2ecf9
on page 17.60:1 ✓

Same cool tint, each step further from the page: darker in light, lighter in dark. A neutral flow (a draw, a sink) fills with neutral-1 and neutral-2 only. neutral-3 is for axes, dividers and text: near-black in light, and in dark too close to the sky brand ink to fill a mark.

Chips, ink on ground over the card, by family

blue-18.77:1 AAblue-28.78:1 AAblue-38.82:1 AAteal-18.71:1 AAteal-28.76:1 AAteal-38.72:1 AAgold-18.84:1 AAgold-28.78:1 AAgold-38.78:1 AApurple-18.85:1 AApurple-28.80:1 AApurple-38.78:1 AAneutral-18.84:1 AAneutral-27.83:1 AAneutral-36.87:1 AA

Status chips, ink on an opaque ground over the card

success7.21:1 AAwarning7.98:1 AAdanger6.34:1 AA

State takes a status variant (Badge variant="success|warning|danger"); identity takes a Chip hue. A status is never painted in a categorical hue, nor a category in a status pair.

08

The token rule

No app declares an --rf-* property, and no component file or stylesheet carries a raw hex or a Tailwind color-ramp class. A design value an app needs is a change to tokens plus the app's adoption; @randomfact/ui/testing enforces it.

Every app's stylesheet

@import "@randomfact/tokens/fonts.css";
@import "tailwindcss";
@import "@randomfact/tokens/theme.css";

Nothing else from the package. palette.css is the file theme.css imports, not an entry.

The gate fails on

  • an --rf-* or --color-rf-* declaration (bar the four --rf-md-* knobs)
  • an import of @randomfact/tokens/palette.css
  • a components/ui/ folder
  • a raw hex or Tailwind color-ramp class in a component file or stylesheet
  • a --radius: declaration
  • a text size off the type table (text-lg, text-[11px], fontSize: 13), including a chart label under 12px
  • a weight above 600 on text that isn't set in Exo
  • an opacity modifier on ink (text-muted-foreground/70)

Allowed by name

  • setting a published knob: --rf-md-block-gap, --rf-md-heading-lead on a docs selector
  • a brand mark's SVG hexes, as a permanent allow.hex entry: a logo is artwork, not a design value
  • a chart not yet moved onto @randomfact/tokens/palette, listed in chartModules: a transition allowance, not a home for a chart's own colors
  • an allow entry with a non-empty reason, which fails as stale once it matches nothing

09

One layer order

What sits over a page stacks in one order, lowest first. The bottom-right corner belongs to the update notice: nothing else an app floats takes it, and toasts sit top-center.

light
Layerz-indexWhat sits there
Page chrome20PageHeader
30WorkbenchShell header, AppShell scrim
Update notice35UpdateNotice
Sidebar40AppShell sidebar
Modal50dialogs, sheets, popovers, menus
Lightbox100the marketing screenshot lightbox
A new version is available.Reload

The update notice appears once the server has a newer build than the tab is running: a popover surface, one sentence in Body and one 36px Reload button, 16px inside the bottom-right corner. It is not a toast: it has no dismiss and no timeout, stays until the user reloads, and never reloads by itself. It sits above the headers and below the sidebar and any dialog. Anything an app pins to the bottom edge clears it by --rf-update-notice-clearance.

dark
Layerz-indexWhat sits there
Page chrome20PageHeader
30WorkbenchShell header, AppShell scrim
Update notice35UpdateNotice
Sidebar40AppShell sidebar
Modal50dialogs, sheets, popovers, menus
Lightbox100the marketing screenshot lightbox
A new version is available.Reload

The update notice appears once the server has a newer build than the tab is running: a popover surface, one sentence in Body and one 36px Reload button, 16px inside the bottom-right corner. It is not a toast: it has no dismiss and no timeout, stays until the user reloads, and never reloads by itself. It sits above the headers and below the sidebar and any dialog. Anything an app pins to the bottom edge clears it by --rf-update-notice-clearance.

RandomFact

App Guidelines, tokens 0.5.4, accepted 2026-09-23. Values are read from @randomfact/tokens. Mock chrome (wordmarks, nav labels, sample data) is illustrative; the sizes and colors are the published ones.

Sources

  • Palettepalette.json
  • Theme tokenstheme.css
  • Webfontsfonts.css
  • DecisionsDESIGN.md
  • The mark, color and voiceBrand guidelines