@7365admin1/layer-common 3.2.2-staging.118 → 3.2.2-staging.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/assets/css/primitives.css +168 -0
  2. package/assets/css/screens.css +76 -1
  3. package/components/AccessCard/AvailableStats.vue +24 -16
  4. package/components/AddPassKeyToVisitor.vue +95 -73
  5. package/components/AppDateField.vue +15 -1
  6. package/components/AppField.vue +25 -0
  7. package/components/BuildingUnitFormEdit.vue +5 -1
  8. package/components/Card/DeleteConfirmation.vue +5 -2
  9. package/components/HidAccessLogDashboard.vue +288 -447
  10. package/components/HidIntercomManagement.vue +387 -521
  11. package/components/HidQrCodeConfiguration.vue +293 -369
  12. package/components/HidReaderManagement.vue +197 -320
  13. package/components/HidServiceSettingsPanel.vue +38 -26
  14. package/components/HidUserEnrollment.vue +239 -344
  15. package/components/IncidentReport/Authorities.vue +30 -18
  16. package/components/Nfc/NFCTagForm.vue +5 -1
  17. package/components/ScanVisitorQRCode.vue +64 -28
  18. package/components/SearchVehicleNumberUser.vue +39 -37
  19. package/components/TableMain.vue +50 -1
  20. package/components/VehicleAddSelection.vue +48 -32
  21. package/components/VehicleForm.vue +112 -129
  22. package/components/VehicleUpdateMoreAction.vue +59 -46
  23. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  24. package/components/VisitorFormSelection.vue +40 -37
  25. package/components/VisitorPassKeyQRScanner.vue +65 -18
  26. package/components/VisitorSocketPopUp.vue +21 -13
  27. package/components/VisitorsReportPreview.vue +44 -29
  28. package/package.json +1 -1
  29. package/tools/render-harness/baselines.json +95 -0
  30. package/tools/render-harness/harness-init.ps1 +4 -0
  31. package/utils/status.test.ts +33 -0
  32. package/utils/status.ts +36 -0
  33. package/utils/theme-aa-ledger.ts +142 -0
  34. package/utils/theme.test.ts +95 -34
  35. package/utils/theme.ts +55 -28
