Skip to main content

max / alloy

Take text off the tinted ramp The core principle put surfaces, text, borders and chrome on one tinted greyscale, "never neutral grey". The ink is now the exception: colour on text has to encode something, and a tint toward the theme's own hue encodes nothing. Surfaces, borders, box-drawing and scrollbars are unchanged and stay warm. The floor at the extremes still holds and now has a mechanical reason rather than only a taste one: pure black collapses the tonal-step derivation, which travels from the ink toward the page until it clears a contrast floor against the ink, and from `#000000` it lands `muted` where `secondary` belongs. Contrast tables regenerated for the neutral ink.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01EhbCuw3Z8VHWq6VNZ4XK2B
Author: Max Johnson <me@maxj.phd> · 2026-09-08 17:42 UTC
Signed with PGP, not checked
Commit: 3ff130009b0d0b1ba42476cda198461cecf9487c
Parent: 8b9ec58
2 files changed, +21 insertions, -9 deletions
@@ -6,7 +6,9 @@
6 6
7 7 ## Core principle: tinted greyscale chrome, color as information
8 8
9 - The UI is monochrome in a single tinted greyscale ramp. "Tinted" is the load-bearing word: the ramp is biased toward a hue (warm beige in light mode, warm charcoal in dark), never neutral grey, never pure white or black at the extremes. Surfaces, text, borders, box-drawing, scrollbars, focus borders, all live on this ramp.
9 + The UI is monochrome in a single tinted greyscale ramp. "Tinted" is the load-bearing word: the ramp is biased toward a hue (warm beige in light mode, warm charcoal in dark), never pure white or black at the extremes. Surfaces, borders, box-drawing, scrollbars and focus borders all live on this ramp.
10 +
11 + **The ink is the exception, and it is neutral.** `content.primary` is a greyscale — `#111111` light, `#f0f0f0` dark. Colour on text has to mean something, and an ink tinted toward the theme's own hue spends that channel on nothing while making the warm ground read as a wash over everything rather than as ground. The tonal steps below it (`content.secondary`, `content.muted`) are derived by travelling from that ink toward the page, so they pick up a trace of the tint as they recede, which is the ramp working rather than a tinted ink. Still never pure black or pure white: `#000000` collapses the step derivation, and the reasoning is in [TOKENS.md](TOKENS.md#why-the-ink-is-a-greyscale).
10 12
11 13 Color is reserved for information. A red value means something is wrong. A green bar means utilization. A syntax-highlighted token carries semantic weight worth the eye's attention. If a color does not encode information, it does not appear.
12 14
M docs/TOKENS.md +18 -8
@@ -94,9 +94,9 @@
94 94
95 95 | Pair | vs sunken | vs page | vs raised | vs overlay |
96 96 |---|---:|---:|---:|---:|
97 - | `content.primary` | 11.19 | 14.38 | 16.74 | 18.06 |
98 - | `content.secondary` | 8.96 | 11.51 | 13.39 | 14.45 |
99 - | `content.muted` | 3.72 | 4.78 | 5.56 | 6.00 |
97 + | `content.primary` | 11.42 | 14.68 | 17.08 | 18.43 |
98 + | `content.secondary` | 9.25 | 11.88 | 13.83 | 14.92 |
99 + | `content.muted` | 3.85 | 4.95 | 5.76 | 6.21 |
100 100
101 101 `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.
102 102
@@ -104,7 +104,7 @@
104 104
105 105 | Pair | vs page | vs raised | vs overlay |
106 106 |---|---:|---:|---:|
107 - | `border-strong` (derived) | 6.34 | 7.38 | 7.96 |
107 + | `border-strong` (derived) | 6.34 | 7.37 | 7.95 |
108 108 | `line.border` | 3.11 | 3.62 | 3.90 |
109 109 | `border-subtle` (derived) | 1.37 | 1.59 | 1.72 |
110 110
@@ -128,15 +128,15 @@
128 128
129 129 | Pair | vs sunken | vs page | vs raised | vs overlay |
130 130 |---|---:|---:|---:|---:|
131 - | `content.primary` | 16.68 | 14.29 | 11.90 | 10.15 |
132 - | `content.secondary` | 12.81 | 10.97 | 9.14 | 7.79 |
133 - | `content.muted` | 5.96 | 5.10 | 4.25 | 3.63 |
131 + | `content.primary` | 17.05 | 14.60 | 12.16 | 10.37 |
132 + | `content.secondary` | 13.10 | 11.22 | 9.34 | 7.97 |
133 + | `content.muted` | 6.09 | 5.22 | 4.35 | 3.71 |
134 134
135 135 **Borders on surfaces:**
136 136
137 137 | Pair | vs page | vs raised | vs overlay |
138 138 |---|---:|---:|---:|
139 - | `border-strong` (derived) | 10.77 | 8.97 | 7.65 |
139 + | `border-strong` (derived) | 11.03 | 9.19 | 7.83 |
140 140 | `line.border` | 4.33 | 3.61 | 3.08 |
141 141 | `border-subtle` (derived) | 2.33 | 1.94 | 1.65 |
142 142
@@ -160,6 +160,16 @@
160 160
161 161 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.
162 162
163 + ## Why the ink is a greyscale
164 +
165 + `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.
166 +
167 + 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.
168 +
169 + `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.
170 +
171 + 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.
172 +
163 173 ## Why bright red is no longer the accent
164 174
165 175 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.