Dark first, quiet until it matters.

This is the design system behind my homelab console. Dark is the default because that is what I prefer, but the system ships four accents, two themes and two backgrounds, and every combination is a real supported option. Everything below is live. Use the panel in the bottom right to switch, and watch the components and the swatches follow along.

01Principles

Six rules, and one thing that is up to you

The rules below hold whichever look you pick. What you pick is your call. Indigo on ink dark is only the default. Teal on flat light is just as valid, and if that suits the thing you are building, use it. Sixteen combinations, all of them checked.

01

Neutrals come from the ramp

Never #000, never #fff, never untinted grey. On dark the ramp leans blue, on light it stays cool. Both feel like a place rather than an inverted settings panel.

02

One accent, your pick of four

Indigo, teal, ember or lime. Choose one at the root and let it own active states, primary buttons and focus rings. The rule is one accent per app, not one accent for everyone.

03

State twice over

Status is a tint you feel and a word you read. Never colour on its own, never a bare label. This is what keeps the palette swappable without losing meaning.

04

Depth from glass, or not at all

Things lift with blur and translucency instead of shadows, and only the top layer gets a real shadow. Flat mode drops the blooms and the system still works.

05

Hide the detail

A row is one line until you open it. Density is something you ask for, not something the page does to you.

06

Texture, barely

Five percent grain, soft colour blooms underneath, both optional. Atmosphere is fine. Noise is not.

rule: pick a combination, set it once at the root, and stay there. Every rule above survives the switch. Your taste decides which of the sixteen you ship.
02Colour

Three families, strict jobs

Neutrals carry the interface. One accent carries attention. Semantic colours carry state. The pale versions are for pills and fills. The darker validated versions are for chart marks. They do not swap jobs.

Neutrals

void#0A0B12
panel#12141F
panel-2#181B2A
ink-low#666B8A
ink-mid#A9ADC7
ink-hi#EFF0F8

Accent and state, pale steps for pills

accent#8D8DF5
accent-deep#5757D9
mint, fine#5EDBA6
amber, soon#F5C951
rose, broken#F8929F
sky, note#6EC6F5

Chart marks, checked for contrast

series-1#6A6AE8
series-2#2FA97C
series-3#B8831F
series-4#D95C6E
series-5#3E9CD6
comparison#3A3F58

Palette check, computed rather than eyeballed

The chart colours were checked against #12141F on dark and #F7F8FC on light using an OKLab and colour vision test. The light set (#5050D6 #1F8A63 #8A6200 #D23B72 #2380BC) passes everything with no warnings.
CheckResultDetail
Lightness bandPASSall five sit inside OKLCH L 0.48 to 0.67
Chroma floorPASSall five at or above 0.10
Colour visionWARNrose and amber sit 7.5 apart under deuteranopia. Allowed, because every status also has a word and a dot
Normal vision floorPASSclosest pair 15.2, floor is 15
Contrast on surfacePASSall five at 3:1 or better
rule: a pale step never becomes a chart mark, and a chart colour never becomes pill text. Same hue, different job.
03Type

One family for every heading

Outfit sets all text and every heading. Heading sizes follow one rule: each level down divides by the square root of the golden ratio, so two levels make one factor of φ, and h6 is body size. IBM Plex Mono handles labels, hostnames, timestamps and anything a machine produced. Instrument Serif is kept for quotations and the drop cap that opens a long article. All three are woff2 files in fonts/.

h1 · 48.3
Four machines
h2 · 38.0
Thirty three services
h3 · 29.8
Disk on jupiter
h4 · 23.5
Requests per hour
h5 · 18.4
Recent events
h6 · 14.5
Access tokens
body · 14.5
Proxima watches the hosts I run and the services on them. When something is wrong it says so once, clearly, with a note about what to check first.
sm · 12.9
Controls, table cells and secondary text.
label / mono
( REQUESTS PER HOUR )
data / mono
ganymede · vm on jupiter · up 42d
quote / serif
The only thing that should glow is the thing that is wrong.
rule: the serif is for quotations and the drop cap. headings, numbers and controls stay in Outfit.
04Numbers

Two ways to show one

A is the default. B puts a soft gradient behind the number. Use at most one B on a screen. Numbers use tabular figures so columns of them line up.

A · default
Services up
31 / 33
2 downacross 4 hosts
B · gradient
Requests today
584k
+8.2%on yesterday
05Tables

One line each until you ask

Closed, a row is a name, what it is, a couple of facts and a status pill. Open, it adds the full detail, a note about what is wrong and the buttons that act on it. Click a row and see.

Services

