Measuring legibility on a bench console: 35 of 95 text elements below 4.5:1, and the controls at 1.12:1
A rendered-page contrast audit, taking each visible element's colour against the background actually behind it, found 35 of 95 text elements under WCAG 4.5:1 and every button, input and card edge at 1.12-1.18:1 (effectively invisible), plus one real bug where an undefined CSS variable turned a header name white-on-white; a measured palette took the failures to 0 of 95 with no change to size, spacing or layout.
Status
First pass shipped 1 October 2026: commit db1dfb6, synced to both
sticks and seen on the station ("better than before"). Second pass
(darker, below): built and verified in the preview, not yet committed or
synced. One file:
tools/gui/index.html, CSS only. No size, spacing, layout or JavaScript
change: the page geometry measured identically before and after.
The problem
The operator, looking at the station: the text and layout "look kind of faint", "like a reflection", "you have to strain your eyes", and the buttons do not show that they are buttons or react properly under the pointer.
Measured on the main screen as rendered (headless Edge, 1366 × 768, every visible text element's colour against the background actually behind it, WCAG 2 contrast ratio):
- 35 of 95 pieces of text were under the 4.5:1 minimum.
- 20 at 2.56:1: the header subtitle and every small label (Cosmetic
grade, Processor, Select drive…). They were all
--mut2#94a3b8. - 6 at 2.96:1: every card title (Workflow, Audit, Hardware test…).
- The "Not tested" badges at 4.34:1, the green "Connected" at 3.3:1, and the selected workflow button at 4.22:1.
- 20 at 2.56:1: the header subtitle and every small label (Cosmetic
grade, Processor, Select drive…). They were all
- Every button, input, select and card edge was 1.12–1.18:1, in effect invisible. On a good panel the buttons still read as buttons from their text and position. On a washed-out second-hand panel, the kind the station runs on, they did not.
- Hover was nearly invisible too: a ghost button went from white to #f1f4f8.
- One real bug: hovering the operator's name in the header set its colour
to
var(--fg,#e8edf2), and--fgis not defined on this page, so the name turned near-white on the white header and vanished.
Why it matters
The station runs on whatever laptop is being audited, often old screens with poor contrast and viewing angles. A technician who has to hunt for the labels and buttons is slower and more likely to press the wrong thing. On a console that wipes drives, that matters.
What was built
The palette was chosen from measured ratios against white. It was tightened rather than redesigned, and it is still light.
| Token | Was | Now | Ratio now | Used for |
|---|---|---|---|---|
--mut | #64748b (4.76) | #475569 | 7.58 | descriptions, secondary text |
--mut2 | #94a3b8 (2.56) | #5b6b80 | 5.44 | small uppercase labels, placeholders |
--ink2 (new) | — | #334155 | 10.35 | card titles (were #8b97a8, 2.96), header items |
--edge (new) | — | #8592a6 | 3.15 | the outline of anything pressable or typable |
--line | #e8ecf3 (1.18) | #d3dae4 | 1.41 | card borders and dividers (deliberately soft) |
--bg | #f7f9fc | #f1f4f9 | — | the page, a step deeper so the white cards stand out |
--green | #16a34a (3.3) | #15803d | 5.02 | "Connected" and other green status text |
- Buttons (ghost, primary, danger, icon, workflow switch):
- At rest: a 3:1 outline and a faint shadow, so they sit "up" off the card.
- Under the pointer: they darken: grey #e5eaf1 with a darker edge for the ghost buttons, deeper blue and red for the solid ones, with a soft shadow.
- When pressed: they darken again.
- Disabled: hover and press apply only to
:not(:disabled), so a disabled button (every Test button during a run) never lights up as if it would act. - Keyboard focus: a visible ring.
- Fields (selects, comment box, inputs):
- the same 3:1 edge
- a darker edge under the pointer
- a soft blue ring on focus
- The header:
- a stronger edge and a light shadow, which separate it from the page
- the subtitle at 7.58:1
- the user, operator and date at 10.35:1
- the operator hover fixed: the name turns blue
- Drive rows (the wipe checkboxes) answer the pointer as well.
What was deliberately NOT built
- No bold, no larger type. The operator was explicit: "I'm not saying it should be bold", and "not saying it should be dark". The problem was contrast, not weight or size, and changing sizes would move every card.
- The coloured status pills are left alone (green 4.79, amber 4.84; both pass). Darkening them changes how health reads at a glance, which is a separate decision.
- Card borders stay soft (1.41:1). A 3:1 border on every card would look boxed-in and heavy. Cards are not controls, so they don't need it.
- Disabled buttons keep their 45% opacity, as WCAG exempts disabled controls. That faintness is what tells the technician "not now".
How it was proved
- Contrast audit script (scratchpad
contrast.js) over the rendered page: 35 failing text elements before, 0 after; the lowest text now 4.79:1 (the unchanged green pill), and all greys 5.44:1 or higher. Control edges 3.15:1, up from 1.18:1. - Hover/press check: each kind of button captured at rest and under the pointer, in the old copy (served from a scratch directory, not the repo) and the new one. Computed colours confirm each one changes. A disabled Test button's background is identical at rest and under the pointer.
- Layout unchanged: the column, card and tile measurements at 1366 px are identical to the hardware-test-tiles record.
- All 31 Python tool test suites pass. No test pins these colours.
Unverified: the stick's Firefox and the Dell's panel, which is where the complaint came from. The ratios are computed from sRGB values. A washed-out panel lowers every ratio, which is why most of the text now has well over the minimum.
Second pass, same day: one step darker
Seen on the stick after the first pass (commit db1dfb6): "better than before", but the text "still looks very white". The first pass had stopped at comfortable AA margins. This one takes every grey one step further down the slate scale, keeping the order titles > labels > hints:
| Token | First pass | Now | Ratio now |
|---|---|---|---|
--ink2 (card titles, header items) | #334155 (10.35) | #1e293b | 14.63 |
--mut (descriptions, secondary text) | #475569 (7.58) | #334155 | 10.35 |
--mut2 (small labels) | #5b6b80 (5.44) | #475569 | 7.58 |
| pill wording, green | #15803d (4.79) | #166534 | 6.81 |
| pill wording, amber | #b45309 (4.84) | #92400e | 6.84 |
- The comment box's placeholder stays at #5b6b80 (5.44). Taken with the labels to 7.58 it would look like text someone had typed, and an empty box would read as filled.
- Not changed: the dots and the
--amber/--greentokens themselves (only the pill wording got darker), the control edges (3.15:1), and the white-on-blue/red button text. - Measured after: 0 of 95 under 4.5:1. Every grey is now 7.58:1 or higher. The lowest text left is white on the red Wipe button (4.83).
- Unchanged: layout geometry, and 31/31 Python tool tests pass.
Open questions
- Whether the operator wants this on the sticks (a sync of
gui/index.html). - If the panel still looks faint, the next step is weight (500 for secondary text) rather than more darkening. It was held back because the operator asked for "not bold".
A published copy. Commit references and internal identifiers have been removed and the operator is not named; the engineering, the counts and the stated limits are unchanged.