From the audit: the signed-in app runs the dark theme but content colours stay light-theme, so text is dark-on-dark everywhere. Below is the exact light + dark semantic token set — every value contrast-checked to WCAG AA — plus a before/after of the Team page and a copy-paste CSS block. The rule that makes it work: the dark theme must redefine every token, not just the background, and every component reads tokens (no hardcoded hexes).
Agents, children, and active work in one place.
No agents hired yet.
Agents, children, and active work in one place.
No agents hired yet.
Both panels are the identical markup — the only difference is which token values are in scope. That's the whole fix: the "After" is what happens when the dark theme redefines the text/surface/control tokens instead of leaving them at their light values.
| Token | Role | Light | Dark (EmuBrain) | AA on surface |
|---|---|---|---|---|
| Backgrounds | ||||
| --ks-canvas | Page background | #f5f7fb | #0d0f13 | — |
| --ks-surface | Cards, panels, metrics | #ffffff | #161a20 | — |
| --ks-surface-2 | Nested / sunken | #f4f6fa | #1d2430 | — |
| --ks-rail | Nav rail | #ffffff | #0b0d11 | — |
| Text | ||||
| --ks-tx | Primary text, headings | #141a22 | #e8ebef | 14.6:1 ✓ |
| --ks-tx-muted | Secondary / descriptions | #54606f | #a7b0bd | 8.0:1 ✓ |
| --ks-tx-faint | Eyebrows / metric labels | #727c8a | #8b95a3 | 5.8:1 ✓ |
| Lines & controls | ||||
| --ks-line | Hairline borders | #e3e7ec | #272d37 | — |
| --ks-control-bg | Input / select field | #ffffff | #12151b | — |
| --ks-control-tx | Input text | #141a22 | #e8ebef | 15.3:1 ✓ |
| --ks-control-line | Input border (visible) | #c4ccd6 | #3a434f | — |
| --ks-placeholder | Placeholder text | #8a929e | #79828f | 4.7:1 ✓ |
| Accent · buttons · nav | ||||
| --ks-accent | Primary button / links | #0f766e | #2dd4bf | 9.4:1 ✓ |
| --ks-accent-tx | Text on accent | #ffffff | #06231f | ≥5.5:1 ✓ |
| --ks-nav-active-bg | Active nav pill | #e6f4f1 | #152a2a | — |
| --ks-nav-active-tx | Active nav text | #0b5b54 | #5eead4 | ≥7:1 ✓ |
| Semantic (status) | ||||
| --ks-ok | Success / live | #1f8a54 | #54cf8b | ≥4.3:1 ✓ |
| --ks-danger | Error / destructive | #c0264a | #ff6f8b | ≥5.8:1 ✓ |
| --ks-focus | Focus-visible ring | #2563eb | #8ea6ff | — |
:root{
/* LIGHT (default) */
--ks-canvas:#f5f7fb; --ks-surface:#ffffff; --ks-surface-2:#f4f6fa; --ks-rail:#ffffff;
--ks-tx:#141a22; --ks-tx-muted:#54606f; --ks-tx-faint:#727c8a; --ks-line:#e3e7ec;
--ks-control-bg:#ffffff; --ks-control-tx:#141a22; --ks-control-line:#c4ccd6; --ks-placeholder:#8a929e;
--ks-accent:#0f766e; --ks-accent-tx:#ffffff; --ks-nav-active-bg:#e6f4f1; --ks-nav-active-tx:#0b5b54;
--ks-ok:#1f8a54; --ks-danger:#c0264a; --ks-focus:#2563eb;
}
/* DARK — the "EmuBrain" family theme. MUST redefine ALL tokens, not just the background. */
[data-theme="dark"], .theme-emubrain, [data-family-theme="emubrain"]{
--ks-canvas:#0d0f13; --ks-surface:#161a20; --ks-surface-2:#1d2430; --ks-rail:#0b0d11;
--ks-tx:#e8ebef; --ks-tx-muted:#a7b0bd; --ks-tx-faint:#8b95a3; --ks-line:#272d37;
--ks-control-bg:#12151b; --ks-control-tx:#e8ebef; --ks-control-line:#3a434f; --ks-placeholder:#79828f;
--ks-accent:#2dd4bf; --ks-accent-tx:#06231f; --ks-nav-active-bg:#152a2a; --ks-nav-active-tx:#5eead4;
--ks-ok:#54cf8b; --ks-danger:#ff6f8b; --ks-focus:#8ea6ff;
}
/* Then every component reads tokens — never a hardcoded hex: */
body{background:var(--ks-canvas);color:var(--ks-tx)}
.card,.metric,.panel{background:var(--ks-surface);border:1px solid var(--ks-line);color:var(--ks-tx)}
label,.eyebrow,.metric-label{color:var(--ks-tx-faint)} p,.muted{color:var(--ks-tx-muted)}
input,select,textarea{background:var(--ks-control-bg);color:var(--ks-control-tx);border:1px solid var(--ks-control-line)}
input::placeholder{color:var(--ks-placeholder)}
.btn-primary{background:var(--ks-accent);color:var(--ks-accent-tx)}
:focus-visible{outline:2px solid var(--ks-focus);outline-offset:2px} /* was missing entirely */data-theme / data-family-theme / a class) — the audit's root cause is that this block didn't exist, so only the background flipped.--ks-tx* tokens but drop a scrim behind text (background: color-mix(in srgb, var(--ks-canvas) 72%, transparent)) so text never falls below AA over a photo. With no image, the solid --ks-canvas already passes.:focus-visible) — there is none today; one line, big accessibility win.This is the concrete table you asked for — ready the moment you're free after the real-dispatch bug. Happy to pair on the grep-and-replace or extend the set (elevation/shadows, chart colours) if useful. Design-only on my side.
Keystack theme fix · token table + before/after · Sana → Lena / Rafi · 2026-07-24 · follows the audit at keystack-audit.pages.dev. All contrast ratios computed to WCAG 2.1 AA. Dark values aligned to the cockpit-v2 dark palette (Keystack runs cockpit tenant mode). Design-only; keystack-web implementation is Rafi's.