---
name: adom-theme-system
description: THE Adom application theme system, the canonical spec every Adom desktop app implements (Hydrogen Desktop is the reference; Adom Desktop is the next implementer). Five colour schemes with exact tokens, slugs vs product labels, per-scheme font pairings, the VS Code theme contract, storage migration rules, and the guard scripts that keep implementations honest. Trigger words - adom theme, adom themes, color scheme, colour scheme, theme tokens, adom design tokens, implement adom themes, adom studio theme, kickstand, adom slate, bright inverse, scheme slugs, adom fonts, satoshi, jetbrains mono, familjen grotesk, vs code adom theme.
---

# The Adom Theme System

One theme system for every Adom desktop surface. Hydrogen Desktop (HD) is the REFERENCE
IMPLEMENTATION; this skill is the contract for implementing the same system elsewhere,
starting with Adom Desktop (AD). The design was measured into place on 2026-07-25 (contrast
maths, live pixel sampling, in-workbench font probes), so treat the numbers here as decisions,
not suggestions.

Source of truth files (HD repo, hydrogen-desktop - PRIVATE):
- `src/routes/styles.css` - the token set and all five scheme blocks
- `tools/gen-vscode-themes.mjs` - generates the VS Code theme pack FROM the tokens (pack v7.2.0)
- `scripts/check-scheme-parity.sh` + `scripts/check-installed-themes.sh` - the guards
- `scripts/install-brand-fonts.sh` - webfont delivery into code-server
- `src/lib/stores/settings.ts` - FONTS_FOR_SCHEME, slugs, labels, migrations

Because that repo is private, the wiki page carries usable copies (pinned @ a4c764be, pack
v7.2.0) at https://wiki.adom.inc/adom/adom-theme-system :
- `tokens.css` - all five scheme blocks as drop-in CSS custom properties
- `vscode/` - the complete generated theme pack; copy into
  `~/.local/share/code-server/extensions/adom.adom-themes-<version>/` (the page's install.sh
  does this automatically when code-server is present)
- `fonts/` - the REDISTRIBUTABLE faces only: JetBrains Mono + Familjen Grotesk (both OFL 1.1,
  license texts beside the files). Satoshi binaries are NOT there and must never be uploaded
  to any public server (Fontshare EULA); `fonts/satoshi/README.md` documents the official
  Fontshare endpoints and the page's install.sh fetches the faces from them automatically.
  Full rules: `fonts/LICENSES.md` on the page.
- `reference/` - byte-exact copies of the generator, template, font installer, and guards

## 1. The five schemes

| slug (storage key) | product label | character |
|---|---|---|
| `studio` | **Adom Studio** | THE DEFAULT (John 2026-07-27; this design briefly staged as 'Adom Studio v2', then took over the name — zero-migration rename). VS Code's Dark 2026 architecture re-grounded on the brand: body #161b22, base text #c9d1d9, every gray on the brand slate hue at 2026's exact contrast steps, product blue re-issued as Adom teal, syntax untouched. Generated by `reference/gen-adom-studio-v2.mjs` from `reference/reference-2026-dark.json`. The old template-projected Studio is abandoned. |
| `studio-bright` | Adom Studio Dark (Brighter Text) | Everything on the deep ground, brighter interface text. |
| `studio-dark` | Adom Studio Dark | The original screen-tuned dark set, quieter text. This is the TOKEN BASE. |
| `kickstand` | Adom Kickstand | The design firm's identity palette: #191919 ground, muted navy chrome, headline face in prose. |
| `slate` | Adom Slate | Calm cool blue-grey; uses the code font Windows already ships. |

**Invariant history:** the old "Adom Studio == Adom Studio Dark (Brighter Text) with `--bg`/`--surface`
traded" parity guard applied to the ABANDONED template-projected Studio and is retired with
it. The new invariant for Adom Studio (since 2026-07-27) is: it is a pure function of
`reference/reference-2026-dark.json` + the two brand anchors, regenerated by
`reference/gen-adom-studio-v2.mjs` — never hand-edited. `check-scheme-parity.sh` still
guards the APP schemes (studio vs studio-bright tokens in styles.css); it no longer
constrains the VS Code Studio theme.

