Built to be read all day.
The interface is designed to stay open for hours and to be scanned more than read. Surfaces are near-neutral and text uses three inks; the product opens in a dark graphite theme meant for long sessions in a dim room.
Design language Product
Zero Door is designed to be read all day by people who watch a fleet for a living, so the interface speaks in the calm register. Surfaces stay neutral, motion stays short and type stays quiet, so that the one signal that matters is the thing that stands out.
Zerone speaks one design language in two registers. The website has a case to make, so it is assertive. The interface is an instrument that stays open for a whole shift, so it is calm: what is not information steps back, and colour is kept for status.
The interface is designed to stay open for hours and to be scanned more than read. Surfaces are near-neutral and text uses three inks; the product opens in a dark graphite theme meant for long sessions in a dim room.
Among the interface colour roles, only the four status signals are saturated. The brand colour is a low-chroma blue that marks actions, links, selection and checked controls. Tooltips and tags carry no status colour, and charts that show something other than severity use their own calibrated ramp, kept measurably clear of every signal.
Keyboard actions taken many times a day, such as opening the command palette, happen in a single frame. Motion is kept for feedback, and the feedback is short: most of it is one 130 millisecond step.
| Public rule | In the interface |
|---|---|
| Nothing is trusted for having been there yesterday. | A rule about what the product trusts, not about how the interface looks, so it has no visual form of its own here. |
| Evidence checks out without us. | Long values are shortened, never lost. A path keeps its file name and a digest keeps both ends; the exact value stays in the title and in text for screen readers. |
| Change is a question, not an alert. | Saturated colour is kept for the status signals, so a coloured word means one thing: look here. |
| Every detection runs behind your air gap. | Both typefaces and the icons ship inside the product, and a CI check fails if shipped code points at an outside host. An air-gapped install looks and reads the same. |
The four public rules, and the decisions each one forces, are set out in Principles.
Both themes declare the same colour roles under the same names, so a component is written once; the 15 interface roles are shown here, and a small chart ramp is left out. Dark is graphite, a near-neutral grey (OKLCH hue 250); light is warm paper (hue 75). The product opens in the dark theme and does not follow the operating system setting; each person can switch from the user menu.
Navigation sits on the subtle surface and content on the base surface; panels, tooltips and the command palette sit on the raised surface. In the dark theme each step up is a little lighter; in the light theme the raised surface is the brightest of the three.
Surfaces
Ink
Lines
Brand and focus
Signals
Surfaces
Ink
Lines
Brand and focus
Signals
Keyboard focus is a 2 pixel ring in the focus-ring role, set 2 pixels off the control with a gap in the surface colour, so it reads on any fill. A table row takes the ring inset, inside its own edge. The ring appears only on keyboard focus and is held to 3:1 on every surface.
Every text and signal role reads at 4.5:1 or better on all three surfaces, in both themes.
WhyStatus is carried by small words. A retuned token that slipped under the line would make a status unreadable, so the values published here are checked in this repository, and the product checks its own in CI.
TestEnforced by a test on every change.
fg-subtle is pinned at the quietest calibration that still reads at 4.5:1 on every surface.
WhyA later edit cannot pale the tertiary ink without failing the product’s test. The pin covers the token only: in use, some labels are set at reduced opacity, and the test does not check those.
In the productEnforced by a test in the Zero Door repository.
The brand role also colours links in the interface, but it is not one of the text roles held to 4.5:1: in the dark theme it reaches 4.36:1 on the raised surface. The gap is set out in Colour.
The interface is ink on a neutral ground. Among the 15 interface colour roles shown above, only the four signals are saturated: their OKLCH chroma runs from 0.11 to 0.18, and none of the other roles goes above 0.1. So a coloured word means one thing: look here. There is no row wash and no filled chip; colour stays in a dot, a word or an icon, and it never carries the meaning alone.
Severity uses three of the signals: danger for critical, warning for high and info for medium. Low and info share the quiet ink fg-subtle and are told apart by the word alone. Status badges use all four signals. Charts that show something other than severity use their own calibrated ramp, kept measurably clear of the signals, and the focus ring, a muted blue, appears only on keyboard focus.
In use, severity in a table or a list is a small dot and a coloured word: each row of the alert, incident and vulnerability tables shows it this way. The word carries the meaning; the dot repeats it in colour.
| Severity | Role | Dark | Light |
|---|---|---|---|
| Critical | signal-danger | oklch(0.66 0.18 25) | oklch(0.53 0.17 25) |
| High | signal-warning | oklch(0.76 0.13 75) | oklch(0.53 0.12 70) |
| Medium | signal-info | oklch(0.66 0.12 240) | oklch(0.54 0.12 240) |
| Low | fg-subtle | oklch(0.640 0.010 250) | oklch(0.540 0.010 60) |
| Info | fg-subtle | oklch(0.640 0.010 250) | oklch(0.540 0.010 60) |
A status that belongs to a check or a state is a badge: an icon and a word on a quiet tint of the signal, with a thin border in the same colour. In use, badges appear in detail panels, settings and some tables.
| Signal | Icon | Colour role |
|---|---|---|
| success | Check in a circle | signal-success |
| warning | Triangle with a mark | signal-warning |
| danger | Cross in a circle | signal-danger |
| info | Letter i in a circle | signal-info |
| neutral | none | fg-muted on surface-subtle, zd-border edge |
| Part | md (default) | sm |
|---|---|---|
| Height | 24px | 20px |
| Word | 12px · 500 | 11px · 500 |
| Icon | 12px | 12px |
| Icon to word | 6px | 4px |
| Side padding | 8px | 6px |
| Corner | radius-sm · 4px | radius-sm · 4px |
Among the interface colour roles, only the four signals are saturated.
WhyBrand, focus and every neutral stay low in chroma, so a saturated word reads as status. The chart ramp, left out above, is the one other saturated set, and the product’s test below keeps it clear of the signals. This site pins the published snapshot; a sync that breaks the rule fails the build here.
TestEnforced by a test on every change.
A status badge always carries an icon and a word, never colour alone.
WhyColour blindness, a greyscale print or glare on a screen all remove the colour. The word and the shape remain.
In the productEnforced by a test in the Zero Door repository.
Chart colours that do not mean severity keep a measured distance from every signal colour.
WhyA high alert and a modified file must never share a colour on neighbouring panels.
In the productEnforced by a test in the Zero Door repository.
Interface text is set in Inter, from a single variable font file that ships inside the product, so an air-gapped install needs no font server. Hierarchy comes from size and weight: four fixed roles for the dense parts of the screen, a 24 pixel page title and an uppercase table header, in the weights 400, 500 and 600. Nothing heavier is used.
InterVariable.woff2, so the letter shapes and widths here are not the product’s.| Role | Size / line (px) | Tracking | Weight | In use |
|---|---|---|---|---|
| Page title | 24 / 30 | 0 | 600 | The title in a page header |
| Body | 14 / 20 | 0 | set per use | Table cells, panels and forms |
| Compact | 13 / 18 | 0 | set per use | Navigation rows, small buttons and fields |
| Meta | 12 / 16 | 0 | set per use | Secondary lines in panels and lists |
| Overline | 11 / 16 | 0.08em | 500 | Uppercase context lines above a table |
| Table header | 12 / 16 | 0.05em | 500 | Table header cells, in uppercase |
Tables set every figure in tabular numerals, so columns of counts and times line up. Machine values that must align character by character, such as a digest before and after a change, are set in Geist Mono, the face this site uses for machine text, from a second variable font file that also ships inside the product.
Three dense roles are pinned to exact sizes and line heights, and the overline role to its tracking and weight.
WhyA dense screen stays legible only if every table, list and panel draws from the same few sizes.
In the productEnforced by a test in the Zero Door repository.
Hierarchy uses the weights 400, 500 and 600 only.
WhyA heavier weight or a display face would make the interface louder than its data.
ReviewKept by review, not by a test.
The website’s typefaces and type scale are set out in Typography.
The token file defines a scale of 12 steps, from 2 to 64 pixels. In use, components space with the matching Tailwind steps, plus 10 and 14 pixel steps that the scale does not define. A page gutter of 24 pixels lines up tables, page headers and footers.
--space-12px--space-24px--space-36pxCompact row padding--space-48px--space-512pxComfortable row padding--space-616px--space-720px--space-824pxPage gutter--space-932px--space-1040px--space-1148px--space-1264pxThere are two density modes: comfortable by default, compact on request, switched from the user menu. Compact changes one thing: the vertical padding of table rows drops from 12 to 6 pixels. The type size does not change, so a compact table is denser without being smaller.
Density changes row padding, never type size.
WhyShrinking the text to fit more rows would trade legibility for count. The product keeps the type and tightens the rows.
ReviewKept by review, not by a test.
The website’s container, rhythm and radius are set out in Layout and shape.
Corners come in 3 sizes and stop at 12 pixels. Depth comes mostly from the surface steps and the hairlines; the token file defines two shadows, a flat one that can stand in for a border and a raised one for things that float.
--radius-sm4pxFields, badges, tags, tooltips and small buttons.--radius-md8pxButtons, error states and bordered empty states.--radius-lg12pxDialogs, sheets, the command palette and floating action bars.The flat shadow lifts a row off a quiet layer, as the active row in the sidebar does. The raised shadow is for side panels, tooltips and the command palette. Each theme sets its own values, because a shadow that reads on paper disappears on graphite.
The radius scale stops at 12 pixels.
WhySmall corners keep dense layouts tight and aligned; the largest radius marks surfaces that float. This site pins the published snapshot; a sync that breaks the rule fails the build here.
TestEnforced by a test on every change.
The token file defines two shadows: flat and raised.
WhyDepth is carried by surface steps and hairlines first, so a shadow still means that something floats. In use, a small default Tailwind shadow also appears twice: on the switch thumb and on one panel.
TestEnforced by a test on every change.
Motion in the interface confirms an action or keeps a change in view; it does not decorate. The duration tokens run from 130 to 400 milliseconds, and in use nearly all motion is the 130 millisecond feedback step on the ease-out curve. Three things run outside the tokens: a row touched by a live update glows for 2200 milliseconds, a skeleton pulses slowly while it waits and a busy button spins.
--duration-feedback130 msPress, colour and row feedback across the interface. Almost all motion in use.--duration-fast160 msA few hover borders and moves in the story graph.--duration-default200 msDefined. No component uses it yet.--duration-slow300 msThe arc of the posture score.--duration-drawer400 msDefined for drawers. No component uses it yet.The token file defines 3 curves. Pick a duration and play them to compare how each one arrives.
--ease-outcubic-bezier(0.23, 1, 0.32, 1)Press and colour feedback, tooltips and row states. Almost all motion in use.--ease-in-outcubic-bezier(0.77, 0, 0.175, 1)Defined for movement across the screen. No component uses it yet.--ease-drawercubic-bezier(0.32, 0.72, 0, 1)Defined for drawers. No component uses it yet.Buttons press to 0.97 of their size over 130 milliseconds on the ease-out curve and spring back the same way. A disabled button does not move. Large cards and panels use a gentler press, 0.99.
Keyboard actions taken many times a day do not animate: the command palette opens and closes in a single frame. Charts draw without animation. When the reader asks for reduced motion, smooth scrolling stops and every animation and transition is cut to a hundredth of a millisecond and played once.
| Element | Motion | Duration | Curve |
|---|---|---|---|
| Button press | Scales to 0.97 | 130 ms | ease-out |
| Tooltip | Grows from 0.96 at its trigger, after a 400 ms wait | 130 ms | ease-out |
| Row hover and selection | Colour change | 130 ms | ease-out |
| Command palette | None: opens in one frame | 0 ms | none |
| Charts | None | 0 ms | none |
Shared parts transition named properties only: never all, never layout geometry.
WhyTransforms and colours are cheap and predictable. Animating width, height or margins makes a dense screen reflow and jitter.
In the productEnforced by a build check in the Zero Door repository.
Reduced motion turns smooth scrolling off.
WhyA reader who asked for less motion should not have the page glide under them.
In the productEnforced by a test in the Zero Door repository.
Keyboard actions taken many times a day do not animate.
WhyAn animation repeated many times a day is a delay every time.
ReviewKept by review, not by a test.
The duration tokens run from 130 to 400 milliseconds.
WhyShort enough to confirm without making anyone wait, long enough to be seen. This site pins the published snapshot, not the product’s components; a sync that breaks the range fails the build here.
TestEnforced by a test on every change.
The website’s motion, which explains rather than confirms, is set out in Motion.
The interface is built from a few dozen shared parts, named here by role. Buttons show rest, hover, focus, pressed, disabled and loading; fields show focus, invalid and disabled; data views load with skeletons.
Buttons come in four kinds (primary, secondary, ghost and danger) and three heights: 32, 36 and 44 pixels. Icon-only buttons grow to a 44 pixel target on touch screens. A loading button keeps its label and shows a small spinner beside it; a disabled button is drawn at half strength and does not press.
The confirm button arms on the first press and shows the question and its consequence.
WhyClosing a thousand alerts should take as much thought as closing one takes effort.
In the productEnforced by a test in the Zero Door repository.
Terminal or wide actions use the two-step confirmation.
WhyThe product’s test holds how the button behaves, not where it is used; which actions take it is decided in review.
ReviewKept by review, not by a test.
Text fields match the button heights, so a field and its button sit on one line. An invalid field gets a danger border and is marked invalid for assistive technology. The search field waits 300 milliseconds after the last key before it searches. Selection controls (the checkbox and the switch) use the brand colour when on, never a status colour.
The dot and word and the badge, described above. A tag is a quiet neutral chip for facets and filters and never carries a status colour. Tooltips stay neutral too, even when they explain a critical count.
Icons are sized on a ladder of 12, 16, 20, 24 and 32 pixels, and the stroke thickens as an icon gets smaller so that a small icon does not fade into the surface. An icon takes the colour of the text beside it and is hidden from screen readers unless it is the only carrier of meaning. The 12 pixel step is kept for icons inside a labelled badge.
Icons are decorative unless they are the only carrier of meaning.
WhyAn icon beside a word would make a screen reader say the same thing twice.
In the productEnforced by a test in the Zero Door repository.
Every icon size on the ladder has its own stroke width.
WhyA single stroke drawn at every size turns into a hairline at the small steps and fades into the surface.
In the productEnforced by a test in the Zero Door repository.
A skeleton is a quiet block in the shape of what is coming, hidden from screen readers. An empty state teaches: it says what will appear and how to make it appear, with at most one quiet action. An error state says what failed, offers a retry and keeps the technical detail one click away.
A skeleton is hidden from assistive technology.
WhyA shape that means loading has nothing to say to a screen reader; the region announces its content when it arrives.
In the productEnforced by a test in the Zero Door repository.
An empty block inside a section is a status line, not a new heading.
WhyA heading inside a dense panel would break the outline that screen reader users move through.
In the productEnforced by a test in the Zero Door repository.
Data views load with skeletons, not a spinner in the middle of the screen.
WhyA skeleton keeps the layout still and shows what is coming; a spinner hides both.
ReviewKept by review, not by a test.
An empty state teaches what will appear and how to make it appear.
WhyAn empty screen is often a first visit, and a bare “nothing here” leaves the reader with nowhere to go.
ReviewKept by review, not by a test.
A sidebar of 240 pixels collapses to a 56 pixel rail. The top bar is 56 pixels tall and centres on the command palette trigger, which shows the Command key and K on Apple devices and Ctrl K elsewhere. Paged tables share one footer: previous appears only after the first page and next only before the last; a single page shows no controls at all.
Detail opens in a side panel on the end edge, 480 pixels wide by default, so the list stays in view. It sits on the raised surface with the raised shadow, and when it closes, focus returns to whatever opened it.
Header labels are small uppercase words on a header row that stays in place while the rows scroll, and a sortable header tells assistive technology which way it sorts. A row can be selected, open and under the keyboard cursor at once. Selection is a faint brand tint and a checked box, the open row takes the subtle surface and the cursor is a thin inset ring; on a row that is both selected and open, the selection tint shows. Above 100 rows the table renders only what is on screen.
Sortable and plain table headers share one casing.
WhyOne header row in two casings reads as two kinds of column.
In the productEnforced by a test in the Zero Door repository.
A narrow column shortens a path at the directory and keeps the file name; it shortens a digest in the middle and keeps both ends, and it writes a large count in compact form. Point at a value to see it whole; screen readers read the whole value.
12.4k carries 12,431 in its title. On a narrow screen the same values stand in rows.Every token value on this page (colour, space, radius, shadow, curve and duration) is read at build time from a snapshot of the product token file, and every ratio is computed from it. The same sync script reads further values from the product source code: the two typefaces, the default theme, the reduced-motion settings, the press scale, the badge, button, field and icon sizes, the four dense type roles, the shell widths and a digest of the glyph path. It stops with an error when a pattern is missing or an assumption no longer holds, and its check mode reports any drift from the product. A test in this repository runs that check wherever the product repository is present, which leaves out CI. Some mappings and measurements are still copied by hand into one file, each with its source path, among them the severity dot, the tooltip, the focus ring, the spinner, the page title and table header roles and a few timings. No test compares those with the product.
| Field | Value |
|---|---|
| Branch | main |
| Token file last changed | 2026-08-23 |
| Component files last changed | 2026-10-09 |
The snapshot is published as product-tokens.json, and the website’s own token file as tokens.css.
{
"source": {
"ref": "main",
"date": "2026-08-23"
},
"radius": {
"radius-sm": "4px",
"radius-md": "8px",
"radius-lg": "12px"
},
"ease": {
"ease-out": "cubic-bezier(0.23, 1, 0.32, 1)",
"ease-in-out": "cubic-bezier(0.77, 0, 0.175, 1)",
"ease-drawer": "cubic-bezier(0.32, 0.72, 0, 1)"
},
"duration": {
"duration-feedback": "130ms",
"duration-fast": "160ms",
"duration-default": "200ms",
"duration-slow": "300ms",
"duration-drawer": "400ms"
}
}Rule cards on this page say what holds each rule: a test in this website's repository, or a test or a build check in the Zero Door product repository, which runs in the product's own CI.
| Check | Kind |
|---|---|
| Text and signal contrast, both themes | Unit test |
| Dense type roles | Unit test |
| Named transitions only | CI check |
| No outside hosts in shipped code | CI check |
| Badge carries an icon and a word | Unit test |
| Icon ladder sizes and strokes | Unit test |
| Chart colours clear of signals | Unit test |
| Published snapshot reads at 4.5:1 | Unit test, this site |
| Snapshot shape, sync errors, the glyph digest and drift from the product | Unit test, this site |
| Shared curve, neutrals, radius, durations, shadows, signal chroma and the product facts this page states in words | Unit test, this site |