…
```
**Distribution.** `@v0.4.0` pins a release; `@main` tracks the latest and moves on every push. Minified
builds are served as `css/kit.min.css` and `js/_78.min.js`. Self-hosting is the same files at your own
path — the kit contains no absolute URLs.
**What `kit.css` pulls in.** `tokens.css`, `reset.css`, and every component stylesheet. Library adapters
are deliberately *not* imported: include only the ones you use.
**What `_78.js` does on load.** It wires every `._78-theme-toggle`, mounts every `._78-tabs`, mounts every
`._78-menu-wrap`, wires the app shell (`._78-nav-toggle`, `._78-nav-collapse`, `._78-scrim`), runs
`_78.viz.mountAll()` over the document, and delegates a click handler for `._78-alert-close`. It is safe
to load with `defer`, and all mounting is idempotent — calling a `mount` function twice does nothing.
**Markup added later.** After injecting HTML, re-mount just that subtree:
```js
_78.viz.mountAll(container); // sparklines, progress, bars, donuts, trends
_78.tabs.mountAll(container); // tab lists
_78.shell.mountMenus(container); // dropdown menus
```
**The reset.** `reset.css` is minimal: `box-sizing: border-box`, zeroed margins, `body` painted with
`--bg` / `--text` / `--font`, block-level media, unstyled `button`, links in `--accent`, themed thin
scrollbars, a `:focus-visible` ring in `--accent`, and a `prefers-reduced-motion` block that neutralizes
animation. It styles `body`, so it is the one part of the kit that is not purely opt-in.
---
## 2. Theming
### 2.1 The rule
A theme is **only color**. Color tokens live in the `[data-theme]` blocks; everything else — radius, type
scale, spacing, control heights, layout widths, transitions — lives in a plain `:root` shared by every
theme. Nothing in the kit hardcodes a color; every component reads tokens. That is the entire theming
mechanism, and it is the only one: never fork `tokens.css`, override it.
### 2.2 The mechanism
```css
:root { /* non-color tokens, shared by every theme */ }
:root, :root[data-theme="dark"] { color-scheme: dark; /* color tokens */ }
:root[data-theme="light"] { color-scheme: light; /* color tokens */ }
```
Bare `:root` carries the dark palette too, so the kit still looks right if JS never runs. `color-scheme`
is set per theme, so native scrollbars, form controls and the browser's own UI follow along.
The pre-paint snippet reads `localStorage['_78-theme']` (`"dark"` / `"light"` / `"system"`), resolves
`"system"` through `matchMedia('(prefers-color-scheme: light)')`, and writes two attributes on ``:
- `data-theme` — the **resolved** theme, `"dark"` or `"light"`. This is what the CSS keys off.
- `data-theme-pref` — the **preference**, `"dark"` / `"light"` / `"system"`. This is what the toggle icon
keys off.
### 2.3 Overriding tokens
Put overrides in a stylesheet or `
```
Tokens cascade on any element, not just `:root`, so scoping one to a subtree re-themes just that subtree:
```html
…
```
### 2.4 The `-lo` tints
Every accent and semantic color ships a `-lo` companion: the same hue at low alpha, used as a
**background** under solid text in the full-strength color. This is why the kit never tints with
`opacity` — `opacity` washes out the label along with the fill.
```css
._78-badge-success { background: var(--success-lo); color: var(--success); }
```
### 2.5 Full token list
#### Core color — per theme
| Token | What it is |
|---|---|
| `--bg` | App background, the furthest-back surface |
| `--bg2` | Raised surface: cards, topbar, sidebar, modal, menu, toast |
| `--bg3` | Inset surface: inputs, badges, modal footer, empty states |
| `--border` | Every 1px edge, and the track behind progress / donut fills |
| `--text` | Primary text |
| `--dim` | Secondary text, icons, table headings, placeholders |
| `--accent` | The project's own color — the main per-project override |
#### Accent derived — per theme
| Token | What it is |
|---|---|
| `--accent-hover` | The accent's hover state (lighter on dark, darker on light) |
| `--accent-text` | Text drawn *on* an accent fill; must pass contrast against `--accent` |
| `--accent-lo` | Low-alpha accent tint, for fills behind accent-colored text |
| `--accent-lo-hover` | The tint's hover state |
| `--accent-glow` | The `box-shadow` on a primary button |
#### Semantic — per theme
`--success` · `--warn` · `--danger` · `--info`, each with a matching `-lo` tint (`--success-lo`,
`--warn-lo`, `--danger-lo`, `--info-lo`).
Semantic hues stay conventional — green / amber / red / blue — and deliberately never follow the accent.
A "themed red that isn't red" is a usability trap.
#### Categories — per theme
`--cat-1` … `--cat-6`, each with a `-lo` tint (`--cat-1-lo` …). Six label colors for things that are
**not states** — a source, a team, a type. Their hues (violet, teal, rose, olive, orchid, bronze) are
kept clear of the semantic hues so a category never reads as success / warn / danger, and like the
semantics they don't follow the accent. Each clears 4.5:1 as badge text on its own tint in both themes.
Use them in order: `cat-6` (bronze) is the one nearest a state (warn), so a short list never reaches it.
#### Charts — per theme
`--series-2` — the second series in a chart: a true gray that recedes behind `--accent` and, having no
hue, can never sit near it in hue whatever the accent is. It clears 3:1 on `--bg2` in both themes. The
Chart.js adapter uses it for the second dataset (§15.3); past two series it continues with `--cat-*`.
#### Surfaces, rows, focus — per theme
| Token | What it is |
|---|---|
| `--row-hover` | Table / menu / list row hover wash |
| `--row-border` | The lighter rule between table rows (vs. `--border` for the frame) |
| `--row-selected` | Selected row background; defaults to `var(--accent-lo)` |
| `--ring` | Focus ring color for form controls, drawn at `--ring-w` thickness |
| `--overlay` | The modal backdrop |
| `--shadow` | Resting elevation (cards) |
| `--shadow-lg` | Raised elevation (modal, toast, hovered card) |
#### Non-color — shared `:root`, one set for every theme
| Group | Tokens | Shipped values |
|---|---|---|
| Type | `--font`, `--font-mono` | Inter + system stack; `ui-monospace` stack |
| Type scale | `--fs-xs` `--fs-sm` `--fs-base` `--fs-lg` `--fs-xl`, `--lh` | 11 / 12 / 14 / 16 / 20px, 1.5 |
| Spacing | `--space-1` … `--space-6` | 4 / 8 / 12 / 16 / 24 / 32px |
| Radii | `--radius-sm` `--radius` `--radius-lg` `--radius-pill` | 6 / 8 / 10 / 999px |
| Controls | `--control-h` `--control-h-sm` `--icon-btn-size` `--ring-w` | 36 / 28 / 32 / 3px |
| Layout | `--sidebar-w` `--rail-w` `--header-h` `--content-max` | 240 / 60 / 56 / 1120px |
| Motion | `--transition` `--transition-slow` | 140ms ease / 240ms ease |
### 2.6 `_78.theme` API
```js
_78.theme.pref // getter → 'dark' | 'light' | 'system'
_78.theme.current // getter → the resolved 'dark' | 'light'
_78.theme.set('light') // set + persist + apply; returns the resolved theme
_78.theme.cycle() // Dark → Light → System → Dark; returns the new preference
_78.theme.apply('dark') // apply without persisting; returns the resolved theme
_78.theme.mount(el) // wire an element as a toggle (idempotent)
_78.theme.KEY // '_78-theme' — the localStorage key
_78.theme.PREPAINT // the pre-paint snippet, as a string (for docs / tooling)
```
Free behavior: while the preference is `"system"` the page live-follows the OS setting, and a theme change
made in another tab is picked up through the `storage` event.
### 2.7 The `_78:themechange` event
Fires on `document` after every change, including the initial apply on load.
```js
document.addEventListener('_78:themechange', function (e) {
console.log(e.detail.theme); // 'dark' | 'light' — resolved
console.log(e.detail.pref); // 'dark' | 'light' | 'system'
myCanvasThing.redraw(); // the hook for anything that can't read CSS variables
});
```
Anything drawn to a `