## 2. The tokens, per scheme (generated from styles.css, do not hand-edit)

| token | Adom Studio | Adom Studio Dark (Brighter Text) | Adom Studio Dark | Adom Kickstand | Adom Slate |
|---|---|---|---|---|---|
| `--bg` | `#161b22` | `#0d1117` | `#0d1117` | `#191919` | `#17212e` |
| `--surface` | `#0d1117` | `#161b22` | `#161b22` | `#202127` | `#202a37` |
| `--elevated` | `#1c2128` | `#1c2128` | `#1c2128` | `#282a30` | `#28323f` |
| `--overlay` | `#21262d` | `#21262d` | `#21262d` | `#0e1c31` | `#313b48` |
| `--border` | `#30363d` | `#30363d` | `#30363d` | `#3d3f46` | `#3d4754` |
| `--text` | `#e6edf3` | `#e6edf3` | `#e6edf3` | `#f5f7f9` | `#f0f6fc` |
| `--text-body` | `#c9d1d9` | `#c9d1d9` | `#c9d1d9` | `#d7dde3` | `#d3e0ef` |
| `--text-2` | `#c9d1d9` | `#c9d1d9` | `#8b949e` | `#97a0aa` | `#93a6bd` |
| `--text-3` | `#6e7681` | `#6e7681` | `#484f58` | `#5a626b` | `#4a5a70` |
| `--accent` | `#00b8b1` | `#00b8b1` | `#00b8b1` | `#00b8b1` | `#00b8b1` |
| `--accent-hover` | `#00d4cb` | `#00d4cb` | `#00d4cb` | `#00d4cb` | `#00d4cb` |
| `--accent-bright` | `#00e6dc` | `#00e6dc` | `#00e6dc` | `#8fd3d4` | `#5fe6de` |
| `--on-accent` | `#05221f` | `#05221f` | `#05221f` | `#003c3f` | `#04211f` |
| `--blue` | `#64abff` | `#64abff` | `#64abff` | `#64abff` | `#64abff` |
| `--blue-fill` | `#0061ef` | `#0061ef` | `#0061ef` | `#0061ef` | `#0061ef` |
| `--purple` | `#8c6bf7` | `#8c6bf7` | `#8c6bf7` | `#8c6bf7` | `#8c6bf7` |
| `--green` | `#3fb950` | `#3fb950` | `#3fb950` | `#3fb950` | `#3fb950` |
| `--red` | `#f85149` | `#f85149` | `#f85149` | `#f85149` | `#f85149` |
| `--yellow` | `#d29922` | `#d29922` | `#d29922` | `#d29922` | `#d29922` |

