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 Brain becomes the Chat's full-bleed background (see #56), so every component will sit on a translucent surface over a moving, sometimes bright-yellow graph. Legibility must be guaranteed by construction, and that guarantee has to exist and be tested before any component is restyled — the earlier build shipped a tint that silently failed AA over its worst frame.
Approach
Build the Glass token layer as a standalone, contrast-proven artifact decoupled from any component work. A theme-agnostic generator derives two tint tiers — standard (prose, bubbles, composer) and dense (code, diffs, run-plots, tool-cards, more opaque) — whose opacities are computed, not eyeballed, to clear the WCAG-AA floor with a small safety margin over a per-mode reference frame: the worst-case opaque feature the Brain engine's own palette actually paints in that mode (dark: the peak-bloom thought #fff676, the brightest value, worst for near-white text; light: the darkest solid feature the light palette paints, #8f8000, worst for near-black text). The frame is fixed — a synthetic worst case, never per-frame readback — but it is not mode-independent. Blur is one high constant shared by both tiers (calm, cheap, never credited toward contrast); the tint carries all contrast. The generator keys off the chat theme's own resolved tokens and emits CSS exposing the tiers via a stable data-glass hook, restyling nothing. A headless contrast test that composites tint-over-reference-frame is the legibility guarantee.
Scope
In: a theme-agnostic two-tier derivation (pure function of a resolved theme's tokens + that mode's worst-case reference frame); the generator + generate:glass script + committed generated CSS; the data-glass="standard"|"dense" hook and per-tier CSS vars keyed by the chat theme's existing data-theme/data-color-scheme attributes; a @supports not (backdrop-filter) opaque fallback; a headless contrast test proving the default chat theme oc-2 (light + dark) clears every floor. Out: floating/restyling any real component on Glass (no component touched — later slices consume the hook; keeps this slice a pure, provable token layer). The Brain-as-background mount and live re-validation on theme switch (this slice ships the derivation + keying; wiring is downstream). Hand-validating the other 35 bundled third-party themes (their palettes are author-owned and some ship native sub-3:1 marks a backing tint cannot raise; body-text AA still generalizes by construction). Blur radius tuning and any perf work (blur is a single constant here, not contrast-load-bearing).
Acceptance Criteria
Standard tint, body text: for the default chat theme oc-2 in both light and dark modes, the shipped standard tint composited over that mode's reference frame (dark = opaque #fff676; light = opaque #8f8000) yields body-text (text-strong) contrast ≥ 4.5:1, and the value the derivation targeted is ≥ 4.8:1 (the small safety margin is actually present, not sitting on the 4.5 cliff). Measured numerically against the fixture, no rendering.
Dense tint, code/diff text: for oc-2 light + dark, the shipped dense tint over that mode's reference frame yields code/diff text (text-strong) contrast ≥ 4.5:1 (target ≥ 4.8:1).
Dense tint, graphical marks (WCAG 1.4.11): for oc-2 light + dark, for every graphical-mark color in the set {theme syntax tokens, diff add/delete fills, run-plot series strokes}, its contrast over the dense composite is ≥ 3:1 whenever that mark clears 3:1 on the theme's own base surface, and in all cases is no more than 0.2 below its contrast over the theme's own base surface (the dense tint never degrades a mark relative to native rendering).
Dense is strictly more opaque than standard: derived dense alpha > standard alpha for each oc-2 mode.
Blur is one shared constant: the emitted blur radius is identical for both tiers, greater than zero, and blur is never used as a term in the derivation or the contrast test (tint alone clears every floor).
Chat-theme-keyed, not hardcoded: the derivation is a pure function of the resolved theme tokens (plus that mode's reference frame) — feeding two token sets that differ in text/surface (e.g. oc-2 dark vs. oc-2 light) produces different standard alphas. The emitted CSS scopes each tier's tokens under html[data-theme="<id>"][data-color-scheme="<mode>"], matching the attributes the chat theme system already stamps.
Stable hook, nothing restyled: the generated CSS exposes exactly data-glass="standard" and data-glass="dense" surfaces (background tint + shared blur + hairline edge + elevation); applying no data-glass attribute anywhere, the app's rendered output is unchanged (this slice adds the layer, wires it to no component).
No-backdrop-filter fallback: under @supports not (backdrop-filter), both tiers fall back to a near-opaque tint so text can never land on the bare animated canvas.
Generated CSS is in sync: re-running the generator reproduces the committed generated CSS byte-for-byte (drift guard).
Testing Decisions
CREATE one new headless test file for the glass token module (there is no glass test on the current branch to extend; slice 1 adds no glass surface). Follow the existing headless discipline of the brain-engine test — bun:test, pure numbers, no DOM, no rAF. Reuse the WCAG helpers ported from the old branch (toLinear/luminance/composite/contrast) rather than reimplementing sRGB math; the test imports the module's exported derive + contrast functions and pulls oc-2's concrete token values through the theme resolve API. The reference frame per mode is the brain engine's own worst-case painted feature — dark #fff676, light #8f8000 — read from the engine's PALETTES (not a mode-independent literal). Concrete tests to write, all as numeric assertions over the fixture: (1) standard body contrast ≥ 4.5 and derived target ≥ 4.8, for oc-2 light and dark, each over its mode's reference frame; (2) dense text contrast ≥ 4.5 / target ≥ 4.8, both modes; (3) dense graphical-mark rule — iterate the mark set {syntax tokens ∪ diff add/delete fills ∪ run-plot series strokes}, assert each mark over the dense composite is ≥ 3:1 when it is ≥ 3:1 over the theme's base surface, and is never more than 0.2 below its base-surface contrast, both modes; (4) dense alpha > standard alpha per mode; (5) blur constant equal across tiers, > 0, and (guard) the derive path takes no blur argument; (6) purity/keying — derive over dark tokens+frame vs light tokens+frame gives distinct standard alphas; (7) fallback — the @supports block resolves both tiers to alpha ≥ 0.92; (8) drift — generateGlassCss() output equals the committed generated CSS string. No visual/screenshot test in this slice; contrast is proven arithmetically. This test file is the surface later slices EXTEND when they add a theme or a mark class.
Key Decisions
Reference frame = a fixed, per-mode worst-case Brain feature, read from the engine's own PALETTES[mode] — a synthetic worst case, NOT per-frame readback (the rejected dynamic-contrast path). Two backdrops, because the worst frame differs by mode: dark = the peak-bloom thought #fff676 (brightest palette value; near-white text-strong sits at ~1:1 on bare yellow, so the tint must pull the composite dark); light = the darkest solid feature the light palette paints, #8f8000 (the light live-thought / darkest node fill, per the old branch's derived-dark thought; worst for near-black text-strong). A single mode-independent #fff676 is WRONG on light: the light brain never paints #fff676 (its live-thought is #8f8000 and its category nodes are all darker than the surface), so near-black text over #fff676 clears ~16:1 and the derivation would return a near-transparent tint that silently fails AA over the real light frame — the exact failure this slice exists to prevent.
Tint base per tier = the theme's resolved surface-base; body reference = resolved text-strong. Both tiers tint from surface-base (glass reads as the theme's own surface pulled over the brain); dense differs only by a higher derived alpha. Derivation: sweep alpha upward, standard alpha = least alpha where text-strong over composite hits the target ratio (4.8), rounded up to 0.01; dense alpha = max(standard alpha, least alpha where the mark set converges to within 0.2 of native and code/diff text hits 4.8), rounded up. Safety margin is a fixed +0.3 on the ratio (target 4.8, floor 4.5).
Guarantee is the default theme; mechanism generalizes. The generator is theme-agnostic and emits tiers for every bundled theme so switching re-keys correctly, but the shipped-and-tested guarantee is oc-2 (light + dark). Body-text AA holds for any theme by construction (at full alpha the composite collapses to the theme's own surface, which the theme already renders body text legibly on); the dense mark rule is only asserted for oc-2 because third-party palettes may ship native sub-3:1 marks a backing tint cannot lift.
Data contract: per-tier CSS custom properties (--glass-standard-bg, --glass-dense-bg as rgba with the derived alpha; one --glass-blur; plus hairline border + elevation tokens) scoped under html[data-theme="<id>"][data-color-scheme="<mode>"]; two utility selectors [data-glass="standard"] and [data-glass="dense"] apply background + shared backdrop-filter blur (with -webkit- prefix for the webview) + border + shadow. Graphical-mark set for the dense check = resolved syntax-* tokens + diffAdd/diffDelete fills + run-plot series stroke colors.
Wire the generator like the existing token generators — a script/-level generator run via a generate:glass package script (mirroring the existing generate:tailwind / generate:v2-oc2), emitting a committed generated CSS file that is not hand-edited.
Constraints & Invariants
Blur is never a contrast term. The derivation and the test assume zero blur benefit; blur is a conservative bonus only. A dev must not "buy back" transparency by leaning on blur.
The standard tint is bounded below by the contrast floor — it cannot be made ultra-transparent (the exact failure the earlier build hit). Consequently muted/secondary text (text-base) is NOT guaranteed on the standard tier; it must ride the dense tier or a locally-dimmed zone. Do not attempt to float muted grey on standard.
Restyle nothing. This slice ships tokens + generated CSS + test only. The data-glass hook exists but is applied to no component; the app's rendered output is byte-identical until a later slice opts a component in.
Key off the theme system's existing signals (data-theme and data-color-scheme on documentElement) — do not invent a new attribute or a separate mode flag.
Yellow stays the brain's, never the glass's.#fff676 is the dark-mode reference backdrop and the dark brain's live-thought color (the light brain's live-thought is #8f8000); neither is ever the glass fill and never ink (amicode brand law). The glass tint is the theme's neutral surface + a hairline edge (light hairline on dark, dark hairline on light).
Generated CSS is derived output — regenerate via the script; never hand-edit, and keep it in sync (the drift test enforces this).
Prior Art / Patterns
Old-branch raw material to port (design-neutral, de-risked):packages/ui/src/amicode/glass-tokens.ts on the kate/chat-brain-atmosphere worktree carries the reusable WCAG machinery (toLinear, luminance, composite, contrast, tierContrast) and the generateGlassCss() emit skeleton, and packages/ui/src/amicode/glass.css shows the emit shape — [data-glass] utility selectors, -webkit- prefixing, and the @supports not (backdrop-filter) opaque fallback. Reuse the math and the emit skeleton; change three things: collapse the three tiers (airy/dense/chrome) to two (standard/dense); re-key from data-color-scheme-only to data-theme + data-color-scheme; and derive each alpha from the resolved theme tokens instead of the hardcoded GLASS constants (the old file's tint alphas were per-theme literals — this slice computes them). Carry forward the old file's two documented facts (no-blur figures are a conservative floor; muted greys can't clear AA on the most translucent tier) and its per-mode worst backdrops — dark #fff676 / light #8f8000 (the old file's worstBackdrop values, matching the current brain engine's dark/light thought palette).
Current branch seams: the theme resolve API (resolveTheme / resolveThemeVariant in packages/ui/src/theme/resolve.ts) yields the concrete token values the generator and test read; the theme context (packages/ui/src/theme/context.tsx) is what stamps data-theme + data-color-scheme on documentElement and injects --text-strong / --surface-base / --syntax-*; packages/ui/src/theme/themes/oc-2.json is the default-theme source (its dark.overrides and light.overrides are the fixtures). packages/ui/src/amicode/brain-engine.ts confirms the per-mode worst backdrops — PALETTES.dark.thought = #fff676 and PALETTES.light.thought = #8f8000 (its accent#fff676 is defined but never painted). The mark set's plot-line colors come from the run-plot series (run-plot.ts / run-series.ts).
Harness patterns:packages/ui/src/amicode/brain-engine.test.ts is the headless-discipline model (bun:test, no DOM, recording stubs) — the contrast test is even simpler (pure arithmetic). Generator wiring mirrors the existing generate:tailwind / generate:v2-oc2 scripts in packages/ui/package.json.
Durable record: ADR packages/app/docs/adr/0002-brain-as-chat-background.md (this slice implements its "Tint — opacity pinned to clear the WCAG-AA floor over a fixed reference frame … Two tints … keyed to the chat's own theme" decision).
Important
Problem
The Brain becomes the Chat's full-bleed background (see #56), so every component will sit on a translucent surface over a moving, sometimes bright-yellow graph. Legibility must be guaranteed by construction, and that guarantee has to exist and be tested before any component is restyled — the earlier build shipped a tint that silently failed AA over its worst frame.
Approach
Build the Glass token layer as a standalone, contrast-proven artifact decoupled from any component work. A theme-agnostic generator derives two tint tiers — standard (prose, bubbles, composer) and dense (code, diffs, run-plots, tool-cards, more opaque) — whose opacities are computed, not eyeballed, to clear the WCAG-AA floor with a small safety margin over a per-mode reference frame: the worst-case opaque feature the Brain engine's own palette actually paints in that mode (dark: the peak-bloom thought
#fff676, the brightest value, worst for near-white text; light: the darkest solid feature the light palette paints,#8f8000, worst for near-black text). The frame is fixed — a synthetic worst case, never per-frame readback — but it is not mode-independent. Blur is one high constant shared by both tiers (calm, cheap, never credited toward contrast); the tint carries all contrast. The generator keys off the chat theme's own resolved tokens and emits CSS exposing the tiers via a stabledata-glasshook, restyling nothing. A headless contrast test that composites tint-over-reference-frame is the legibility guarantee.Scope
In: a theme-agnostic two-tier derivation (pure function of a resolved theme's tokens + that mode's worst-case reference frame); the generator +
generate:glassscript + committed generated CSS; thedata-glass="standard"|"dense"hook and per-tier CSS vars keyed by the chat theme's existingdata-theme/data-color-schemeattributes; a@supports not (backdrop-filter)opaque fallback; a headless contrast test proving the default chat themeoc-2(light + dark) clears every floor.Out: floating/restyling any real component on Glass (no component touched — later slices consume the hook; keeps this slice a pure, provable token layer). The Brain-as-background mount and live re-validation on theme switch (this slice ships the derivation + keying; wiring is downstream). Hand-validating the other 35 bundled third-party themes (their palettes are author-owned and some ship native sub-3:1 marks a backing tint cannot raise; body-text AA still generalizes by construction). Blur radius tuning and any perf work (blur is a single constant here, not contrast-load-bearing).
Acceptance Criteria
oc-2in both light and dark modes, the shipped standard tint composited over that mode's reference frame (dark = opaque#fff676; light = opaque#8f8000) yields body-text (text-strong) contrast ≥ 4.5:1, and the value the derivation targeted is ≥ 4.8:1 (the small safety margin is actually present, not sitting on the 4.5 cliff). Measured numerically against the fixture, no rendering.oc-2light + dark, the shipped dense tint over that mode's reference frame yields code/diff text (text-strong) contrast ≥ 4.5:1 (target ≥ 4.8:1).oc-2light + dark, for every graphical-mark color in the set {theme syntax tokens, diff add/delete fills, run-plot series strokes}, its contrast over the dense composite is ≥ 3:1 whenever that mark clears 3:1 on the theme's own base surface, and in all cases is no more than 0.2 below its contrast over the theme's own base surface (the dense tint never degrades a mark relative to native rendering).oc-2mode.oc-2dark vs.oc-2light) produces different standard alphas. The emitted CSS scopes each tier's tokens underhtml[data-theme="<id>"][data-color-scheme="<mode>"], matching the attributes the chat theme system already stamps.data-glass="standard"anddata-glass="dense"surfaces (background tint + shared blur + hairline edge + elevation); applying nodata-glassattribute anywhere, the app's rendered output is unchanged (this slice adds the layer, wires it to no component).@supports not (backdrop-filter), both tiers fall back to a near-opaque tint so text can never land on the bare animated canvas.Testing Decisions
CREATE one new headless test file for the glass token module (there is no glass test on the current branch to extend; slice 1 adds no glass surface). Follow the existing headless discipline of the brain-engine test —
bun:test, pure numbers, no DOM, no rAF. Reuse the WCAG helpers ported from the old branch (toLinear/luminance/composite/contrast) rather than reimplementing sRGB math; the test imports the module's exported derive + contrast functions and pullsoc-2's concrete token values through the theme resolve API. The reference frame per mode is the brain engine's own worst-case painted feature — dark#fff676, light#8f8000— read from the engine'sPALETTES(not a mode-independent literal). Concrete tests to write, all as numeric assertions over the fixture: (1) standard body contrast ≥ 4.5 and derived target ≥ 4.8, foroc-2light and dark, each over its mode's reference frame; (2) dense text contrast ≥ 4.5 / target ≥ 4.8, both modes; (3) dense graphical-mark rule — iterate the mark set {syntax tokens ∪ diff add/delete fills ∪ run-plot series strokes}, assert each mark over the dense composite is ≥ 3:1 when it is ≥ 3:1 over the theme's base surface, and is never more than 0.2 below its base-surface contrast, both modes; (4) dense alpha > standard alpha per mode; (5) blur constant equal across tiers, > 0, and (guard) the derive path takes no blur argument; (6) purity/keying — derive over dark tokens+frame vs light tokens+frame gives distinct standard alphas; (7) fallback — the@supportsblock resolves both tiers to alpha ≥ 0.92; (8) drift —generateGlassCss()output equals the committed generated CSS string. No visual/screenshot test in this slice; contrast is proven arithmetically. This test file is the surface later slices EXTEND when they add a theme or a mark class.Key Decisions
PALETTES[mode]— a synthetic worst case, NOT per-frame readback (the rejected dynamic-contrast path). Two backdrops, because the worst frame differs by mode: dark = the peak-bloom thought#fff676(brightest palette value; near-whitetext-strongsits at ~1:1 on bare yellow, so the tint must pull the composite dark); light = the darkest solid feature the light palette paints,#8f8000(the light live-thought / darkest node fill, per the old branch's derived-dark thought; worst for near-blacktext-strong). A single mode-independent#fff676is WRONG on light: the light brain never paints#fff676(its live-thought is#8f8000and its category nodes are all darker than the surface), so near-black text over#fff676clears ~16:1 and the derivation would return a near-transparent tint that silently fails AA over the real light frame — the exact failure this slice exists to prevent.surface-base; body reference = resolvedtext-strong. Both tiers tint fromsurface-base(glass reads as the theme's own surface pulled over the brain); dense differs only by a higher derived alpha. Derivation: sweep alpha upward, standard alpha = least alpha wheretext-strongover composite hits the target ratio (4.8), rounded up to 0.01; dense alpha = max(standard alpha, least alpha where the mark set converges to within 0.2 of native and code/diff text hits 4.8), rounded up. Safety margin is a fixed +0.3 on the ratio (target 4.8, floor 4.5).oc-2(light + dark). Body-text AA holds for any theme by construction (at full alpha the composite collapses to the theme's own surface, which the theme already renders body text legibly on); the dense mark rule is only asserted foroc-2because third-party palettes may ship native sub-3:1 marks a backing tint cannot lift.--glass-standard-bg,--glass-dense-bgas rgba with the derived alpha; one--glass-blur; plus hairline border + elevation tokens) scoped underhtml[data-theme="<id>"][data-color-scheme="<mode>"]; two utility selectors[data-glass="standard"]and[data-glass="dense"]apply background + sharedbackdrop-filterblur (with-webkit-prefix for the webview) + border + shadow. Graphical-mark set for the dense check = resolvedsyntax-*tokens +diffAdd/diffDeletefills + run-plot series stroke colors.script/-level generator run via agenerate:glasspackage script (mirroring the existinggenerate:tailwind/generate:v2-oc2), emitting a committed generated CSS file that is not hand-edited.Constraints & Invariants
text-base) is NOT guaranteed on the standard tier; it must ride the dense tier or a locally-dimmed zone. Do not attempt to float muted grey on standard.data-glasshook exists but is applied to no component; the app's rendered output is byte-identical until a later slice opts a component in.data-themeanddata-color-schemeondocumentElement) — do not invent a new attribute or a separate mode flag.#fff676is the dark-mode reference backdrop and the dark brain's live-thought color (the light brain's live-thought is#8f8000); neither is ever the glass fill and never ink (amicode brand law). The glass tint is the theme's neutral surface + a hairline edge (light hairline on dark, dark hairline on light).Prior Art / Patterns
packages/ui/src/amicode/glass-tokens.tson thekate/chat-brain-atmosphereworktree carries the reusable WCAG machinery (toLinear,luminance,composite,contrast,tierContrast) and thegenerateGlassCss()emit skeleton, andpackages/ui/src/amicode/glass.cssshows the emit shape —[data-glass]utility selectors,-webkit-prefixing, and the@supports not (backdrop-filter)opaque fallback. Reuse the math and the emit skeleton; change three things: collapse the three tiers (airy/dense/chrome) to two (standard/dense); re-key fromdata-color-scheme-only todata-theme+data-color-scheme; and derive each alpha from the resolved theme tokens instead of the hardcodedGLASSconstants (the old file's tint alphas were per-theme literals — this slice computes them). Carry forward the old file's two documented facts (no-blur figures are a conservative floor; muted greys can't clear AA on the most translucent tier) and its per-mode worst backdrops — dark#fff676/ light#8f8000(the old file'sworstBackdropvalues, matching the current brain engine's dark/lightthoughtpalette).resolveTheme/resolveThemeVariantinpackages/ui/src/theme/resolve.ts) yields the concrete token values the generator and test read; the theme context (packages/ui/src/theme/context.tsx) is what stampsdata-theme+data-color-schemeondocumentElementand injects--text-strong/--surface-base/--syntax-*;packages/ui/src/theme/themes/oc-2.jsonis the default-theme source (itsdark.overridesandlight.overridesare the fixtures).packages/ui/src/amicode/brain-engine.tsconfirms the per-mode worst backdrops —PALETTES.dark.thought=#fff676andPALETTES.light.thought=#8f8000(itsaccent#fff676is defined but never painted). The mark set's plot-line colors come from the run-plot series (run-plot.ts/run-series.ts).packages/ui/src/amicode/brain-engine.test.tsis the headless-discipline model (bun:test, no DOM, recording stubs) — the contrast test is even simpler (pure arithmetic). Generator wiring mirrors the existinggenerate:tailwind/generate:v2-oc2scripts inpackages/ui/package.json.Source
packages/app/docs/adr/0002-brain-as-chat-background.md(this slice implements its "Tint — opacity pinned to clear the WCAG-AA floor over a fixed reference frame … Two tints … keyed to the chat's own theme" decision).