Commit Graph

3 Commits

Author SHA1 Message Date
morgmart
f8f07a3b33 fix(desktop-next): make coloured text legible on tint hovers, and widen the audit
A second pass over the colour system, checking the surfaces the first audit did
not cover.

The gap was tint hovers. DESIGN.md says a tint carries meaning and takes coloured
text, but the audit only measured text against the four neutral surfaces — so
`--bg-*-tint-hover`, the hardest surface in each coloured family, was never
checked. Four pairings were below target: light danger (Lc 56.6) and warning
(59.2), and dark accent and info (58.4 each). All four passed WCAG AA, which is
why nothing else caught them.

Size step 11 against the tint hover rather than the neutral panel, the same rule
already applied to the neutral ramp: light danger-11 #b91c1c -> #a80008, light
warning-11 #a16207 -> #9c5e00, dark accent-11 and info-11 #8fbaff -> #95c0ff.
Minimum moves in OKLCH holding hue and chroma, so the hues do not shift.

The audit now covers every tint and tint hover in all five families. Verified by
falsification: reverting any one of the four fails the check.

Two findings that are deliberately not changes:

Hairline dividers stay at 1.2-1.8:1. WCAG's 3:1 non-text rule covers boundaries
needed to identify a control or its state, not grouping lines; Radix ships 1.29
and Apple 1.26 for the same job. Raising them would draw the box the fill already
implies. Recorded in DESIGN.md, along with the note that a control's own outline
is a different question when the input layer lands.

`--ring-focus` is an accent against accent: legible on every neutral surface at
3.0-3.5:1, but 1.1:1 on `bg-accent`, where it vanishes. Not yet reachable — the
token is unregistered and unused — so the constraint is documented at the token
instead of being solved speculatively: the component layer that introduces focus
owns a surface-coloured offset, and there is no second ring token.

Glass surfaces were checked by compositing each translucent value over the app
gradient's own stops; all clear the target in both modes, which the relit
neutral ramp is what earns.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-04 11:07:31 -07:00
morgmart
b7dbe90001 fix(desktop-next): relight the dark text ramp and enforce contrast in CI
Three fixes, and a guard so they cannot regress silently.

`text-on-warning` pointed at `var(--neutral-12)`, which is near-black in light
mode and near-white in dark. The warning fill is the same amber in both, so dark
mode put #f5f5f5 on #eab308: APCA Lc 34, WCAG 1.76:1. It is a literal now — a
paired-text token follows its fill, never the mode.

The dark neutral text steps were too dim. `text-secondary` (#8f8f8f) measured
Lc 38–41 and `text-tertiary` (#6e6e6e) Lc 23–26 against an Lc 60 body target,
while passing WCAG AA at 4.4–5.6:1 — legal, and visibly murky. Raise dark
neutral-9 to #a6a6a6 and neutral-10 to #c1c1c1, sized against bg-float
(#2a2a2a), the lightest dark surface and so the binding case. Light neutral-9
moves #a3a3a3 -> #909090 for the same reason at the meta target. Both ramps stay
monotonic, and bg-inverse-hover (the other consumer of neutral-10) still carries
its paired text at Lc 70.

This is APCA's polarity asymmetry doing real work: light-on-dark needs more
separation than the same WCAG ratio implies, so the dark ramp's text steps sit
above where mirroring the light ramp would put them.

`text-disabled` stays below target deliberately — low contrast is the signal
that a control is unavailable, and WCAG exempts inactive controls for the same
reason. Raising it would make disabled read as enabled. Recorded as the one
exception, with that reason.

The guard is the durable part. `scripts/check-contrast.mjs` measures every text
role against every surface it can sit on, in both modes, and fails the build
below target. It parses `tokens.css` and resolves each `var()` chain rather than
holding a copied list of values, so it cannot drift from the system it audits.
`scripts/apca.mjs` is the algorithm, asserted against the published reference
values on every run so a bad edit to the maths fails loudly rather than quietly
shifting every verdict. Verified by falsification: reverting either fix fails
the check, and perturbing one APCA exponent trips the reference assertion.

Also corrects an earlier misreading: the two `.dark` blocks are not duplicate
declarations. The first holds the dark ramp, the second overrides surface roles
so the panel lifts off pure black. That is the intended layering.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 21:41:51 -07:00
morgmart
30847f42ff feat(desktop-next): add the type system, fix unreachable border roles
The design system site rendered black borders and dividers while the tokens
said `#d4d4d4`. The border roles were declared in layer 2 and never registered
with Tailwind, so `border-primary` silently resolved to `--color-primary` — the
*text* colour — and every hairline drew at text contrast. Tailwind's `--color-*`
is a single namespace and text/borders share the emphasis names while holding
different values, so borders now register as `--border-color-*`.

Add typography as the second foundation, same three layers as colour: a nine-step
size ramp plus leading, tracking, and weight ramps in layer 1; ten public roles
in layer 2, each carrying its whole setting via Tailwind v4's
`--text-<name>--<property>` convention, so `text-body` alone produces correctly
set text and two supposedly identical labels cannot drift apart. Inter Variable
and JetBrains Mono, both already shipped in every current Buzz client. Every
size derives from one virtual rem, so keyboard zoom and the font-size preference
work by construction.

Soften the documentation surface to match where the product is going: regions
separated by soft fills rather than outlines, hairlines only for genuine
boundaries and never above `border-secondary`, no page-wide gradient behind
reading columns, and no all-caps labels anywhere.

The `/design/typography` page renders from the registry, so it cannot drift from
the system. Add `scripts/check-type.mjs` enforcing the four rules a token cannot
express — no arbitrary sizes (px or rem), no `uppercase`, no hand-applied
tracking, no size role paired with a weight or leading utility.

Also collapse the nav to a wrapped row below `lg`; the fixed 256px column
overflowed the viewport under 420px.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-03 12:50:39 -07:00