docs: record the semantic palette landing, and one claim that was false
Build & publish Arch package / arch-package (push) Successful in 2m9s
CI / check (push) Successful in 2m31s
Search index maintenance / maintain-index (push) Successful in 6s
CI / e2e (push) Successful in 5m22s

The two findings recorded as too big for the contrast pass are fixed, so
the plan says so. Also corrects a claim I made and did not check: the
chrome does not stay dark under the light ramp -- that screenshot was
taken before the theme propagated, which is the third time in two passes
a picture read at the wrong moment produced a confident wrong claim.
This commit is contained in:
2026-08-13 01:08:17 -04:00
parent 0b7ffd5679
commit 7410109884
3 changed files with 145 additions and 19 deletions
+74
View File
@@ -1954,3 +1954,77 @@ Nine things worth keeping:
and every one was a *different* mechanism. A finding at 110 nodes and every one was a *different* mechanism. A finding at 110 nodes
hides them; at 3 they are individually obvious. Cheap tail, only hides them; at 3 they are individually obvious. Cheap tail, only
reachable from the other side of the main fix. reachable from the other side of the main fix.
## A role, not a value, decides whether a colour can be fixed
Plan 008 phase 2, third landing: the two findings the contrast pass had
recorded as too big to fix in it, fixed — plus the check for the trap
that has now cost four sessions.
The generalisation: **a token that is used for two different jobs will
be wrong at one of them, and no amount of choosing a better value
fixes it.** `--yj-error` was "the colour of error", which is two
questions. As the *background of a danger button* it wants to stay red
in every theme, and it does. As *the word `failed` on a surface* it
cannot be one value at all — a single colour cannot clear 4.5:1 against
both a near-black and a near-white background, which is why the fixed
set measured 2.314.28:1 on nearly every combination. Splitting the
token by the question it answers made both answerable; picking better
hexes never would have.
The same shape twice more in the same landing:
- **A fill's foreground cannot be written down**, because the accent is
a colour picker. `color: #000` is right for the current default
yellow and wrong for a navy one. `readableOn()` computes it — white
where white clears 4.5:1, black otherwise — which keeps a red danger
button conventional (4.51:1) and flips a green one (3.45:1).
- **`var(--yj-bg-base)` as a foreground is a token used for the wrong
meaning.** Two accent buttons did that. It reads as "the opposite of
the accent" and it is not: it inverts with the ramp, so the light
theme rendered white on yellow at 1.43:1. The bug is not the value,
it is that the *name* did not mean what the call site needed.
Six more things worth keeping:
- **The picture and the number disagreed, and the number was mine —
again.** I recorded "the header and player chrome stay dark while the
body goes light" as a finding, in the plan and in these notes. It is
false: `.top-bar` is `#e9ecef` and `.sidebar` `#f8f9fa` under the
light ramp, and a re-taken screenshot agrees with the DOM. The
original was captured before the theme had propagated. That is the
third time in two passes a screenshot read at the wrong *moment*
produced a confident wrong claim, after a spec reading the DOM before
a fetch and a sweep that had moved the app to another ramp. **A
screenshot has a timestamp and a state; check both before quoting
it.**
- **A `color:` regex matches `border-color:`.** Twice in one landing —
3 borders while rewriting semantic text, then 30 more while rewriting
accent text. Both caught by grepping the *result*, not by any test,
because a border in the wrong shade fails nothing and looks fine.
When a mechanical rewrite is the right tool, the review is a grep of
what it did, not a run of the suite.
- **A fix at the ubiquitous case makes the rare ones findable.** The
greyscale fix took the dark ramp to 0 and the light ramp from 50 to a
list short enough to read individually — at which point every
remaining item was a *different* mechanism, and two of them were the
findings above. A queue of 110 hides its own structure.
- **Knowledge that has been ignored three times is not a knowledge
problem.** The backtick-in-a-`css`-comment trap is documented in
`CLAUDE.md`, in the skill and here; I read it twice in the session it
then cost a cycle in. It is `make css-check` now — a pre-commit hook
and a CI step. The detection is exact rather than heuristic: if a
backtick in a comment closed the literal early, the text the parser
took as the literal contains an unterminated `/*`, and nothing else
produces that.
- **The value of that check is the sentence, not the failure.** `tsc`
already failed on it — with `Class static side 'typeof NowPlaying'
incorrectly extends base class static side` and `Property 'scroll'
does not exist on type 'CSSResult'`, pointing at a line of prose. The
check was verified by breaking a file on purpose and reading both
reports side by side, which is also the only way to know it fires.
- **A change can be invisible to the tier that looks the most like it
covers it.** `make ui-visual` passed unchanged through a whole
palette rewrite, twice, because the component tier has no `:root` and
renders the fallbacks. The tier that *did* catch things was a unit
test over the palette table and a probe against the running app.
+54 -19
View File
@@ -325,23 +325,60 @@ access to any value by not being able to. **Stays dropped.**
#### Two things the measurements found that are not in the audit #### Two things the measurements found that are not in the audit
Both are bigger than what they were found under, and neither is fixed: Both were bigger than what they were found under. Both are now fixed
see the third landing below.
- **The semantic colours are fixed across ramps, and a fixed colour - **The semantic colours were fixed across ramps, and a fixed colour
cannot serve a near-black and a near-white background.** `--yj-error` cannot serve a near-black and a near-white background.** `--yj-error`
is 3.42:1 on dark's surface and 2.55:1 on its elevated; `--yj-info` measured 3.42:1 on dark's surface and 2.55:1 on its elevated;
is 3.10:1 and 2.31:1; success and warning fail on dark and light `--yj-info` 3.10:1 and 2.31:1; success and warning failed on dark and
both. As *backgrounds* under white text, success (3.45) and warning light both. As *backgrounds* under white text, success (3.45) and
(3.58) fail too. The fix is a per-ramp semantic palette, which is a warning (3.58) failed too.
decision about the app's colour identity rather than a value. - **The light ramp was not a usable theme.** With the greyscale fixed
- **The light ramp is not a supported theme.** With the greyscale fixed it still had **50 failing nodes**: the accent yellow under white text
it still has **50 failing nodes**: the accent yellow under white text (1.43:1) and the autotag diff's pale greens and reds on white
(1.43:1), the autotag diff's pale greens and reds on white (1.362.59:1).
(1.362.59:1), and the header and player chrome staying dark while
the body goes light. Read in a screenshot — the "Low confidence pick" ### Phase 2, third landing — the ramp reaches the semantic colours
banner is invisible and the primary button is white-on-yellow. This
is a design job, and the honest question it raises is whether the **2237 nodes across three ramps and twelve views, 0 failing.**
light theme should ship at all in its current state.
The split is by the question a colour answers. A **fill** is "what
colour is a danger button" — red in every theme, unchanged. A **text**
colour is "what colour is the word *failed* on this background" — per
ramp, because one value cannot clear 4.5:1 against both a near-black and
a near-white surface. `bgOverlay` keeps the exception it already had on
the dark ramp.
And every fill now carries a **computed foreground**, because the accent
is a colour picker and no fixed answer survives one: white if white
clears 4.5:1, else black. That keeps a red danger button white and
flips a green or amber one to black. Accent-as-text goes through
`accentTextOn()`, which mixes along the hue until it clears the ramp's
surface and stops — returning the accent *unchanged* on both dark
ramps, so the dark themes are visually untouched by that half.
`make ui-test` 608 → **649**.
#### Where this pass was wrong
- **"The chrome stays dark while the body goes light" was mine, and it
was false.** I read it off a screenshot; the DOM says `.top-bar` is
`#e9ecef` and `.sidebar` `#f8f9fa` under the light ramp, and a
re-taken screenshot agrees. The first one was captured before the
theme had propagated. Third time in two passes that a screenshot read
at the wrong moment produced a confident wrong claim — and the second
time this pass that **the picture and the number disagreed and the
number was mine**.
- **A `color:` regex matches `border-color:`.** Twice: once rewriting
semantic text colours (3 borders) and once rewriting accent text (30
more). A border is a fill, not text. Caught by grepping the result
rather than by any test, because nothing renders differently enough
to fail.
- **Two accent buttons took their foreground from `--yj-bg-base`**,
which inverts with the ramp — white on yellow at 1.43:1. That is not
a colour that was chosen badly; it is a token used for the wrong
meaning, and it only shows up in the theme nobody looks at.
--- ---
@@ -358,10 +395,8 @@ Two of them are not one-liners and should be treated as such:
minimum at 800×600. Reflow at high zoom is the same question one minimum at 800×600. Reflow at high zoom is the same question one
variable over. It may want its own landing. variable over. It may want its own landing.
- **The unnamed `<select>`s** from Phase 1, with `26`. - **The unnamed `<select>`s** from Phase 1, with `26`.
- **The semantic palette** and **the light ramp**, from Phase 2. Both - ~~**The semantic palette** and **the light ramp**~~ — both landed in
are larger than the rest of this tail put together and may not belong Phase 2's third pass rather than waiting for this tail.
in it at all — the light ramp in particular is a question about
whether that theme ships, not a contrast fix.
- **`22`** asks for a non-colour marker on the playing row, which is a - **`22`** asks for a non-colour marker on the playing row, which is a
visual change to the densest list in the app and moves a baseline. visual change to the densest list in the app and moves a baseline.
+17
View File
@@ -505,6 +505,23 @@ first track arrives) and `job-indicator`, whose label swings between
"Scanning Music", "3 background jobs" and "Finished". The notification "Scanning Music", "3 background jobs" and "Finished". The notification
surface already had one from Phase 3. surface already had one from Phase 3.
**A colour's role decides whether it can be fixed across themes.** A
*fill* — `--yj-error`, `--yj-success` — is "what colour is a danger
button", which is red in every theme and stays fixed. A *text* colour
— `--yj-error-text` — is "what colour is the word *failed* on this
background", which cannot be: one value cannot clear 4.5:1 against both
a near-black and a near-white surface, and the old fixed set measured
2.314.28:1 on nearly all of them. And every fill carries a **computed**
foreground (`--yj-accent-fg`, `--yj-*-fg`) rather than a written-down
one, because the accent is a colour picker: white on the default
`#ffd43b` is 1.43:1. `readableOn()` keeps white where white clears and
flips to black where it does not, and `accentTextOn()` mixes the accent
along its own hue until it clears the ramp's surface — returning it
unchanged on both dark ramps. Two accent buttons used to take their
foreground from `--yj-bg-base`, which *inverts with the ramp*; that is a
token used for the wrong meaning, and it only shows in the theme nobody
looks at.
**Contrast is a property of the ramp, and the ramps are data.** **Contrast is a property of the ramp, and the ramps are data.**
`theme-store`'s `SHADE_PALETTES` — not `tokens.css.ts`, which holds only `theme-store`'s `SHADE_PALETTES` — not `tokens.css.ts`, which holds only
the type scale and icon sizes — is where the colours live, applied to the type scale and icon sizes — is where the colours live, applied to