31 / 33 up Memory in use 38.4 GB
immich:v1.119
ganymede
14 Jul · 12d ago
4.0 GB · exceeded
Killed at 18:12. It went over its memory limit while transcoding. Either raise the limit or cap the transcode workers.
postgres:16.4
kryptonane
02 Jun · 54d ago
92 of 100 · near limit
caddy:2.8
argon
21 Jul · 5d ago
5d 6h · stable
Renews 14 certificates. The next one is git.home, due in 31 days.
rule: the buttons live inside the opened row. A closed row offers exactly one thing to do, which is open it.
06Boards

Colour you feel before you read

Each column carries a faint wash of its state colour, so you know which lane you are looking at before reading a single word. The cards stay neutral glass so the wash comes through them.

Online2
JU
jupiterworkstation · 32 cores
load 1.262 / 128 GBup 42d
AR
argoncloud · general compute
load 0.43 / 8 GBup 91d
Needs a look1
GA
ganymedevm on jupiter · media
load 3.815 / 16 GB1 service down
Powered off1
MA
marsspare box · shut down
off since 09 Jul2 vms idle
07Charts

Thin marks, labelled directly

One line with a dotted forecast on the end, comparison bars with the subject picked out, a segmented meter, and a stacked bar that splits one whole into its parts. Text always uses text colours. Colour belongs to the marks.

Requests per hour

Last 24 hours. The dotted part is the forecast.
MeasuredForecast

Idle memory

Whole stack, lower is better
Proxima48 MB
Metrics stack940 MB
Full suite2.1 GB

Disk on jupiter

780 GB used of 1 TB · 78 percent

Source lines

1,515 read · every line accounted for
In the target ledger
1,49798.8%
Merged duplicates
90.6%
Held back by a person
90.6%
Waiting on you
00%
Not yet pushed
00%
rule: one axis, always. Two measures on different scales become two charts, never one chart with two axes.
08Responsive

The navigation goes off canvas, nothing else disappears

A phone gets the same pages, not a reduced set. Grids collapse on a fixed ladder, the app sidebar and this rail slide in from the left rather than vanishing, and every chart stays readable and inspectable with a finger.

1 · The ladder

Six widths, each owned by a single decision. If a change does not belong to one of these, it does not happen.
WidthWhat changesWhy
1140.settings-cols single column, .subnav hidesthe "on this page" list stops earning its width
1100.mosaic spans halvetwelve columns stop reading as twelve
980.cols-main, .panes and the marketing embed go single columntwo columns of data stop fitting side by side
880.side and .rail become drawers, .navtoggle and .brand-sm appearthe app shell loses its sidebar column
720nav.top .links becomes a dropdown panelthe marketing links stop fitting beside the logo
620table rows restack, comparison bars narrow, frame padding tightensthe last width where two columns of anything work
rule: a collapsed grid column is minmax(0, 1fr), never 1fr. A bare 1fr is minmax(auto, 1fr) and cannot shrink below its content, which is how a chart ends up half off the screen.

2 · The drawer, .navtoggle and data-nav-toggle

One button, one attribute, whose value is a selector for the panel it opens. Everything else is wired for you: the shared scrim, focus moved into the drawer and trapped there, Escape to close and return focus to the button, a tap on any link inside to close, and a forced close when the viewport goes back above the breakpoint. Add data-nav-breakpoint when the panel collapses somewhere other than 880, as the marketing pages do at 720.
<button class="navtoggle" data-nav-toggle="#app-nav">
Narrow this window past 880 and the live one appears in the top left, over the rail it opens. On the console pages it sits at the head of the topbar instead, because there is already a bar to put it in.
rule: a closed drawer is visibility: hidden, not display: none. Hidden keeps its links out of the tab order, which display would also do, but it still allows the panel to slide.

3 · Charts under a finger

A spark chart measures its own box and sets its viewBox so that one user unit is one CSS pixel. Nothing is scaled unevenly, which is what keeps the dotted forecast dotted at 390px wide instead of thinning into blank space. Press and drag the chart above for the crosshair and the readout, and let go to dismiss it. The chart claims sideways drags and leaves vertical ones to the page, so scrolling over it still works.
rule: never set preserveAspectRatio="none" on a chart. It buys a chart that fills its box and costs you every stroke width, dash and radius in it.

4 · The background on a small screen

The blooms are placed as fractions of the canvas box rather than at fixed coordinates, so they stay round on a tall narrow screen instead of smearing into vertical bands. Below 880 the blur softens from 90px to 60px, and the grain drops its blend mode, which removes a full screen compositing layer and the banding it used to show over flat mode. Flat itself now stops painting and animating the ink layer altogether rather than leaving it running at zero opacity.
rule: flat is a real mode, not a hidden one. If the switch says the atmosphere is off, nothing about it should still be costing the device work.
Theme
Background
Accent