This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Foundations

The primitives — colour, typography, spacing, shape, and motion. Everything else composes from these.

Foundations are the values the rest of the system is built from. They are deliberately small in number and fixed: a component may choose which token to use, but it does not invent new ones.

1 - Colour

Three 12-step scales in two modes, their step roles, and the contrast rules that govern them.

The palette is three scales — midnight, orange, and slate — each expressed as a 12-step ramp in a light and a dark variant. 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.

  • Primary
    #1d3352
    Text, headings, primary surfaces
  • Primary Light
    #2b4d78
    Hover states, dark-mode primary
  • Primary Dark
    #132440
    Dark backgrounds, navbar, code blocks
  • Accent
    #E05232
    CTAs, highlights, active states
  • 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)
  • Secondary
    #546a82
    Supporting text, borders

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

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 both modes.

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 modes.

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 three scales cover every surface that has a light mode. A terminal does not: it has one surface, it is dark, and every colour in it is measured against that one background. It 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 the terminal palette is a fourth member of the system rather than a fifth scale — a fixed sixteen-slot palette plus its chrome, derived from the ramps and measured against midnight-dark-1.

#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.

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

  • Never use pure #000 or #fff as text. Use step 12 of the relevant scale: #142438 on light, #c5daf0 on dark.
  • 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 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:

RoleLightDarkOn #0e1720
Success#2f7d65#6cc0903.65:1 → 8.24:1
Warning#8b6508#e0a92a3.41:1 → 8.50:1
Danger#a8261c#f08b802.55:1 → 7.49:1
Info#3a5a82#8aacc82.55:1 → 7.59:1

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

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.

Dark mode

Both modes are equally supported. See Dark mode for the implementation rules — theme switching, persistence, image treatment, and the always-dark code surface.

2 - Typography

Three typefaces, three jobs, and a fixed type ramp.

The system uses three typefaces, each with one job. All three are licensed under the SIL Open Font License 1.1 and served from Google Fonts, so there are no bundled font binaries to license or redistribute.

The three families

FamilyWeightsRoleLicence
Plus Jakarta Sans400 · 500 · 600 · 700 · 800Headings, display, buttons, navigation, wordmarkSIL OFL 1.1
Source Sans 3400 · 500 · 600Body copy, UI labels, captionsSIL OFL 1.1
IBM Plex Mono400 · 500Code, terminal output, dataSIL OFL 1.1

Loading them:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">

The ramp

Every row below is set in the family, size, weight, and line-height it documents — this is the live specimen, not a picture of one.

Display · 48/800/1.1
Redesigning work
H1 · 36/700/1.15
Agent-first consulting
H2 · 28/700/1.2
Composable infrastructure
H3 · 22/600/1.25
How the system fits together
H4 · 18/600/1.3
Working in the open
H5 · 15/600/1.35
Supporting detail
Body L · 16/400/1.65
Augmenting people's strengths through composable Cloud, Agile, and Agentic AI infrastructure.
Caption · 12/400/1.5
Figure 1 — pipeline stages and their policy gates.
Overline · 11/600/1.3
SECTION LABEL
Code · 13/400/1.6
createPipeline({ policy: "strict" })

Rules

  • Letter-spacing tightens to −0.3px above 20px. Large type set at default tracking reads loose and unresolved.
  • Overline is a label, not a heading. 11px, 600, uppercase, 0.08em tracking. Use it above a heading, never as one.
  • Body copy is 16px / 1.65. This is the reading size for documentation and long-form prose. Do not set body text below 14px.
  • Never set body copy in Plus Jakarta Sans, and never set headings in Source Sans 3. The split is what makes the voice recognisable.
Do

Pair an overline with an H2 to label a section. Keep the ramp intact — skip sizes rather than inventing intermediate ones.

Don't

Introduce a fourth typeface, use a weight outside the listed set, or fake a weight with font-synthesis.

3 - Space, shape, and motion

The 4px spacing base, the radius and elevation ladders, and the motion tokens.

Spacing

A 4px base with a nine-step scale. Every margin, padding, and gap resolves to one of these values.

TokenValueTypical use
--space-14pxIcon-to-label gaps
--space-28pxTight component padding
--space-312pxControl padding
--space-416pxDefault element spacing
--space-524pxCard padding, paragraph rhythm
--space-632pxComponent separation
--space-748pxSub-section separation
--space-864pxSection separation
--space-996pxPage-level bands

Content sits inside a 1100px measure. Wider viewports gain margin, not line length.

Radius

TokenValueApplied to
--radius-sm3pxTags, chips, small indicators
--radius-md6pxButtons, inputs, code blocks
--radius-lg9pxCards, panels
--radius-xl13pxLarge panels, modals
--radius-full9999pxPills, avatars

Radius is a signal of scale: the larger the surface, the larger the radius. Mixing radii on nested surfaces reads as a mistake — a 6px control inside a 9px card is correct; a 13px control inside a 6px card is not.

Elevation

Four levels. Shadows are soft and low-contrast; the system leans on borders and surface tint before it reaches for shadow.

TokenValueApplied to
--shadow-0noneFlat surfaces, default
--shadow-10 1px 3px rgba(0,0,0,0.06)Cards at rest
--shadow-20 4px 12px rgba(0,0,0,0.08)Hover, dropdowns
--shadow-30 8px 24px rgba(0,0,0,0.12)Modals, popovers

In dark mode, elevation is expressed by lightening the surface rather than deepening the shadow — a shadow on a near-black background is invisible.

Motion

Things slide into place. They do not bounce.

Durations

TokenValueUse
--duration-micro100msColour and opacity changes
--duration-standard200msHover, focus, small moves
--duration-expand300msHeight and width changes, accordions
--duration-page400msRoute and view transitions

Easing

TokenValueUse
--ease-outcubic-bezier(0.33, 1, 0.68, 1)Entering — arriving on screen
--ease-incubic-bezier(0.32, 0, 0.67, 0)Exiting — leaving screen
Radius ladder — 3 / 6 / 9 / 13 / full
sm 3px
md 6px
lg 9px
xl 13px
full
Elevation — shadow-0 through shadow-3
shadow-0
shadow-1
shadow-2
shadow-3
Motion — slides into place, never bounces
200ms · ease-out — entering
100ms · colour and opacity

Motion rules

  • All animation wraps in @media (prefers-reduced-motion: no-preference), or is disabled under prefers-reduced-motion: reduce.
  • CSS-only for HTML. For React, use Framer Motion with these same timing values.
  • Never animate text character-by-character.
  • Page transitions cap at 400ms.
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
Do

Use --ease-out for anything appearing and --ease-in for anything leaving. Keep durations on the ladder.

Don't

Add spring or bounce easing, animate layout-shifting properties, or exceed 400ms for any single transition.

4 - Responsive

The four breakpoints, how the 12-column grid collapses, touch-target sizes, and the mobile navigation pattern.

The rest of the foundations describe values that do not change with viewport width. This page describes the four points at which the layout does.

Breakpoints

Four breakpoints, declared in brand/tokens/variables.css and normative here:

NameMin widthThe layout it describes
sm640pxLarge phone, landscape phone
md768pxSmall tablet — the first two-column layout
lg1024pxTablet landscape, small laptop — sidebars appear
xl1280pxDesktop — the 1100px measure is fully margined

Below sm is not a breakpoint; it is the base. Write the narrow layout first and add width with min-width queries, so an unstyled or unsupported viewport gets the single-column layout rather than a clipped desktop one.

/* Base — single column, 360px and up. */
.panel-row { display: grid; gap: var(--space-4); }

