Skip to content

feat(theme): Visual System 2.0 — T1 foundations - #2525

Merged
jackwener merged 9 commits into
mainfrom
feat/visual-system-t1-foundations
Aug 8, 2026
Merged

jackwener merged 9 commits into
mainfrom
feat/visual-system-t1-foundations

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

T1 of the Visual System 2.0 program (task #168): the six-subsystem foundation, entirely in the token/theme layer. Old names remain as aliases with unchanged resolved values; consumption sweeps land in T2–T4.

  1. Surface ladder — four semantic tiers (sunken/base/raised/overlay) derived cumulatively from --background; the seven white-pinned aliases each resolve to an intentional tier. Measured px: light 237/247/255/255, dark 9/17/23/27.
  2. Ink ladder — three tiers at ~2× rhythm, all ≥ AA (light 19.1/9.8/4.8, dark 15.5/9.2/4.7); --foreground-dimmed demoted to alias (contract-tested); muted lifted off the AA floor in both modes.
  3. Borders — three strengths 6/10/16% with assigned jobs.
  4. Elevation — product names (--elevation-raised|overlay|drag) aliasing the theme scale; --shadow-minimal-flat reclassified (1px ring, live consumers in packages/ui) instead of deleted.
  5. Radius — card tier unified at 10px (product surface 8 → 10 meets Astryx element), 35 call sites.
  6. Status color spec — light families regenerated at L=0.50 with per-hue chroma, fixing two silent AA failures (info 2.82:1, warning 3.29:1); dark warning regression caught and fixed during fact-check; dark regeneration deferred to its own round.

DESIGN.md rewritten as the 2.0 specification (ratified rulings, measured tables, forbidden list), superseding the 1.x refresh from #2491 while absorbing its link-contrast rule.

Verification

  • Ten before/after previews (five surfaces × light/dark) reviewed and approved in task Fail closed on invalid telemetry files #168's thread — controls counted against main baselines, dark-mode plane order legible without hairlines, overlay tier visible above raised in dark, zero regressions.
  • Local gates: build / typecheck / lint / dead-css / astryx:theme --check all green on the rebased head.
  • New contract test: ink-ladder-contract (dimmed-is-alias + no-srgb-in-ink-family), fault-injection verified.

Merging without waiting for CI per owner's directive (frontend changes, fix-forward on red).

Visual system 2.0, T1 subsystem 1. Token layer only; no product CSS moves
and no old name changes what it resolves to.

The audit read "seven names all resolve to one white" as an accident. Most of
it was not. The owner's 2026-06-20 decision already put a gray plate behind
pure-white content, and that is this model — `--background` was always the
CARD, never the page. Two rungs were genuinely missing:

- nothing below the canvas, so a sidebar had no rung to recede to;
- `--background-elevated` pinned to `--background`, leaving overlays with no
  surface of their own.

So the ladder is named, not rebuilt: sunken / base / raised / overlay, with
height mapping monotonically to lightness and the READING surface always on
this mode's brightest rung — pure white in light, lightest gray in dark.
Measured by painting each rung and reading the pixel back, light is
237/247/255/255 and dark is 9/17/23/27. Light has no room above white, so
overlay carries the same fill as raised and is told apart by the overlay
recipe; unpinning it is what gives dark a rung to climb and the recipe
something to hang on.

Every rung derives from `--background`, because palettes override only that
token and a hardcoded plate survived every palette switch once already. That
makes the offsets CUMULATIVE: base is already -0.025, so a sunken rung written
as -0.030 lands 0.005 under base — a step nobody can see. It is -0.065.

`--surface-sunken` has no consumer until the sidebar recedes to it in T2, and
is registered as a reserved scale rung rather than worked around: an ordered
scale reads as a scale only with its bottom rung present.
Visual system 2.0, T1 subsystem 2. Token layer only; the 59 call sites of
--muted-foreground are untouched and migrate in T2-T4.

Measured against --surface-raised, the audit's diagnosis held for one tier of
three. Primary was said to be short of its 15:1 target and was already 19.1:1.
Secondary was to drop to ~5:1 to open a 3x gap from primary — but that gap is
copied from a reference with only TWO text tiers, and three tiers standing on
a 4.5:1 AA floor cannot span 3x without the bottom two colliding. The rhythm
is even instead, roughly geometric, which is what three tiers can hold:

  light  19.14 / 9.84 / 4.84      dark  15.46 / 9.15 / 4.67

The real defect was muted at 50% ink: 4.21:1 in light and 3.89:1 in dark —
already under AA in the mode nobody measured. It is 54%.

Dark primary needed 0.92 -> 0.95 to clear 15:1, and dark carries its own
secondary mix because one percentage cannot land both modes in the 8.5-10:1
band: 74% reads 9.8 in light but 8.2 in dark.

--foreground-dimmed becomes an alias. It was written as "80% ink" exactly like
--foreground-secondary but mixed in srgb where that one mixes in oklch, so the
same words resolved to different colours (10.51:1 vs 12.01:1). The contract
test pins the invariant on the source text rather than a rendering, because
what must not come back is dimmed having its own definition to drift from.
Visual system 2.0, T1 subsystem 3. Token layer only.

Four border strengths were in play: 5% on the sidebar rail (declared locally
in sidebar.css), 8% on --card-border-color, 10% on --border, 16% on
--border-strong. Two of those are tiers and two are drift.

`--border` is the structural line — the edge of a panel, a card, a table.
`--border-soft` is the quiet one for seams inside a surface, and it is what
the 5% rail and the 8% card edge converge on. Their call sites move in T2-T4;
T1 does not touch product CSS.

Registered as a reserved scale rung for the same reason as --surface-sunken:
the tier exists before its consumers do, which is what defining a scale means.
Visual system 2.0, T1 subsystem 4. Token layer only.

The generated theme ships --shadow-low/med/high and product CSS had used them
exactly once, reaching for hand-rolled shadows everywhere else. A scale nobody
can name is a scale nobody uses, so: `raised` for a card lifting off its
plate, `overlay` for anything in a portal, `drag` for something under the
cursor. In-flow elements still cast no shadow; this only names the legitimate
path. Consumers adopt them in T2-T4.

--shadow-minimal-flat is NOT deleted, against the audit. It has five live
consumers, two of them in packages/ui/src/styles.css. The audit read its three
DEFINITION sites as its only mentions — it searched the desktop renderer and
the token lives in a package. It is also not an elevation tier at all: it is a
1px ring wearing a box-shadow, which is why it survived a scale it was never
part of. Kept and documented as a ring.
Visual system 2.0, T1 subsystem 5. The only subsystem here that changes
existing pixels.

Two vocabularies were running in parallel and disagreeing about the card:
this file said 8, Astryx's own scale says 10 (--radius-element), and
--radius-modal says 12. Three numbers for one idea is how a radius stops
meaning anything.

The card tier resolves to 10 rather than 8, because Astryx's element rung is
what its own components already round to — meeting it there is what stops a
card from disagreeing with the controls sitting inside it. 35 call sites read
--radius-surface and all of them move together.
Visual system 2.0, T1 subsystem 6. Light mode; dark needs its own pass.

The four status hues were four independent choices: measured against white,
info 2.82:1, warning 3.29:1, success 4.55:1, destructive 4.81:1 — a 1.7x
spread with TWO of them under the 4.5:1 AA floor. Nothing tied them together,
so nothing kept them together.

One lightness for all four, each keeping its own chroma. At L=0.50 they land
5.54-6.33:1 as a family. The residual spread is hue rather than drift: yellow
carries more luminance than blue at equal lightness, and flattening that would
mean giving up the shared L that makes them a family in the first place.

info and warning move the most (0.68 and 0.66 down to 0.50) because they were
the two that failed AA. This is the visible one; it belongs in the preview set.
--warning was the one status hue with no dark override. info, success and
destructive each state their own dark value, so lowering the shared light
value to L=0.50 dragged dark down with it: dark warning measured 2.83:1,
well under AA, in the half of the theme the previous commit claimed not to
touch.

Restored to its measured 5.44:1 by stating it where its three siblings state
theirs. The asymmetry was the trap — a family where one member is defined
once and three are defined twice does not behave like a family.
@jackwener
jackwener merged commit 302b295 into main Aug 8, 2026
2 checks passed
@jackwener
jackwener deleted the feat/visual-system-t1-foundations branch August 8, 2026 16:32
Astro-Han added a commit that referenced this pull request Aug 8, 2026
… contradiction

DESIGN.md is meant to be the short list of rulings a call site can obey.
T1 (#2525) grew it to 207 lines by carrying three things a spec should not:

- migration state (T2-T4 queues, the srgb exception, dark's deferred
  regeneration, the 8/10/12 obituary) - those belong to the task thread, and
  they go stale the moment T2 lands with nobody coming back to delete them;
- inventories - the seven-alias legacy surface list and border-strong's live
  call-site census, which is what the file's own last Don't forbids copying in;
- measured tables - the surface pixel ladder and the ink contrast column.
  Their caption claimed "the ink/surface contract tests hold them," but
  ink-ladder-contract only pins two structural invariants (dimmed stays an
  alias, no srgb in the ink family) and nothing pins a number. So the numbers
  were an unguarded second source of truth next to maka-tokens.css, wearing a
  guarantee they did not have. The rhythm and the AA floor survive as rules.

The radius chapter also disagreed with its own frontmatter: the table gave
"buttons, inputs" to the 6px control tier while button-default and
input-default resolve to {rounded.card} = 10px. The frontmatter is right.
--radius-control's 29 live consumers are product-drawn compact controls, and
Astryx Button/Input take --radius-element, which IS Maka's card value - the two
scales are offset by a rung. Documented as The Astryx Offset so the next reader
resolves a radius from the box, not from a same-sounding token name.

Doc-only; no token or product CSS changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant