Skip to content

Palette: profit and loss are the same brightness, and inputs have no visible border #532

Description

@eaitbrahim

Spec: docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md § Accessibility.

Independent of the rest of the web UI work. This improves the UI that ships today, needs none of the rewrite, and should not wait behind seven other issues.

1. --good and --bad are photometrically identical

keel/web/render.py:41-49, light mode:

good #1f5f4f  relative luminance 0.0904
bad  #96322a  relative luminance 0.0893
                        delta 0.0011  →  1.01:1

Profit and loss are separated by hue alone. render.py:82 confirms nothing else carries the distinction:

.good { color: var(--good); } .warn { color: var(--warn); } .bad { color: var(--bad); }

This fails WCAG 1.4.1 Use of Color in an application whose central signal is gain versus loss. Roughly one in twelve men has red-green colour deficiency and cannot reliably tell them apart; greyscale, e-ink and direct sunlight collapse the distinction for everyone. Dark mode is better but thin at 1.34:1.

Fix: never encode P&L in colour alone. A glyph and sign carry the meaning — ▲ +2.4% / ▼ −2.4% — and colour reinforces it. Separate the two luminances as well.

Also split --accent from --good. They are byte-identical in both themes today (#1f5f4f light, #6fbf9f dark), so a link and a gain render alike.

2. Form inputs have no visible boundary

render.py:96-97:

.field input, .field select { ... border: 1px solid var(--line); background: var(--bg); ... }

The input's background is the page background, so a --line border at 1.27:1 is the only thing marking where the control is. Fails WCAG 1.4.11 Non-text Contrast (3:1 for UI component boundaries).

Decorative dividers at 1.27:1 are exempt and stay — table rules, the footer line, card edges. Form controls are not, and this lands squarely on the first-run wizard (#437), which is almost entirely forms, for an audience that has never seen the app before.

3. Assert contrast in CI

The WCAG contrast formula is about twenty lines of Python with no dependencies. Every foreground/background pair asserts its minimum ratio, and good/bad assert a minimum luminance separation, so a palette regression fails the build instead of being noticed a year later.

No contrast tooling, no dependency — just the arithmetic in a readable test.

Acceptance

  • good and bad differ in luminance by a margin the test pins, not only in hue.
  • P&L renders with a glyph and sign; removing all colour leaves the meaning intact.
  • --accent and --good are distinct values.
  • Form control boundaries reach 3:1 against their own background.
  • A CI test asserts every pair's ratio; changing a palette value to something failing makes it fail.
  • Text contrast does not regress — everything passes AA today, most passes AAA, and that stands.

Metadata

Metadata

Assignees

Labels

fixBug fix (groups under Fixes)parallel-okNo intra-phase dependency; parallelizable in a worktree

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions