# Alloy Tokens The design-token system for Alloy. Storage format, authoring discipline, extension rules, and how Alloy renders any theme downloaded from the future theme catalog. The rules these tokens implement live in [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md). This file is where the rules become storage, derivation, and gates. ## Storage format: makeover Alloy consumes themes in the [makeover](https://makenot.work/git/max/makeover) TOML format, the same schema GoingsOn, Balanced Breakfast, audiofiles, and makenot.work already consume. A theme file has seven sections: ```toml [meta] name = "Akari Dawn" variant = "light" # "light" | "dark" | "high-contrast" [surface] page = "#e4ded6" # main canvas raised = "#ede7de" # elevated / focused / popup base (lighter than page in light mode) sunken = "#cfc4b6" # recessed panels (darker than page in light mode) overlay = "#f0ece4" # modal / top-most temporary UI [content] primary = "#1a1816" # body text secondary = "#222d38" # section headers, labels muted = "#514b45" # disabled, placeholder, metadata [action] primary = "#8a4530" # the theme's primary action / brand accent [status] danger = "#6a2828" # error success = "#3a5830" # healthy / ok warning = "#b07840" # near-threshold info = "#304050" # informational / link / in-progress [line] border = "#cabeae" # single border tone; Alloy derives -subtle and -strong (see below) [category] one = "#8a4530" # categorical series colors (chart / syntax / tag rows) two = "#3a5830" three = "#304050" four = "#b07840" five = "#806080" six = "#305858" ``` Alloy's default light theme is **[Akari Dawn](https://makenot.work/git/max/makeover/tree/main/themes/akari-dawn.toml)**; the default dark is **[Akari Night](https://makenot.work/git/max/makeover/tree/main/themes/akari-night.toml)**. Both are based on Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT). A mode is picked in `alloy settings`, which writes two things: the console's own theme key, effective immediately, and `~/.config/alloy/mode`, which every other themed config follows at the next login via `alloy theme apply`. A console told nothing follows what it is drawn on instead, asking `$COLORFGBG` first and the mode file second. `--theme` overrides either for one run. Users can also drop any makeover `.toml` into `~/.config/alloy/themes/` and name it. There is no OSC background query. It is the only other way to learn a terminal's background, and it means writing an escape sequence and waiting for a reply before the first frame, which is not worth the stall. **Why makeover as the storage format:** the future user-facing theme catalog (a website of downloadable `.toml` files) is only useful if every make-family app renders any downloaded theme the same way. Sharing the format across apps buys that with zero adapter code. ## Alloy-specific derived tokens Alloy needs three token tiers makeover ships one of: `border-subtle`, `border`, `border-strong`. Rather than extend the schema (which would break existing consumers), Alloy computes the outer two from `line.border` at load time: ``` border-subtle = mix(line.border, surface.page, 60%) # pulled toward surface — decorative divider border-strong = mix(line.border, content.primary, 65%) # pulled toward text — focus rings, selection ``` Mix is in linear sRGB. The 65%-toward-text ratio for `border-strong` is calibrated to reach WCAG AA-UI (≥3.0:1) against `surface.page` even on soft-border themes (Akari's `line.border` is `#cabeae`; the mix produces `#7f786d` which lands at 3.27:1). Themes with crisper borders will overshoot; that's fine. The formulas live in `alloy_tui`, not in the theme file. Theme files stay minimal and cross-app compatible. ## Authoring discipline: OKLCH mental model, hex on disk Themes on disk are hex. The design *reasoning* behind those hex values (Alloy-authored ones or curated ones like Akari) still uses OKLCH: - **Ramp discipline.** Surface tiers should follow monotonic L ordering per mode (light: `sunken < page < raised < overlay`; dark: inverted). Accents live in the mid-L / mid-C range where they read as color-carrying-information on either ramp. - **Tinted-not-neutral chrome.** Chrome tones should carry a hue tint (see [DESIGN-LANGUAGE.md](DESIGN-LANGUAGE.md#core-principle-tinted-greyscale-chrome-color-as-information)); Akari's warm-clay palette is the canonical worked example. - **Accent-on-glyph, not on body text.** Confirmed by contrast math in the audit: accents that clear WCAG AA-text (4.5:1) on the theme's surface can be used freely; accents that only clear AA-UI (3.0:1) go on glyphs and edge markers, with body text staying at `content.primary`. Because storage is hex, OKLCH doesn't need to be preserved in the file; it's the mental model of the person authoring the theme. ## Gate: the WCAG audit Any theme, downloaded or authored, is audited by [`tools/wcag_audit.py`](../tools/wcag_audit.py). It reads a makeover `.toml`, converts hex to linear sRGB to WCAG 2.1 relative luminance, and reports pass/fail for every affordance-carrying pair (text-on-surface, borders-on-surface, accents-on-surface, elevation deltas). Also computes `border-subtle` / `border-strong` via the same derivation Alloy uses at runtime. Run: ``` python3 tools/wcag_audit.py ../Libraries/makeover/themes/akari-dawn.toml ``` The audit is not a hard-fail gate: a theme with accents that only reach AA-UI on some surface tiers is still shippable if the accent-on-glyph rule is honored. The audit's job is to make trade-offs visible. ## Verified contrast: Akari Dawn Method: OKLCH-agnostic; reads hex, computes WCAG 2.1 relative luminance directly. Every number below comes out of `tools/wcag_audit.py`; regenerate rather than hand-edit. **Text on surfaces:** | Pair | vs sunken | vs page | vs raised | vs overlay | |---|---:|---:|---:|---:| | `content.primary` | 11.42 | 14.68 | 17.08 | 18.43 | | `content.secondary` | 9.25 | 11.88 | 13.83 | 14.92 | | `content.muted` | 3.85 | 4.95 | 5.76 | 6.21 | `primary` and `secondary` are AAA on every tier. `muted` is AA-text everywhere but the sunken well, where it holds AA-UI, which is what a deliberately recessive tone is for. **Borders on surfaces:** | Pair | vs page | vs raised | vs overlay | |---|---:|---:|---:| | `border-strong` (derived) | 6.34 | 7.37 | 7.95 | | `line.border` | 3.11 | 3.62 | 3.90 | | `border-subtle` (derived) | 1.37 | 1.59 | 1.72 | `line.border` now clears AA-UI on its own, so a control's edge is a boundary a reader can find rather than a hint the derivation has to rescue. `border-strong` is the emphatic tier above it and `border-subtle` stays decorative, not required to meet 3.0. **Accents on surfaces:** | Accent | vs sunken | vs page | vs raised | vs overlay | Notes | |---|---:|---:|---:|---:|---| | `action.primary` (`#5a41a2`) | 4.68 | 6.02 | 7.00 | 7.56 | AA-text on all four | | `status.danger` (`#8f2420`) | 5.20 | 6.69 | 7.78 | 8.40 | AA-text or AAA everywhere | | `status.success` (`#2f5424`) | 5.26 | 6.76 | 7.87 | 8.49 | AA-text or AAA everywhere | | `status.warning` (`#875000`) | **4.00** | 5.14 | 5.98 | 6.45 | AA-text on three of four; AA-UI in the sunken well | | `status.info` (`#25415c`) | 6.39 | 8.20 | 9.55 | 10.30 | AAA / AA-text everywhere | One tier misses AA-text, and it misses on the one surface a light theme cannot help: `surface.sunken` is a recessed cream, and an amber legible against it is no longer an amber. Warning keeps AA-UI there and stays a glyph-plus-edge accent in a well, per the accent-on-glyph rule. ## Verified contrast: Akari Night **Text on surfaces:** | Pair | vs sunken | vs page | vs raised | vs overlay | |---|---:|---:|---:|---:| | `content.primary` | 17.05 | 14.60 | 12.16 | 10.37 | | `content.secondary` | 13.10 | 11.22 | 9.34 | 7.97 | | `content.muted` | 6.09 | 5.22 | 4.35 | 3.71 | **Borders on surfaces:** | Pair | vs page | vs raised | vs overlay | |---|---:|---:|---:| | `border-strong` (derived) | 11.03 | 9.19 | 7.83 | | `line.border` | 4.33 | 3.61 | 3.08 | | `border-subtle` (derived) | 2.33 | 1.94 | 1.65 | **Accents on surfaces:** | Accent | vs sunken | vs page | vs raised | vs overlay | Notes | |---|---:|---:|---:|---:|---| | `action.primary` (`#b9a3f2`) | 8.87 | 7.59 | 6.32 | 5.39 | AA-text or AAA everywhere | | `status.danger` (`#f78077`) | 7.69 | 6.58 | 5.48 | 4.68 | AA-text everywhere | | `status.success` (`#96c97e`) | 10.13 | 8.68 | 7.23 | 6.16 | AA-text or AAA everywhere | | `status.warning` (`#e5ac62`) | 9.63 | 8.25 | 6.87 | 5.86 | AA-text or AAA everywhere | | `status.info` (`#93aec6`) | 8.43 | 7.22 | 6.01 | 5.13 | AA-text or AAA everywhere | Nothing on the dark side falls below AA-text. The overlay column is the tight one, since a popover face is the lightest surface a dark theme has and every accent is read against it. ## Why the accent is a lavender `action.primary` is the one hue the whole system wears: the focus ring, the caret, the pressed button, the selected row, `category.one`, and the keyword class in every generated editor theme. Akari's own accent is a lantern orange, which put the accent in the same corner of the wheel as `status.warning` and `status.danger` — so the color meaning "this is where you are" and the color meaning "something is wrong" were neighbors, and telling them apart on a chip or a one-cell gutter marker came down to saturation. A violet accent has no semantic neighbor. Red stays available for danger and amber for warning, and the ring is unmistakably not either of them. The warm tones are still the theme's ground: `surface.*` is cream on the light side and a warm near-black on the dark, and the selection wash stays amber precisely so that a lavender caret sitting inside it does not disappear into its own hue. The consequence to know about: on a light theme a lavender legible as text has to be a deep violet (`#5a41a2`), not the pale tint the word "lavender" suggests. A pale lavender is available to a consumer as a *surface* tint via `mix(surface.page, action.primary, ...)`; it is not what the ink can be. ## Why the ink is a greyscale `content.primary` is `#111111` on the light theme and `#f0f0f0` on the dark, and both are neutral: no warmth is carried in the ink even though every surface under it is warm. This is the other half of the accent rule. Colour on text has to *mean* something — a severity, a syntax class, a category — and an ink tinted toward the theme's own hue spends that channel on nothing. Neutralising it costs no contrast (the light ink went from 14.38:1 to 14.68:1 against the page) and it makes the warm ground read as ground rather than as a wash over everything. `alloy_tui`'s `text::action` is the one accent-on-text use that is not a severity, and it survives for the reason its own docs give: a key hint *is* the actionable element. `content.secondary` and `content.muted` are tonal steps of that ink toward the page, so they do pick up a trace of the ground as they recede. That is the ramp working, not a tint: light `muted` lands at `#615f5c`, which is three points off neutral. Pure black is available and is the wrong choice. The tonal steps are derived by mixing toward the page until they clear a contrast floor against the ink, and from `#000000` that scan lands `muted` at 7.66:1 against the page instead of the ramp's usual ~4.9 — muted text that does not recede. `#111111` keeps the ramp intact. ## Why bright red is no longer the accent ANSI slot 9 used to resolve to `action.primary`, on the reasoning that the accent was a warm orange-red and so was bright red. That held only while the accent happened to be warm; pointed at a violet it turns every `\e[91m` in the terminal purple. Slot 9 is now `status.danger`, the same relationship slots 10, 11 and 12 have with their normal-intensity counterparts. A program painting an error in bright red is naming red, not naming the theme's accent. ## What's deferred - **Calibrated-display verification** on real hardware in real terminals (truecolor and 256-color). Pre-1.0 gate. - **256-color downgrade table** for terminals that report no truecolor. - **Figlet / block-element recipes** for the large numeric readouts a display font would otherwise carry. - **Swapping the desktop's mode without a re-login.** `alloy theme apply` ships and the settings row writes the mode file, but the configs it lays down are read once by programs already running, so the switch lands at the next login. The console itself re-themes immediately. ## How this file is consumed The themes [makeover ships](https://makenot.work/git/max/makeover/tree/main/themes/) are the ground truth. `alloy_tui` at load time: 1. Reads the selected theme file via the `makeover` crate. 2. Computes the two derived tokens (`border-subtle`, `border-strong`) from `line.border`. 3. Exposes the full token map as a runtime `Theme` of ratatui `Style` / `Color` values. 4. Optionally emits a 256-color fallback table for terminals without truecolor. No hex values are hard-coded in Rust. Changing the theme file changes every authored Alloy surface after re-launch (or hot-swap once `alloy theme` ships). Third-party themes downloaded into `~/.config/alloy/themes/` work with zero adapter code.