@media (min-width: 768px) {
  .panel-row { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .panel-row { grid-template-columns: 1.5fr 1fr; }
}
Breakpoint ruler — base · 640 · 768 · 1024 · 1280
base
1 col
640
1 col
768
2 col
1024
+ sidebar
1280
margined

How the grid collapses

The grid is 12 columns inside the 1100px measure. It does not stay twelve columns all the way down — it resolves to four column counts:

FromColumnsGutterPage padding
base416px16px
md 768px816px24px
lg 1024px1224px32px
xl 1280px1224pxauto — the measure caps at 1100px

Below md, every multi-column region becomes one column. There is no two-column layout on a phone: two 160px columns are two unreadable columns. Regions stack in source order, so the DOM must already be in reading order — do not rely on order or grid-area to fix a sequence that is wrong in the markup, because that breaks the focus and reading order.

Spacing steps down one rung when the grid collapses: a --space-6 (32px) section gap on desktop becomes --space-5 (24px) below md. Radius, type scale, and border weights do not change — a card is a 9px card at every width.

Touch targets

44×44px minimum for anything tappable, at every viewport — the floor is a finger, not a breakpoint.

The default control heights are 32 / 40 / 48px, so the sm and md sizes are below the floor on their own. Two ways to resolve it, in order of preference:

  1. Grow the hit area, not the control. Keep the 40px visual button and give it a transparent 2px vertical extension, or wrap it in a 44px-tall row. The measurement stays normative; the target clears the floor.
  2. Use lg (48px) on touch-primary surfaces. Correct for the primary action on a phone, where a 40px button next to a 48px one reads as demoted.

Never shrink to sm (32px) for a touch target. Spacing between adjacent targets is at least --space-2 (8px), so a mis-tap does not fire the neighbour.

/* Preferred: 40px control, 44px target. */
.pj-btn--md {
  min-height: 40px;
  position: relative;
}
.pj-btn--md::after {
  content: "";
  position: absolute;
  inset: -2px 0;   /* 40 + 2 + 2 = 44 */
}

Mobile navigation

Use a bottom tab bar for application surfaces. Use a top drawer for documentation and marketing surfaces. The choice follows the shape of the navigation, not the shape of the device:

  • An application has a small, flat set of destinations — the dashboard example has five. Five or fewer flat destinations fit a tab bar, which keeps the current location permanently visible, sits in the thumb arc, and costs no taps to reach. That is worth the 56px of permanent screen it occupies.
  • Documentation has a deep, nested tree. A tab bar cannot express it, and flattening the tree to fit one is worse than a drawer. A drawer behind a labelled disclosure control costs one tap and can show the whole hierarchy.

The failure mode is a tab bar with six or more items, or one that hides the primary destination behind “More”. If the destinations do not fit, the surface is a drawer surface.

Both patterns carry the same obligations: the current destination is marked with an accent underline or fill plus the label, never colour alone; the drawer traps focus while open, closes on Esc, and returns focus to its trigger — the same contract as a modal.

Bottom tab bar — 5 destinations, current marked with fill and label
screen content
Overview Agents Alerts Account

Tables, code, and diagrams

Three content types cannot reflow, and each has one answer:

  • Tables scroll horizontally inside their own container with the first column pinned — never the page. See wide tables.
  • Code blocks scroll horizontally. Do not soft-wrap code: a wrapped line changes what the code appears to say.
  • Diagrams get a scrollable container or a re-drawn narrow variant. Do not scale a diagram down until its labels are unreadable — an 8px label is not a responsive diagram, it is a broken one.

Worked example

Mobile onboarding — three screens in a device frame: an onboarding panel on midnight, an engagement list of stacked cards, and a finding-review screen with two side-by-side actions. It is the dashboard below md: same tokens, same components, one column, tab bar instead of sidebar.

Do

Write the base layout first and add width with min-width queries. Put regions in the DOM in reading order. Give every tappable thing a 44px target.

Don't

Keep two columns below 768px, reorder regions with CSS to fix source order, scale a diagram until its labels are illegible, or let a wide table scroll the whole page.