Color
Spec
Two layers: primitive scales, and semantic roles that map to one primitive in each theme. Contrast: every text role passes 4.5:1 on its surfaces; border-control passes 3:1.
Primitives
Green
100
#E3FCF0
#E3FCF0
200
#C0F2DB
#C0F2DB
300
#8BE3BF
#8BE3BF
400
#5ED0A5
#5ED0A5
500
#44B68D
#44B68D
600
#269973
#269973
700
#1A7E5D
#1A7E5D
800
#0C5E45
#0C5E45
900
#06402E
#06402E
Slate
100
#F4F7F5
#F4F7F5
200
#E4E9E7
#E4E9E7
300
#C7D0CC
#C7D0CC
400
#A9B8B1
#A9B8B1
500
#778880
#778880
600
#4A5A53
#4A5A53
700
#33413B
#33413B
800
#232E29
#232E29
900
#16211D
#16211D
Amber
100
#FFF2DC
#FFF2DC
200
#FFDDB2
#FFDDB2
300
#FCC17C
#FCC17C
400
#F0A646
#F0A646
500
#D98B09
#D98B09
600
#B97000
#B97000
700
#9A5B00
#9A5B00
800
#754200
#754200
900
#502B00
#502B00
Red
100
#FFEFED
#FFEFED
200
#FFD6D1
#FFD6D1
300
#FFB1A9
#FFB1A9
400
#FF8E86
#FF8E86
500
#FA6863
#FA6863
600
#DE4544
#DE4544
700
#BD2E31
#BD2E31
800
#901E21
#901E21
900
#621516
#621516
Plus white (surface-raised in light) and black (the dark shadow). Components never use these directly.
Semantic roles
| Role | Light | Dark | Use |
|---|---|---|---|
| surface | slate-100 | slate-900 | Page background |
| surface-raised | white | slate-800 | Panels, inputs, header, dialogs |
| surface-action | green-800 | green-200 | Primary button fill |
| surface-action-hover | green-700 | green-400 | Primary button, hover |
| surface-action-pressed | green-900 | green-100 | Primary button, pressed |
| surface-disabled | slate-200 | slate-700 | Disabled primary button fill |
| surface-selected | slate-900 | slate-100 | Selected chip ("All"), chosen date |
| surface-accent | green-700 | green-400 | Filled cleared circle, selected calendar day, checkbox |
| surface-accent-subtle | green-100 | green-900 | Selected row, highlighted option, deposit sign, slim entry row |
| surface-accent-bright | green-400 | green-400 | Decoration and the payday marker only; never behind text on light |
| surface-badge | green-100 | green-900 | "Trial" and "Paid" badges |
| surface-warning | amber-100 | amber-900 | Behind warning notes |
| surface-negative | red-100 | red-900 | Behind below-zero notes |
| surface-error | red-100 | red-900 | Behind error messages |
| surface-scrim | slate-900 at 45% | #0000008c | The dimmed page behind a dialog (the one role with transparency) |
| text-primary | slate-900 | slate-100 | Body text, payees, figures |
| text-secondary | slate-600 | slate-400 | Descriptions, "Not cleared", placeholders, dates |
| text-inverse | slate-100 | slate-900 | Text and check marks on surface-action, surface-selected and surface-accent (4.65:1 on green-700) |
| text-disabled | slate-500 | slate-500 | Text on surface-disabled |
| text-accent | green-700 | green-400 | Links, secondary button text, deposits, labels, today and payday |
| text-badge | green-800 | green-200 | Text on surface-badge (7.2:1 light, 9.5:1 dark) |
| shadow-color | slate-900 at 16% | black at 45% | The color of shadow-float (the one role named for a shadow) |
| surface-row-hover | slate-200 | slate-700 | A register row under the pointer (wide screens) |
| surface-secondary-hover | green-300 | green-700 | A secondary button under the pointer |
| surface-secondary-pressed | green-500 | green-800 | A secondary button while pressed |
| surface-tertiary-hover | slate-200 | slate-700 | A tertiary button under the pointer |
| surface-tertiary-pressed | slate-300 | slate-600 | A tertiary button while pressed |
| text-on-accent-bright | slate-900 | slate-900 | The "$" on the runway's payday marker, on surface-accent-bright (green-400 in both themes; 8.6:1) |
| text-warning | amber-700 | amber-400 | Short once everything clears; a bill that would overdraw |
| text-negative | red-700 | red-400 | A balance below zero now |
| text-error | red-700 | red-400 | Form validation and error messages |
| border | slate-300 | slate-700 | Panel and chip borders |
| border-subtle | slate-200 | slate-700 | Row dividers inside panels |
| border-control | slate-500 | slate-500 | Input borders and the empty not-cleared circle (3:1 on every row surface) |
| border-accent | green-700 | green-400 | Current-page underline, selected-row bar, highlighted option outline |
| border-focus | green-700 | slate-400 | Keyboard focus outline (2px, 2px away) |
| border-error | red-700 | red-400 | Invalid field borders |
Rules
- Every role starts with what it colors: surface-, text- or border-, then its meaning.
- Amber means overdraft, nothing else: only a bill that would take Immediate below zero before payday, and the runway after it.
- Red means something is wrong: a balance already below zero, or an error.
- Color is never the only signal: warnings and negatives carry a ▲ or minus sign and words.
- Mint stays an accent, never a large fill. Primary buttons use surface-action, not surface-accent.
Where it comes from
- Figma variables are the only source.
npm run tokenswritestokens.generated.css. - One role is code-only until Figma has it: text-on-accent-bright.
- Light surface-scrim and shadow-color are aliases with opacity in Figma; the export writes them empty and the pipeline fills them in. That fallback is permanent.
Changes
- 1.0.0, Oct 2 2026: Primitives (green, slate, amber, red, 100 to 900) and 37 semantic roles per theme, from Figma variables.