Skip to content

Contrast: the faint hint colour was under WCAG AA; raised, and test:ui now measures every text element - #56

Closed
deanrie wants to merge 2 commits into
seQRets:mainfrom
deanrie:fix/contrast-and-audit
Closed

deanrie wants to merge 2 commits into
seQRets:mainfrom
deanrie:fix/contrast-and-audit

Conversation

@deanrie

@deanrie deanrie commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #49 (adds a check to its harness). Found by a contrast audit I ran across all three projects — the sisters document "≥4.5:1 for every text token" and measure clean in both themes; IttyBitz had no such claim and one real failure.

The failure

--faint: #6e6e77 colours the 13px password hint ("· min 24 chars, upper, lower, number, symbol") and, hard-coded, the recovery tool's "(only if one was used)". Against the card background it measures 3.92:1; WCAG AA asks 4.5:1 of normal text. #838391 measures 5.2:1 and still reads as the quiet colour it is meant to be. Both pages fixed.

The check

verify-ui.mts gains a contrast audit for both pages: every visible text node against its background composited up the ancestor chain over the page colour (so translucent cards are handled), 4.5:1 for normal text, 3:1 for large. Text over a gradient on a close ancestor — the orange buttons, black on orange, about 8:1 by eye — is skipped as unmeasurable this way; body's faint radial glow is deliberately not treated as a gradient, or nothing would be measured (my first version did exactly that and reported "0 measured" — a check that cannot fail). The gradient-filled <h1> is skipped via its transparent fill colour.

Verified

32 elements measured on the app, 17 on the recovery tool, all pass. With the old token swapped back in, both pages fail on exactly the two elements above and nothing else. test:ui 62/62, test:crypto 91/91.

…by crypto.ts

The crypto regression suite proves the core; nothing exercised the page
around it. scripts/verify-ui.mts drives both shipped files through their
real controls in headless Chrome, zero dependencies, with src/lib/crypto.ts
running in Node as the independent judge: what the UI encrypts must open
under crypto.ts, and crypto.ts ciphertext must decrypt through the UI.

Covers: CSP pins recomputed for both files, no unsafe-inline, no network
source, SHA256SUMS current; both pages over file:// and http with no
violation, exception or request; weak password refused, strong accepted;
encrypt → Base64 with an IBTZ v1 header that opens under crypto.ts;
password and secret cleared afterwards; decrypt blurred until the eye;
wrong password → generic failure, no output; a v1.0 headerless fixture
decrypts and its seed is recognised (fingerprint 73c5da0a, SeedQR offered);
SeedQR opens blurred as Standard SeedQR, Reveal enables download; Copy
arms a 60 s clear that is skipped if anything else was copied; 20
generated passwords all pass the strength rule; no sideways scroll at
320/390/1440; the recovery tool decrypts a real fixture and carries no
encrypt function; both pages refuse a cross-origin frame.

Every check was confirmed to fail against deliberately broken code: weak
password accepted, password left in the field, output unblurred, clipboard
clear ignoring a foreign copy, frame guard removed, stale CSP pin. Also
run under Linux Chromium via act: 60/60. ui.yml runs it on every push.
… test:ui now measures every text element

--faint #6e6e77 coloured the 13px password hint on the app and the
'(only if one was used)' label on the recovery tool: 3.92:1 against the
card, under the 4.5:1 WCAG AA asks of normal text. #838391 measures
5.2:1 and still reads as the quiet colour it is meant to be.

verify-ui.mts gains a contrast audit for both pages: every visible text
node against its background composited up the ancestor chain over the
page colour, 4.5:1 normal / 3:1 large. Text over a gradient on a close
ancestor (the orange buttons: black on orange, ~8:1 by eye) is skipped
as unmeasurable this way; body's faint radial glow is not treated as a
gradient, or nothing would be measured. 32 + 17 elements measured;
with the old token swapped back in, both pages fail on exactly the two
elements above. 62/62.
@deanrie

deanrie commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Also available merged with every other review PR, in dependency order and verified together, as #57 — merge that or the individual PRs, not both.

seQRets pushed a commit that referenced this pull request Oct 9, 2026
The faint grey #6e6e77 coloured the 13px password hint ("min 24 chars,
upper, lower, number, symbol") at 3.9:1 against the card, under the 4.5:1
WCAG AA asks of normal text. #838391 measures 5.2:1 and still reads as
quiet text.

Changed everywhere it was used: the --faint token in the app, the Recovery
tool's .hint class and its "(only if one was used)" label, and the offline
notice in sw.js, which measured 4.16:1 on black and was missed by the PR.

Taken from #56 without the test harness it was stacked on (#49). The
colour codes are the same length, so no shipped file changes size.

Cryptography untouched; regression suite 91/91; build reproducible.

Co-Authored-By: Dean Rie <deanalexrie@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@seQRets

seQRets commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Thank you, Dean. Great catch on the contrast.

The colour fix from this PR is live in v3.0.13, credited to you. I also applied it to the offline notice in site/sw.js, which used the same grey (4.16:1 on black).

I'm closing this without merging because it's stacked on the test harness from #49, which I'm not taking (see that PR). Please keep the suggestions coming!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants