v3.0.2 is out — restored design-system guidance and corrected navigation. Read the notes
projectious·work
On this page

Colour

Three 12-step scales, three appearances, semantic pairs, and the contrast rules that govern them.

The palette is three scales — midnight, orange, and slate — each expressed as a 12-step ramp in light and dark variants. Semantic aliases map those ramps into light, navy dark, and deep dark appearances. The step numbering follows the Radix convention, which assigns every step a role. Using a step outside its role is the single most common way to break the system.

Core colours#

These are the named aliases most projects reach for first. They are shortcuts into the scales, not a separate palette.

The light app background is midnight-1 (#f8f9fb), not white. White is the raised surface. Links use midnight-11, never the accent. Colour carries meaning: do not introduce bluish-purple gradients, rainbow categorisation, or decorative colour families.

Primary
#1d3352
Text, headings, primary surfaces
Accent
#e05232
CTAs, highlights, active states
Secondary
#546a82
Supporting text, borders
Identity variants
Primary Light
#2b4d78
Hover states, dark-mode primary
Primary Dark
#132440
Dark backgrounds, navbar, code blocks
Accent Light
#ea7558
Accent hover on dark, syntax strings
Accent Dark
#b84228
Accent pressed state
Accent Solid
#cc4528
Fill for solid controls with white text (4.72:1)

Step roles#

Every scale uses the same twelve roles in the same order:

StepsRoleUsed for
1–2App and subtle backgroundsPage and section surfaces
3–5Element backgroundsComponent fills, hover, active
6–8BordersSubtle, default, and strong borders
9–10SolidSolid fills and their hover state
11–12TextLow-emphasis and high-emphasis text
Only 11 and 12 are text steps

Steps 8, 9, and 10 are border and solid-surface roles. They are not held to text contrast thresholds and must not be used for body text. If you need dimmer text than step 11, define a dedicated token and verify its contrast — see the code-comment token for a worked example.

Midnight#

Midnight light · step 9 #1d3352
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text
Midnight dark · step 9 #1d3352
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text

Midnight is the primary. It carries structure, text, and the calm end of the system. Step 9 (#1d3352) is the brand primary and is identical in every appearance.

Orange#

Orange light · step 9 #e05232
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text
Orange dark · step 9 #e05232
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text

Orange is the accent. It marks the primary action, the active state, and little else. Step 9 (#e05232) is constant across appearances.

Do

Use orange for the single most important action in a view, active navigation state, and focus emphasis.

Don't

Use orange for large background fills, body text, or more than one competing call to action on the same screen.

Slate#

Slate light · step 9 #546a82
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text
Slate dark · step 9 #546a82
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text

Slate is the secondary — supporting text, borders, and neutral surfaces. Step 9 (#546a82) is the brand secondary and is constant across modes.

Terminal#

The dark terminal is the default and every colour in it is measured against midnight-dark-1. An optional light companion is available for explicitly light code and terminal panels; it is a separate measured palette, never an automatic colour-mode substitution. A terminal also needs six hues where the interface needs three, because programs have been writing to sixteen ANSI slots since long before this system existed.

So each terminal palette is a companion to the three interface scales rather than another scale: sixteen fixed ANSI slots plus chrome, derived from the ramps and measured against its own background.

#NameNormalOn surfaceBrightOn surfaceProvenance
0black #0e1720— #2e4b682.00:1midnight-dark-1 / midnight-dark-6 — Box drawing and rules, not text — deliberately below the floor.
1red #e55b5b5.15:1 #f08b807.49:1bright = danger-dark — Never the accent — an error and the brand must not look alike.
2green #3f9d745.41:1 #6cc0908.24:1bright = success-dark
3yellow #c08a1e5.93:1 #e0a92a8.50:1bright = warning-dark — Gold, matching the warning role in the interface.
4blue #6289b34.95:1 #8aacc87.59:1bright = midnight-dark-11
5magenta #bd6d964.98:1 #d491b47.32:1terminal-only — The brand defines no magenta; this slot exists only here.
6cyan #3f97a35.31:1 #74c0c98.71:1terminal-only — The brand defines no cyan; this slot exists only here.
7white #97a8b87.41:1 #c5daf012.62:1slate-dark-11 / midnight-dark-12

Read the provenance column carefully. The bright ramp is the brand: where a hue already exists in the system, the bright slot takes that step verbatim. The normal ramp has no brand equivalent — the scales define one value per semantic role, not a dim and a bright — so each normal slot is its bright counterpart darkened until it reads a step back while still clearing the floor.

Magenta and cyan exist in neither half of the brand. They are here because a terminal requires them, and nowhere else.

A terminal value is not a brand value

#e55b5b is the terminal’s red. It is not the light danger solid, #d92d20. The normal ramp exists to fill ANSI slots and is measured only against the terminal surface; using one of its values in the interface puts an unmeasured colour on an unrelated background.

Terminal chrome#

The accent gets no ANSI slot, because it is not semantic — it marks where you are. That, and the surfaces around the sixteen, live here.

RoleValueMeasured
Background #0e1720the surface
Foreground #c5daf012.62:1
Cursor #e052324.67:1
Cursor text #0e17204.67:1 on the cursor
Selection background #20354d—
Selection text #c5daf08.74:1
Dim / comment #72889d4.93:1
Status bar surface #131e2b—
Status bar text #c5daf011.74:1
Inactive tab and pane label #7b8da34.95:1
Active tab fill #e052324.67:1 with #0e1720 text
Active pane border #e052324.67:1
Inactive pane border #7b8da35.32:1

Every non-background value clears 4.5:1 against the surface; the measured floor is 4.95:1. ANSI 0 bright is the one deliberate exception — programs use it for box drawing and rules, not for text.

Configuration for tmux, WezTerm, Kitty, Ghostty, iTerm2, and Zellij is on the Terminal theming page.

Contrast rules#

  • Use semantic foreground roles for text. The fixed white token is valid on measured solid controls; screen copy uses the appearance’s foreground role.
  • Step 9 is constant across modes. The solid accent does not shift when the theme changes.
  • Body text targets 4.5:1, large text (≥24px, or ≥18.66px bold) targets 3:1.
  • Verify against the actual surface. A step that passes on the app background may fail on an elevated panel.

Where an identity colour cannot carry text#

Being the brand colour does not make a value a legible background. White on orange-9 (#e05232) measures 3.87:1 — fine as a mark or a border, but below the floor for button labels. Rather than dilute the accent, the system adds a separate fill for that job:

TokenHexWith white text
--color-accent#e052323.87:1 — identity only, not for text
--color-accent-solid#cc45284.72:1 — solid controls
--color-accent-dark#b842285.46:1 — hover and pressed

The identity accent is never body text. Light-mode accent text uses orange-11 (#c04424); dark-mode accent text uses orange-dark-10 (accent-light, #ea7558). A solid control with a white label always uses accent-solid.

The same principle produced the code-comment token: when no existing step can do the job accessibly, name a new one rather than misuse a step.

Semantic colours are mode-specific#

The callout hues are tuned for dark text on tinted light backgrounds. Used as foregrounds on the dark app surface they fall below AA, so dark mode has its own set:

Success
5.03:1 6.43:1
#17945f · #d1ebe0 · fg #17734c #6cc090 · #16302a
A completed, evidenced check
Warning
5.01:1 6.81:1
#ef8b0b · #fff1e0 · fg #b3520c #eda44a · #3a2611
A condition to assess. Gold, never the accent
Danger
5.32:1 6.40:1
#d92d20 · #fce8e8 · fg #b3261e #f08b80 · #3a1c19
A failure needing a next action
Info
4.73:1 6.04:1
#2563c9 · #dae2ec · fg #2f5fa8 #8aacc8 · #1a2b3e
Neutral context, no judgement
RoleLight solidLight tint foregroundDark solid
Success#17945f#17734c on #d1ebe0#6cc090
Warning#ef8b0b#b3520c on #fff1e0#eda44a
Danger#d92d20#b3261e on #fce8e8#f08b80
Info#2563c9#2f5fa8 on #dae2ec#8aacc8

Use the matching -fg on a tint. Use --on-solid-* on a solid status fill: white in light, dark ink on the pale solids used by dark appearances. A muted page role is not automatically valid on a semantic tint.

Data visualisation#

A chart palette introduces no new colours. It is a set of rules for which existing steps may sit beside each other in a plot, and — more usefully — for when colour stops being the right tool.

Categorical: three series#

SeriesTokenHexOn white
1--midnight-9#1d335212.75:1
2--orange-9#e052323.87:1
3--slate-9#546a825.58:1

One step-9 solid per family, in that order. All three clear the 3:1 non-text contrast floor against a white plot area, so a bar or a line is visible without a border.

Assign them in order and keep the assignment stable across every chart in a deck or a dashboard: if midnight is “cloud” on slide four, it is “cloud” on slide nine. A series that changes colour between charts costs the reader more than a fourth series would have gained them.

Three-series grouped bars — the whole categorical palette
Cloud Agentic AI Agile
Orange and slate differ in hue, not in value

orange-9 against slate-9 measures 1.44:1. On screen they are easy to tell apart — orange against blue-grey is also one of the safest pairs for the common colour-vision deficiencies. Printed in greyscale, or on a projector with the colour turned down, they merge.

So when exactly two series are being compared, use midnight-9 and orange-9 (3.29:1) and leave slate for the third. And direct-label every series — the legend is the fallback, not the mechanism.

There is no fourth series#

The palette stops at three, and extending it is the wrong fix. The step-6 tier is not an option: midnight-6 and slate-6 measure 1.03:1 against each other — the same colour, for practical purposes — and all three step-6 values sit at 1.8–2.0:1 against white, below the 3:1 floor for a mark you have to see.

When a chart has more than three categories, one of these is the answer:

  • Group the tail. Rank the categories and collapse everything past the third into “Other”. If the fourth is genuinely interesting, it is the subject of its own chart.
  • Small multiples. One chart per category, same axes, same scale. Reading eight small charts is faster than decoding an eight-colour legend.
  • Direct labelling with one highlight. Draw every series in slate-7, draw the one being discussed in orange-9, and label it in place. This is the house style for a line chart in a deck — one idea per slide holds for charts too.
  • Stop using colour. A ranked bar chart in a single colour, sorted by value, answers “which is biggest” better than any palette does.

Sequential and ordinal scales#

A magnitude scale uses one family, steps 3 through 8:

--midnight-3 → --midnight-4 → --midnight-5 → --midnight-6 → --midnight-7 → --midnight-8

Six levels, stepping evenly in luminance (each 1.06–1.40× its neighbour) — which is what makes the ramp readable as an ordered scale rather than as six colours.

Step 9 is not the top of that ramp. It is 3.87× darker than step 8, which is a jump the eye reads as a category boundary rather than one more level. Use it deliberately for exactly that: a four-bucket choropleth of 3 · 5 · 7 · 9, where the top bucket is meant to separate itself. Do not append it to a six-level heatmap.

Steps 3–7 are all below 3:1 against a white plot area, so a sequential fill needs an edge: give the plot a 1px slate-4 cell grid, or the reader loses the boundary between a light cell and the page.

For a diverging scale — where the middle is neutral and both ends are extreme — run midnight-8 → midnight-3 → orange-3 → orange-8, with --midnight-1 at the midpoint. Never build a diverging scale from success and danger: those hues carry a judgement, and “below average” is not “wrong”.

Chart furniture#

ElementValue
Axis line, ticks--slate-5
Grid lines--slate-3, horizontal only
Axis labels, legend--slate-11, 12px
Value labels--midnight-12, 12px, IBM Plex Mono
Plot backgroundnone — the page surface
Annotation, callout rule--orange-9

Numbers are set in IBM Plex Mono, right-aligned, for the same reason table numerics are: digits have to line up to be compared.

Do

Keep categorical charts to three series and label them directly. Hold a series' colour constant across a deck. Use one family’s steps 3–8 for magnitude.

Don't

Invent a fourth categorical colour from the step-6 tier, rely on a legend as the only way to identify a series, append step 9 to a sequential ramp, or build a diverging scale from the success and danger hues.

Three appearances#

Light, navy dark, and deep dark are equally supported. See Appearances for the derivative relationship, surface ladders, selection, elevation, and default-dark code and terminal surfaces.

Updated Aug 17, 2026