diff --git a/dashboard/audit-baselines/agents__dark__1440.png b/dashboard/audit-baselines/agents__dark__1440.png
index 903f78612e..1c46648efb 100644
Binary files a/dashboard/audit-baselines/agents__dark__1440.png and b/dashboard/audit-baselines/agents__dark__1440.png differ
diff --git a/dashboard/audit-baselines/agents__dark__320.png b/dashboard/audit-baselines/agents__dark__320.png
index 6134e7bdf6..ccc9f7a991 100644
Binary files a/dashboard/audit-baselines/agents__dark__320.png and b/dashboard/audit-baselines/agents__dark__320.png differ
diff --git a/dashboard/audit-baselines/agents__dark__768.png b/dashboard/audit-baselines/agents__dark__768.png
index 95bd207ab4..09d65564c4 100644
Binary files a/dashboard/audit-baselines/agents__dark__768.png and b/dashboard/audit-baselines/agents__dark__768.png differ
diff --git a/dashboard/audit-baselines/agents__light__1440.png b/dashboard/audit-baselines/agents__light__1440.png
index 5196fc8f6e..fa01c74115 100644
Binary files a/dashboard/audit-baselines/agents__light__1440.png and b/dashboard/audit-baselines/agents__light__1440.png differ
diff --git a/dashboard/audit-baselines/agents__light__320.png b/dashboard/audit-baselines/agents__light__320.png
index f7618e1b41..641a76725f 100644
Binary files a/dashboard/audit-baselines/agents__light__320.png and b/dashboard/audit-baselines/agents__light__320.png differ
diff --git a/dashboard/audit-baselines/agents__light__768.png b/dashboard/audit-baselines/agents__light__768.png
index 3dee298992..570e3b25be 100644
Binary files a/dashboard/audit-baselines/agents__light__768.png and b/dashboard/audit-baselines/agents__light__768.png differ
diff --git a/dashboard/audit-baselines/automations__dark__1440.png b/dashboard/audit-baselines/automations__dark__1440.png
index 6edc8e5e46..4fa0e861c8 100644
Binary files a/dashboard/audit-baselines/automations__dark__1440.png and b/dashboard/audit-baselines/automations__dark__1440.png differ
diff --git a/dashboard/audit-baselines/automations__dark__320.png b/dashboard/audit-baselines/automations__dark__320.png
index 7714019f9d..5dfa63f47a 100644
Binary files a/dashboard/audit-baselines/automations__dark__320.png and b/dashboard/audit-baselines/automations__dark__320.png differ
diff --git a/dashboard/audit-baselines/automations__dark__768.png b/dashboard/audit-baselines/automations__dark__768.png
index 335d1bdb81..4d4de5c334 100644
Binary files a/dashboard/audit-baselines/automations__dark__768.png and b/dashboard/audit-baselines/automations__dark__768.png differ
diff --git a/dashboard/audit-baselines/automations__light__1440.png b/dashboard/audit-baselines/automations__light__1440.png
index 9bbdd8244f..6ee031b05e 100644
Binary files a/dashboard/audit-baselines/automations__light__1440.png and b/dashboard/audit-baselines/automations__light__1440.png differ
diff --git a/dashboard/audit-baselines/automations__light__320.png b/dashboard/audit-baselines/automations__light__320.png
index 4608e8b6df..76835c4f06 100644
Binary files a/dashboard/audit-baselines/automations__light__320.png and b/dashboard/audit-baselines/automations__light__320.png differ
diff --git a/dashboard/audit-baselines/automations__light__768.png b/dashboard/audit-baselines/automations__light__768.png
index 9ae359c1f2..59e302cc26 100644
Binary files a/dashboard/audit-baselines/automations__light__768.png and b/dashboard/audit-baselines/automations__light__768.png differ
diff --git a/dashboard/audit-baselines/brain__dark__1440.png b/dashboard/audit-baselines/brain__dark__1440.png
index 483e55330e..4527179170 100644
Binary files a/dashboard/audit-baselines/brain__dark__1440.png and b/dashboard/audit-baselines/brain__dark__1440.png differ
diff --git a/dashboard/audit-baselines/brain__dark__320.png b/dashboard/audit-baselines/brain__dark__320.png
index afd246865e..6eacbb763d 100644
Binary files a/dashboard/audit-baselines/brain__dark__320.png and b/dashboard/audit-baselines/brain__dark__320.png differ
diff --git a/dashboard/audit-baselines/brain__dark__768.png b/dashboard/audit-baselines/brain__dark__768.png
index 45ac5afb05..fd2891ae5b 100644
Binary files a/dashboard/audit-baselines/brain__dark__768.png and b/dashboard/audit-baselines/brain__dark__768.png differ
diff --git a/dashboard/audit-baselines/brain__light__1440.png b/dashboard/audit-baselines/brain__light__1440.png
index ab692ee3b1..ead6e9f25a 100644
Binary files a/dashboard/audit-baselines/brain__light__1440.png and b/dashboard/audit-baselines/brain__light__1440.png differ
diff --git a/dashboard/audit-baselines/brain__light__320.png b/dashboard/audit-baselines/brain__light__320.png
index 109c351bc5..8ea8995484 100644
Binary files a/dashboard/audit-baselines/brain__light__320.png and b/dashboard/audit-baselines/brain__light__320.png differ
diff --git a/dashboard/audit-baselines/brain__light__768.png b/dashboard/audit-baselines/brain__light__768.png
index af8e595b8b..167f926b80 100644
Binary files a/dashboard/audit-baselines/brain__light__768.png and b/dashboard/audit-baselines/brain__light__768.png differ
diff --git a/dashboard/audit-baselines/code__dark__1440.png b/dashboard/audit-baselines/code__dark__1440.png
index be786a6ed7..a95ebd41b0 100644
Binary files a/dashboard/audit-baselines/code__dark__1440.png and b/dashboard/audit-baselines/code__dark__1440.png differ
diff --git a/dashboard/audit-baselines/code__dark__320.png b/dashboard/audit-baselines/code__dark__320.png
index e4fc6a08f6..4d9e88678e 100644
Binary files a/dashboard/audit-baselines/code__dark__320.png and b/dashboard/audit-baselines/code__dark__320.png differ
diff --git a/dashboard/audit-baselines/code__dark__768.png b/dashboard/audit-baselines/code__dark__768.png
index b6ac8d5da0..4796242828 100644
Binary files a/dashboard/audit-baselines/code__dark__768.png and b/dashboard/audit-baselines/code__dark__768.png differ
diff --git a/dashboard/audit-baselines/code__light__1440.png b/dashboard/audit-baselines/code__light__1440.png
index 2fe0dee40e..5b435597b4 100644
Binary files a/dashboard/audit-baselines/code__light__1440.png and b/dashboard/audit-baselines/code__light__1440.png differ
diff --git a/dashboard/audit-baselines/code__light__320.png b/dashboard/audit-baselines/code__light__320.png
index 3e099aa06f..7f21b8e58e 100644
Binary files a/dashboard/audit-baselines/code__light__320.png and b/dashboard/audit-baselines/code__light__320.png differ
diff --git a/dashboard/audit-baselines/code__light__768.png b/dashboard/audit-baselines/code__light__768.png
index 4acc25f5c0..b16569d04c 100644
Binary files a/dashboard/audit-baselines/code__light__768.png and b/dashboard/audit-baselines/code__light__768.png differ
diff --git a/dashboard/audit-baselines/costs__dark__1440.png b/dashboard/audit-baselines/costs__dark__1440.png
index a2b416d9d3..1d0894e09b 100644
Binary files a/dashboard/audit-baselines/costs__dark__1440.png and b/dashboard/audit-baselines/costs__dark__1440.png differ
diff --git a/dashboard/audit-baselines/costs__dark__320.png b/dashboard/audit-baselines/costs__dark__320.png
index 60fa0e7fab..7bf242ce44 100644
Binary files a/dashboard/audit-baselines/costs__dark__320.png and b/dashboard/audit-baselines/costs__dark__320.png differ
diff --git a/dashboard/audit-baselines/costs__dark__768.png b/dashboard/audit-baselines/costs__dark__768.png
index 9785bbd792..e6aa885366 100644
Binary files a/dashboard/audit-baselines/costs__dark__768.png and b/dashboard/audit-baselines/costs__dark__768.png differ
diff --git a/dashboard/audit-baselines/costs__light__1440.png b/dashboard/audit-baselines/costs__light__1440.png
index 1fdaa75c3e..93773cab28 100644
Binary files a/dashboard/audit-baselines/costs__light__1440.png and b/dashboard/audit-baselines/costs__light__1440.png differ
diff --git a/dashboard/audit-baselines/costs__light__320.png b/dashboard/audit-baselines/costs__light__320.png
index 6756d4b763..94f6ec1741 100644
Binary files a/dashboard/audit-baselines/costs__light__320.png and b/dashboard/audit-baselines/costs__light__320.png differ
diff --git a/dashboard/audit-baselines/costs__light__768.png b/dashboard/audit-baselines/costs__light__768.png
index 6537e32346..73e71d7ac8 100644
Binary files a/dashboard/audit-baselines/costs__light__768.png and b/dashboard/audit-baselines/costs__light__768.png differ
diff --git a/dashboard/audit-baselines/delivery__dark__1440.png b/dashboard/audit-baselines/delivery__dark__1440.png
index ad4a724ca7..05240addde 100644
Binary files a/dashboard/audit-baselines/delivery__dark__1440.png and b/dashboard/audit-baselines/delivery__dark__1440.png differ
diff --git a/dashboard/audit-baselines/delivery__dark__320.png b/dashboard/audit-baselines/delivery__dark__320.png
index a7b118b29f..9080049d4d 100644
Binary files a/dashboard/audit-baselines/delivery__dark__320.png and b/dashboard/audit-baselines/delivery__dark__320.png differ
diff --git a/dashboard/audit-baselines/delivery__dark__768.png b/dashboard/audit-baselines/delivery__dark__768.png
index a1bcc8f967..a461853295 100644
Binary files a/dashboard/audit-baselines/delivery__dark__768.png and b/dashboard/audit-baselines/delivery__dark__768.png differ
diff --git a/dashboard/audit-baselines/delivery__light__1440.png b/dashboard/audit-baselines/delivery__light__1440.png
index 1bcb197ac2..508513c0a8 100644
Binary files a/dashboard/audit-baselines/delivery__light__1440.png and b/dashboard/audit-baselines/delivery__light__1440.png differ
diff --git a/dashboard/audit-baselines/delivery__light__320.png b/dashboard/audit-baselines/delivery__light__320.png
index 7de19f50c4..9fb112d4f3 100644
Binary files a/dashboard/audit-baselines/delivery__light__320.png and b/dashboard/audit-baselines/delivery__light__320.png differ
diff --git a/dashboard/audit-baselines/delivery__light__768.png b/dashboard/audit-baselines/delivery__light__768.png
index ea0e705674..45ab9cd17d 100644
Binary files a/dashboard/audit-baselines/delivery__light__768.png and b/dashboard/audit-baselines/delivery__light__768.png differ
diff --git a/dashboard/audit-baselines/explorer__dark__1440.png b/dashboard/audit-baselines/explorer__dark__1440.png
index 42dea10fdd..6ef0c42c69 100644
Binary files a/dashboard/audit-baselines/explorer__dark__1440.png and b/dashboard/audit-baselines/explorer__dark__1440.png differ
diff --git a/dashboard/audit-baselines/explorer__dark__320.png b/dashboard/audit-baselines/explorer__dark__320.png
index 47edad9ed9..11735d9c61 100644
Binary files a/dashboard/audit-baselines/explorer__dark__320.png and b/dashboard/audit-baselines/explorer__dark__320.png differ
diff --git a/dashboard/audit-baselines/explorer__dark__768.png b/dashboard/audit-baselines/explorer__dark__768.png
index 29e7226113..c61138b455 100644
Binary files a/dashboard/audit-baselines/explorer__dark__768.png and b/dashboard/audit-baselines/explorer__dark__768.png differ
diff --git a/dashboard/audit-baselines/explorer__light__1440.png b/dashboard/audit-baselines/explorer__light__1440.png
index 38b93b8191..78861f9492 100644
Binary files a/dashboard/audit-baselines/explorer__light__1440.png and b/dashboard/audit-baselines/explorer__light__1440.png differ
diff --git a/dashboard/audit-baselines/explorer__light__320.png b/dashboard/audit-baselines/explorer__light__320.png
index 4779badb3f..d79d1481eb 100644
Binary files a/dashboard/audit-baselines/explorer__light__320.png and b/dashboard/audit-baselines/explorer__light__320.png differ
diff --git a/dashboard/audit-baselines/explorer__light__768.png b/dashboard/audit-baselines/explorer__light__768.png
index f499904a53..5a8179d70b 100644
Binary files a/dashboard/audit-baselines/explorer__light__768.png and b/dashboard/audit-baselines/explorer__light__768.png differ
diff --git a/dashboard/audit-baselines/knowledge__dark__1440.png b/dashboard/audit-baselines/knowledge__dark__1440.png
index 82a51ec278..7f6511d10c 100644
Binary files a/dashboard/audit-baselines/knowledge__dark__1440.png and b/dashboard/audit-baselines/knowledge__dark__1440.png differ
diff --git a/dashboard/audit-baselines/knowledge__dark__320.png b/dashboard/audit-baselines/knowledge__dark__320.png
index 652cd1a606..221c55c207 100644
Binary files a/dashboard/audit-baselines/knowledge__dark__320.png and b/dashboard/audit-baselines/knowledge__dark__320.png differ
diff --git a/dashboard/audit-baselines/knowledge__dark__768.png b/dashboard/audit-baselines/knowledge__dark__768.png
index de8e74ab40..09fc4582b4 100644
Binary files a/dashboard/audit-baselines/knowledge__dark__768.png and b/dashboard/audit-baselines/knowledge__dark__768.png differ
diff --git a/dashboard/audit-baselines/knowledge__light__1440.png b/dashboard/audit-baselines/knowledge__light__1440.png
index 165bdf6e94..0f3133e66f 100644
Binary files a/dashboard/audit-baselines/knowledge__light__1440.png and b/dashboard/audit-baselines/knowledge__light__1440.png differ
diff --git a/dashboard/audit-baselines/knowledge__light__320.png b/dashboard/audit-baselines/knowledge__light__320.png
index b15db7f157..25871bc9c0 100644
Binary files a/dashboard/audit-baselines/knowledge__light__320.png and b/dashboard/audit-baselines/knowledge__light__320.png differ
diff --git a/dashboard/audit-baselines/knowledge__light__768.png b/dashboard/audit-baselines/knowledge__light__768.png
index 0c9cdbcdfd..8122e59b52 100644
Binary files a/dashboard/audit-baselines/knowledge__light__768.png and b/dashboard/audit-baselines/knowledge__light__768.png differ
diff --git a/dashboard/audit-baselines/loom__dark__1440.png b/dashboard/audit-baselines/loom__dark__1440.png
index f76dfc3b48..4c83047d01 100644
Binary files a/dashboard/audit-baselines/loom__dark__1440.png and b/dashboard/audit-baselines/loom__dark__1440.png differ
diff --git a/dashboard/audit-baselines/loom__dark__320.png b/dashboard/audit-baselines/loom__dark__320.png
index 42282f5ba7..a6513abc0d 100644
Binary files a/dashboard/audit-baselines/loom__dark__320.png and b/dashboard/audit-baselines/loom__dark__320.png differ
diff --git a/dashboard/audit-baselines/loom__dark__768.png b/dashboard/audit-baselines/loom__dark__768.png
index c42e3c8222..47ef00e9e5 100644
Binary files a/dashboard/audit-baselines/loom__dark__768.png and b/dashboard/audit-baselines/loom__dark__768.png differ
diff --git a/dashboard/audit-baselines/loom__light__1440.png b/dashboard/audit-baselines/loom__light__1440.png
index 3df0502fc4..4d8972ff97 100644
Binary files a/dashboard/audit-baselines/loom__light__1440.png and b/dashboard/audit-baselines/loom__light__1440.png differ
diff --git a/dashboard/audit-baselines/loom__light__320.png b/dashboard/audit-baselines/loom__light__320.png
index fd9b7ee62a..c8a234a932 100644
Binary files a/dashboard/audit-baselines/loom__light__320.png and b/dashboard/audit-baselines/loom__light__320.png differ
diff --git a/dashboard/audit-baselines/loom__light__768.png b/dashboard/audit-baselines/loom__light__768.png
index ad00c63715..0ede6fd1e2 100644
Binary files a/dashboard/audit-baselines/loom__light__768.png and b/dashboard/audit-baselines/loom__light__768.png differ
diff --git a/dashboard/audit-baselines/observatory__dark__1440.png b/dashboard/audit-baselines/observatory__dark__1440.png
index db87e135b0..8bf0430712 100644
Binary files a/dashboard/audit-baselines/observatory__dark__1440.png and b/dashboard/audit-baselines/observatory__dark__1440.png differ
diff --git a/dashboard/audit-baselines/observatory__dark__320.png b/dashboard/audit-baselines/observatory__dark__320.png
index 74908288cf..bdad2912e9 100644
Binary files a/dashboard/audit-baselines/observatory__dark__320.png and b/dashboard/audit-baselines/observatory__dark__320.png differ
diff --git a/dashboard/audit-baselines/observatory__dark__768.png b/dashboard/audit-baselines/observatory__dark__768.png
index 9622ef9998..5d4106d136 100644
Binary files a/dashboard/audit-baselines/observatory__dark__768.png and b/dashboard/audit-baselines/observatory__dark__768.png differ
diff --git a/dashboard/audit-baselines/observatory__light__1440.png b/dashboard/audit-baselines/observatory__light__1440.png
index 68a599b8ee..a0c05a9354 100644
Binary files a/dashboard/audit-baselines/observatory__light__1440.png and b/dashboard/audit-baselines/observatory__light__1440.png differ
diff --git a/dashboard/audit-baselines/observatory__light__320.png b/dashboard/audit-baselines/observatory__light__320.png
index 87f8505bf7..655e3932c2 100644
Binary files a/dashboard/audit-baselines/observatory__light__320.png and b/dashboard/audit-baselines/observatory__light__320.png differ
diff --git a/dashboard/audit-baselines/observatory__light__768.png b/dashboard/audit-baselines/observatory__light__768.png
index 6b1f74a0bd..310f866bed 100644
Binary files a/dashboard/audit-baselines/observatory__light__768.png and b/dashboard/audit-baselines/observatory__light__768.png differ
diff --git a/dashboard/audit-baselines/sessions__dark__1440.png b/dashboard/audit-baselines/sessions__dark__1440.png
index 2b264710e2..c0d0e736ed 100644
Binary files a/dashboard/audit-baselines/sessions__dark__1440.png and b/dashboard/audit-baselines/sessions__dark__1440.png differ
diff --git a/dashboard/audit-baselines/sessions__dark__320.png b/dashboard/audit-baselines/sessions__dark__320.png
index a8ac9d4f13..0ba4e0bf47 100644
Binary files a/dashboard/audit-baselines/sessions__dark__320.png and b/dashboard/audit-baselines/sessions__dark__320.png differ
diff --git a/dashboard/audit-baselines/sessions__dark__768.png b/dashboard/audit-baselines/sessions__dark__768.png
index ce7fc24bf1..5694d5eb68 100644
Binary files a/dashboard/audit-baselines/sessions__dark__768.png and b/dashboard/audit-baselines/sessions__dark__768.png differ
diff --git a/dashboard/audit-baselines/sessions__light__1440.png b/dashboard/audit-baselines/sessions__light__1440.png
index ae8342f607..a2b8fd0489 100644
Binary files a/dashboard/audit-baselines/sessions__light__1440.png and b/dashboard/audit-baselines/sessions__light__1440.png differ
diff --git a/dashboard/audit-baselines/sessions__light__320.png b/dashboard/audit-baselines/sessions__light__320.png
index d2841a123b..e25fea0607 100644
Binary files a/dashboard/audit-baselines/sessions__light__320.png and b/dashboard/audit-baselines/sessions__light__320.png differ
diff --git a/dashboard/audit-baselines/sessions__light__768.png b/dashboard/audit-baselines/sessions__light__768.png
index 8430272443..d72c894712 100644
Binary files a/dashboard/audit-baselines/sessions__light__768.png and b/dashboard/audit-baselines/sessions__light__768.png differ
diff --git a/dashboard/audit-baselines/settings__dark__1440.png b/dashboard/audit-baselines/settings__dark__1440.png
index ceffa65e62..41d3dcff7d 100644
Binary files a/dashboard/audit-baselines/settings__dark__1440.png and b/dashboard/audit-baselines/settings__dark__1440.png differ
diff --git a/dashboard/audit-baselines/settings__dark__320.png b/dashboard/audit-baselines/settings__dark__320.png
index 85d5f74c42..a5a5f10b48 100644
Binary files a/dashboard/audit-baselines/settings__dark__320.png and b/dashboard/audit-baselines/settings__dark__320.png differ
diff --git a/dashboard/audit-baselines/settings__dark__768.png b/dashboard/audit-baselines/settings__dark__768.png
index 22c9c0f0e1..0d383f9322 100644
Binary files a/dashboard/audit-baselines/settings__dark__768.png and b/dashboard/audit-baselines/settings__dark__768.png differ
diff --git a/dashboard/audit-baselines/settings__light__1440.png b/dashboard/audit-baselines/settings__light__1440.png
index 89d5390892..69410f4f61 100644
Binary files a/dashboard/audit-baselines/settings__light__1440.png and b/dashboard/audit-baselines/settings__light__1440.png differ
diff --git a/dashboard/audit-baselines/settings__light__320.png b/dashboard/audit-baselines/settings__light__320.png
index 06a17ad732..36c694f690 100644
Binary files a/dashboard/audit-baselines/settings__light__320.png and b/dashboard/audit-baselines/settings__light__320.png differ
diff --git a/dashboard/audit-baselines/settings__light__768.png b/dashboard/audit-baselines/settings__light__768.png
index 44a255911b..b587636381 100644
Binary files a/dashboard/audit-baselines/settings__light__768.png and b/dashboard/audit-baselines/settings__light__768.png differ
diff --git a/dashboard/audit-baselines/work__dark__1440.png b/dashboard/audit-baselines/work__dark__1440.png
new file mode 100644
index 0000000000..ccedb080e8
Binary files /dev/null and b/dashboard/audit-baselines/work__dark__1440.png differ
diff --git a/dashboard/audit-baselines/work__dark__320.png b/dashboard/audit-baselines/work__dark__320.png
new file mode 100644
index 0000000000..b20c4a2296
Binary files /dev/null and b/dashboard/audit-baselines/work__dark__320.png differ
diff --git a/dashboard/audit-baselines/work__dark__768.png b/dashboard/audit-baselines/work__dark__768.png
new file mode 100644
index 0000000000..09f0eee71c
Binary files /dev/null and b/dashboard/audit-baselines/work__dark__768.png differ
diff --git a/dashboard/audit-baselines/work__light__1440.png b/dashboard/audit-baselines/work__light__1440.png
new file mode 100644
index 0000000000..e24ced9dcd
Binary files /dev/null and b/dashboard/audit-baselines/work__light__1440.png differ
diff --git a/dashboard/audit-baselines/work__light__320.png b/dashboard/audit-baselines/work__light__320.png
new file mode 100644
index 0000000000..102a66e290
Binary files /dev/null and b/dashboard/audit-baselines/work__light__320.png differ
diff --git a/dashboard/audit-baselines/work__light__768.png b/dashboard/audit-baselines/work__light__768.png
new file mode 100644
index 0000000000..971da86af8
Binary files /dev/null and b/dashboard/audit-baselines/work__light__768.png differ
diff --git a/dashboard/audit-baselines/workflows__dark__1440.png b/dashboard/audit-baselines/workflows__dark__1440.png
new file mode 100644
index 0000000000..627c913423
Binary files /dev/null and b/dashboard/audit-baselines/workflows__dark__1440.png differ
diff --git a/dashboard/audit-baselines/workflows__dark__320.png b/dashboard/audit-baselines/workflows__dark__320.png
new file mode 100644
index 0000000000..f6247e387e
Binary files /dev/null and b/dashboard/audit-baselines/workflows__dark__320.png differ
diff --git a/dashboard/audit-baselines/workflows__dark__768.png b/dashboard/audit-baselines/workflows__dark__768.png
new file mode 100644
index 0000000000..8f4f41a8c5
Binary files /dev/null and b/dashboard/audit-baselines/workflows__dark__768.png differ
diff --git a/dashboard/audit-baselines/workflows__light__1440.png b/dashboard/audit-baselines/workflows__light__1440.png
new file mode 100644
index 0000000000..f19039f66f
Binary files /dev/null and b/dashboard/audit-baselines/workflows__light__1440.png differ
diff --git a/dashboard/audit-baselines/workflows__light__320.png b/dashboard/audit-baselines/workflows__light__320.png
new file mode 100644
index 0000000000..91ac132e5e
Binary files /dev/null and b/dashboard/audit-baselines/workflows__light__320.png differ
diff --git a/dashboard/audit-baselines/workflows__light__768.png b/dashboard/audit-baselines/workflows__light__768.png
new file mode 100644
index 0000000000..4fc9052528
Binary files /dev/null and b/dashboard/audit-baselines/workflows__light__768.png differ
diff --git a/dashboard/e2e/responsive.ts b/dashboard/e2e/responsive.ts
new file mode 100644
index 0000000000..c7abea6535
--- /dev/null
+++ b/dashboard/e2e/responsive.ts
@@ -0,0 +1,677 @@
+/**
+ * The Plan 11 responsive/zoom/media matrix, and the two measurements the plan
+ * names that no axe rule performs.
+ *
+ * Split out of `axe-harness.ts` for the same reason `visibility.ts` was: the
+ * probes are pure DOM measurement and the pass/fail rules are pure functions
+ * over their reports, so neither needs a Playwright import and both can be
+ * driven against a real DOM outside the browser harness.
+ *
+ * What the plan mandates (11-dashboard-frontend.md, "Responsive,
+ * accessibility, performance, and usability gates"):
+ *
+ * "Automated tests cover 320x568, 390x844, 768x1024, 1024x768, 1280x720,
+ * and 1440x900 CSS pixels, 200% and 400% zoom, prefers-reduced-motion,
+ * prefers-contrast: more, and forced colors. At 320 pixels and 400% zoom
+ * there is no page-level horizontal scroll, clipped truth state, lost
+ * scope/provenance, or inaccessible action; labeled code/table/graph
+ * regions may scroll internally. Touch targets are at least 44x44 CSS
+ * pixels."
+ *
+ * ZOOM IS A CSS VIEWPORT, NOT `deviceScaleFactor`. Playwright's
+ * `deviceScaleFactor` raises rasterization density and leaves layout at the
+ * unzoomed CSS size, so a page captured at `deviceScaleFactor: 4` reflows
+ * exactly as it did at 1x — which is the one thing WCAG 1.4.10 is about. Real
+ * browser zoom shrinks the CSS viewport: 400% zoom of a 1280x720 window is a
+ * 320x180 CSS viewport, and that is what is emulated here. It is also why the
+ * 400% row and the 320px row test the same reflow width from opposite
+ * directions.
+ */
+
+/** WCAG 2.5.5 / Plan 11 minimum, in CSS pixels. */
+export const MIN_TOUCH_TARGET_PX = 44;
+
+/** The window this harness models browser zoom against. */
+const ZOOM_BASE = { width: 1280, height: 720 } as const;
+
+export interface Viewport {
+ /** Stable id: file names, findings keys, console tags. */
+ readonly id: string;
+ readonly width: number;
+ readonly height: number;
+ /** Browser zoom this CSS viewport models, as a percentage. */
+ readonly zoom: number;
+ /**
+ * Where the plan forbids page-level horizontal scroll outright — "at 320
+ * pixels and 400% zoom". Elsewhere the measurement is still taken and
+ * reported; only these two gate the run.
+ */
+ readonly reflowGated: boolean;
+}
+
+function zoomed(zoom: number): Viewport {
+ const scale = zoom / 100;
+ return {
+ id: `zoom${zoom}`,
+ width: Math.round(ZOOM_BASE.width / scale),
+ height: Math.round(ZOOM_BASE.height / scale),
+ zoom,
+ reflowGated: zoom >= 400,
+ };
+}
+
+function device(width: number, height: number, reflowGated = false): Viewport {
+ return { id: `${width}x${height}`, width, height, zoom: 100, reflowGated };
+}
+
+/**
+ * The three the full scenario sweep runs, in both themes.
+ *
+ * These are the plan's own sizes, not the harness's previous 320/768/1440 at a
+ * uniform height of 900 — a 320-wide viewport 900 tall is a phone width with a
+ * tablet's vertical room, which hides exactly the "does the truth state fit"
+ * question the narrow row exists to ask.
+ */
+export const SHOWCASE_VIEWPORTS: readonly Viewport[] = [
+ device(320, 568, true),
+ device(768, 1024),
+ device(1440, 900),
+];
+
+/** The rest of the plan's matrix, swept over a representative subset. */
+export const MATRIX_VIEWPORTS: readonly Viewport[] = [
+ device(390, 844),
+ device(1024, 768),
+ device(1280, 720),
+ zoomed(200),
+ zoomed(400),
+];
+
+export const PLAN_VIEWPORTS: readonly Viewport[] = [...SHOWCASE_VIEWPORTS, ...MATRIX_VIEWPORTS];
+
+/**
+ * `reduced-motion` is the harness default and the only mode the full sweep
+ * runs; the other two are emulated live, without reloading, because both
+ * resolve through CSS media queries.
+ */
+export type MediaMode = 'reduced-motion' | 'contrast-more' | 'forced-colors';
+
+export const MEDIA_MODES: readonly MediaMode[] = ['reduced-motion', 'contrast-more', 'forced-colors'];
+
+export type Theme = 'light' | 'dark';
+
+export interface Combination {
+ readonly viewport: Viewport;
+ readonly theme: Theme;
+ readonly media: MediaMode;
+}
+
+/**
+ * Everything the matrix subset is scanned at, beyond what the full sweep
+ * already covers. Thirty combinations per subset scenario:
+ *
+ * - 10: the five viewports the full sweep does not reach (390x844, 1024x768,
+ * 1280x720, and the two zoom levels), in both themes, under reduced motion;
+ * - 16: all eight plan viewports under `prefers-contrast: more`, and all
+ * eight under forced colors, in light;
+ * - 4: a dark spot check of both media modes at 1440x900 and 390x844, the
+ * widest and the narrowest, where a raised-contrast or forced palette is
+ * likeliest to collide with the dark token set.
+ *
+ * Forced colors replaces the palette wholesale, so sweeping it through both
+ * themes at every size would buy near-identical renders; the dark rows are the
+ * spot check rather than a full second pass. That is a deliberate cost
+ * decision, recorded here rather than left implicit in a nested loop.
+ */
+export const RESPONSIVE_MATRIX: readonly Combination[] = [
+ ...MATRIX_VIEWPORTS.flatMap((viewport): Combination[] => [
+ { viewport, theme: 'light', media: 'reduced-motion' },
+ { viewport, theme: 'dark', media: 'reduced-motion' },
+ ]),
+ ...PLAN_VIEWPORTS.flatMap((viewport): Combination[] => [
+ { viewport, theme: 'light', media: 'contrast-more' },
+ { viewport, theme: 'light', media: 'forced-colors' },
+ ]),
+ ...[device(1440, 900), device(390, 844)].flatMap((viewport): Combination[] => [
+ { viewport, theme: 'dark', media: 'contrast-more' },
+ { viewport, theme: 'dark', media: 'forced-colors' },
+ ]),
+];
+
+export function combinationTag(c: Combination): string {
+ return `${c.theme}/${c.viewport.id}/${c.media}`;
+}
+
+/* ==========================================================================
+ * Reflow: page-level horizontal scroll, and what is running past the edge.
+ * ========================================================================== */
+
+export interface OverflowOffender {
+ /**
+ * `page-overflow` widens the document itself. `clipped` is content that runs
+ * past the viewport inside an ancestor that hides its horizontal overflow —
+ * unreachable rather than merely off-screen, which is the plan's "clipped
+ * truth state".
+ */
+ readonly kind: 'page-overflow' | 'clipped';
+ readonly selector: string;
+ readonly clipper: string;
+ readonly right: number;
+ readonly text: string;
+}
+
+/** A region that scrolls horizontally on its own, and whether it announces a
+ * name — the plan permits "labeled code/table/graph regions" to do this. */
+export interface InternalScroller {
+ readonly selector: string;
+ readonly label: string;
+ readonly role: string;
+}
+
+/**
+ * A scroll container with content in it and no height to show it in.
+ *
+ * `overflow: auto` on a flex child that lost its height resolves to
+ * `clientHeight: 0`, which clips every row inside it AND removes the scrollbar
+ * that would have reached them. Nothing on the page looks broken: the header
+ * above it still reports "4 loaded of 4 matching rows".
+ *
+ * This is the plan's "clipped truth state" in its unambiguous form, and unlike
+ * the overflow heuristics it needs no judgement — a scroller holding text with
+ * a zero-height viewport is never intentional.
+ */
+export interface CollapsedScroller {
+ readonly selector: string;
+ readonly scrollHeight: number;
+ readonly clientHeight: number;
+ readonly hidden: string;
+}
+
+export interface ReflowReport {
+ readonly scrollWidth: number;
+ readonly clientWidth: number;
+ readonly offenders: readonly OverflowOffender[];
+ readonly internalScrollers: readonly InternalScroller[];
+ readonly collapsedScrollers: readonly CollapsedScroller[];
+}
+
+/**
+ * Everything a pointer or the keyboard can operate. Shared by both probes so
+ * "an inaccessible action" and "an undersized target" are drawn from one
+ * definition of what an action is.
+ */
+const INTERACTIVE_SELECTOR =
+ 'a[href],area[href],button,input:not([type="hidden"]),select,textarea,summary,' +
+ '[role="button"],[role="link"],[role="tab"],[role="checkbox"],[role="radio"],' +
+ '[role="switch"],[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],' +
+ '[role="option"],[tabindex]';
+
+/** Shared in-page helpers, prepended to both probe sources. */
+const PROBE_PRELUDE = `
+ var INTERACTIVE = ${JSON.stringify(INTERACTIVE_SELECTOR)};
+ var describe = function (el) {
+ if (!el) return '(none)';
+ var out = el.tagName.toLowerCase();
+ if (el.id) out += '#' + el.id;
+ for (var a = 0; a < el.attributes.length; a += 1) {
+ var attr = el.attributes[a];
+ if (attr.name.indexOf('data-') === 0) { out += '[' + attr.name + ']'; break; }
+ }
+ var cls = typeof el.className === 'string' ? el.className.trim() : '';
+ if (cls !== '') out += '.' + cls.split(/\\s+/).slice(0, 3).join('.');
+ return out.slice(0, 120);
+ };
+ var ownText = function (el) {
+ var out = '';
+ for (var c = 0; c < el.childNodes.length; c += 1) {
+ if (el.childNodes[c].nodeType === 3) out += el.childNodes[c].nodeValue || '';
+ }
+ return out.trim().replace(/\\s+/g, ' ');
+ };
+`;
+
+/**
+ * Measured inside the page, as source text rather than a function: `tsx`
+ * compiles this file with esbuild's `keepNames`, whose `__name` helper does
+ * not exist in the page, so a named callback handed to `page.evaluate` throws
+ * on arrival.
+ *
+ * Decorative geometry is deliberately excluded. A glow layer or a gradient
+ * that bleeds past the right edge carries no truth state, and listing those
+ * would bury the rows, figures and controls that do — so an element is only an
+ * offender if it holds its own text or is something a person operates or
+ * reads.
+ */
+export const REFLOW_PROBE = `(function () {${PROBE_PRELUDE}
+ var doc = document.documentElement;
+ var limit = doc.clientWidth;
+ var offenders = [];
+ var scrollers = [];
+ var collapsed = [];
+ var seen = {};
+ var all = document.body ? document.body.querySelectorAll('*') : [];
+ for (var i = 0; i < all.length; i += 1) {
+ var el = all[i];
+ var style = getComputedStyle(el);
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
+ // Checked BEFORE the size guard below, because a collapsed scroller is
+ // precisely an element with no painted height, and skipping it as "not
+ // rendered" is how it stayed invisible to this probe in the first place.
+ if ((style.overflowY === 'auto' || style.overflowY === 'scroll') &&
+ el.clientHeight < 1 && el.scrollHeight > 1) {
+ var trapped = (el.textContent || '').trim().replace(/\\s+/g, ' ');
+ if (trapped !== '') {
+ collapsed.push({
+ selector: describe(el),
+ scrollHeight: el.scrollHeight,
+ clientHeight: el.clientHeight,
+ hidden: trapped.slice(0, 80),
+ });
+ }
+ }
+ var rect = el.getBoundingClientRect();
+ if (rect.width < 1 || rect.height < 1) continue;
+ if ((style.overflowX === 'auto' || style.overflowX === 'scroll') &&
+ el.scrollWidth > el.clientWidth + 1) {
+ scrollers.push({
+ selector: describe(el),
+ label: (el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') ||
+ el.getAttribute('title') || '').trim(),
+ role: el.getAttribute('role') || '',
+ });
+ }
+ if (rect.right <= limit + 1) continue;
+ var carriesContent =
+ ownText(el) !== '' || el.matches(INTERACTIVE) ||
+ el.tagName === 'IMG' || el.tagName === 'CANVAS' || el.tagName === 'SVG';
+ if (!carriesContent) continue;
+ var clipper = null;
+ var reachable = false;
+ for (var up = el.parentElement; up; up = up.parentElement) {
+ var ov = getComputedStyle(up).overflowX;
+ if (ov === 'auto' || ov === 'scroll') { reachable = true; break; }
+ if (ov === 'hidden' || ov === 'clip') { clipper = up; break; }
+ }
+ if (reachable) continue;
+ var kind = clipper ? 'clipped' : 'page-overflow';
+ var key = describe(el) + '|' + kind;
+ if (seen[key]) continue;
+ seen[key] = 1;
+ offenders.push({
+ kind: kind,
+ selector: describe(el),
+ clipper: clipper ? describe(clipper) : '',
+ right: Math.round(rect.right),
+ text: ownText(el).slice(0, 80),
+ });
+ }
+ return {
+ scrollWidth: doc.scrollWidth,
+ clientWidth: limit,
+ offenders: offenders.slice(0, 12),
+ internalScrollers: scrollers.slice(0, 12),
+ collapsedScrollers: collapsed.slice(0, 12),
+ };
+})()`;
+
+/**
+ * The plan's own sentence, as a predicate: at 320 CSS pixels and at 400% zoom
+ * the document itself must not scroll sideways.
+ *
+ * Returns the failures rather than throwing, so one scan can report a reflow
+ * failure, an undersized target and an axe violation together instead of
+ * hiding two of them behind whichever threw first.
+ */
+export function reflowFailures(report: ReflowReport, tag: string): string[] {
+ if (report.scrollWidth <= report.clientWidth + 1) return [];
+ const worst = report.offenders
+ .filter((o) => o.kind === 'page-overflow')
+ .slice(0, 4)
+ .map((o) => `${o.selector} reaches ${o.right}px${o.text === '' ? '' : ` (${o.text})`}`);
+ return [
+ `${tag}: the page scrolls horizontally — document scrollWidth ${report.scrollWidth} > ` +
+ `clientWidth ${report.clientWidth}. ` +
+ (worst.length === 0
+ ? 'No single content element reaches past the edge, so the width comes from a ' +
+ 'decorative or zero-text box.'
+ : `Widest content past the edge: ${worst.join('; ')}`),
+ ];
+}
+
+/**
+ * The other half of the plan's sentence: no clipped truth state at 320 CSS
+ * pixels or 400% zoom.
+ *
+ * Only collapsed scrollers are gated. `OverflowOffender.kind === 'clipped'`
+ * stays a diagnostic because deciding whether an element that reaches past the
+ * viewport inside a hidden ancestor has actually lost anything takes judgement;
+ * a scroller with content and zero height has lost all of it, measurably.
+ */
+export function clippedContentFailures(report: ReflowReport, tag: string): string[] {
+ return report.collapsedScrollers.map(
+ (s) =>
+ `${tag}: ${s.selector} scrolls vertically but has collapsed to ${s.clientHeight}px tall ` +
+ `while holding ${s.scrollHeight}px of content, so none of it can be reached and no ` +
+ `scrollbar offers to. Trapped: "${s.hidden}"`,
+ );
+}
+
+/* ==========================================================================
+ * Forced colors: who opts out.
+ * ========================================================================== */
+
+export interface ForcedColorsOptOut {
+ readonly selector: string;
+ readonly color: string;
+ readonly background: string;
+ readonly text: string;
+}
+
+/**
+ * Elements that decline the forced palette.
+ *
+ * This exists because axe's `color-contrast` rule cannot be trusted here, and
+ * saying so without replacing it would be exactly the kind of quietly narrowed
+ * check this harness was rebuilt to prevent. Measured against axe-core 4.12.1
+ * under Playwright's `forcedColors: 'active'`:
+ *
+ * - Chromium forces the palette correctly. Live `getComputedStyle` on the
+ * reported nodes returns `rgb(0, 0, 0)` text and `rgb(255, 255, 255)`
+ * backgrounds, and the captures are legible.
+ * - Axe nonetheless reports the AUTHORED foreground (`#ecedee`, the dark
+ * theme's text token) against the FORCED white background, so it scores
+ * 143 serious contrast failures in dark and none in light — a verdict that
+ * tracks which theme is selected rather than anything a user sees.
+ * - axe-core 4.12.1 contains no `forced-colors` or high-contrast handling of
+ * any kind, so this is not a detection it is opting out of; it simply does
+ * not know the mode exists.
+ *
+ * So `color-contrast` is disabled in that one mode, and the real risk it was
+ * standing in for is measured directly instead: forced colors can only fail a
+ * reader where an element opts out with `forced-color-adjust: none`. Opting out
+ * is legitimate where colour IS the information — a swatch, a chart series — so
+ * this reports rather than gates, and the report is what makes an illegitimate
+ * opt-out visible.
+ */
+export const FORCED_COLORS_PROBE = `(function () {${PROBE_PRELUDE}
+ var out = [];
+ var seen = {};
+ var all = document.body ? document.body.querySelectorAll('*') : [];
+ for (var i = 0; i < all.length; i += 1) {
+ var el = all[i];
+ var style = getComputedStyle(el);
+ if (style.forcedColorAdjust !== 'none') continue;
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
+ var selector = describe(el);
+ if (seen[selector]) continue;
+ seen[selector] = 1;
+ out.push({
+ selector: selector,
+ color: style.color,
+ background: style.backgroundColor,
+ text: ownText(el).slice(0, 60),
+ });
+ }
+ return out.slice(0, 20);
+})()`;
+
+/* ==========================================================================
+ * Touch targets.
+ * ========================================================================== */
+
+export interface UndersizedTarget {
+ readonly selector: string;
+ readonly name: string;
+ readonly width: number;
+ readonly height: number;
+}
+
+export interface TouchTargetReport {
+ readonly examined: number;
+ readonly exempt: number;
+ readonly undersized: readonly UndersizedTarget[];
+}
+
+/**
+ * Every operable target's rendered box.
+ *
+ * Exemptions, and why each is not a loophole:
+ *
+ * - `[data-axe-canary]`: markup this harness plants to prove the scan is
+ * live. It is 24px square on purpose and is not product UI.
+ * - disabled / `aria-disabled`: not operable, so there is no target.
+ * - `tabindex="-1"` on a non-native control: programmatically focusable, not
+ * in the tab order and not a pointer target.
+ * - not rendered: `display: none`, `visibility: hidden`, or a zero box.
+ * - the visually-hidden idiom — a 1px box that is also clipped, which is how
+ * `sr-only` parks the skip link off-screen until it takes focus. It is not
+ * a pointer target at 1x1; it is a keyboard affordance that becomes
+ * full-size on focus. Both halves are required, so a genuinely tiny
+ * unclipped control is still reported.
+ * - WCAG 2.5.5's inline exception: a link inside a sentence is sized by the
+ * line box around it and cannot be enlarged without breaking the text.
+ * Detected as `display: inline` with sibling text in the same parent.
+ *
+ * Known limit, stated rather than papered over: the measurement is the
+ * element's border box. A control that enlarges its hit area with an absolutely
+ * positioned pseudo-element reads as undersized here even though a pointer can
+ * reach the larger area. No component in this dashboard uses that pattern
+ * today; if one adopts it, this probe needs the pseudo-element box, not a
+ * suppression.
+ *
+ * Reading a failure: CSS pixels, not `rem`. `tailwind.css` sets
+ * `html { font-size: 14px }`, and Tailwind's spacing scale is rem-based, so a
+ * control written `min-h-11` or `size-11` — chosen because 11 x 4px reads as
+ * 44 — lands at 38.5 CSS pixels. Several offenders were sized for this
+ * threshold and still miss it for that reason, so check the computed box before
+ * concluding a utility class is wrong.
+ */
+export const TOUCH_TARGET_PROBE = `(function () {${PROBE_PRELUDE}
+ var MIN = ${MIN_TOUCH_TARGET_PX};
+ var NATIVE = 'a[href],area[href],button,input,select,textarea,summary';
+ var nodes = document.querySelectorAll(INTERACTIVE);
+ var examined = 0;
+ var exempt = 0;
+ var undersized = [];
+ var seen = {};
+ for (var i = 0; i < nodes.length; i += 1) {
+ var el = nodes[i];
+ if (el.closest('[data-axe-canary]') !== null) { exempt += 1; continue; }
+ if (el.disabled === true || el.getAttribute('aria-disabled') === 'true') { exempt += 1; continue; }
+ var ti = el.getAttribute('tabindex');
+ if (ti !== null && Number(ti) < 0 && !el.matches(NATIVE)) { exempt += 1; continue; }
+ var style = getComputedStyle(el);
+ if (style.display === 'none' || style.visibility === 'hidden') { exempt += 1; continue; }
+ var rect = el.getBoundingClientRect();
+ if (rect.width < 1 || rect.height < 1) { exempt += 1; continue; }
+ var clipped = (style.clipPath !== 'none' && style.clipPath !== '') ||
+ (style.clip !== 'auto' && style.clip !== '');
+ if (rect.width <= 1 && rect.height <= 1 && clipped) { exempt += 1; continue; }
+ if (style.display === 'inline' && el.parentElement !== null &&
+ ownText(el.parentElement) !== '') { exempt += 1; continue; }
+ examined += 1;
+ if (rect.width >= MIN - 0.5 && rect.height >= MIN - 0.5) continue;
+ var selector = describe(el);
+ if (seen[selector]) continue;
+ seen[selector] = 1;
+ undersized.push({
+ selector: selector,
+ name: (el.getAttribute('aria-label') || el.textContent || '')
+ .trim().replace(/\\s+/g, ' ').slice(0, 60),
+ width: Math.round(rect.width * 10) / 10,
+ height: Math.round(rect.height * 10) / 10,
+ });
+ }
+ return { examined: examined, exempt: exempt, undersized: undersized.slice(0, 20) };
+})()`;
+
+/**
+ * A probe that examined nothing proved nothing — the same failure mode the
+ * visibility sweep was rewritten to close. Every audited surface renders a nav
+ * rail full of links, so zero operable targets means the probe did not run
+ * against the page it was aimed at.
+ */
+export function touchTargetFailures(report: TouchTargetReport, tag: string): string[] {
+ if (report.examined === 0) {
+ return [
+ `${tag}: the touch-target probe found no operable target at all, so it ` +
+ `proved nothing about this capture (${report.exempt} exempt)`,
+ ];
+ }
+ if (report.undersized.length === 0) return [];
+ const listed = report.undersized
+ .slice(0, 6)
+ .map((t) => `${t.selector} ${t.width}x${t.height}${t.name === '' ? '' : ` "${t.name}"`}`);
+ return [
+ `${tag}: ${report.undersized.length}/${report.examined} operable targets are smaller than ` +
+ `${MIN_TOUCH_TARGET_PX}x${MIN_TOUCH_TARGET_PX} CSS pixels: ${listed.join('; ')}`,
+ ];
+}
+
+/* ==========================================================================
+ * The workspace header's one line: does anything on it render outside the box.
+ * ========================================================================== */
+
+export interface HeaderOverflowChild {
+ readonly header: string;
+ readonly selector: string;
+ readonly text: string;
+ /** CSS pixels past each edge of the header's padding box. 0 means inside. */
+ readonly pastRight: number;
+ readonly pastLeft: number;
+ readonly pastTop: number;
+ readonly pastBottom: number;
+ readonly width: number;
+ readonly height: number;
+ readonly headerWidth: number;
+ readonly contentWidth: number;
+ /** Distance from the child's right edge to the viewport's. Negative means the
+ * child is painted off-screen entirely. */
+ readonly viewportSlack: number;
+}
+
+export interface HeaderBoxReport {
+ /** Workspace headers on the page. Zero is legitimate — not every route
+ * renders one — which is why this is reported rather than asserted. */
+ readonly headers: number;
+ readonly examined: number;
+ readonly offenders: readonly HeaderOverflowChild[];
+}
+
+/**
+ * Every rendered child of a workspace header, measured against that header's
+ * padding box.
+ *
+ * This exists because `document.scrollWidth` cannot see the defect it is named
+ * for. The state chip on `/work` rendered 19 CSS pixels outside its header at
+ * 320px and at 400% zoom — bezel sheared, label flush against the screen edge —
+ * through 78 clean scans, because the shell clips instead of scrolling, so the
+ * document never widened and the reflow gate never fired. The two heuristics
+ * either side of it missed it for reasons worth recording, since both are still
+ * in place:
+ *
+ * - `REFLOW_PROBE` skips anything whose `rect.right <= clientWidth + 1`, and
+ * the chip's LABEL ended at exactly 320 of 320. One pixel of luck.
+ * - It also skips elements carrying no text of their own, and the chip's text
+ * lives in child spans, so the chip's own box was never a candidate.
+ *
+ * Measuring against the container instead of the viewport closes both gaps: a
+ * child that overruns its header is a defect at any width, whether or not the
+ * viewport happens to be wider, and it needs no judgement about what the
+ * element carries.
+ *
+ * Out-of-flow children are excluded. An absolutely positioned overlay does not
+ * spend the line's width budget and is routinely meant to escape it; only
+ * elements laid out IN the header are held to the header's box.
+ */
+export const HEADER_BOX_PROBE = `(function () {${PROBE_PRELUDE}
+ var headers = document.querySelectorAll('[data-workspace-header]');
+ var examined = 0;
+ var offenders = [];
+ var seen = {};
+ var round = function (n) { return Math.round(n * 10) / 10; };
+ for (var h = 0; h < headers.length; h += 1) {
+ var header = headers[h];
+ var hs = getComputedStyle(header);
+ if (hs.display === 'none' || hs.visibility === 'hidden') continue;
+ var hb = header.getBoundingClientRect();
+ var innerLeft = hb.left + (parseFloat(hs.paddingLeft) || 0);
+ var innerRight = hb.right - (parseFloat(hs.paddingRight) || 0);
+ for (var i = 0; i < header.children.length; i += 1) {
+ var el = header.children[i];
+ var style = getComputedStyle(el);
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
+ if (style.position === 'absolute' || style.position === 'fixed') continue;
+ var r = el.getBoundingClientRect();
+ if (r.width < 1 && r.height < 1) continue;
+ examined += 1;
+ var pastRight = r.right - innerRight;
+ var pastLeft = innerLeft - r.left;
+ var pastTop = hb.top - r.top;
+ var pastBottom = r.bottom - hb.bottom;
+ if (pastRight <= 0.5 && pastLeft <= 0.5 && pastTop <= 0.5 && pastBottom <= 0.5) continue;
+ var key = describe(el);
+ if (seen[key]) continue;
+ seen[key] = 1;
+ offenders.push({
+ header: describe(header),
+ selector: key,
+ text: (el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 60),
+ pastRight: round(Math.max(0, pastRight)),
+ pastLeft: round(Math.max(0, pastLeft)),
+ pastTop: round(Math.max(0, pastTop)),
+ pastBottom: round(Math.max(0, pastBottom)),
+ width: round(r.width),
+ height: round(r.height),
+ headerWidth: Math.round(hb.width),
+ contentWidth: Math.round(innerRight - innerLeft),
+ viewportSlack: round(window.innerWidth - r.right),
+ });
+ }
+ }
+ return { headers: headers.length, examined: examined, offenders: offenders.slice(0, 12) };
+})()`;
+
+/**
+ * Gated at every width, unlike reflow, and for every child rather than only
+ * state chips.
+ *
+ * Reflow is gated only at 320 and 400% zoom because the plan's sentence is
+ * about those two sizes. This is a different claim with no width in it: content
+ * placed inside a container must render inside that container. A wide layout
+ * that pushes its own header content out of the box is the same defect with a
+ * bigger budget, so narrowing this to the reflow-gated viewports would only
+ * hide the easier half.
+ *
+ * The chip-only exemption is gone with the bug that motivated it. It existed
+ * because the 564-scan sweep found one pre-existing offender — a 494.8px
+ * snapshot/revision strip in the `/settings` header rendering 356.9px outside
+ * it, 276px of that off-screen — on a surface that change did not own, and
+ * gating it would have turned a hand-off into someone else's red build. That
+ * strip now shrinks and wraps, the same sweep records no offender on any
+ * surface, so the check can finally hold every header child to the invariant
+ * it always claimed to measure. Anything that overruns its header from here is
+ * a new defect, and it fails.
+ */
+export function headerBoxFailures(report: HeaderBoxReport, tag: string): string[] {
+ return report.offenders
+ .map((o) => {
+ const past = [
+ o.pastRight > 0 ? `${o.pastRight}px past its right edge` : '',
+ o.pastLeft > 0 ? `${o.pastLeft}px past its left edge` : '',
+ o.pastTop > 0 ? `${o.pastTop}px above it` : '',
+ o.pastBottom > 0 ? `${o.pastBottom}px below it` : '',
+ ].filter((part) => part !== '');
+ return (
+ `${tag}: ${o.selector} renders outside ${o.header} — ${past.join(', ')}. ` +
+ `The child is ${o.width}x${o.height} CSS px and the header offers ` +
+ `${o.contentWidth}px of content box within ${o.headerWidth}px` +
+ (o.viewportSlack < 0 ? `; ${round1(-o.viewportSlack)}px of it is off-screen` : '') +
+ (o.text === '' ? '' : `. Carrying: "${o.text}"`)
+ );
+ });
+}
+
+function round1(n: number): number {
+ return Math.round(n * 10) / 10;
+}
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 59428ccb3f..baa9196f86 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -8,8 +8,8 @@
"name": "@tracedecay/dashboard",
"version": "0.0.0",
"dependencies": {
- "@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/ibm-plex-sans": "^5.3.0",
+ "@fontsource/chakra-petch": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@radix-ui/react-dialog": "^1.1.4",
"@tanstack/react-query": "^5.62.0",
@@ -27,7 +27,6 @@
"zustand": "^5.0.2"
},
"devDependencies": {
- "@axe-core/playwright": "^4.12.1",
"@playwright/test": "1.61.1",
"@rsbuild/core": "^1.7.6",
"@rsbuild/plugin-react": "^1.1.0",
@@ -112,19 +111,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@axe-core/playwright": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz",
- "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "axe-core": "~4.12.1"
- },
- "peerDependencies": {
- "playwright-core": ">= 1.0.0"
- }
- },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -807,19 +793,19 @@
}
}
},
- "node_modules/@fontsource-variable/archivo": {
+ "node_modules/@fontsource-variable/ibm-plex-sans": {
"version": "5.3.0",
- "resolved": "https://registry.npmjs.org/@fontsource-variable/archivo/-/archivo-5.3.0.tgz",
- "integrity": "sha512-HogK8FJelrD1o7TlZlkIVtHgc20bO5PZRWE7mUeUTdMN055alznQV6/00J00IBeu8FQAH4s3zW9UJNvKExXf+g==",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz",
+ "integrity": "sha512-agG8tXFEo0hD9+J7npa4vbbWult52eMLVaQ6WQRlhs/iCAojrMAoejru85W9HTVXHfyUj96KM7gp/KGAS87XaQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
- "node_modules/@fontsource-variable/ibm-plex-sans": {
+ "node_modules/@fontsource/chakra-petch": {
"version": "5.3.0",
- "resolved": "https://registry.npmjs.org/@fontsource-variable/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz",
- "integrity": "sha512-agG8tXFEo0hD9+J7npa4vbbWult52eMLVaQ6WQRlhs/iCAojrMAoejru85W9HTVXHfyUj96KM7gp/KGAS87XaQ==",
+ "resolved": "https://registry.npmjs.org/@fontsource/chakra-petch/-/chakra-petch-5.3.0.tgz",
+ "integrity": "sha512-QZvbM/q2R6n1pg2x+TJPftZmBNS+ADU37L4Z4L/6UJIz+JZ/SRaEf8usPFRiP7Ht28sIideMu/Bm1SnGlSrwNw==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -2594,16 +2580,6 @@
"node": ">=12"
}
},
- "node_modules/axe-core": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
- "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==",
- "dev": true,
- "license": "MPL-2.0",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
diff --git a/dashboard/package.json b/dashboard/package.json
index 552b8a360f..5096f40696 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -18,8 +18,8 @@
"live:multiproject": "tsx stories/live-multiproject.ts"
},
"dependencies": {
- "@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/ibm-plex-sans": "^5.3.0",
+ "@fontsource/chakra-petch": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@radix-ui/react-dialog": "^1.1.4",
"@tanstack/react-query": "^5.62.0",
diff --git a/dashboard/src/app/RouteChunkBoundary.dom.test.tsx b/dashboard/src/app/RouteChunkBoundary.dom.test.tsx
index 000e5fe631..c6930a46df 100644
--- a/dashboard/src/app/RouteChunkBoundary.dom.test.tsx
+++ b/dashboard/src/app/RouteChunkBoundary.dom.test.tsx
@@ -40,4 +40,39 @@ describe('RouteChunkBoundary', () => {
expect(screen.queryByText('Offline')).toBeNull();
expect(loadCalls).toBeGreaterThan(callsAfterFailure);
});
+
+ /**
+ * Every route mounts this same component type at the same Outlet slot, so a
+ * client-side navigation is a PROP update, not a remount. The boundary must
+ * swap to the new loader's page; pinning `lazy(load)` in the constructor
+ * left the whole app stuck on whichever workspace rendered first.
+ */
+ it('swaps to the new page when the load prop changes without a remount', async () => {
+ const loadBrain = () =>
+ Promise.resolve({ default: () =>
);
}
+
+/** A register divider: the engraved group stamp over its channels. Collapsed
+ * rails keep the hairline so the grouping survives at icon width, they just
+ * lose the word. */
+function RailRegisterLabel({ label }: { label: string }) {
+ return (
+
+ {label}
+
+
+ );
+}
diff --git a/dashboard/src/app/shell/Shell.tsx b/dashboard/src/app/shell/Shell.tsx
index 9f4c887ea6..63cd9f9557 100644
--- a/dashboard/src/app/shell/Shell.tsx
+++ b/dashboard/src/app/shell/Shell.tsx
@@ -3,7 +3,7 @@ import { Outlet } from 'react-router';
import { NavRail } from './NavRail';
import { ScopeBar } from './ScopeBar';
import { ScopeUrlSync } from '../../data/scope/UrlSync.tsx';
-import { QueryActivityStatus, StatusStrip } from './StatusStrip';
+import { QueryActivityStatus, SourceProvenance, StatusStrip } from './StatusStrip';
const CommandPalette = lazy(() =>
import('./CommandPalette').then((m) => ({ default: m.CommandPalette })),
@@ -63,7 +63,14 @@ export function Shell() {
>
- } />
+
+
+
+ >
+ }
+ />
);
diff --git a/dashboard/src/app/shell/StatusStrip.tsx b/dashboard/src/app/shell/StatusStrip.tsx
index cb91dfe311..b54e7daa17 100644
--- a/dashboard/src/app/shell/StatusStrip.tsx
+++ b/dashboard/src/app/shell/StatusStrip.tsx
@@ -1,4 +1,4 @@
-import type { ReactNode } from 'react';
+import { useCallback, useSyncExternalStore, type ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
useEventStreamState,
@@ -53,7 +53,12 @@ function feedReading(sync: ProjectionSync): {
}
}
-/** Telemetry bar for the real `/api/events` connection state. */
+/**
+ * Telemetry bar for the real `/api/events` connection state, drawn as a
+ * segmented subsystem header: every cell is a numbered register (01 LINK,
+ * 02 FEED, 03 SOURCE, 04 QUERY) so the strip reads as the instrument's own
+ * status word rather than as a footer of prose.
+ */
export function StatusStrip({ queryActivity }: { queryActivity?: ReactNode } = {}) {
const { state } = useEventStreamState();
const feed = feedReading(useProjectionSync());
@@ -68,17 +73,17 @@ export function StatusStrip({ queryActivity }: { queryActivity?: ReactNode } = {
className="flex min-h-8 shrink-0 items-stretch border-t border-edge-subtle bg-surface-1"
aria-label="Status"
>
-
+
{link.value}
-
-
+
+
{/*
* One region over the state and its reason together.
*
@@ -110,6 +115,62 @@ export function StatusStrip({ queryActivity }: { queryActivity?: ReactNode } = {
);
}
+/**
+ * Where the readings on screen actually come from, so the strip cannot lie by
+ * omission: a healthy plate over a dead link is a CAPTURED read, not a live
+ * one.
+ *
+ * LIVE the event stream is up; plates follow the daemon.
+ * CAPTURED the stream is down but resolved reads are still on screen —
+ * fixtures, or the last answers before the link dropped. Stamped
+ * in the alert register because it is exactly the state a reader
+ * must not mistake for live.
+ * NO SOURCE the stream is down and nothing has answered; the plates are
+ * empty frames, which is its own honest state.
+ */
+export function SourceProvenance() {
+ const { state } = useEventStreamState();
+ const hasData = useAnyResolvedRead();
+ // Only a LIVE stream earns the live stamp. A connecting stream is a stream
+ // that is not delivering: whatever the plates show meanwhile is a captured
+ // read, and stamping it anything softer would be the strip vouching for
+ // freshness it cannot see.
+ const source =
+ state === 'live'
+ ? { value: 'live', tone: 'bg-state-ready', ink: 'text-text-primary' }
+ : hasData
+ ? { value: 'captured', tone: 'bg-alert', ink: 'text-alert' }
+ : { value: 'no source', tone: 'bg-state-offline', ink: 'text-text-muted' };
+ return (
+
+
+
+ {source.value}
+
+
+ );
+}
+
+/** Whether any read model on screen has resolved with data — the difference
+ * between CAPTURED (plates hold a real read) and NO SOURCE (empty frames). */
+function useAnyResolvedRead(): boolean {
+ const client = useQueryClient();
+ const subscribe = useCallback(
+ (onChange: () => void) => client.getQueryCache().subscribe(onChange),
+ [client],
+ );
+ return useSyncExternalStore(subscribe, () =>
+ client
+ .getQueryCache()
+ .getAll()
+ .some((query) => query.state.data !== undefined),
+ );
+}
+
/** Query-aware cell mounted by Shell, which is inside QueryClientProvider.
* Keeping it separate lets the transport strip remain usable in isolation
* without inventing a second QueryClient. */
@@ -121,7 +182,7 @@ export function QueryActivityStatus() {
if (activeQuery !== undefined) {
return (
-
+
{activeQuery.label}
@@ -145,7 +206,7 @@ export function QueryActivityStatus() {
}
return lastCancellation === null ? null : (
-
+
cancelled · {lastCancellation.label}
@@ -153,9 +214,22 @@ export function QueryActivityStatus() {
);
}
-function Cell({ label, children }: { label: string; children: ReactNode }) {
+/** One numbered register of the strip: segment number engraved first, the
+ * subsystem stamp beside it, the reading after the hairline. */
+function Cell({
+ code,
+ label,
+ children,
+}: {
+ code: string;
+ label: string;
+ children: ReactNode;
+}) {
return (
+
+ {code}
+ {label}{children}
diff --git a/dashboard/src/theme/fonts.css b/dashboard/src/theme/fonts.css
index 462d232ff9..de7fa85dfd 100644
--- a/dashboard/src/theme/fonts.css
+++ b/dashboard/src/theme/fonts.css
@@ -8,10 +8,12 @@
*
* The three faces and why each is here:
*
- * Archivo (display) — SIL OFL 1.1. A signage grotesque with a real width
- * axis. Set expanded at the uppercase letterspaced tiers it reads as an
- * engraved faceplate legend, which is the one thing Inter cannot do at
- * 10px. Two axes (wght + wdth) ship in one file.
+ * Chakra Petch (display) — SIL OFL 1.1. A squared industrial grotesque with
+ * chamfered terminals, drawn for engineering signage. At the 10-11px
+ * uppercase letterspaced tiers this console engraves everywhere, the
+ * clipped corners survive where a round grotesque blurs into anybody's
+ * sans — it is the stamp face of the instrument, the one register the
+ * body face cannot reach.
* IBM Plex Sans (body) — SIL OFL 1.1. Drawn for engineering documentation,
* so it holds its counters at the 11-13px this console actually sets, and
* its flared stems and single-storey detailing are unmistakably not Inter.
@@ -31,27 +33,69 @@
* missing glyph rather than reveal it.
*/
-/* ---- Archivo Variable: display (wght 100-900, wdth 62-125%) ---- */
+/* ---- Chakra Petch: display / stamps (500 legends, 600 titles, 700 marks) ---- */
@font-face {
- font-family: 'Archivo Variable';
+ font-family: 'Chakra Petch';
font-style: normal;
font-display: swap;
- font-weight: 100 900;
- font-stretch: 62% 125%;
- src: url('@fontsource-variable/archivo/files/archivo-latin-ext-wdth-normal.woff2')
- format('woff2-variations');
+ font-weight: 500;
+ src: url('@fontsource/chakra-petch/files/chakra-petch-latin-ext-500-normal.woff2')
+ format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
- font-family: 'Archivo Variable';
+ font-family: 'Chakra Petch';
font-style: normal;
font-display: swap;
- font-weight: 100 900;
- font-stretch: 62% 125%;
- src: url('@fontsource-variable/archivo/files/archivo-latin-wdth-normal.woff2')
- format('woff2-variations');
+ font-weight: 500;
+ src: url('@fontsource/chakra-petch/files/chakra-petch-latin-500-normal.woff2')
+ format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
+ U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
+ U+2215, U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 600;
+ src: url('@fontsource/chakra-petch/files/chakra-petch-latin-ext-600-normal.woff2')
+ format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
+ U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
+ U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 600;
+ src: url('@fontsource/chakra-petch/files/chakra-petch-latin-600-normal.woff2')
+ format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
+ U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
+ U+2215, U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 700;
+ src: url('@fontsource/chakra-petch/files/chakra-petch-latin-ext-700-normal.woff2')
+ format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
+ U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
+ U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 700;
+ src: url('@fontsource/chakra-petch/files/chakra-petch-latin-700-normal.woff2')
+ format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
U+2215, U+FEFF, U+FFFD;
diff --git a/dashboard/src/theme/tailwind.css b/dashboard/src/theme/tailwind.css
index 82aa2c9804..c9f2cf35f0 100644
--- a/dashboard/src/theme/tailwind.css
+++ b/dashboard/src/theme/tailwind.css
@@ -18,6 +18,8 @@
--color-edge-strong: var(--raw-edge-strong);
--color-accent: var(--raw-accent);
--color-accent-emphasis: var(--raw-accent-emphasis);
+ --color-alert: var(--raw-alert);
+ --color-alert-emphasis: var(--raw-alert-emphasis);
--color-state-loading: var(--raw-state-loading);
--color-state-ready: var(--raw-state-ready);
@@ -66,7 +68,7 @@
--font-mono: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', 'Cascadia Mono', monospace;
/* The engraved tier. Only ever reached through this token — never a
* hard-coded family in a component. */
- --font-display: 'Archivo Variable', 'IBM Plex Sans Variable', system-ui, sans-serif;
+ --font-display: 'Chakra Petch', 'IBM Plex Sans Variable', system-ui, sans-serif;
/* Instrument type scale. `3xs` is the annotation size — used only for
* letterspaced uppercase legends and units, never for prose.
@@ -128,13 +130,13 @@
* wears this — small, letterspaced, uppercase, quiet.
*
* This is where the display face earns its keep. At 10px uppercase almost
- * nothing of a face's personality survives except weight and WIDTH, so the
- * display tier is set expanded: an expanded grotesque under heavy tracking is
- * the faceplate-legend idiom, and it is the one register the body face cannot
- * reach. The expansion is capped at `--display-width` because it is paid for
- * in horizontal room — measured across all twelve surfaces at 1440 and 768,
- * 112% costs one extra clipped label out of 464 while 125% costs thirteen,
- * and a clipped label is lost information, which outranks the flourish. */
+ * nothing of a face's personality survives except weight and STROKE GEOMETRY,
+ * and Chakra Petch's chamfered terminals are exactly the detail that does
+ * survive: under heavy tracking the legends read as machine-engraved stamps,
+ * the register a round grotesque cannot reach. `font-stretch` stays declared
+ * so a future variable face with a width axis picks the tuned width up
+ * without touching every call site; Chakra Petch ships one width and
+ * ignores it. */
.td-legend {
font-family: var(--font-display);
font-stretch: var(--display-width);
@@ -311,7 +313,16 @@
* A quantity printed as text has no size; the same quantity given a length
* can be compared across a column without reading a single digit. Every list
* of numbers in this console wears one of these under or beside its value,
- * which is what turns thirty identical rows into a distribution. */
+ * which is what turns thirty identical rows into a distribution.
+ *
+ * The fill is SEGMENTED — 5px cells on a 7px pitch — because a continuous
+ * pill is paint and a row of cells is an instrument register: the same
+ * length now also reads as a count of lit segments, and a wall of meters
+ * stops looking like a bar chart smeared across the page. The mask keeps
+ * the segmentation on whatever hue a call site's `tone` sets. Hairline
+ * (1px) meters are below the cell size and drop the mask via the
+ * height-conditional below — a 1px rule diced into 5px dashes reads as a
+ * dotted border, not a measurement. */
.td-meter {
position: relative;
overflow: hidden;
@@ -322,5 +333,13 @@
inset-block: 0;
left: 0;
background: var(--raw-accent);
+ mask-image: repeating-linear-gradient(
+ to right,
+ black 0 5px,
+ transparent 5px 7px
+ );
+ }
+ .h-px > .td-meter-fill {
+ mask-image: none;
}
}
diff --git a/dashboard/src/theme/tokens.css b/dashboard/src/theme/tokens.css
index a00671dad2..e02da6dab4 100644
--- a/dashboard/src/theme/tokens.css
+++ b/dashboard/src/theme/tokens.css
@@ -28,6 +28,12 @@
--raw-edge-strong: oklch(0.46 0.006 260);
--raw-accent: oklch(0.75 0.15 200);
--raw-accent-emphasis: oklch(0.64 0.16 200);
+ /* The second signal hue: amber is ALERT AND ACTIVITY — elevated but not
+ * broken. Cyan stays the live/selected signal, red stays critical-only;
+ * amber is the register in between, for captured provenance, attention
+ * counts, and activity that wants an eye without an alarm. */
+ --raw-alert: oklch(0.78 0.15 75);
+ --raw-alert-emphasis: oklch(0.68 0.16 65);
/* Graticule: the instrument grid drawn behind canvases and readout fields.
* Must stay at the edge of visibility — discipline, never texture. */
@@ -93,6 +99,7 @@
--raw-graph-text: oklch(0.84 0.035 248);
--raw-graph-edge: oklch(0.43 0.075 245);
--raw-graph-accent: oklch(0.82 0.16 205);
+ --raw-graph-alert: oklch(0.8 0.15 75);
--raw-graph-dim: oklch(0.28 0.035 252);
/* Five layers, front to back: a vignette that pulls the corners down so the
* field has a centre to look INTO rather than an even wash; the central lift
@@ -242,6 +249,9 @@
* type can now wear it. */
--raw-accent: oklch(0.47 0.14 220);
--raw-accent-emphasis: oklch(0.43 0.15 220);
+ /* Amber as ink on paper needs the same darkening the accent got. */
+ --raw-alert: oklch(0.55 0.14 70);
+ --raw-alert-emphasis: oklch(0.5 0.15 65);
--raw-grid: oklch(0.895 0.002 260);
--raw-grid-minor: oklch(0.945 0.001 260);
/* Complete first-class light mappings for every domain state (plan 11a:
@@ -273,6 +283,7 @@
--raw-graph-text: oklch(0.86 0.035 248);
--raw-graph-edge: oklch(0.47 0.075 245);
--raw-graph-accent: oklch(0.84 0.16 205);
+ --raw-graph-alert: oklch(0.8 0.15 75);
--raw-graph-dim: oklch(0.3 0.035 252);
--raw-graph-field:
radial-gradient(125% 125% at 50% 44%, transparent 34%, oklch(0.045 0.012 272 / 0.58) 100%),
@@ -350,6 +361,28 @@
background: var(--raw-graph-field);
}
+/** The night optical window: EVERY analytical drawing in the product — chart,
+ * map, canvas, distribution well — is projected light on this recessed dark
+ * glass, inside either shell theme. The graph canvas gets the full five-layer
+ * field above; this is the flat-glass sibling for the smaller apertures, so a
+ * 70px sparkline pays for a hairline and a vignette rather than nebulae.
+ *
+ * It is a class, not a per-surface style, because the claim it makes is
+ * global: bloom and brightness encodings only mean anything on a dark field,
+ * so no quantitative mark may sit directly on paper. */
+.td-optic {
+ position: relative;
+ background:
+ radial-gradient(130% 130% at 50% 40%, transparent 40%, oklch(0.05 0.012 272 / 0.5) 100%),
+ linear-gradient(160deg, oklch(0.135 0.022 268) 0%, var(--raw-graph-substrate) 60%, oklch(0.08 0.018 272) 100%);
+ border: 1px solid oklch(0.32 0.05 250 / 0.55);
+ box-shadow: var(--shadow-field);
+}
+[data-contrast='more'] .td-optic {
+ background: var(--raw-surface-0);
+ border-color: var(--raw-edge-strong);
+}
+
/** Sensor grain over a field, so no region of it is perfectly even.
*
* Requires a positioned ancestor. Stacked UNDER `.td-scanlines`' own ::after so
diff --git a/dashboard/src/viz/chart/Chart.tsx b/dashboard/src/viz/chart/Chart.tsx
index 6be189eac8..dc1149dea8 100644
--- a/dashboard/src/viz/chart/Chart.tsx
+++ b/dashboard/src/viz/chart/Chart.tsx
@@ -32,10 +32,17 @@ function themedOption(
const style = getComputedStyle(container);
const token = (name: string, fallback: string) =>
style.getPropertyValue(name).trim() || fallback;
- const text = token('--raw-text-secondary', '#aab0bd');
- const muted = token('--raw-text-muted', '#8a90a0');
- const edge = token('--raw-edge-subtle', '#333a46');
- const accent = token('--raw-accent', '#7aa2f7');
+ // Night-window palette, not shell ink: every chart canvas is projected
+ // light on the dark optical glass (`.td-optic`), inside either shell
+ // theme, so its marks sample the graph tokens. Under
+ // `data-contrast='more'` those tokens collapse to the shell palette and
+ // the chart flattens with the rest of the atmosphere.
+ const text = token('--raw-graph-text', '#aab0bd');
+ // Axis labels are the smallest type on the glass, so they take the full
+ // night-text ink; only the ruled split lines drop to the dim register.
+ const muted = text;
+ const edge = token('--raw-graph-edge', '#333a46');
+ const accent = token('--raw-graph-accent', '#7aa2f7');
// The chart's type has to come from the same token as the rest of the app.
// This used to name Inter directly, which meant a chart axis silently kept
// typing in the old face after the design system changed its body face --
@@ -54,7 +61,9 @@ function themedOption(
yAxis: undefined,
grid: { left: 8, right: 8, top: 24, bottom: 8, containLabel: true },
tooltip: {
- backgroundColor: token('--raw-surface-2', '#22252d'),
+ // The tooltip floats over the night glass, so it is dark glass too —
+ // a paper tooltip over the optical window would blow the exposure.
+ backgroundColor: token('--raw-graph-substrate', '#0b0d14'),
borderColor: edge,
textStyle: { color: text, fontSize: 11 },
},
@@ -170,5 +179,19 @@ export const Chart = memo(function Chart({
);
}
- return ;
+ return (
+ // The night optical window: the canvas is inset a hairline from the
+ // glass so axis ink never touches the bezel, and the sensor grain keeps
+ // the field from reading as a flat fill. `td-optic` carries the
+ // positioned ancestor `td-grain` requires.
+
+
+
+ );
});
diff --git a/dashboard/src/workspaces/agents/AgentsPage.dom.test.tsx b/dashboard/src/workspaces/agents/AgentsPage.dom.test.tsx
index f836b89564..cb6a5b44b2 100644
--- a/dashboard/src/workspaces/agents/AgentsPage.dom.test.tsx
+++ b/dashboard/src/workspaces/agents/AgentsPage.dom.test.tsx
@@ -188,7 +188,9 @@ describe('AgentsPage read coverage', () => {
renderAgents();
expect(await screen.findByText('Handoff frontier')).toBeTruthy();
- expect(screen.getByText('Tool activity')).toBeTruthy();
+ // Tool activity lives on the demoted telemetry register, which renders
+ // once its own usage read lands — awaited rather than assumed.
+ expect(await screen.findByText('Tool activity')).toBeTruthy();
expect(screen.getByText('Failure context')).toBeTruthy();
// Tool activity is fed by the diagnostics read that landed.
diff --git a/dashboard/src/workspaces/agents/AgentsPage.transport.dom.test.tsx b/dashboard/src/workspaces/agents/AgentsPage.transport.dom.test.tsx
index 0bb94a1a31..aa3141583c 100644
--- a/dashboard/src/workspaces/agents/AgentsPage.transport.dom.test.tsx
+++ b/dashboard/src/workspaces/agents/AgentsPage.transport.dom.test.tsx
@@ -16,10 +16,10 @@ import { AgentsPage } from './AgentsPage.tsx';
* that always succeeds and varies the body; these vary the transport instead,
* through the same MSW handlers the visual audit resolves its fixtures from.
*
- * Agents is the subject for the whole matrix because its outer boundary wraps
- * the entire page: a failed read has nowhere to hide, so the single rendered
- * state is the page's whole answer, and any invented number would have to
- * appear beside it.
+ * Agents is the subject for the whole matrix because every plate on it is its
+ * own read boundary: under a page-wide fault each plate must report that
+ * fault on its own face, no plate may invent a number, and no two faults with
+ * different meanings may collapse into one reading.
*/
const server = fixtureServer();
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
@@ -68,26 +68,31 @@ const MATRIX: ReadonlyArray<{ fault: HttpFault; kind: FailKind; detail: string |
describe('AgentsPage under HTTP transport faults', () => {
it.each(MATRIX)(
- 'renders $fault as the $kind state and invents no analytics counts',
+ 'renders $fault as the $kind state on every plate and invents no analytics counts',
async ({ fault, kind, detail }) => {
server.use(allRoutesFail(fault));
const { container } = renderAgents();
- const chip = await findChip(container);
- expect(chip.getAttribute('data-state')).toBe(kind satisfies DomainStateKind);
- if (detail) expect(chip.textContent).toContain(detail);
+ const chips = await findFaultChips(container);
+ // Every read boundary on the page reports the same wire fault; a plate
+ // rendering any other state would be inventing a reading.
+ for (const chip of chips) {
+ expect(chip.getAttribute('data-state')).toBe(kind satisfies DomainStateKind);
+ if (detail) expect(chip.textContent).toContain(detail);
+ }
- // The state's own sentence, and neither of the other two failure
- // sentences: an unreachable daemon must not read as a bad payload.
- expect(screen.getByText(GUIDANCE[kind])).toBeTruthy();
+ // The state's own sentence (once per failed plate), and none of the
+ // other failure sentences: an unreachable daemon must not read as a bad
+ // payload.
+ expect(screen.getAllByText(GUIDANCE[kind]).length).toBeGreaterThan(0);
for (const other of Object.keys(GUIDANCE) as FailKind[]) {
- if (other !== kind) expect(screen.queryByText(GUIDANCE[other])).toBeNull();
+ if (other !== kind) expect(screen.queryAllByText(GUIDANCE[other])).toHaveLength(0);
}
- // Nothing was read, so nothing may be reported. The page's success
- // surfaces are absent rather than present-and-empty: no content region,
- // no event-window readouts, and none of the counts the fixtures serve.
- expect(screen.queryByRole('region', { name: 'Agents content' })).toBeNull();
+ // The delegation plates stay on screen as named, failed regions — the
+ // frame is honest — but nothing was read, so nothing may be reported:
+ // no event-window readouts and none of the counts the fixtures serve.
+ expect(screen.getByRole('region', { name: 'Agents content' })).toBeTruthy();
expect(screen.queryByText('Event window')).toBeNull();
expect(screen.queryByText('mcp tool calls')).toBeNull();
expect(screen.queryByText('hook calls')).toBeNull();
@@ -105,8 +110,8 @@ describe('AgentsPage under HTTP transport faults', () => {
server.resetHandlers();
server.use(allRoutesFail(fault));
const view = renderAgents();
- const chip = await findChip(view.container);
- rendered.set(fault, `${chip.getAttribute('data-state')}${chip.textContent}`);
+ const [chip] = await findFaultChips(view.container);
+ rendered.set(fault, `${chip!.getAttribute('data-state')}${chip!.textContent}`);
view.unmount();
}
@@ -129,7 +134,8 @@ describe('AgentsPage under HTTP transport faults', () => {
server.use(faultHandler('*/api/plugins/analytics/diagnostics', 'server_error'));
renderAgents();
- await screen.findByRole('region', { name: 'Agents content' });
+ // The telemetry register renders once its own usage read lands.
+ await screen.findByText('events (capped)');
// The neighbouring read is untouched and still reports its own measurement.
expect(readout('events (capped)')).toBe('10,000');
// The failed one reports nothing, and an em dash is not a quantity.
@@ -163,12 +169,14 @@ function renderAgents() {
);
}
-/** The one state chip a fully failed page renders, once it has settled. */
-async function findChip(container: HTMLElement): Promise {
- await screen.findByText(/^(Error|Offline|Unauthorized|Denied|Unsupported schema)$/);
+/** Every plate's state chip once the fully failed page has settled: each read
+ * boundary reports the fault on its own face, so there are several, and every
+ * one of them is part of the page's answer. */
+async function findFaultChips(container: HTMLElement): Promise {
+ await screen.findAllByText(/^(Error|Offline|Unauthorized|Denied|Unsupported schema)$/);
const chips = [...container.querySelectorAll('[data-state]')];
- expect(chips).toHaveLength(1);
- return chips[0]!;
+ expect(chips.length).toBeGreaterThan(0);
+ return chips;
}
/** The value printed under a readout's engraved legend, or null when that
diff --git a/dashboard/src/workspaces/agents/AgentsPage.tsx b/dashboard/src/workspaces/agents/AgentsPage.tsx
index e00b7fef96..9e8737c6a0 100644
--- a/dashboard/src/workspaces/agents/AgentsPage.tsx
+++ b/dashboard/src/workspaces/agents/AgentsPage.tsx
@@ -10,6 +10,9 @@ import {
AnalyticsUnderusedPayloadV1Schema,
AnalyticsUsageSummaryV1Schema,
type AnalyticsAgentUsageV1,
+ type AnalyticsDiagnosticsPayloadV1,
+ type AnalyticsUnderusedPayloadV1,
+ type AnalyticsUsageSummaryV1,
} from '../../contracts/generated.ts';
import { envelopePayload, useEnvelope } from '../../data/query/useEnvelope.ts';
import { logFraction } from '../../viz/scale.ts';
@@ -111,315 +114,359 @@ export function AgentsPage() {
const attemptFailures = readAttemptFailures(workGraph.isPending ? undefined : workGraph.data);
return (
-
- {(envelope) => {
- const data = envelope.payload;
- const rows: UsageRow[] = data.by_category;
- const dominance = summarizeDominance(rows);
- const diag = envelopePayload(diagnostics.data);
- const diagnosticsRead = diag?.available === false ? undefined : diag ?? undefined;
- // A dash in the diagnostics figures is three different facts: a read
- // still in flight, a read that failed, and a source that answered and
- // declared itself unavailable. Only the last one may be called
- // "unavailable" — the strip used to label all three that way.
- const diagnosticsAbsence = diagnostics.isPending
- ? 'diagnostics still loading'
- : diag == null
- ? 'analytics diagnostics could not be read'
- : diag.available === false
- ? 'analytics diagnostics unavailable'
- : null;
- const window = describeWindow(
- data.available ? data.event_count : diagnosticsRead?.event_count,
- diagnosticsRead?.events_per_hour,
- );
- const hookNote = diagnosticsRead
- ? diagnosticsRead.hook_window.truncated
- ? `recent suffix · ${diagnosticsRead.hook_window.rows_scanned.toLocaleString()} rows scanned`
- : `${diagnosticsRead.hook_window.rows_scanned.toLocaleString()} hook rows scanned`
- : (diagnosticsAbsence ?? 'analytics diagnostics unavailable');
- return (
-