@@ -0,0 +1,142 @@
1
+ /**
2
+ * THE LIGHT THEME'S KNOWN AA FAILURES, MEASURED.
3
+ *
4
+ * The owner's 2026-08-14 decision is 100% design fidelity over WCAG AA, so the
5
+ * six light values that had been darkened are back on the handoff and the light
6
+ * theme no longer clears 4.5:1 everywhere. This file is the ledger of exactly
7
+ * what that costs: every pair, its measured ratio, and what it needs.
8
+ *
9
+ * It is data, not prose - `theme.test.ts` asserts each entry still measures what
10
+ * it says, so a later change that drifts a colour (in either direction) fails
11
+ * the build and has to come back here and say so.
12
+ *
13
+ * `run` prints the table: `node --experimental-strip-types utils/theme-aa-ledger.ts`
14
+ */
15
+ import { PALETTE, LIGHT_THEME, DARK_THEME } from "./theme.ts";
16
+
17
+ const luminance = (hex: string): number => {
18
+ const c = [0, 2, 4]
19
+ .map((i) => parseInt(hex.slice(1 + i, 3 + i), 16) / 255)
20
+ .map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
21
+ return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
22
+ };
23
+
24
+ export const contrast = (a: string, b: string): number => {
25
+ const [x, y] = [luminance(a), luminance(b)];
26
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
27
+ };
28
+
29
+ export const over = (fg: string, bg: string, alpha: number): string =>
30
+ "#" +
31
+ [0, 2, 4]
32
+ .map((i) =>
33
+ Math.round(
34
+ parseInt(fg.slice(1 + i, 3 + i), 16) * alpha +
35
+ parseInt(bg.slice(1 + i, 3 + i), 16) * (1 - alpha),
36
+ )
37
+ .toString(16)
38
+ .padStart(2, "0"),
39
+ )
40
+ .join("");
41
+
42
+ const L = PALETTE.light;
43
+
44
+ /** `--thead` is a 2% ink wash over the card, not a colour of its own. */
45
+ export const thead = over("#14161a", L.card, 0.02);
46
+
47
+ /**
48
+ * `--hover` is a 5% ink wash. On the page background it is the fill a DISABLED
49
+ * control sits on (the HID reader identification methods, greyed until Online
50
+ * Mode is on), so `--muted` label on `--hover` is a pair a person actually reads.
51
+ */
52
+ export const hoverOnBg = over("#14161a", L.bg, 0.05);
53
+
54
+
55
+ /** The soft chip backgrounds, exactly as the design states them. */
56
+ export const chip = {
57
+ ok: over("#0f8a62", L.card, 0.1),
58
+ warn: over("#be8c19", L.card, 0.13),
59
+ err: over("#cf4b4b", L.card, 0.1),
60
+ info: over("#3b6fd4", L.card, 0.1),
61
+ };
62
+
63
+ /** `--accent-soft` is transparent, so it takes the colour of what is behind it. */
64
+ export const accentSoft = {
65
+ sidebar: over(L.accent, L.sidebar, 0.12),
66
+ card: over(L.accent, L.card, 0.12),
67
+ };
68
+
69
+ /**
70
+ * Every LIGHT pair a person actually reads. `need` is 4.5 for body text and 3
71
+ * for large text (>=18.66px, or >=14px at 700+) and for non-text edges.
72
+ */
73
+ export const LIGHT_PAIRS: Array<{
74
+ what: string;
75
+ fg: string;
76
+ bg: string;
77
+ need: number;
78
+ }> = [
79
+ { what: "muted on a card (table header text, sub-lines)", fg: L.muted, bg: L.card, need: 4.5 },
80
+ { what: "muted on the page background", fg: L.muted, bg: L.bg, need: 4.5 },
81
+ { what: "muted on a table header band", fg: L.muted, bg: thead, need: 4.5 },
82
+ { what: "muted on the sidebar", fg: L.muted, bg: L.sidebar, need: 4.5 },
83
+ { what: "muted on a disabled control's hover fill", fg: L.muted, bg: hoverOnBg, need: 4.5 },
84
+ { what: "ok label on its own chip", fg: L.ok, bg: chip.ok, need: 4.5 },
85
+ { what: "warn label on its own chip", fg: L.warn, bg: chip.warn, need: 4.5 },
86
+ { what: "err label on its own chip", fg: L.err, bg: chip.err, need: 4.5 },
87
+ { what: "info label on its own chip", fg: L.info, bg: chip.info, need: 4.5 },
88
+ { what: "ok as plain text on a card", fg: L.ok, bg: L.card, need: 4.5 },
89
+ { what: "warn as plain text on a card", fg: L.warn, bg: L.card, need: 4.5 },
90
+ { what: "err as plain text on a card (the Logout row)", fg: L.err, bg: L.card, need: 4.5 },
91
+ { what: "info as plain text on a card", fg: L.info, bg: L.card, need: 4.5 },
92
+ { what: "accent-text as a link on a card", fg: L.accentText, bg: L.card, need: 4.5 },
93
+ { what: "accent-text on accent-soft over the sidebar (active nav item)", fg: L.accentText, bg: accentSoft.sidebar, need: 4.5 },
94
+ { what: "accent-text on accent-soft over a card (active tab, avatar initials)", fg: L.accentText, bg: accentSoft.card, need: 4.5 },
95
+ { what: "white on the filled success colour", fg: "#ffffff", bg: L.ok, need: 4.5 },
96
+ { what: "white on the filled warning colour", fg: "#ffffff", bg: L.warn, need: 4.5 },
97
+ { what: "white on the filled error colour", fg: "#ffffff", bg: L.err, need: 4.5 },
98
+ { what: "white on the filled info colour", fg: "#ffffff", bg: L.info, need: 4.5 },
99
+ { what: "white on primary-button (accent-strong) - NOT reverted", fg: "#ffffff", bg: L.accentStrong, need: 4.5 },
100
+ { what: "text on a card", fg: L.text, bg: L.card, need: 4.5 },
101
+ { what: "text2 on a card", fg: L.text2, bg: L.card, need: 4.5 },
102
+ ];
103
+
104
+ export const measure = () =>
105
+ LIGHT_PAIRS.map((p) => ({
106
+ ...p,
107
+ ratio: Math.round(contrast(p.fg, p.bg) * 100) / 100,
108
+ passes: contrast(p.fg, p.bg) >= p.need,
109
+ }));
110
+
111
+ /** The dark theme was exact as handed over and clears AA - this proves it still does. */
112
+ export const DARK_PAIRS = () => {
113
+ const D = PALETTE.dark;
114
+ const dthead = over("#ffffff", D.card, 0.02);
115
+ return [
116
+ { what: "muted on a card", fg: D.muted, bg: D.card, need: 4.5 },
117
+ { what: "muted on the page background", fg: D.muted, bg: D.bg, need: 4.5 },
118
+ { what: "muted on a table header band", fg: D.muted, bg: dthead, need: 4.5 },
119
+ { what: "muted on the sidebar", fg: D.muted, bg: D.sidebar, need: 4.5 },
120
+ { what: "text on a card", fg: D.text, bg: D.card, need: 4.5 },
121
+ { what: "text2 on a card", fg: D.text2, bg: D.card, need: 4.5 },
122
+ { what: "ok on its chip", fg: D.ok, bg: over("#41c795", D.card, 0.12), need: 4.5 },
123
+ { what: "warn on its chip", fg: D.warn, bg: over("#dcaf4e", D.card, 0.13), need: 4.5 },
124
+ { what: "err on its chip", fg: D.err, bg: over("#e57373", D.card, 0.12), need: 4.5 },
125
+ { what: "info on its chip", fg: D.info, bg: over("#7da6ec", D.card, 0.12), need: 4.5 },
126
+ { what: "accent-text on accent-soft over the sidebar", fg: D.accentText, bg: over(D.accent, D.sidebar, 0.12), need: 4.5 },
127
+ { what: "accent-text on accent-soft over a card", fg: D.accentText, bg: over(D.accent, D.card, 0.12), need: 4.5 },
128
+ ].map((p) => ({ ...p, ratio: Math.round(contrast(p.fg, p.bg) * 100) / 100, passes: contrast(p.fg, p.bg) >= p.need }));
129
+ };
130
+
131
+ if (process.argv[1]?.endsWith("theme-aa-ledger.ts")) {
132
+ const row = (r: { what: string; fg: string; bg: string; ratio: number; need: number; passes: boolean }) =>
133
+ `${r.passes ? "PASS" : "FAIL"} ${String(r.ratio).padStart(5)}:1 / ${r.need} ${r.fg} on ${r.bg} ${r.what}`;
134
+ console.log("LIGHT");
135
+ for (const r of measure()) console.log(" " + row(r));
136
+ console.log("\nDARK");
137
+ for (const r of DARK_PAIRS()) console.log(" " + row(r));
138
+ const f = measure().filter((r) => !r.passes).length;
139
+ console.log(`\n${f} of ${LIGHT_PAIRS.length} light pairs below AA; ${DARK_PAIRS().filter((r) => !r.passes).length} dark.`);
140
+ void LIGHT_THEME;
141
+ void DARK_THEME;
142
+ }
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
3
 