Rationale you must not undo:
- Body text lives in the 9-13:1 contrast band. Below strains; above ~14:1 dark-mode text
  haloes (that is why Adom Studio's primary text is 13.0:1, not 15.8:1).
- The teal accent must stay >= 6:1 on every ground. That constraint is why there is no teal
  or purple GROUND: they were built and measured into failure.
- Five distinct dark grounds are impossible inside those constraints (closest achievable pair
  measured 24 apart, i.e. invisible). The family therefore differentiates on AXES: hue
  (Slate), elevation (Adom Studio), legibility (Bright), accents (Kickstand).
- `--blue-fill` (#0061ef) is 3.6:1 on dark grounds: FILLS AND ACCENTS ONLY, never text.
  Links and informational text use `--blue` (#64abff).
- `--green/--red/--yellow` are semantic (success/destructive/warning) and nothing else.
  A settings confirmation is not "success"; use the accent.
- Kickstand's chrome navy is a DERIVATION, not a new colour: mix(#00204f 45%, #191919 55%)
  = #0e1c31. Full-strength navy as a dropdown fill read as "that weird blue" and was demoted
  to accents only.
- Kickstand's derived chrome greys are COOL-SHIFTED (correction, 2026-07-26): the firm's PDF
  specifies only the #191919 Black ground, which stays byte-exact. The greys we derived from
  it (`--surface` #202127, `--elevated` #282a30, `--border` #3d3f46) carry a few points of
  blue because pure neutral grey beside the blue-tinted sibling schemes and the wiki is
  perceived as warm brown (simultaneous contrast; the shipped pixels measured exactly
  neutral, and still read "brownish"). Implementers copying Kickstand must copy these
  corrected values, not re-derive neutral ones from the ground.

## 3. Token roles (what drives what)

| token | role |
|---|---|
| `--bg` | the app's deepest ground / the editor sheet |
| `--surface` | panels, toolbars, dialogs |
| `--elevated` | hover surfaces, raised cards, and THE ACTIVE TAB (always the raised sheet; mapping tabs to --surface inverts into a black tab in Adom Studio) |
| `--overlay` | menus, dropdowns, tooltips, popovers |
| `--text` | emphasis only (headings, active items) |
| `--text-body` | running text |
| `--text-2` / `--text-3` | secondary / disabled-borders-only (text-3 fails AA as text) |

## 4. Slugs vs labels, and the migration law

Slugs are storage keys (persisted preferences, data attributes, CSS selectors). Labels are
product names, and they are EXACTLY the VS Code theme names, so one look has one name on
every surface.

History that bites: on 2026-07-25 the slug `studio` CHANGED MEANING (it was the dark base,
it is now the flagship). Any store that ever held old values must translate them ONCE under
old semantics, guarded by a version marker, because the string alone cannot say which era
wrote it. HD's pattern: a NEW localStorage key (`hd-color-scheme2`) whose legacy-key read
translates {studio->studio-dark, studio-bright-inverse->studio, inverse->studio,
blueprint->slate, contrast->studio-bright, graphite->slate} then deletes the legacy key;
plus `desktop.scheme_slug_version = 2` on the saved-preference layer. If AD stores scheme
state, implement the same translation once, and NEVER reuse a slug again without bumping the
version and extending the map.

Unknown/retired slug at load time: fall back to `studio` explicitly. Note the base tokens
ARE studio-dark's look, so "no attribute set" silently paints Dark, not the default: always
set the scheme attribute explicitly.

## 5. Fonts (part of the theme, three faces only)

Brand faces: **Familjen Grotesk** (headline), **Satoshi** (body/prose), **JetBrains Mono**
(code, SIL OFL 1.1 - ship the licence with the files). Do not add faces; variety for its own
sake is how a design system becomes a font menu.

Per-scheme pairing (FONTS_FOR_SCHEME):

| scheme | code stack | prose stack |
|---|---|---|
| studio, studio-bright, studio-dark | `'JetBrains Mono', 'Cascadia Code', Consolas, ui-monospace, monospace` | `Satoshi, -apple-system, 'Segoe UI', sans-serif` |
| kickstand | same code stack | `'Familjen Grotesk', Satoshi, -apple-system, 'Segoe UI', sans-serif` |
| slate | `'Cascadia Code', Consolas, 'JetBrains Mono', ui-monospace, monospace` | Satoshi stack |

Delivery, learned the hard way:
- Webfonts must be served from YOUR OWN origin with ABSOLUTE paths. HD's relative
  `./adom-fonts/...` URLs 404'd for weeks while a Windows-installed copy silently covered
  for them; nothing anywhere admitted it.
- Isolated webviews (the Claude chat panel) do NOT inherit your page's webfonts. Their brand
  faces resolve only from WINDOWS-INSTALLED fonts, so the installer must keep shipping
  Satoshi (and Familjen) as Windows fonts even when the main app uses webfonts.
- A fallback chain never fails, it silently falls through. Any font UI must display the
  RESOLVED family, measured in the document that renders it, not the configured first name.
- VS Code key trap: `chat.fontFamily` is the chat PROSE; `chat.editor.fontFamily` is the
  chat's CODE BLOCKS. They read backwards and were once wired backwards.

## 6. The VS Code theme contract

The editor themes are GENERATED from the tokens (HD `tools/gen-vscode-themes.mjs`), one
theme per scheme, labels identical to the picker labels. Rules:
- An unknown `workbench.colorTheme` name silently falls back to VS Code's DEFAULT LIGHT
  theme. Renames must therefore update the installed pack AND the live setting in one step,
  and a machine-side guard (`check-installed-themes.sh`) should verify the installed pack,
  because repo-vs-repo checks cannot see a stale machine.
- The generator wipes its output dir before writing so a renamed theme cannot leave a stale
  file behind.
- Current pack: adom.adom-theme (Adom Studio / Adom Studio Dark (Brighter Text) / Adom Studio Dark /
  Adom Kickstand / Adom Slate), version-locked to this package: the extension version and
  the package version are THE SAME NUMBER, bumped together in the same commit, every time.
- The extension is NEVER in any marketplace/gallery, so gallery update checks return null
  for it by design; the only update channel is `adom-wiki pkg update`. Currency check =
  installed adom.adom-theme version vs this package version.
- Legacy: before 2026-08-02 this was `adom-theme-system` + `adom.adom-themes` (7.x line);
  install.sh purges the old extension id on every install.

### Delivering the pack: install it, never copy it

**Copying the pack folder into `~/.local/share/code-server/extensions/` does not
install it.** The folder is absent from that directory's `extensions.json`, and
code-server PRUNES unregistered folders on its next launch. The pack disappears
before the user ever sees it, the picker lists zero "Adom ..." themes, and a
`workbench.colorTheme` of "Adom Studio" then hits the unknown-name rule above and
falls back to stock Light. This shipped in a golden image and rendered the whole
editor off-brand; `install.sh` now packages a `.vsix` and installs it properly.

Three traps worth knowing before you write your own installer:

1. **`code-server --install-extension` takes a `.vsix`, not a directory.** Build one
   at install time: it is a zip of `extension/<the pack>` plus `extension.vsixmanifest`
   and `[Content_Types].xml`. No `vsce`, no network, python's `zipfile` is enough.
2. **The `code-server` on `PATH` inside an editor terminal is the remote-cli shim**
   and cannot install anything. Resolve the real launcher (`/usr/lib/code-server/bin/
   code-server` and friends) and reject any path containing `remote-cli`.
3. **The CLI dies with `error not spawned with IPC`** when it inherits
   `CODE_SERVER_PARENT_PID` (and `VSCODE_IPC_HOOK_CLI`) from the editor terminal that
   is running the installer. `env -u` both before invoking it.

The litmus is registration, not files on disk: assert the `adom.adom-themes` id
appears in `extensions.json` after installing. A directory check passes for a pack
that is about to be deleted, which is exactly how this got past a build gate.
Uninstall through the CLI too, so the registration goes with the folder.

## 7. Implementing in Adom Desktop, the checklist

1. Define the tokens as CSS custom properties on `:root` (the studio-dark values) plus one
   `[data-scheme='<slug>']` block per scheme copied from the table above. Adom Studio's
   block should contain ONLY Bright's text ramp plus the bg/surface swap.
2. Route every colour through the tokens. No hardcoded hex: HD carried 4,641 hardcoded
   values before the sweep and that is why its panels never matched each other.
3. Picker UI: show the LABELS, store the SLUGS, default `studio`, apply via the data
   attribute before first paint (read the cache directly, do not wait for stores).
4. Fonts follow the scheme's pairing; if you expose font pickers, overrides beat the
   pairing, "Match theme" means follow it, and the UI must name what that currently means.
5. Copy the guards: scheme parity, brand-teal component matching (one colour has at least
   five CSS spellings; match rgb components, not strings), installed-theme verification if
   you install editor themes.
6. Do not silently restyle approval/security surfaces with scheme colours that weaken
   semantics: amber stays the heavy-action colour, red stays destructive.

## Version

Spec v1.0.0, extracted from hydrogen-desktop @ 2026-07-26 (pack v7.1.0). When HD's tokens
change, regenerate section 2 from styles.css rather than editing it by hand.
