design: token discipline for type, radii, elevation and colour — 195 detector findings to 0 - #433
Conversation
…AA contrast The type system had no tokens, so there was nothing to drift from. 231 literal font-size declarations had grown into 22 distinct values — eleven of them inside a 4px range, five below the documented 11px floor, with 9/9.5, 10/10.5 and 13.5/14/14.5 all coexisting. At these sizes a 0.5px step buys ~0.26px of x-height: below one device pixel at 1x, and smaller than the variation between platform fallback faces. Those were not tiers, they were drift. Fonts now actually ship. DESIGN.md has always named Inter and JetBrains Mono, but with no @font-face and no CDN allowed (hard rule 4), they rendered only for users who happened to have them installed — everyone else silently got the platform UI face and Menlo/Consolas. build/fonts.mjs inlines latin-subset, upright, variable-weight woff2 as base64 @font-face sources: 88,660 bytes of woff2, +19.6% gzip on the artifact, still zero third-party requests. unicode-range is kept so Cyrillic/CJK cells and the glyphs outside the subset keep deferring to the platform font instead of rendering tofu. Contrast: --fg-faint met no accessibility bar in either theme (2.55:1 light, 3.10:1 dark at worst) while carrying most of the smallest text in the product. Fixing it surfaced a second failure — ClickHouse Blue as *text* is 4.10:1 on light chips and 3.83:1 on dark surfaces — so text takes a new --accent-text (the palette's existing #005F8A in light, a lifted #2596CC in dark) while fills, rings, carets and icons keep --accent, where the 3:1 non-text bar applies. Six surfaces shipped with no CSS rule at all and therefore rendered in browser chrome, not in anything plain. The linked-tab conflict chooser (#343) — the dialog deciding whether to overwrite work saved in another tab — had 13.333px Arial buttons with 2px outset borders and a title visually identical to its description. The query-tab marker warning that a linked query changed rendered as an unstyled stray '!'. workspace-not-found had a 32px h1 and a raw #0000EE underlined link. It had a passing unit test throughout: behaviour coverage cannot see a missing stylesheet, which is why the contract now has its own gate. tests/unit/typography-contract.test.js asserts every font-size resolves to a token, no two steps within a ramp sit closer than 1px, tokens match the DESIGN.md frontmatter, token contrast clears AA in both themes, no class the UI renders is left unmatched by CSS, and the inlined fonts stay inside a byte budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF
… palettes
Same failure mode as the type ramp, three more times over: a documented scale, no
tokens to hold it, and drift filling the gap.
Radii — fourteen values (2/3/4/5/6/7/8/9/10/11/12/16/18/20px) against a documented
four-step scale, with 4/5/6/7px all in play for the same kind of control and
`.dash-tile` at 10px while DESIGN.md's own Dashboard Tiles section said 8px. Now
--r-xs/sm/md/lg chosen by what KIND of surface a box is, plus --r-pill for
capsules: `border-radius: 18px` silently stops being a capsule once the box passes
36px tall, and three chips were written that way.
Elevation — DESIGN.md documents four shadow entries whose whole job is to explain
physical placement. The stylesheet had fourteen distinct shadows across eleven
black alphas, so two popovers could differ for no reason. One token per documented
entry plus --shadow-float for transient surfaces, --scrim for backdrops, and
--ring/--ring-warn/--ring-error so a focus ring states which KIND of state has
focus rather than only that something does.
Colour — three genuine sub-palettes were living as scattered hex. --kind-* and
--role-* (EXPLAIN/schema graph object kinds) were three separate copies of each
hex across node fill, edge and legend swatch, free to drift apart while claiming
to mean the same thing. --sql-* (editor theme) had five [data-theme='light']
rules that are now redundant and deleted. All are documented in DESIGN.md.
Two real bugs fell out of the new gate rather than out of review:
• `.script-cell.rows:hover { background: var(--hover) }` — --hover has never
existed, so the hover affordance on a clickable cell resolved to nothing and
simply never appeared.
• `var(--success, #238636)` / `var(--danger, #cf222e)` referenced tokens that
were NEVER DEFINED, so KPI delta and export-status colours bypassed the theme
entirely and quietly failed AA (#238636 is 3.92:1). A hex fallback turns a
missing token from a visible bug into an invisible one, so the contract now
forbids the pattern outright and separately asserts every var() resolves.
Also: `.cancelled-badge` hard-coded #ef4444 on a 12% tint of itself — 3.23:1 in
light theme — now --error-fg at 5.29:1; and three dead --bg-panel/--bg-sidebar
fallback chains removed (neither token has ever been declared).
Semantic and log-level colours are checked against the surfaces they can ACTUALLY
land on, not the full cross-product: testing every token against every background
reports failures no user can reach (a log level never renders on a chip) and
pressures a correct palette into changing for nothing.
Detector on src/styles.css: 149 findings -> 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF
… layout path
The last seven detector findings, none of which were drift — each was a real
design or performance decision that had never been made explicitly.
KPI cards carried `border-top: 3px solid var(--kpi-accent)` on an 8px radius. Two
problems, and the second is the one that matters. The thick top edge mitres
visibly where it tapers into the 1px sides — but more importantly a coloured
stripe across a metric card is the single most over-used shape in the category,
and PRODUCT.md lists decorative dashboards as an anti-reference. The colour is
real (kpi-panel.ts sets --kpi-accent from the query spec's `presentation.color`,
which the schema calls a per-KPI colour hint), so it stays — as a tint on the
card's own 1px frame, which says the same thing without the cliché. --kpi-accent
now defaults to --border instead of --accent: an author who supplies no hint gets
a neutral card, so ClickHouse Blue is scarce again instead of framing every KPI
in the grid.
My first attempt here moved the stripe from a border to `box-shadow: inset 0 3px 0`
and the detector flagged it again — correctly. That fixed the mitre and kept the
pattern, which was solving the wrong half of the problem.
Reference-doc admonitions had the same shape: a 3px left stripe on a rounded,
tinted callout. They now state their variant with a 1px frame in the variant
colour plus a coloured title word — colour paired with text, which is what the
Evidence Rule actually asks for. Blockquotes keep the left-rule convention at
hairline weight, so they stop competing with the admonitions that need noticing.
Two layout-thrashing transitions, both on high-frequency surfaces:
• the streaming progress strip transitioned `width` on every progress event —
now full-width with a left origin, scaled by results.ts;
• the column/row resize handles transitioned width/height 1px->3px on hover,
and they sit on every column edge — now a fixed 3px bar scaled down at rest.
The Altinity logomark moved to src/ui/brand-logo.ts. Its 26 gradient stops are a
supplied brand asset: fixed by the brand, deliberately outside the DESIGN.md
palette, and never to be borrowed for UI chrome. Giving it its own module lets
.impeccable/config.json exclude exactly that one file from the colour check rather
than silencing all ~60 real icons in icons.ts — and it unclutters a file where one
4 KB string dwarfed sixty small paths.
Detector across the whole src tree: 0 findings (195 before this branch).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF
…e fill Two review blockers, both mine, both in the token pass. 1. The light theme's five --sql-* definitions were self-referential: `--sql-keyword: var(--sql-keyword)`. A custom property that references itself forms a cycle, and per spec that does NOT fall back to the earlier :root value — the computed value becomes invalid, and the .sql-* rules consume these without a fallback. So light-mode keywords, functions, strings, numbers and comments lost their colour entirely and inherited the editor foreground. Cause: the script that pointed hex literals at the new tokens split the file at a marker inside :root, so the light theme block was still in its "rewrite" half and its own definitions got rewritten into references to themselves. I hit exactly this on --ring-error earlier in the same pass, fixed it there, and then verified with a grep scoped to :root — which is why this survived. Neither existing check could see it: the token WAS defined, and every var() reference DID resolve. A cycle needs its own detector, so there is now one, built per declaring block (the same name is legitimately redefined in :root and in each theme) and catching indirect loops as well as direct self-reference. 2. `.cf-overwrite` used --error-fg as a button FILL with white text. A foreground token is tuned to be legible as TEXT on its own plane, so dark theme's #f87171 gave white 2.77:1 — nowhere near AA, on the button that destroys another tab's saved work. New --error-fill is dark enough to carry white in both themes (4.98:1 dark, 6.47:1 light) and still clears 3:1 against the dialog behind it so the button's own edge stays visible (WCAG 1.4.11). Light reuses #B91C1C, so only dark needed a new value. The old test asserted white-on-accent and white-on-error-fg for the LIGHT theme only, which is precisely how this shipped. Enumerating pairs by hand is what failed, so the replacement derives them: it scans the stylesheet for every rule putting white on a tokenized background and checks each in BOTH themes, with a guard test so a regex that stops matching cannot silently pass on an empty set. Both new tests were verified to fail on the reintroduced bugs — the cycle detector on the restored self-reference, and the fill check reporting exactly the 2.77:1 the review cited — then pass on the fix. Light-mode highlighting confirmed rendering in the browser: #AF00DB / #795E26 / #A31515 / #098658 / #008000. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF
|
Both blockers fixed in 9637b9b. You were right on both, and on both the test gap you named was the reason they shipped. 1. Light-mode syntax cycle. Confirmed and fixed — the five light The cause is worth recording: the script that pointed hex literals at the new tokens split the file at a marker inside Your diagnosis of why the suite missed it is exactly right: the token was defined and every Verified rendering in the browser rather than just in the stylesheet — light mode now resolves 2. Destructive button in dark mode. Confirmed at your figure — white on Took the For the test, enumerating pairs by hand is what failed, so the replacement derives them — it scans the stylesheet for every rule putting white text on a tokenized background and checks each in both themes, plus a guard test so a regex that stops matching can't silently pass on an empty set. That check is what would have caught this. Both new tests were verified against the bugs, not just written: I reintroduced each defect and confirmed the cycle detector fails on the restored self-reference and the fill check reports exactly While in there I audited every other text-on-fill and text-on-tint pair this branch introduced, in both themes — Suite: 5120 tests, 164 files, coverage gate intact. Detector still 0 across |
…load The CSP blocked both typefaces this branch went to the trouble of inlining. `deploy/http_handlers.xml` and `deploy/nginx/default.conf.template` both send `font-src 'self'`, and `'self'` is an origin match — it does not cover the `data:` scheme, which has to be listed explicitly. The neighbouring `img-src data:` is there for exactly that reason. Verified rather than reasoned about: served the real dist/sql.html under the exact policy string read from http_handlers.xml and got, twice, Loading the font 'data:font/woff2;base64,…' violates the following Content Security Policy directive: "font-src 'self'". The action has been blocked. with document.fonts reporting status "error" for both families and both measuring identical to the generic-family sentinel — i.e. not applied. What makes this worse than an ordinary missing-config bug is that it fails invisibly. A blocked @font-face renders no tofu: the stack falls through to -apple-system / Menlo and produces perfectly acceptable text. That is bit-for-bit the bug this branch exists to fix, so the deployed app would have gone on rendering platform fonts, looked entirely correct, and left no evidence but a console violation. Meanwhile every local check passed — `npm run local`, a static server and file:// all serve the artifact with no CSP at all, because the policy lives only in the deploy configs. My earlier "verified in the browser" was true and worthless: it never went through a server that sends the header. So the durable fix is the test, not the token. tests/unit/csp-contract.test.js asserts every URL scheme the BUILT ARTIFACT references is permitted by the governing directive, in every config that ships a policy, and that those configs agree on everything but connect-src. It scans the whole artifact rather than just the stylesheet — the favicon lives in build/template.html and CodeMirror injects a `background-image: url(data:…)` from the JS bundle, so a CSS-only scan would have reported a false all-clear. Generalizes past fonts to a future data: image, blob: worker or remote reference. Verified to fail on the reintroduced bug (`font-src must list data:`) and on one-config-only drift. Also from review: • No `font-display`. It describes the display timeline for a NETWORK fetch, and a data: source has none — the face is present when the inline <style> parses. `swap` asserted a load phase that does not exist and requested a flash of fallback that cannot happen; the default is both honest and better behaved. • The in-binary license metadata is only partial, and the review's hypothesis was half right: fontsource's subsetting keeps nameID 0 (Copyright) and nameID 14 (License Info URL) but drops nameID 13 (License Description). Confirmed with fontTools rather than assumed. No compliance gap in the distributed unit, though — dist/sql.html already carries the full OFL text and both copyright lines in its leading notices comment, which is now asserted so the embedding cannot silently regress. • Stale filename in build/fonts.mjs (`typography-contract.test.ts` -> `.js`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF
|
All four addressed in 2f84bf2. The blocker was real and I'd verified it the wrong way — details below, including one place your hypothesis was half right. Blocker: CSP blocked both fonts — confirmedNot reasoned about, reproduced. Served the real
You found one instance; there were two. Your point about which server is the one that stings, and it's the actual root cause of my bad verification claim.
One correction to the suggested scope — scanning the stylesheet isn't enough. The favicon lives in License metadata: checkable, and you were half rightRan your snippet. Result differs from the prediction in one field:
So 13 is dropped as you predicted, but 14 survives — the binaries carry a copyright line and a license URL, just not the license prose. And the distributed unit is already covered: Smaller findings
Stale filename — fixed, The 11px floor — fair, and the PR body now says it outright. The floor moved deliberately to 10.5px: the old 11px floor was fiction, with 36 declarations under it including a 23-use 10.5px tier. The defect was that the floor was undocumented and unenforced, not that 10.5px is too small.
Suite: 165 files, 5127 tests, coverage gate intact. Detector still 0 across |
…rictions
DESIGN.md's Zoomable-Surface Exception reserves --text-nano (9px) for SVG text
inside the EXPLAIN and schema graphs — surfaces that pan and zoom, and whose layout
is measured against that size — and states the floor for anything read without
zooming is --text-micro. Two DOM elements were using it anyway:
• .mnav-badge, the mobile results-count badge, was `font-size: 9px` before the
ramp collapse, and the mechanical 9px -> --text-nano mapping carried it onto the
SVG-only tier without anyone deciding;
• .qtab-external, the !/⌫ conflict-state <span>, I authored at --text-nano in the
same commit that wrote the rule forbidding it — while DESIGN.md assigns state
badges to --text-micro and my own rule comment called it a state badge.
Both are now --text-micro. Verified under the 768px breakpoint (the badge rule is
mobile-only, so measuring at desktop width reads a UA default and proves nothing):
14x14 and 15x15 boxes, no overflow, and a three-digit count stays inside the
viewport.
The deeper problem is that the contract could not see this. Its checks verified a
declaration uses *some* --text-* token, which is syntax; a tier whose entire purpose
is "only here" needs the "only here" asserted. So the restrictions are now
mechanical, all three of them rather than just the one reported:
• --text-nano only on .explain-graph / .schema-graph selectors;
• --text-mark only on the login lockup, --text-metric* only on the KPI value —
this is "The Compact Scale Rule" (nothing in the shell exceeds --text-title)
made enforceable instead of aspirational;
• the document ramp only on Read surfaces (.md-view, .docs-*, .login-h1), so
prose sizes cannot leak into operable chrome.
Each verified to fail on a reintroduced violation, naming the offending selector:
`.qtab-external` for the nano rule, `.logo-name` for a doc-ramp leak.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF
|
Confirmed and fixed in 4811944. You're right, and the second instance has a worse provenance than the first. Both violations, and how each got there
Both now I applied it as two in-place edits rather than the combined Verified under the breakpoint, which matters here: The test gap, generalizedYour diagnosis is the important part: the contract verified that a declaration uses some So rather than special-case nano, I made all three documented restrictions mechanical:
Each verified against a reintroduced violation and each names the offender: Worth noting the pattern across your three reviews, since it's consistent: every finding has been a case where the verification and the thing shipped weren't the same object — a grep scoped to Suite: 165 files, 5130 tests, coverage gate intact. Detector still 0 across |
… branch #436 merged first, so this branch takes main and keeps both sides of the one conflicting hunk (CHANGELOG's `### Added`). `src/styles.css` merged cleanly at the text level and the suite is green apart from three deliberate failures: the type/radius gate this branch adds now reports #425's new `.dash-surface-title` (`font-size: 13px; font-weight: 600`) and `.is-nav-target` (`border-radius: 6px`) — the gate doing exactly its job on code written while it did not yet exist. The next commit resolves them.
#436 was written before the gate in this branch existed, so its new CSS reached main untokenized. Four defects, each of a kind this branch already documented. **Type.** `.dash-surface-title` was `13px/600` — half a pixel above `--text-body`, a step below the One-Pixel Floor and therefore one nobody can see, in a row whose `Back to query` and `View | Edit` controls sit at `--text-label`. It steps up by WEIGHT now (`--text-body`/`--fw-semibold`), per the Compact Scale Rule: a toolbar is not a heading's own space. Measured side by side in Chromium — the 0.5px is invisible, the weight is not. **Radii.** `.is-nav-target` forced `border-radius: 6px` onto whatever it marked, so a `--r-md` dashboard tile visibly CHANGED SHAPE while highlighted and the ring mitred against the corner it was pointing at — the same defect this branch retired from the KPI accent bar. It sets no radius now; a box-shadow already follows the target's own. **Elevation.** That highlight mixed its own translucent accent, `in srgb … 25%`, beside the ring family's `in oklab … 22%` — one halo in two spellings, 3% apart before anyone had a reason. It is `--ring-nav`, a fourth member of that family, documented as the one that marks "you were sent here" rather than focus. **And it painted nothing at all** on a flow-band KPI: those are addressed through `.dash-kpi-member`, which is `display: contents` and generates no box, so navigating to one marked it invisibly. The rule reaches through to the card, as the neighbouring `.dash-drop-target` rule already has to. The focus/scroll half of that bug is #425's behaviour contract and is filed as #438, not patched here. Also: a dead `gap: 8px` on `.dash-surface-toolbar`, overridden by `.dash-toolbar`'s later, equally specific `gap: 10px` since the day it landed. ## The gate had three holes, which is why the above shipped - The shadow check tested for `rgba(` only, so a hand-mixed `color-mix(…, transparent)` sailed through. It now rejects any box-shadow that builds its own translucent colour, and a companion test pins every ring halo to one colour space at one alpha. - The unstyled-class check reads a curated file list, and #425 split `app.ts` into `app-shell.ts` + `workbench/workbench-shell.ts` — moving the application frame, the sidebar and both surface hosts out of its view. The list follows the markup. Each is mutation-proven: reverting #425's exact CSS fails the radius and shadow tests, drifting `--ring-nav`'s halo fails the new one, and an unstyled class in `app-shell.ts` fails the coverage one. ## One unrelated regression this branch already owned `splitters.spec.js` had been failing here, unrun, since c03978e moved the resize indicator off the layout path: a `3px` bar held down by `scaleX(.34)` painted 1.02px and reported `3px` to anything reading the computed box. It scales UP from an honest `1px` now, and the spec measures the painted extent (layout × the transform's own scale) rather than the untransformed box. Verified: 5244 unit tests, 100% statements/lines · `tsc --noEmit` clean · build clean · detector 0 across `src/` · Playwright 298 passing, the 2 remaining WebKit failures reproducing on untouched `origin/main` (#439). Both themes and the highlight on tiles, flow-band KPIs and filter fields checked in real Chromium. Refs #425. Filed #438, #439.
What & why
A design-system audit of font-scale discipline turned into a token-discipline pass:
the mechanical detector reported 195 findings on
src/styles.css; this branchtakes the whole
srctree to 0. Three commits, each a self-contained pass —plus a merge of
mainand a fourth pass bringing #436's newly-merged Dashboardsurface onto the same system, below.
The root cause was the same every time. DESIGN.md documented a scale — type, radii,
elevation — but the stylesheet had no tokens to hold it, so there was nothing to
drift from. 231 literal
font-sizedeclarations had grown into 22 distinct values,14 radius values sat against a documented four-step scale, and 14 shadows spread
across 11 different black alphas. Meanwhile the repo's 100%-per-file coverage gate
found none of it, because none of it is behaviour:
conflict-resolution.tshad apassing unit test the entire time it shipped with no CSS rule at all.
da4cfa3— typefaces, type scale, contrastThe brand typefaces now actually ship. DESIGN.md has always named Inter and
JetBrains Mono, but with no
@font-faceand no CDN allowed (hard rule 4), theyrendered only for users who happened to have them installed locally — everyone
else silently got the platform UI face and Menlo/Consolas. Verified by measuring
against generic-family sentinels in the browser: both were falling back on the
maintainer's own machine.
build/fonts.mjsinlines latin-subset, upright,variable-weight woff2 as base64
@font-facesources.unicode-rangeis kept soCyrillic/CJK cells and the ⌘/↵/→ glyphs keep deferring to the platform font
instead of rendering tofu.
22 font sizes → 6 interface steps (9 / 10.5 / 11.5 / 12.5 / 14 / 16px) plus a
document ramp for Read surfaces and two display sizes. Nine
font:shorthandswere hiding literal sizes from the first sweep. Five values sat below the
documented 11px floor.
The floor moved deliberately, from 11px to 10.5px. The old 11px floor was
fiction — 36 declarations sat under it, including a 23-use 10.5px tier. Rather
than pretend, the ramp documents
--text-micro(10.5px) as the real minimum foranything read without zooming, and confines
--text-nano(9px) to SVG graphlabels via DESIGN.md's Zoomable-Surface Exception: those surfaces pan and zoom,
and
core/dot-layout.jsis measured against that size. So the "five below thefloor" defect is that the floor was undocumented and unenforced, not that 10.5px
is too small.
Contrast.
--fg-faintmet no accessibility bar in either theme (2.55:1 light,3.10:1 dark at worst) while carrying most of the smallest text in the product.
Fixing it surfaced a second failure: ClickHouse Blue as text is 4.10:1 on light
chips and 3.83:1 on dark surfaces, so text now takes
--accent-textwhile fills,rings, carets and icons keep
--accent, where the 3:1 non-text bar applies.Six surfaces had no CSS rule at all and therefore rendered in browser chrome,
not in anything plain. The linked-tab conflict chooser (Cross-tab workspace consistency: refresh before write and invalidate stale tabs #343) — the dialog deciding
whether to overwrite work saved in another tab — had 13.333px Arial buttons with
2px outsetborders and a title visually identical to its description. Thequery-tab marker warning that a linked query changed rendered as an unstyled stray
!.workspace-not-foundhad a 32pxh1and a raw#0000EEunderlined link.0abe492— radii, elevation, palettes--r-pill, chosen by what kind of surface a box is.border-radius: 18pxsilently stops being a capsule once the box passes 36px tall,and three chips were written that way.
.dash-tilewas at 10px while DESIGN.md'sown Dashboard Tiles section said 8px.
direction and strength exist to explain physical placement; two popovers differing
for no reason means the vocabulary had stopped communicating.
--kind-*was threeseparate copies of each hex across node fill, edge and legend swatch — free to
drift apart while claiming to mean the same thing. Five now-redundant
[data-theme='light'] .sql-*rules deleted.c03978e— the last sevenNot drift — decisions never made explicitly.
presentation.colorhint tints the card's own 1px frame instead. My first attempt moved the stripe from
a border to an inset shadow and the detector flagged it again, correctly — that
fixed the mitre against the 8px radius and kept the cliché.
--kpi-accentnowdefaults to
--borderrather than--accent, so an un-tinted KPI is neutral andClickHouse Blue is scarce again instead of framing every card in the grid.
coloured title word, replacing the 3px left stripe — colour paired with text.
progress strip transitioned
widthon every progress event, and the resize handlestransitioned width/height on every pointer cross of every column edge. Both animate
transformnow.src/ui/brand-logo.ts. Its 26 gradient stops are asupplied brand asset — fixed by the brand, outside the palette, never to be
borrowed for UI chrome. Its own module lets
.impeccable/config.jsonexcludeexactly that file rather than silencing all ~60 real icons.
Four real bugs the new gate found
Not review — the test found these:
.script-cell.rows:hover { background: var(--hover) }—--hoverhas neverexisted, so the hover affordance on a clickable cell resolved to nothing and simply
never appeared.
var(--success, #238636)/var(--danger, #cf222e)referenced tokens that werenever defined, so KPI delta and export-status colours bypassed the theme
entirely and quietly failed AA (
#238636is 3.92:1). The contract now forbids hexfallbacks outright — they turn a missing token from a visible bug into an invisible
one — and separately asserts every
var()resolves..cancelled-badgehard-coded#ef4444on a 12% tint of itself: 3.23:1 in light.--bg-panel/--bg-sidebarfallback chains — neither token has everbeen declared anywhere in the repo's history.
The gate
tests/unit/typography-contract.test.js(29 tests) is the point of the branch: everyfont-size/font-weight/line-height/radius/shadow must resolve to a token, no twosteps within a ramp may sit closer than 1px, tokens must match the DESIGN.md
frontmatter, no
var(--token, #hex)fallbacks, everyvar()must resolve, no class theUI renders may be left unmatched by CSS, token contrast must clear WCAG AA in both
themes, and the inlined fonts must stay inside a byte budget.
The 1px floor is the load-bearing rule: at these sizes a 0.5px step buys ~0.26px of
x-height — below one device pixel at 1× and smaller than the variation between platform
fallback faces. It caught my own first ramp, which still had five 0.5px neighbours.
Semantic and log-level colours are checked against the surfaces they can actually
land on, not the full cross-product — testing every token against every background
reports failures no user can reach (a log level never renders on a chip) and pressures a
correct palette into changing for nothing.
f28d31c+dec4f7e— main merged first, and the gate caught it#436 merged before this branch, so main now carries the Dashboard main-surface
work (#425).
src/styles.cssmerged cleanly at the text level; the only conflictwas CHANGELOG's
### Added, where both sides are kept. The suite came out of themerge green apart from three failures in this branch's own gate, reporting
#425's new CSS — code written while the gate did not yet exist. That is the branch
working: 195 → 0 only holds if new code lands on the system too.
.dash-surface-titlefont-size: 13px; font-weight: 600--text-body— below the One-Pixel Floor, so a step nobody can see, in a row whose controls sit at--text-label--text-body/--fw-semibold: it steps up by weight, per the Compact Scale Rule.is-nav-targetborder-radius: 6px--r-mdtile visibly changed shape while highlighted, and the ring mitred against the corner it was pointing atbox-shadowalready follows the target's own.is-nav-targetcolor-mix(in srgb, var(--accent) 25%, transparent)in oklab … 22%— one halo, two spellings, already 3% apart--ring-nav, a fourth member of that family.dash-surface-toolbar { gap: 8px }.dash-toolbarhas equal specificity and comes later in the fileThe title change is the visible one. 13px → 12.5px at the same weight is
imperceptible — which is exactly the argument — but it is worth a look next to the
View | Editswitch.One defect that was not drift
.is-nav-targetpainted nothing at all on a flow-band KPI. Those are addressedthrough
.dash-kpi-member, which isdisplay: contentsand therefore generates nobox:
getBoundingClientRect()is{0,0,0,0}in Chromium, so abox-shadowon itrenders nothing and navigating to such a tile marked it invisibly. Every
renderDashboard — navigation focus (#425)unit test builds agrafana-gridworkspace, so all of them take the other branch. The rule reaches through to the
card now, exactly as the neighbouring
.dash-drop-targetrule already has to.The focus and scroll half of the same root cause —
node.focus()andscrollIntoViewon a zero-box element — is #425's behaviour contract and needs itsown unit + Playwright coverage, so it is filed as #438 rather than patched in a
design pass.
Three holes in the gate, which is why all of it shipped
rgba(only, so a hand-mixedcolor-mix(…, transparent)passed. It now rejects anybox-shadowthat buildsits own translucent colour, and a companion test pins every ring halo to one
colour space at one alpha.
app.tsintoapp-shell.ts+workbench/workbench-shell.ts— moving the application frame,the sidebar and both surface hosts out of its view. The list follows the markup now.
Each is mutation-proven: reverting #425's exact CSS fails the radius and shadow
tests, drifting
--ring-nav's halo fails the new one, and an unstyled class inapp-shell.tsfails the coverage one.One regression this branch already owned
splitters.spec.jshad been failing here, unrun, sincec03978emoved theresize indicator off the layout path: a
3pxbar held down byscaleX(.34)painted1.02px and reported
3pxto anything reading the computed box. It scales up froman honest
1pxnow, and the spec measures the painted extent (layout size × thetransform's own scale) rather than the untransformed box — which is what it always
meant to assert. Verified by reproducing the failure at the pre-merge branch tip.
Verified
npm test5244 passing, 167 files, 100% statements/lines ·tsc --noEmitclean ·npm run buildclean · the mechanical design detector reports 0 acrosssrc/.Playwright 298 passing across chromium/firefox/webkit. The 2 remaining failures
(
multi-select.spec.js, WebKit only) reproduce on untouchedorigin/mainin aclean worktree — WebKit honours Safari's "Press Tab to highlight each item"
preference, which is off by default, so
Tabnever reaches the button whose focusoutline the spec asserts. Filed as #439; not from this branch.
Checked in real Chromium in both themes: the surface toolbar at desktop and narrow
widths with a long Dashboard name, and the navigation highlight on a grid tile, a
flow-band KPI, and a filter field.
Reviewer notes
two self-hosted typefaces.
FONT_BYTE_BUDGETinbuild/fonts.mjsis asserted, soadding latin-ext or an italic cut has to be a reviewed edit. Italics are not shipped;
the browser synthesizes an oblique for the eight secondary uses.
@fontsource-variable/*are dev dependencies, same footing as Ajv: no code fromthem ships, the build reads their woff2 files. No new runtime dependency.
dashboard before merge.
choice between pulling 13px down or pushing 69 declarations up; I took the denser
option for a dense tool. Easy to flip.
.docs-md-quotewas arguably a false positive — a 3px neutral left rule on ablockquote is conventional typography, not the pattern the rule targets. Taken to
1px anyway because it was competing with the admonitions that need noticing.
previously-unstyled surfaces and the redesigned KPI/admonition treatments. No
overflow or clipping from the ramp collapse, checked against the deployed app's real
header markup.
track to re-scope.
CLAUDE.mdhard rule 4 andTHIRD-PARTY-NOTICES.md(SIL OFL forboth faces) updated.
Checklist
npm testpasses (the per-file coverage gate is non-negotiable) — 5115 tests, 164 files, 100% statements/linesnpm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code
https://claude.ai/code/session_01MJ9X6AZHTmQ15qBujFpcPF