4
4
  import { DARK_THEME, LIGHT_THEME, PALETTE, type TPalette } from "./theme.ts";
5
+ import { DARK_PAIRS, measure } from "./theme-aa-ledger.ts";
5
6
 
6
7
  /**
7
8
  * The dark theme was shipped once with `primary` chosen for the one place it
@@ -262,13 +263,19 @@ test("the dark divider clears the 3:1 a boundary wants", () => {
262
263
  /* ==================================================================== */
263
264
  /* THE REDESIGN PALETTE. */
264
265
  /* */
265
- /* Measured against the design as handed over, the light theme failed */
266
- /* AA on 27 text pairs and the dark theme on 6. The owner's instruction */
267
- /* was "minimal darkening, keep the look", so what these tests hold in */
268
- /* place is BOTH halves of that: every pair a person reads clears AA, */
269
- /* AND the values that were not the problem are still exactly the ones */
270
- /* the design specifies. A later phase that "tidies" a colour back to */
271
- /* its handoff value, or drifts one away from it, fails here. */
266
+ /* The owner's decision on 2026-08-14 is 100% DESIGN FIDELITY over WCAG */
267
+ /* AA, reversing 2026-08-13's "minimal darkening". So the two themes */
268
+ /* are now held to two different standards, on purpose: */
269
+ /* */
270
+ /* DARK - exact as handed over AND fully AA. Asserted below. */
271
+ /* LIGHT - exact as handed over, 16 of 22 read pairs BELOW AA. Not */
272
+ /* asserted as passing; every ratio is PINNED to its measured */
273
+ /* value in `utils/theme-aa-ledger.ts` instead, so the cost is */
274
+ /* recorded rather than hidden and any drift - lighter OR */
275
+ /* darker - still fails the build and has to be declared. */
276
+ /* */
277
+ /* A later phase that "tidies" a light colour to make a contrast test */
278
+ /* green is exactly what the ledger test is here to catch. */
272
279
  /* ==================================================================== */
