Skip to content

cod-skeleton HUD failed AAA at 4.51:1 — the contrast coverage the #721 exclusion owed #715

Description

@TortoiseWolfe

The HUD was failing AAA at 4.51:1. Fixed in #722bg-base-300/70/90, now 8.14:1 (light) / 8.78:1 (dark).

This ticket started as "the contrast sweep times out on /game/cod-skeleton". That half is
resolved: #721 excluded the route with a printed reason, because the timeout is #719
compositing a continuous WebGL canvas on a GPU-less runner goes through a synchronous
GLES2::ReadPixels, traced at 10.7 s inside a 13 s window. No timeout would have settled it.

What that exclusion cost, and said so in its own body, was coverage: the page's contrast
became unmeasured. This ticket is now that debt, and paying it found a real violation.

The finding

The HUD is three chips — stance badge, quality <select>, controls hint — all
text-base-content on bg-base-300/70, floating over an unconstrained WebGL scene.
Bracketing that scene between black and white, worst endpoint per theme:

alpha light dark
/70 5.00:1 4.51:1 shipped before #722 — both fail the 7:1 gate
/85 7.24:1 7.43:1 passes by 1.03×
/90 8.14:1 8.78:1 shipped

/85 was rejected for the reason globals.css:1716 already rejected it for the twin nav:
a 1.03× margin is erased by one theme tweak. Each theme fails at the endpoint where the
scene works against the text — dark over white, light over black — and the light case is not
even adversarial, since ProceduralSky hour={16.5} puts a bright sky directly under the
top-left and top-right chips.

How it is measured now

tests/e2e/cod-skeleton-hud-contrast.spec.ts. It loads the route for its stylesheet with
WebGL denied via addInitScript, so isWebGLAvailable() returns false, FallbackPanel
renders, and no WebGLRenderer is ever constructed — #719 cannot recur. It injects the real
utility classes, lets the browser resolve them, and reads back through a 1×1 canvas, the only
place OKLCH and color-mix become sRGB correctly. Needs no GPU by construction, so it runs
identically in the container and in CI.

Three approaches were rejected, each of which passes while measuring nothing:

  • jsdom + axe color-contrast — DaisyUI's stylesheet never applies there, so the node
    lands in incomplete and toHaveNoViolations() ignores it. Three tests in this repo named
    for colour contrast already ship green having never measured a ratio.
  • Loading the route normally — reintroduces the Drape upload (250ms) and synchronous shader link block the twin's main thread #719 freeze.
  • Hand-computing the alpha blend — Tailwind emits color-mix(in oklab, …); recomputing
    it in sRGB measures a colour the browser never paints. That trap once read a real 4.13:1
    as 1.03:1.

Three honesty guards, each proven to fire

  • alpha guard — the surface must be translucent. It fired unprompted on the first draft,
    which probed on /themes/ and read rgba(0,0,0,0): Next.js splits CSS per route, so
    bg-base-300/70 ships only in the chunk for the route that uses it. It caught a
    measurement of a rule that page never loaded.
  • token guardbase-300 must resolve and differ from base-content.
  • source guard — the classes measured must be the classes shipped. Its first version was
    too weak: restyling one of three chips left it green because the other two still matched.
    It now asserts the count and rejects any chip drifting to another alpha.

Known limitation

The spec measures a class string it injects itself, anchored to the component source by the
source guard. The higher-fidelity fix is to extract a CodSkeletonHud component and measure
the real nodes, which would remove the need for that guard entirely. Not done here: it means
production code plus a plop-scaffolded 5-file component and the component-structure workflow,
a larger change than the debt required. Worth revisiting if the HUD grows past three chips.

Still open, separately

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions