1What was found
Every #RRGGBB hex literal, every StyleSheet magic number, every
fontSize that appears in src/screens and
src/components was tabulated and clustered.
1aColors
Total distinct hex literals across screens + components: ~60. Top of the frequency table:
| Count | Hex | Role found in code | Mapped to |
|---|---|---|---|
| 51 | #FFF | card / sheet / input background | surface.primary / fixed.white |
| 31 | #000 | overlay / icon | fixed.black / overlay.* |
| 22 | #E3FF00 | accent / chartreuse highlights | brand.accent |
| 21 | #1A1814 | body text, primary buttons | text.primary / brand.primary |
| 14 | #FCFAF7 | screen background | surface.background |
| 11 | #6B6359 | caption / metadata text | text.secondary |
| 9 | #3B9C4F | deal status, secondary green | feedback.success.soft |
| 7 | #F8FFB3 | accent-tinted callouts | brand.accentSoft / surface.tinted |
| 7 | #F8F4EC | dark-mode text / light cards | paper100 primitive |
| 7 | #F2EEE8 | secondary surface (chips etc.) | surface.secondary |
| 7 | #16A34A | verified badge / success | feedback.success.default |
| 6 | #FF5F1F | Projectversterking brand | brand.flame |
| 6 | #FBBF24 | boost / warning soft | feedback.warning.soft |
| 6 | #DC2626 | destructive / error | feedback.error.default |
| 6 | #15130F | dark-mode background | dark900 primitive |
The de-facto Soft Pro palette
Collapses made
Where multiple hexes were doing the same job:
- Flame variants —
#FF6B00(1) +#FF6B35(4) +#FF5F1F(6) → singlebrand.flame(#FF5F1F) - Greens — eight green variants →
feedback.success.{default,strong,soft,muted,onSurface}. Kept#3B9C4Fas.softbecause it's used distinctly in the deal flow. - Reds — four red variants →
feedback.error.{default,strong,soft,muted} - Ambers — ten amber/yellow variants doing the same job →
feedback.warning.*(5 buckets) - Blues — six blue variants →
feedback.info.*plus dedicatedurgency.today -
Cool grays leaking in —
#6B7280,#4A4A4A,#64748B,#888,#111,#1A1A1A,#E0E0E0,#E8E8E8,#F5F5F5,#E2E8F0. These don't belong in Soft Pro and were collapsed to warm equivalents (text.secondary,border.subtle,surface.secondary). Anywhere a screen uses one of these today, it should switch to the warm token.
1bSpacing
Distinct integer literals on padding* / margin* / gap:
27 values. After clustering onto an 8pt grid:
| Step (px) | Hits | Token |
|---|---|---|
| 2 | 42 | space.hairline |
| 4 | 63 | space.xs |
| 8 | 54 | space.sm |
| 12 | 51 | space.md |
| 16 | 34 | space.lg |
| 20 | 16 | space.xl |
| 24 | 13 | space['2xl'] |
| 32 | 3 | space['3xl'] |
| 48 | 1 | space['4xl'] |
Off-grid drift
3(14×) →xs(4) — drift in chip internals5(9×) →xs(4)6(31×) →sm(8) — most common off-grid value7,9→sm(8)10(40×) →md(12) — heavy hitter; rounds up14(25×) →lg(16)18(19×) →xl(20)22,28→2xl(24)
inset.* and gap.* layer over these,
so screens can express intent (inset.card) rather than
step (space.lg).
1cFont size
22 distinct sizes (11 → 80). Clustered into a clean ramp:
| Token | px | Hits | Replaces drift |
|---|---|---|---|
| micro | 11 | 44 | tab labels |
| caption | 12 | 44 | small captions |
| captionLg | 13 | 54 | dominant caption variant |
| label | 14 | 40 | field labels, button copy |
| body | 16 | 18 | body |
| bodyLg | 17 | 9 | emphasized body |
| subheading | 18 | 3 | small section header |
| heading | 22 | 11 | card title |
| title | 26 | 2 | screen heading |
| display | 32 | 3 | hero number |
| hero | 40 | 1 | landing hero |
Outliers collapsed: 9→11, 10 (8×)→11, 15 (33×)→14, 20→18, 24→22, 28 (5×)→26, 30 (2×)→32, 34 (2×)→32.
1dBorder radius
15 distinct values; 999 (pill) is its own thing. Clustered to:
| Token | px | Hits | Notes |
|---|---|---|---|
| xs | 4 | 16 | |
| sm | 8 | 4 | chips prefer pill |
| md | 12 | 12 | controls |
| lg | 16 | 21 | cards |
| xl | 20 | 40 | hero cards — biggest bucket |
| 2xl | 24 | 0 | sheet/modal |
| pill | 999 | 10 | chips / avatars |
Outliers collapsed: 3→4, 6→4, 10 (19×)→12, 13 (8×)→12, 14 (37×)→16, 17 (4×)→16, 18 (4×)→20, 22 (8×)→20, 28 (4×)→24.
1eBorder width
| Token | px | Hits |
|---|---|---|
| hairline | 1 | 110 |
| thin | 1.5 | 12 |
| thick | 2 | 15 |
1fFont weight
'600' (102) · '700' (80) · '500' (29) ·
'400' (5). Exposed as
semibold | bold | medium | regular.
1gSizes (controls, icons, avatars)
The width/height audit revealed a clear control + avatar ladder:
- 44 (23×) — tap target minimum (Apple HIG) →
controlMd - 52 (8×) — primary button height →
controlLg(matches existingbuttonHeight) - 56 (14×) — large control / large avatar →
controlXl/avatarLg - 72 (10×), 80 (14×) →
avatarXl,avatar2xl - 20 (35× in icon-size grep) — default icon →
iconMd
1hElevation
shadow* properties across the
entire mobile src tree. Soft Pro is intentionally flat; borders
carry hierarchy. The token set keeps elevation.none as the default
and offers sheet / modal for the two cases where lift
is genuinely needed.
2What was already correct
The existing src/theme/themes.ts already had a coherent light/dark
split and most of the brand semantics. The audit kept its names where they
made sense:
lightColors.primary,.accent,.spoedChartreuse,.projectFlame→ carried over- Urgency tokens (
urgencyASAP/Today/Scheduled/Flexible) → carried over. Scheduled and Today were identical hex; collapsed. - Badge tokens (
verified,boosted,privatePool) → carried over - Motion
quick/standard/slow→ carried over verbatim
The gap was that screens weren't using these tokens consistently —
they fell back to hardcoded hex and ad-hoc spacing. The new tokens file
makes the existing palette explicit and adds the missing buckets
(feedback.success.soft, surface.tinted,
border.strong, etc.) the screens were already inventing inline.
3How dark mode is derived
Dark mode is built in theme.ts, not duplicated in tokens. Each
semantic key in darkColor re-maps to the same palette
primitives (paper, ink, warm, chartreuse) but inverted:
surface.background→dark900(deep warm dark)text.primary→paper100(warm paper text)text.link→chartreuse500(links pop on dark)border.default→ translucent paper at 12% opacity- Status colors shift one step lighter (
green600→green400) for contrast on dark surfaces
chartreuse500) and flame (flame500)
intentionally stay identical across modes — they're the brand signal.
4Public API
import { useTheme } from './theme';
function MyScreen() {
const t = useTheme();
return (
<View
style={{
backgroundColor: t.color.surface.primary,
padding: t.inset.card,
borderRadius: t.cornerRadius.card,
borderWidth: t.borderWidth.hairline,
borderColor: t.color.border.subtle,
}}
>
<Text style={[t.textStyle.heading, { color: t.color.text.primary }]}>
Hello
</Text>
</View>
);
}
useTheme()works with or without<ThemeProvider>— falls back touseColorScheme().<ThemeProvider scheme="dark">lets previews / screenshots force a specific mode.useThemedStyles(t => ({...}))is a memoized helper for screens that preferStyleSheetergonomics.
5What this audit did NOT do
- No screens or components were edited. Migrating screens to the new tokens is a separate pass.
- No UI behavior was changed. This is purely additive.
- The legacy
src/theme/{themes,theme,ThemeContext}.tsfiles are untouched — they continue to work for existing imports.
A future migration pass would replace inline hex / magic numbers in screens
with useTheme() references, screen-by-screen.