273
280
 
274
281
  const L = PALETTE.light;
@@ -301,15 +308,16 @@ const soft = (p: TPalette, key: "ok" | "warn" | "err" | "info", on: string) => {
301
308
 
302
309
  const accentSoft = (p: TPalette, on: string) => over(p.accent, on, 0.12);
303
310
 
304
- for (const [name, p] of [
305
- ["light", L],
306
- ["dark", D],
307
- ] as const) {
311
+ /*
312
+ * DARK ONLY. The light theme is measured by the ledger test further down, not
313
+ * asserted here - see the section note above.
314
+ */
315
+ for (const [name, p] of [["dark", D]] as const) {
308
316
  /**
309
317
  * `--muted` is the table header of every table in the product, the
310
- * breadcrumb, and every KPI sub-line. It failed on all four of its surfaces
311
- * in the light theme at 2.99-3.24:1, which is the single most-read text in
312
- * the design.
318
+ * breadcrumb, and every KPI sub-line. The dark theme's `#868c98` clears AA
319
+ * on all four of its surfaces as designed; the light theme's `#8b8f98` does
320
+ * not, at 2.99-3.24:1, and that is the ledger's headline entry.
313
321
  */
314
322
  test(`${name}: muted text is readable on every surface it is used on`, () => {
315
323
  for (const [where, bg] of [
@@ -473,30 +481,83 @@ test("the design's own values are unchanged wherever they already passed", () =>
473
481
  });
474
482
 
475
483
  /**
476
- * Minimal darkening means MINIMAL. Each light value that moved is checked to
477
- * still be recognisably the colour it was - if a later pass "fixes" contrast
478
- * by reaching for a different green, this is what objects.
484
+ * THE SIX THAT WERE DARKENED ON 2026-08-13 ARE BACK ON THE HANDOFF, EXACTLY.
485
+ * Hex equality, not a tolerance: "design-exact" has no tolerance.
479
486
  */
480
- test("the light values that moved are still the same colours", () => {
481
- const moved: Array<[string, string, string]> = [
482
- ["muted", "#8b8f98", L.muted],
483
- ["ok", "#0f8a62", L.ok],
484
- ["warn", "#a97a14", L.warn],
485
- ["err", "#cf4b4b", L.err],
486
- ["info", "#3b6fd4", L.info],
487
- ["accent-strong vs accent", "#5b8def", L.accentStrong],
488
- ];
487
+ test("the light palette is the handoff's, value for value", () => {
488
+ assert.equal(L.muted, "#8b8f98");
489
+ assert.equal(L.ok, "#0f8a62");
490
+ assert.equal(L.warn, "#a97a14");
491
+ assert.equal(L.err, "#cf4b4b");
492
+ assert.equal(L.info, "#3b6fd4");
493
+ // `color-mix(in oklab, #5b8def 75%, #191b1f)`, resolved in a browser.
494
+ assert.equal(L.accentText, "#4a6eb6");
495
+ });
489
496
 
490
- for (const [name, before, after] of moved) {
491
- const shift = contrast(before, after);
492
- assert.ok(
493
- shift < 1.9,
494
- `${name} moved from ${before} to ${after}, a ${shift.toFixed(
495
- 2
496
- )}:1 jump - that is a different colour, not a darkened one`
497
+ /**
498
+ * THE LEDGER. The light theme's known sub-AA pairs, each pinned to the ratio it
499
+ * actually measures. This is the "recorded, not hidden" half of the owner's
500
+ * decision: nobody can quietly darken a value to make a test green, and nobody
501
+ * can lighten one further without this failing and making them write it down.
502
+ *
503
+ * The numbers come from `utils/theme-aa-ledger.ts`, which is also runnable:
504
+ * node --experimental-strip-types utils/theme-aa-ledger.ts
505
+ */
506
+ test("the light theme's sub-AA pairs measure exactly what the ledger says", () => {
507
+ const EXPECTED: Record<string, number> = {
508
+ "muted on a card (table header text, sub-lines)": 3.24,
509
+ "muted on the page background": 2.99,
510
+ "muted on a table header band": 3.1,
511
+ "muted on the sidebar": 3.13,
512
+ "muted on a disabled control's hover fill": 2.71,
513
+ "ok label on its own chip": 3.82,
514
+ "warn label on its own chip": 3.38,
515
+ "err label on its own chip": 3.86,
516
+ "info label on its own chip": 4.2,
517
+ "ok as plain text on a card": 4.35,
518
+ "warn as plain text on a card": 3.83,
519
+ "err as plain text on a card (the Logout row)": 4.41,
520
+ "info as plain text on a card": 4.76,
521
+ "accent-text as a link on a card": 5,
522
+ "accent-text on accent-soft over the sidebar (active nav item)": 4.29,
523
+ "accent-text on accent-soft over a card (active tab, avatar initials)": 4.42,
524
+ "white on the filled success colour": 4.35,
525
+ "white on the filled warning colour": 3.83,
526
+ "white on the filled error colour": 4.41,
527
+ "white on the filled info colour": 4.76,
528
+ "white on primary-button (accent-strong) - NOT reverted": 4.57,
529
+ "text on a card": 17.24,
530
+ "text2 on a card": 9.62,
531
+ };
532
+
533
+ const measured = measure();
534
+ assert.equal(
535
+ measured.length,
536
+ Object.keys(EXPECTED).length,
537
+ "a pair was added to or removed from the ledger without updating this test"
538
+ );
539
+
540
+ for (const row of measured) {
541
+ assert.equal(
542
+ row.ratio,
543
+ EXPECTED[row.what],
544
+ `${row.what}: ledger says ${EXPECTED[row.what]}:1, measured ${row.ratio}:1`
497
545
  );
498
- assert.ok(shift > 1, `${name} did not move at all`);
499
546
  }
547
+
548
+ const failing = measured.filter((r) => !r.passes).length;
549
+ assert.equal(
550
+ failing,
551
+ 17,
552
+ `${failing} light pairs are below AA; the recorded, accepted count is 17. ` +
553
+ "If this moved, the owner's design-fidelity decision needs re-stating."
554
+ );
555
+ });
556
+
557
+ /** The dark theme carries no such debt, and that is worth asserting out loud. */
558
+ test("the dark theme has no sub-AA pairs at all", () => {
559
+ const bad = DARK_PAIRS().filter((r) => !r.passes);
560
+ assert.deepEqual(bad, [], "dark mode was exact AND compliant - keep it that way");
500
561
  });
501
562
 
502
563
  /**
package/utils/theme.ts CHANGED
@@ -27,28 +27,55 @@
27
27
  */
28
28
 
29
29
  /**
30
- * WHAT MOVED FROM THE DESIGN, AND WHY - the owner's decision, 2026-08-13:
31
- * "minimal darkening, keep the look".
30
+ * THE SIX LIGHT VALUES ARE BACK ON THE DESIGN - the owner's decision,
31
+ * 2026-08-14, which REVERSES the 2026-08-13 one recorded below.
32
32
  *
33
- * Measured against the design as handed over, the LIGHT theme failed AA on 27
34
- * text pairs and the DARK theme on 6. Six light values move, none by enough to
35
- * read as a different colour, and the dark theme's own palette is untouched:
33
+ * 2026-08-13 was "minimal darkening, keep the look": the light theme failed AA
34
+ * on 27 text pairs as handed over, so six values were darkened just enough to
35
+ * clear 4.5:1. 2026-08-14 is "100% design fidelity over WCAG AA" - the darker
36
+ * `--muted` in particular flattened the hierarchy the design is built on, and
37
+ * that was visible on staging. All six go back to the handoff, exactly:
36
38
  *
37
- * --muted #8b8f98 -> #6b7079 (table headers, breadcrumbs, sub-lines)
38
- * --ok #0f8a62 -> #0e7c58 (-10%)
39
- * --warn #a97a14 -> #8e6611 (-16%)
40
- * --err #cf4b4b -> #bc4444 (-9%)
41
- * --info #3b6fd4 -> #3869c9 (-5%)
42
- * --accent-text #4a6eb6 -> #476aaf (-4%)
39
+ * --muted #6b7079 -> #8b8f98
40
+ * --ok #0e7c58 -> #0f8a62
41
+ * --warn #8e6611 -> #a97a14
42
+ * --err #bc4444 -> #cf4b4b
43
+ * --info #3869c9 -> #3b6fd4
44
+ * --accent-text #476aaf -> #4a6eb6
43
45
  *
44
- * The soft chip BACKGROUNDS are left exactly as designed. The design states
45
- * them literally rather than deriving them from the foreground, so darkening a
46
- * label does not drag its chip with it - a Pending chip is the same colour it
47
- * was, its text is simply readable on it.
46
+ * WHAT THAT KNOWINGLY COSTS: 16 of the 22 light pairs a person reads are now
47
+ * below AA. `utils/theme-aa-ledger.ts` is the full list with measured ratios and
48
+ * `theme.test.ts` pins every one of them, so the failures are recorded rather
49
+ * than hidden and any later drift - in either direction - still fails the build.
50
+ * The worst, measured (WCAG 2.1, alpha composited):
51
+ *
52
+ * --muted on the page bg 2.99:1 (needs 4.5)
53
+ * --muted on a table header band 3.10:1
54
+ * --muted on the sidebar 3.13:1
55
+ * --muted on a card 3.24:1 - every table header, breadcrumb,
56
+ * KPI sub-line and empty state
57
+ * --warn on its Pending chip 3.38:1
58
+ * --ok on its chip 3.82:1
59
+ * --warn / white on warn fill 3.83:1
60
+ * --err on its chip 3.86:1
61
+ *
62
+ * Six light pairs still pass: text, text2, info as text, white on the info fill,
63
+ * accent-text as a link, and white on primary-button.
64
+ *
65
+ * The DARK theme was exact as handed over and still is, and its AA assertions
66
+ * are untouched - dark mode remains fully compliant.
67
+ *
68
+ * `--accent-strong` (#4b74c4) is NOT one of the six and does not move. It is an
69
+ * addition, not a darkening: the design puts white on `--accent` at 3.23:1, and
70
+ * `--accent` itself is still the design's exact colour everywhere it is a line,
71
+ * a fill edge or a soft background. See the note below.
72
+ *
73
+ * The soft chip BACKGROUNDS are, and always were, exactly as designed.
48
74
  *
49
75
  * `--accent-text` is `color-mix(in oklab, var(--accent) 75%, var(--text))` in
50
76
  * the design. It is resolved to a plain hex here because Vuetify needs a
51
- * concrete colour and because a test cannot measure a `color-mix()`.
77
+ * concrete colour and because a test cannot measure a `color-mix()`; the hex
78
+ * below is that mix resolved in a browser, not an approximation.
52
79
  */
53
80
 
54
81
  /**
@@ -122,22 +149,22 @@ export const PALETTE = {
122
149
  text: "#191b1f",
123
150
  /** Secondary text. 9.62:1 on a card. */
124
151
  text2: "#41454d",
125
- /** Table headers, breadcrumbs, KPI sub-lines. 4.98:1 on a card. */
126
- muted: "#6b7079",
127
- /** Completed / Paid / Available / Open. 4.58:1 on its chip. */
128
- ok: "#0e7c58",
129
- /** Pending. 4.56:1 on its chip. */
130
- warn: "#8e6611",
131
- /** In Use / Closed / Checkout. 4.55:1 on its chip. */
132
- err: "#bc4444",
133
- /** Role and info tags. 4.58:1 on its chip. */
134
- info: "#3869c9",
152
+ /** Table headers, breadcrumbs, KPI sub-lines. The design's. 3.24:1 on a card - below AA, knowingly. */
153
+ muted: "#8b8f98",
154
+ /** Completed / Paid / Available / Open. The design's. 3.82:1 on its chip - below AA, knowingly. */
155
+ ok: "#0f8a62",
156
+ /** Pending. The design's. 3.38:1 on its chip - below AA, knowingly. */
157
+ warn: "#a97a14",
158
+ /** In Use / Closed / Checkout. The design's. 3.86:1 on its chip - below AA, knowingly. */
159
+ err: "#cf4b4b",
160
+ /** Role and info tags. The design's. 4.20:1 on its chip - below AA, knowingly. */
161
+ info: "#3b6fd4",
135
162
  /** The brand accent, exactly as designed. Never carries white lettering. */
136
163
  accent: "#5b8def",
137
164
  /** The only place the accent is darkened: fills with a white label. 4.57:1. */
138
165
  accentStrong: "#4b74c4",
139
- /** Links, active nav item, active tab. 4.57:1 on `--accent-soft`. */
140
- accentText: "#476aaf",
166
+ /** Links, active nav item, active tab. The design's oklab mix, resolved. 4.29:1 on `--accent-soft` over the sidebar - below AA, knowingly. */
167
+ accentText: "#4a6eb6",
141
168
  /** Labels on the filled colours above. Light's fills carry white. */
142
169
  onFill: "#ffffff",
143
170
  },