A
?
← Handbook

Design

The look lives in src/app/globals.css, not in the pages. Every surface is built from Tailwind tokens — 433 uses of text-muted, 230 of border-line, 126 of bg-surface — so retuning tokens moves all 37 routes at once. Adding a hex value to a page opts that page out of the system and out of every future change to it. Don't.

Check any change on /style. It reads no data, so it is the only route that renders without a database.

Two materials

Class Job Why
.glassSurfaces that carry contentTranslucent and blurred, refracting the mesh behind them
.softAnything pressable — chips, controls, active statesExtruded: a light, a shade and an ambient shadow
.sunkTracks and grooves behind bars and metersThe same shadows, inverted
.shellThe outer containerMore transparent than the panels inside it, so layers read as layers

If everything is glass, nothing reads as pressable. That is the whole reason there are two.

.glass is applied automatically to bg-surface and bg-raised through a :where() rule, so existing cards became glass without any page being edited.

One hue drives everything

--accent-h and --accent-s feed the accent, the page ground, the neutrals, the borders, the shadows and the first mesh blob. The other three blobs are fixed offsets around the wheel, so no base colour lands the portal in a monochrome wash.

Themes are named, not numbered — components/accent-picker.tsx holds the list. A theme that only looks right in one hue is a bug: check /style in at least Nocturne, Kopi and Graphite.

Severity colour is not themed. Green for good and rose for bad stay put when the hue changes, because they mean something. Only chrome follows the theme.

Layers, not specificity

Component CSS goes in @layer components and element defaults in @layer base.

Unlayered CSS outranks *every* layered rule, and Tailwind puts its utilities in a layer. An unlayered .glass { position: relative } therefore beats absolute on the same element no matter what the page asks for. That is not theoretical — it pinned the theme popover into flow and stretched the nav to contain it. Anything unlayered here is a bug waiting to surface on whichever page next tries to override it.

dark: is remapped to [data-theme="dark"] by a @custom-variant. Without it, Tailwind's default dark: tracks the OS while everything else tracks the explicit choice — so a Mac set to dark at OS level rendered the light theme with dark-theme severity colours on top of it.

Type

Hierarchy is carried by weight, not size alone: 800 headings, 700 labels, 450 body. Inter has the range; the previous stack did not, and everything read as one weight.

Charts must say something

components/chart/trend-chart.tsx renders the curve; lib/charts/trend.ts reads it. The sentence under every chart is computed from the series, so it cannot drift from what is drawn.

Three rules, all enforced in readTrend and covered in trend.test.ts:

  • Under four real observations, it says it cannot read the series. Nulls do not pad a series into readability. A chart that always produces a confident sentence will eventually produce a wrong one.
  • Movement inside ±6% is reported as noise, not direction.
  • If one period is most of the total movement, it says so. "Up 90%" is true of a cost-per-lead that sat flat and then spiked once, and acting on it as a trend would be wrong.

Pass better="down" for cost metrics. The default assumes up is good, which paints a climbing cost-per-lead green — worse than no colour at all.

Running the UI locally

.env.local has no DATABASE_URL — it lives on the dev box. Data routes will 500 without it; /style will not. To boot the shell locally, add:

DATABASE_URL=postgres://localhost:5432/atlas_none
ATLAS_OPEN_ACCESS=true

postgres.js connects lazily, so the stub lets module init succeed without reaching anything, and ATLAS_OPEN_ACCESS is already refused when APP_ENV=production.