You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The HUD was failing AAA at 4.51:1. Fixed in #722 — bg-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:
/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.
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 guard — base-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.
The HUD was failing AAA at 4.51:1. Fixed in #722 —
bg-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 isresolved: #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 — alltext-base-contentonbg-base-300/70, floating over an unconstrained WebGL scene.Bracketing that scene between black and white, worst endpoint per theme:
/70/85/90/85was rejected for the reasonglobals.css:1716already 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 thetop-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 withWebGL denied via
addInitScript, soisWebGLAvailable()returns false,FallbackPanelrenders, and no
WebGLRendereris ever constructed — #719 cannot recur. It injects the realutility classes, lets the browser resolve them, and reads back through a 1×1 canvas, the only
place OKLCH and
color-mixbecome sRGB correctly. Needs no GPU by construction, so it runsidentically in the container and in CI.
Three approaches were rejected, each of which passes while measuring nothing:
color-contrast— DaisyUI's stylesheet never applies there, so the nodelands in
incompleteandtoHaveNoViolations()ignores it. Three tests in this repo namedfor colour contrast already ship green having never measured a ratio.
color-mix(in oklab, …); recomputingit 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
which probed on
/themes/and readrgba(0,0,0,0): Next.js splits CSS per route, sobg-base-300/70ships only in the chunk for the route that uses it. It caught ameasurement of a rule that page never loaded.
base-300must resolve and differ frombase-content.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
CodSkeletonHudcomponent and measurethe 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