Theming

Every token is a custom property on :root. Override it in unlayered CSS; the components follow.

Defined once in TypeScript, generated into tokens.css, and read by every leaf through var(). The React stylesheet and vlak.css carry the same block. Light values below; dark where they differ.

PropertyLightDarkDoes
--bg#FAF8F2#0E0C0APaper. The page and every surface.
--text#1A1A1A#E8E8E8Ink. Type, borders of solid controls, focus rings.
--text-secondary#6B6B6B#949494Gray. Labels, hints, table cells.
--accent#1A1A1A#E8E8E8Aliases ink. There is no hue in the system.
--dividerrgba(0,0,0,0.08)rgba(255,255,255,0.10)Hairlines: rules, frames, table rows.
--divider-subtlergba(0,0,0,0.06)rgba(255,255,255,0.07)Fills only: hover, skeleton, muted.
--grid-linergba(0,0,0,0.04)rgba(255,255,255,0.05)The quiet 204 verticals behind inner pages.
--table-altrgba(0,0,0,0.02)rgba(255,255,255,0.03)Alternate rows, code block ground.
--control-borderrgba(0,0,0,0.42)rgba(255,255,255,0.38)Form control boundary. 3:1 against the ground.
--radius-sm4pxButtons, boxes, dialogs, sheets.
--radiusvar(--radius-sm)Alias of --radius-sm.
--radius-chrome0pxPage chrome, cards, icon marks, charts.
--radius-inmax(0px, calc(var(--radius) - var(--pad)))Concentric inner corner.
--pad20pxBox padding. 25px at or under 480px.
--gutter20pxGap between modules.
--grid-size204pxThe module: 184 column + 20 gutter.
--grid-imagelinear-gradient(…)The module verticals as a repeating gradient.
--grid-pos20px 0Where the gradient starts.
--hit40pxMinimum hit target. 44px at or under 640px.
--control-h40pxControl height. 44px on the phone.
--control-fs14pxControl type size. 16px on the phone.
--control-label12pxField label size. 15px on the phone.
--duration-snap0.12sState changes the user caused.
--duration0.2sEasing changes.
--duration-confirm0.16sConfirmations.
--easecubic-bezier(0.3, 0, 0.2, 1)The one curve.
--transitionbackground-color, colorThe default transition pair.
--text-scale1Multiplier on reading type. Chrome stays put.
/* your.css, unlayered: it wins over vlak.tokens */
:root {
  --bg: #FFFFFF;
  --radius-sm: 8px;
}
[data-theme="dark"] {
  --bg: #000000;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --bg: #000000; }
}

Vlak's tokens sit in the vlak.tokens cascade layer. Author CSS outside any layer beats it, so a plain :root rule is the whole override. Set the dark value under [data-theme="dark"] and, if you rely on the system preference, under the media query too. The palette is neutral by design; there is no accent token that takes a hue, and the test suite fails on one.

Three states. No attribute: the system preference applies through prefers-color-scheme. data-theme="dark": dark. data-theme="light": light, even on a dark system. Each block also sets color-scheme, so native selects, scrollbars, and form chrome match the page.

<script>
  (function () {
    var t = localStorage.getItem("vlak-theme");
    var dark = t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches);
    if (dark) document.documentElement.dataset.theme = "dark";
  })();
</script>

To persist a choice without a flash, set the attribute before first paint. The ThemeToggle component stores its choice under vlak-theme in localStorage and sets the same attribute; the script above reads it back.

/* Paint the module behind a page. base.css does this on body. */
.page {
  background-image: var(--grid-image);
  background-size: var(--grid-size) 100%;
  background-position: var(--grid-pos);
}

/* Boxes span whole modules: n × 204 − 20. */
.two-up { width: calc(2 * var(--grid-size) - var(--gutter)); }

204px modules: a 184px column and a 20px gutter. Content boxes span whole modules; edges step from grid line to grid line on resize. At or under 480px the field is two columns on 25px gutters and --pad becomes 25px. Turn the verticals off with --grid-image: none.

html { --text-scale: 1.1; }  /* steps: 0.9, 1, 1.1, 1.25, 1.4 */

Reading type (rs-t-*) multiplies by --text-scale; controls, labels, and chrome stay put. The type scale is in rem with no root font-size pin, so a reader's browser setting applies as well.

Desktop controls are 40px tall with 14px type. At or under 640px every interactive control recuts to a 44pt hit with 16px type, through --hit, --control-h, and --control-fs. Override those three to change the whole kit.