diff --git a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md
index f98c3e8f..25f2b080 100644
--- a/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md
+++ b/dashboards/admin-web/docs/roadmaps/UX_INTEGRATION_ADMIN.md
@@ -174,23 +174,43 @@ pnpm --filter @bytelyst/admin-web test:e2e # Playwright + @axe-core (no new fa
only (not scanned by the public e2e set), per the tracker-web axe/opacity caution. test
**21 files / 170 tests** (+2, happy-dom asserts primitives end visible + render all children);
typecheck+lint+build green; format:check no new failures; e2e unchanged.
-- [ ] **UX-6 — System banners (conditional):** if a real broadcasts/maintenance feed is reachable,
- add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` in `(dashboard)/layout.tsx`.
- `NotificationCenter` only with a real feed; else **defer**.
+- [~] **UX-6 — System banners (conditional):** if a real broadcasts/maintenance feed is reachable,
+ add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` in `(dashboard)/layout.tsx`.
+ `NotificationCenter` only with a real feed; else **defer**.
+ — **DEFERRED** · No real broadcasts/maintenance feed is reachable in this environment:
+ `platform-service` (`:4003`) refuses connections (the same backend gap that makes 80/91 e2e
+ fail), so the admin `/api/maintenance` + `/api/broadcasts` proxies have nothing to surface.
+ Per the wave's explicit condition (and the run brief), banners are **not** added against an
+ empty/unreachable feed — adding `BannerStack`/`NotificationCenter` here would be unverifiable
+ and could render a permanent empty/erroring banner. Follow-up recorded in the Deferrals table.
## Cross-cutting
-- [ ] **CC.1** Full suite + build green every wave. · [ ] **CC.2** Dark-mode parity (bridge works in `.dark`).
-- [ ] **CC.3** No new color literals. · [ ] **CC.4** No new a11y violations; labels on all controls.
-- [ ] **CC.5** Bundle: dynamic `import()` for charts/palette; record gzipped sizes / `size:check`.
-- [ ] **CC.6** Final tracker + Deferrals table complete.
+- [x] **CC.1** Full suite + build green every wave. — `typecheck`+`lint`+`build` green and `vitest`
+ passing after each wave; final **22 files / 183 tests**.
+- [x] **CC.2** Dark-mode parity (bridge works in `.dark`). — Bridge maps every `--bl-*` to an admin
+ `--*` var (or `color-mix` of one) that flips between `:root` and `.dark`, so parity is inherited;
+ guarded by `src/__tests__/token-bridge.test.ts`.
+- [x] **CC.3** No new color literals. — All new color values are `var(--*)` tokens (incl. the
+ `var(--chart-*)` categorical palette); bridge introduces only `var()`/`color-mix` (asserted by
+ the token-bridge test); grep of every touched file finds zero new hex/oklch/hsl/rgb literals.
+- [x] **CC.4** No new a11y violations; labels on all controls. — Charts render `role="img"`+`
`
+ (`ariaLabel`); palette dialog has `ariaLabel="Admin command palette"`; `LoadingSpinner` is
+ `role="status"`; motion primitives resolve to opacity 1 (no contrast trap). No new unlabeled
+ controls introduced. (Full `@axe-core` gate needs the backend — deferred; see Deferrals.)
+- [x] **CC.5** Bundle: dynamic `import()` for charts/palette; record gzipped sizes. — Charts are
+ code-split via `next/dynamic` into their own async chunk (**~11.0 KB raw / ~3.8 KB gzip**);
+ the command-palette dialog and `@bytelyst/motion` are also `next/dynamic`/deferred. `size:check`
+ (`bundlesize`) has **no config in this repo** (`Config not found`) → recorded gzip sizes here per
+ the roadmap fallback; adding a `bundlesize` budget config is left as a follow-up.
+- [x] **CC.6** Final tracker + Deferrals table complete. — see below.
## Progress tracker
```
Setup : UX-1 ✅
-Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ✅ UX-6 ⬜(cond)
-Cross : CC.1 ⬜ CC.2 ⬜ CC.3 ⬜ CC.4 ⬜ CC.5 ⬜ CC.6 ⬜
+Adopt : UX-2 ✅ UX-3 ✅ UX-4 ✅ UX-5 ✅ UX-6 ⏭️ deferred (no feed)
+Cross : CC.1 ✅ CC.2 ✅ CC.3 ✅ CC.4 ✅ CC.5 ✅ CC.6 ✅
```
## E2e baseline (captured on UX-1 — 2026-05-29)
@@ -213,9 +233,11 @@ count must stay ≥ 11 passed / ≤ 80 failed. (A full green run requires the ba
## Deferrals (fill in as encountered)
-| Item | Reason (surface/data gate) | Follow-up |
-| ------------ | -------------------------- | --------- |
-| _(none yet)_ | | |
+| Item | Reason (surface/data gate) | Follow-up |
+| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **UX-6 — System banners** | No real broadcasts/maintenance feed: `platform-service` (`:4003`) unreachable in this env, so `/api/maintenance` + `/api/broadcasts` return nothing. | When the backend stack runs, add `@bytelyst/notifications-ui` `BannerStack`/`Announcement` (+`NotificationCenter`) in `(dashboard)/layout.tsx` wired to the live feed, and verify against real data. |
+| `@bytelyst/charts` `StackedBar` | Charts 0.1.1 ships only single-series/vertical bars (StackedBar deferred to charts 0.2.x), so the client-logs stacked severity chart was rendered as single bars colored by dominant severity. | Restore a true stacked severity chart once `@bytelyst/charts` 0.2.x lands `StackedBar`. |
+| e2e full-green / `@axe-core` gate | 80/91 Playwright specs need the auth backend (`:4003`) which is down here; no `@axe-core` harness exists in `e2e/` yet. | Run the full e2e + add an `@axe-core/playwright` a11y gate once the backend/emulator stack is reachable in CI. |
---
diff --git a/dashboards/admin-web/src/__tests__/token-bridge.test.ts b/dashboards/admin-web/src/__tests__/token-bridge.test.ts
new file mode 100644
index 00000000..f2bb6f9d
--- /dev/null
+++ b/dashboards/admin-web/src/__tests__/token-bridge.test.ts
@@ -0,0 +1,61 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { describe, expect, it } from 'vitest';
+
+/**
+ * UX-1 / CC.2 / CC.3 guard for the `--bl-*` → admin-OKLCH token bridge.
+ *
+ * Dark-mode parity is *inherited*: every bridged `--bl-*` token must resolve to
+ * an admin shadcn `--*` var (or a `color-mix` of one), which already flips
+ * between `:root` and `.dark`. This test pins that contract so a future edit
+ * can't silently hardcode a one-mode literal (which would break `.dark` parity
+ * and violate the zero-new-color-literals rule).
+ */
+const css = readFileSync(join(__dirname, '..', 'app', 'globals.css'), 'utf8');
+
+// Isolate the UX-1 bridge block.
+const bridge = css.slice(css.indexOf('@bytelyst/ui token bridge'));
+
+const REQUIRED_MAPPINGS: Array<[string, string]> = [
+ ['--bl-bg-canvas', 'var(--background)'],
+ ['--bl-surface-card', 'var(--card)'],
+ ['--bl-surface-muted', 'var(--muted)'],
+ ['--bl-text-primary', 'var(--foreground)'],
+ ['--bl-text-secondary', 'var(--muted-foreground)'],
+ ['--bl-border', 'var(--border)'],
+ ['--bl-input', 'var(--input)'],
+ ['--bl-accent', 'var(--primary)'],
+ ['--bl-accent-foreground', 'var(--primary-foreground)'],
+ ['--bl-danger', 'var(--destructive)'],
+ ['--bl-focus-ring', 'var(--ring)'],
+];
+
+describe('--bl-* token bridge (dark-mode parity)', () => {
+ it.each(REQUIRED_MAPPINGS)(
+ 'maps %s to %s (an admin var that flips under .dark)',
+ (token, target) => {
+ const re = new RegExp(`${token}\\s*:\\s*${target.replace(/[()]/g, '\\$&')}\\s*;`);
+ expect(re.test(bridge)).toBe(true);
+ }
+ );
+
+ it('introduces no raw color literals in the bridge (only var()/color-mix tokens)', () => {
+ // Strip CSS comments, then look for hex / oklch / hsl / rgb literals.
+ const code = bridge.replace(/\/\*[\s\S]*?\*\//g, '');
+ expect(/#[0-9a-fA-F]{3,8}\b/.test(code)).toBe(false);
+ expect(/\boklch\(/.test(code)).toBe(false);
+ expect(/\bhsl\(/.test(code)).toBe(false);
+ expect(/\brgb\(/.test(code)).toBe(false);
+ });
+
+ it('every bridged --bl-* value references an admin var', () => {
+ // Each `--bl-foo: ;` line inside the bridge must contain `var(--`.
+ const declRe = /(--bl-[a-z0-9-]+)\s*:\s*([^;]+);/g;
+ let m: RegExpExecArray | null;
+ const offenders: string[] = [];
+ while ((m = declRe.exec(bridge))) {
+ if (!m[2].includes('var(--')) offenders.push(`${m[1]}: ${m[2].trim()}`);
+ }
+ expect(offenders).toEqual([]);
+ });
+});