Skip to main content

max / makeover

Repaint the default theme pair: more contrast, a violet accent Akari Dawn and Akari Night are what every app falls back to, and both were too soft to carry the depth model they are rendered through. The light border sat at 1.37:1 against its page and the dark one at 1.39:1, so a control's edge was a hint rather than a boundary; light warning was 2.80:1, under even the AA-UI floor; and neither raised surface cleared the delta-L the bevel derivation needs, which is why both were on the exemption list in `raised_is_distinct_from_page`. Surfaces are pushed apart, the ink deepened, and every text-carrying intent lifted. Every intent now clears AA-text on page and raised in both themes, and the two borders clear AA-UI on every surface a bordered control sits on. One number still misses: light warning is 4.00:1 in the sunken well, where an amber legible against recessed cream is no longer an amber, and it keeps AA-UI there as a glyph accent. The accent moves off Akari's lantern orange to a violet. The accent is the one hue the whole system wears -- ring, caret, pressed button, selected row, `category.one`, keyword class -- and a warm one put it in the same corner of the wheel as warning and danger, so "you are here" and "this is wrong" were told apart by saturation on a one-cell gutter marker. Violet has no semantic neighbor. The warm tones stay as ground and as the selection wash, which is what keeps a violet caret from vanishing into the run it sits in. Two things the repaint exposed rather than caused: ANSI slot 9 resolved to `action.primary`, which held only while the accent happened to be orange-red. Pointed at a violet it turns every `\e[91m` purple. It is `status.danger` now, the relationship slots 10, 11 and 12 already have with their normal-intensity counterparts. `border-strong` was a flat darkening, so on a dark theme it moved toward the page and came out weaker than the border it was strengthening. It now steps away from the page, reading direction off content the way `surface-well` does. Invisible while the dark border was itself invisible. `adapted_themes_carry_inline_attribution` now anchors on the upstream URL rather than the phrase "adapted from": what a header owes upstream changes as a file travels from its source, and a test pinning the wording asks the header to overclaim. The akari pair is descended from Akari, and its values are no longer Akari's.
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:36 UTC
Signed with PGP, not checked
Commit: 6730419dd5ebad6f0577885e52b7059f8bc34b01
Parent: 70f2676
7 files changed, +104 insertions, -51 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover"
3 - version = "3.2.1"
3 + version = "3.3.0"
4 4 edition = "2024"
5 5 description = "Shared theme loading for the make-family apps: TOML theme files parsed into intent-based color tokens, with perceptual derivations and WCAG contrast."
6 6 license = "MIT"
@@ -81,8 +81,11 @@
81 81
82 82 ## Original themes
83 83
84 - `akari-dawn` and `akari-night` are adaptations, listed above. The following are
85 - this project's own work and carry no third-party claim:
84 + `akari-dawn` and `akari-night` are listed above as adaptations, and the credit
85 + stands: they began as Akari re-expressed into this schema. Their current values
86 + are this project's own, having moved for contrast and for a violet accent, so
87 + what remains owed upstream is the ancestry rather than the palette. The
88 + following carry no third-party claim at all:
86 89
87 90 - `makenotwork` — the makenot.work platform palette
88 91 - `goingson` — the GoingsOn application palette
M src/ansi.rs +9 -1
@@ -128,6 +128,14 @@
128 128 /// or dark, since red is the theme's danger tone either way, which is exactly
129 129 /// why the four achromatic slots are not in this table.
130 130 ///
131 + /// `action.primary` is deliberately absent. A theme's accent is whatever hue
132 + /// the theme wants it to be, and slot 9 is *bright red* — a program painting an
133 + /// error in it is naming red, not naming the accent. Binding the two only ever
134 + /// looked right while the accent happened to be a warm orange; point the accent
135 + /// at a lavender and every `\e[91m` in the terminal turns purple. Bright red is
136 + /// the danger tone, the same way slots 10, 11 and 12 are their normal-intensity
137 + /// counterparts.
138 + ///
131 139 /// Here rather than in each consumer, so a program that paints its own palette
132 140 /// at runtime resolves the same slots as one reading a generated config. Slot
133 141 /// 14 is `category.six`.
@@ -138,7 +146,7 @@
138 146 (4, "status.info"),
139 147 (5, "category.five"),
140 148 (6, "category.six"),
141 - (9, "action.primary"), // bright red, the theme's warm accent
149 + (9, "status.danger"),
142 150 (10, "status.success"),
143 151 (11, "status.warning"),
144 152 (12, "status.info"),
M src/dirs.rs +8 -1
@@ -267,6 +267,13 @@
267 267 fn adapted_themes_carry_inline_attribution() {
268 268 // Each adapted file must name its upstream in-file, so the credit
269 269 // survives someone copying a single .toml out of the crate.
270 + //
271 + // The upstream URL is the anchor, not a phrase. What the credit says
272 + // varies with how far a file has travelled from its source — a theme
273 + // whose values we have since moved is descended from its upstream
274 + // rather than adapted from it — and a test that pinned the wording
275 + // would be asking the header to lie about that rather than asking it
276 + // to carry the credit.
270 277 const ORIGINALS: [&str; 5] = [
271 278 "makenotwork",
272 279 "goingson",
@@ -279,7 +286,7 @@
279 286 continue;
280 287 }
281 288 assert!(
282 - source.contains("adapted from"),
289 + source.lines().take(6).any(|l| l.contains("https://")),
283 290 "adapted theme `{id}` is missing its inline attribution header"
284 291 );
285 292 }
M src/intent.rs +35 -6
@@ -242,7 +242,30 @@
242 242 derived.push(("hover-surface".into(), sunken));
243 243 }
244 244 if let Some(border) = get(&intents, "border") {
245 - derived.push(("border-strong".into(), darken(border, 0.05)));
245 + // The emphatic border tier: one step further from the page than
246 + // `border` is, which is not the same as one step darker.
247 + //
248 + // Direction is read off `content` against `surface-page`, the way
249 + // `surface-well` reads it, because "stronger" means "further from the
250 + // ground it is drawn on" and a dark theme's ground is at the bottom of
251 + // the ramp. Darkening unconditionally moves a dark theme's border
252 + // *toward* its page, so the strong tier came out weaker than the plain
253 + // one — invisible for as long as the border itself was, and obvious the
254 + // moment a theme gave the border a real value.
255 + //
256 + // A theme with no page or no content keeps the darkening, which is the
257 + // light-theme answer and the one the majority of a half-written file
258 + // will want.
259 + let away_from_page = match (get(&intents, "content"), get(&intents, "surface-page")) {
260 + (Some(content), Some(page)) => content.to_oklab().l > page.to_oklab().l,
261 + _ => false,
262 + };
263 + let strong = if away_from_page {
264 + lighten(border, 0.05)
265 + } else {
266 + darken(border, 0.05)
267 + };
268 + derived.push(("border-strong".into(), strong));
246 269 }
247 270
248 271 for (token, rgb) in derived {
@@ -494,17 +517,23 @@
494 517 // cannot hold a bevel, not that it is wrong. Shrinking the list is the fix;
495 518 // growing it is a regression in the theme, not in this derivation.
496 519 //
497 - // An entry leaves this list only when the fix is upstream's own, never a
498 - // color we picked. The remaining entries are shallow ramps in published
499 - // palettes, deferred until every app is migrated and eyeballed.
520 + // An imported palette leaves this list only when the fix is upstream's own,
521 + // never a color we picked, or the list becomes a record of what we were
522 + // willing to repaint rather than of what cannot hold a bevel. The remaining
523 + // entries are shallow ramps in published palettes, deferred until every app
524 + // is migrated and eyeballed.
525 + //
526 + // The akari pair is not an imported palette in that sense and is exempt.
527 + // Upstream ships no file in this schema, so its surface ramp was ours to
528 + // choose from the start, and it is the pair every app defaults to: a depth
529 + // model the default theme cannot render is a broken default, not a palette
530 + // we are borrowing on someone else's terms.
500 531 #[test]
501 532 fn raised_is_distinct_from_page() {
502 533 // Below this, a raised surface and the page under it are one surface to
503 534 // a reader, whichever direction the theme ramps in.
504 535 const MIN_DELTA_L: f32 = 0.05;
505 536 const CANNOT_LIFT_OFF_THE_PAGE: &[&str] = &[
506 - "akari-dawn",
507 - "akari-night",
508 537 "ayu-light",
509 538 "ayu-mirage",
510 539 "catppuccin-latte",
@@ -1,39 +1,42 @@
1 - # Colors adapted from Akari, used under the MIT License.
1 + # Descended from Akari, used under the MIT License.
2 2 # https://github.com/cappyzawa/akari-theme
3 3 # Copyright (c) 2025 Shu Kutsuzawa
4 4 #
5 - # Adaptation only: the palette is re-expressed into makeover's intent
6 - # schema. Upstream ships no file in this format. See THIRD-PARTY-NOTICES.md.
5 + # Upstream ships no file in this schema, so this began as a re-expression of
6 + # Akari's palette into makeover's intents and has since departed from it: the
7 + # surfaces and ink were pushed apart for contrast, and the accent moved off
8 + # Akari's lantern orange to a violet. The structure and the debt are Akari's;
9 + # the values are no longer. See THIRD-PARTY-NOTICES.md.
7 10
8 11 [meta]
9 12 name = "Akari Dawn"
10 13 variant = "light"
11 14
12 15 [surface]
13 - page = "#e4ded6"
14 - raised = "#ede7de"
15 - sunken = "#cfc4b6"
16 - overlay = "#f0ece4"
16 + page = "#e8e2da"
17 + raised = "#f7f3ed"
18 + sunken = "#d2c8b9"
19 + overlay = "#fefcf8"
17 20
18 21 [content]
19 - primary = "#1a1816"
22 + primary = "#161310"
20 23
21 24 [action]
22 - primary = "#8a4530"
25 + primary = "#5a41a2"
23 26
24 27 [status]
25 - danger = "#6a2828"
26 - success = "#3a5830"
27 - warning = "#b07840"
28 - info = "#304050"
28 + danger = "#8f2420"
29 + success = "#2f5424"
30 + warning = "#875000"
31 + info = "#25415c"
29 32
30 33 [line]
31 - border = "#cabeae"
34 + border = "#8d7d65"
32 35
33 36 [category]
34 - one = "#8a4530"
35 - two = "#3a5830"
36 - three = "#304050"
37 - four = "#b07840"
38 - five = "#806080"
39 - six = "#305858"
37 + one = "#5a41a2"
38 + two = "#2f5424"
39 + three = "#25415c"
40 + four = "#875000"
41 + five = "#8c2f6a"
42 + six = "#1f5450"
@@ -1,39 +1,42 @@
1 - # Colors adapted from Akari, used under the MIT License.
1 + # Descended from Akari, used under the MIT License.
2 2 # https://github.com/cappyzawa/akari-theme
3 3 # Copyright (c) 2025 Shu Kutsuzawa
4 4 #
5 - # Adaptation only: the palette is re-expressed into makeover's intent
6 - # schema. Upstream ships no file in this format. See THIRD-PARTY-NOTICES.md.
5 + # Upstream ships no file in this schema, so this began as a re-expression of
6 + # Akari's palette into makeover's intents and has since departed from it: the
7 + # surfaces and ink were pushed apart for contrast, and the accent moved off
8 + # Akari's lantern orange to a violet. The structure and the debt are Akari's;
9 + # the values are no longer. See THIRD-PARTY-NOTICES.md.
7 10
8 11 [meta]
9 12 name = "Akari Night"
10 13 variant = "dark"
11 14
12 15 [surface]
13 - page = "#25231f"
14 - raised = "#2e2c27"
15 - sunken = "#1c1a17"
16 - overlay = "#35322c"
16 + page = "#201e1a"
17 + raised = "#302c27"
18 + sunken = "#0e0d0b"
19 + overlay = "#3b3731"
17 20
18 21 [content]
19 - primary = "#e6ded3"
22 + primary = "#f3ede3"
20 23
21 24 [action]
22 - primary = "#e26a3b"
25 + primary = "#b9a3f2"
23 26
24 27 [status]
25 - danger = "#d25046"
26 - success = "#7faf6a"
27 - warning = "#d4a05a"
28 - info = "#7a8fa2"
28 + danger = "#f78077"
29 + success = "#96c97e"
30 + warning = "#e5ac62"
31 + info = "#93aec6"
29 32
30 33 [line]
31 - border = "#3d3a35"
34 + border = "#8a8173"
32 35
33 36 [category]
34 - one = "#e26a3b"
35 - two = "#7faf6a"
36 - three = "#7a8fa2"
37 - four = "#d4a05a"
38 - five = "#8e7ba0"
39 - six = "#6f8f8a"
37 + one = "#b9a3f2"
38 + two = "#96c97e"
39 + three = "#93aec6"
40 + four = "#e5ac62"
41 + five = "#dd94d0"
42 + six = "#84b5ae"