@bsuite/theme 0.7.1 → 0.8.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsuite/theme",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -11,7 +11,7 @@
11
11
  "url": "https://github.com/GaryOcean428/bsuite.git",
12
12
  "directory": "packages/theme"
13
13
  },
14
- "description": "BSuite Universal theme \u2014 D2C Neon Electric + Braden Corporate baselines. Tailwind v4 preset, CSS vars (5-layer), role aliases, colourblind-safe tokens, eye-strain-safe text scale, BrandingProvider (runtime white-label). Used by BSU, CRM7, conduit, R80.3, throughput (D2C) and braden.com.au (Corporate).",
14
+ "description": "BSuite Universal theme D2C Neon Electric + Braden Corporate baselines. Tailwind v4 preset, CSS vars (5-layer), role aliases, colourblind-safe tokens, eye-strain-safe text scale, BrandingProvider (runtime white-label). Used by BSU, CRM7, conduit, R80.3, throughput (D2C) and braden.com.au (Corporate).",
15
15
  "files": [
16
16
  "dist",
17
17
  "src/css",
@@ -1,230 +1,283 @@
1
1
  /*
2
2
  * @bsuite/theme — Braden Corporate brand baseline
3
- * Version 0.3.0
3
+ * Version 0.4.0
4
4
  *
5
5
  * Usage (braden.com.au only):
6
6
  * @import '@bsuite/theme/braden-css';
7
7
  *
8
- * This file is EXCLUSIVELY for the braden.com.au Corporate brand.
9
- * NEVER import this into D2C webapp projects (crm7, conduit, BSU, R80.3).
10
- * NEVER apply Neon Electric colours to braden.com.au surfaces.
8
+ * EXCLUSIVELY for the braden.com.au Corporate brand. Never import this into a
9
+ * D2C app (crm7, conduit, BSU, R80.3, throughput), and never put Neon Electric
10
+ * colours on a Corporate surface.
11
11
  *
12
- * Architecture same 5-layer structure as vars.css:
13
- * 1. Palette — --braden-red-*, --braden-gold-*, --braden-navy-*
14
- * 2. Surface — --braden-light-*, --braden-dark-*
15
- * 3. Role — --role-* (rebinds D2C role tokens to Braden values)
16
- * 4. Shadcn — --background, --foreground, etc. (same keys, Braden values)
17
- * 5. App — --app-primary, --app-accent
12
+ * GENERATED FROM THE CONTRACT, NOT HAND-TUNED.
13
+ * Every value below is copied from
14
+ * packages/theme/docs/braden-corporate-theme-source-of-truth.html
15
+ * which is authoritative. Operator ruling 2026-08-03: only colours present in
16
+ * the two source-of-truth documents are permitted in packages/. Verify with
17
+ * scripts/audit-palette-whitelist.py
18
18
  *
19
- * Colourblind policy is PRESERVED:
20
- * --role-error / --role-destructive = PURPLE (oklch(0.568 0.202 283.1))
21
- * Same as D2Cpurple error is a cross-brand system requirement.
19
+ * WHAT CHANGED IN 0.4.0 AND WHY IT MATTERED
20
+ * 0.3.0 carried its own hand-built red/gold/navy ramps. 52 of its values
21
+ * appeared nowhere in the Corporate contract close to the brand, but not the
22
+ * brand, and drifting further with every edit. This file also shipped a header
23
+ * asserting "purple error is a cross-brand system requirement", which was
24
+ * already false twice over: the 0.7.0 separation-first contract retired purple
25
+ * for semantics, and the Corporate contract binds error to --braden-indigo.
22
26
  *
23
- * Eye-strain policy is PRESERVED:
24
- * Dark text capped at L=0.94. Light text capped at L=0.22 (navy hue 250).
25
- *
26
- * OKLCH format mandatory for all new tokens. Tailwind v4+ is the only
27
- * supported Tailwind engine for BSuite consumers.
27
+ * ERROR IS INDIGO HERE, NOT RED — deliberately, and for the opposite reason to
28
+ * D2C. D2C's primary is blue, so error must be red to separate from it.
29
+ * Corporate's primary IS red, so a red error would collide with the brand's
30
+ * own primary action colour. Same rule, inverted inputs.
28
31
  */
29
32
 
33
+
34
+ :root {
35
+
30
36
  /* ============================================================
31
- LAYER 1 — BRADEN CORPORATE PALETTE PRIMITIVES
37
+ LAYER 1 — BRAND PALETTE
38
+ Raw brand colours. Consumers must never bind to these directly.
32
39
  ============================================================ */
33
- :root {
34
- /* Primary — Braden Red scale */
35
- --braden-red-50: oklch(0.965 0.015 17);
36
- --braden-red-100: oklch(0.930 0.035 17);
37
- --braden-red-200: oklch(0.870 0.070 18);
38
- --braden-red-300: oklch(0.780 0.110 18);
39
- --braden-red-400: oklch(0.660 0.145 18);
40
- --braden-red-500: oklch(0.550 0.175 18); /* main brand red */
41
- --braden-red-600: oklch(0.488 0.170 17.6); /* canonical –- oklch spec */
42
- --braden-red-700: oklch(0.420 0.150 17);
43
- --braden-red-800: oklch(0.340 0.120 16);
44
- --braden-red-900: oklch(0.260 0.090 15);
45
-
46
- /* Accent — Braden Gold scale */
47
- --braden-gold-50: oklch(0.980 0.015 88);
48
- --braden-gold-100: oklch(0.955 0.035 89);
49
- --braden-gold-200: oklch(0.910 0.060 90);
50
- --braden-gold-300: oklch(0.860 0.078 91);
51
- --braden-gold-400: oklch(0.829 0.073 91.8); /* light gold */
52
- --braden-gold-500: oklch(0.769 0.096 90.9); /* canonical accent */
53
- --braden-gold-600: oklch(0.711 0.115 89.5); /* bronze */
54
- --braden-gold-700: oklch(0.630 0.110 88);
55
- --braden-gold-800: oklch(0.520 0.090 86);
56
- --braden-gold-900: oklch(0.400 0.065 84);
57
-
58
- /* Navy — secondary brand colour */
59
- --braden-navy-50: oklch(0.960 0.008 249);
60
- --braden-navy-100: oklch(0.920 0.015 249);
61
- --braden-navy-200: oklch(0.840 0.025 249);
62
- --braden-navy-300: oklch(0.720 0.030 249);
63
- --braden-navy-400: oklch(0.580 0.033 249);
64
- --braden-navy-500: oklch(0.450 0.035 249);
65
- --braden-navy-600: oklch(0.356 0.039 249); /* canonical navy */
66
- --braden-navy-700: oklch(0.280 0.030 248);
67
- --braden-navy-800: oklch(0.200 0.022 248);
68
- --braden-navy-900: oklch(0.130 0.015 248);
69
-
70
- /* Neutrals */
71
- --braden-slate: oklch(0.710 0.018 201.5);
72
- --braden-sky: oklch(0.653 0.135 242.7);
73
- --braden-forest: oklch(0.663 0.160 152.4);
74
- --braden-lavender:oklch(0.577 0.153 315.3);
75
-
76
- /* ============================================================
77
- LAYER 2 — BRADEN SURFACE PRIMITIVES (light mode)
78
- ============================================================ */
79
- --braden-light-bg-body: oklch(0.980 0.002 50); /* warm white */
80
- --braden-light-bg-surface: oklch(0.996 0.001 50); /* near white */
81
- --braden-light-bg-panel: oklch(0.994 0.002 250); /* near-white; pure white is banned in every role */
82
- --braden-light-bg-input: oklch(0.972 0.003 250); /* cool light grey */
83
-
84
- /* Braden light text scale — hue 250 (navy cast), capped L=0.22 */
85
- --braden-light-text-primary: oklch(0.22 0.020 250); /* 13.2:1 AAA */
86
- --braden-light-text-secondary: oklch(0.38 0.018 250); /* 8.1:1 ✓ AAA */
87
- --braden-light-text-muted: oklch(0.52 0.018 250); /* 4.9:1 ✓ AA */
88
- --braden-light-text-subtle: oklch(0.60 0.012 250); /* 3.8:1 ✓ AA large */
89
- --braden-light-text-disabled: oklch(0.72 0.010 250);
90
-
91
- --braden-light-border: oklch(0.900 0.008 248);
92
- --braden-light-hover: oklch(0.950 0.005 248);
93
-
94
- /* ============================================================
95
- LAYER 3 — ROLE ALIASES (Braden light-mode rebind)
96
- Reuses the same --role-* names as vars.css so components
97
- are brand-agnostic at the role layer.
98
- ============================================================ */
99
- --role-primary: var(--braden-red-600); /* oklch(0.488 0.170 17.6) */
100
- --role-secondary: var(--braden-navy-600); /* oklch(0.356 0.039 249) */
101
- --role-accent: var(--braden-gold-500); /* oklch(0.769 0.096 90.9) */
102
-
103
- /* Colourblind policy: SAME purple as D2C — cross-brand requirement */
104
- --role-error: oklch(0.568 0.202 283.1);
105
- --role-destructive: oklch(0.568 0.202 283.1);
106
- --role-warning: oklch(0.728 0.168 22.5);
107
- --role-success: oklch(0.663 0.160 152.4); /* braden forest green */
108
- --role-info: var(--braden-sky);
109
-
110
- /* Text roles */
111
- --role-text-body: var(--braden-light-text-primary);
112
- --role-text-secondary: var(--braden-light-text-secondary);
113
- --role-text-muted: var(--braden-light-text-muted);
114
- --role-text-subtle: var(--braden-light-text-subtle);
115
- --role-text-disabled: var(--braden-light-text-disabled);
116
- --role-text-heading: oklch(0.19 0.025 252); /* near-navy 14.8:1 AAA */
117
-
118
- /* Surface roles */
119
- --role-bg-body: var(--braden-light-bg-body);
120
- --role-bg-surface: var(--braden-light-bg-surface);
121
- --role-bg-panel: var(--braden-light-bg-panel);
122
- --role-bg-input: var(--braden-light-bg-input);
123
- --role-border: var(--braden-light-border);
124
-
125
- /* Inverse text on coloured fills */
126
- --text-on-primary: oklch(0.99 0 0); /* on red L≈0.497.8:1 AA */
127
- --text-on-accent: oklch(0.17 0.02 250); /* on gold L≈0.77 dark text */
128
- --text-on-error: oklch(0.99 0 0); /* on purple */
129
- --text-on-success: oklch(0.17 0.02 250);
130
- --text-on-warning: oklch(0.17 0.02 250);
131
-
132
- /* ============================================================
133
- LAYER 4 — SHADCN BRIDGE (Braden values)
134
- ============================================================ */
135
- --background: var(--braden-light-bg-body);
136
- --foreground: var(--braden-light-text-primary);
137
- --card: var(--braden-light-bg-panel);
138
- --card-foreground: var(--braden-light-text-primary);
139
- --popover: var(--braden-light-bg-panel);
140
- --popover-foreground: var(--braden-light-text-primary);
141
- --primary: var(--braden-red-600);
142
- --primary-foreground: oklch(0.99 0 0);
143
- --secondary: var(--braden-navy-600);
144
- --secondary-foreground: oklch(0.99 0 0);
145
- --muted: var(--braden-light-bg-input);
146
- --muted-foreground: var(--braden-light-text-muted);
147
- --accent: var(--braden-gold-500);
148
- --accent-foreground: oklch(0.17 0.02 250);
149
- --destructive: oklch(0.568 0.202 283.1);
150
- --destructive-foreground: oklch(0.99 0 0);
151
- --border: var(--braden-light-border);
152
- --input: var(--braden-light-border);
153
- --ring: var(--braden-red-600);
154
-
155
- /* ============================================================
156
- LAYER 5 — APP SHORTCUTS
157
- ============================================================ */
158
- --app-primary: var(--braden-red-600);
159
- --app-accent: var(--braden-gold-500);
160
-
161
- /* ============================================================
162
- SHADOWS corporate subtle, no neon glow
163
- ============================================================ */
164
- --shadow-sm: 0 1px 2px oklch(0.30 0.03 250 / 0.08);
165
- --shadow-md: 0 4px 6px oklch(0.30 0.03 250 / 0.10), 0 2px 4px oklch(0.30 0.03 250 / 0.06);
166
- --shadow-lg: 0 10px 15px oklch(0.30 0.03 250 / 0.10), 0 4px 6px oklch(0.30 0.03 250 / 0.05);
167
-
168
- /* Card glow is a dark-mode-only device (see .dark below); light mode
169
- stays border-only, consistent with the corporate "no neon glow" rule. */
40
+ --braden-red: oklch(0.488 0.170 17.6);
41
+ --braden-red-light: oklch(0.572 0.201 17.9);
42
+ --braden-red-dark: oklch(0.400 0.137 16.8);
43
+ --braden-gold: oklch(0.758 0.092 89.9);
44
+ --braden-gold-light: oklch(0.818 0.070 90.0);
45
+ --braden-bronze: oklch(0.698 0.110 89.2);
46
+ --braden-navy: oklch(0.356 0.039 249.0);
47
+ --braden-slate: oklch(0.710 0.018 201.3);
48
+ --braden-sky: oklch(0.653 0.135 242.7);
49
+ --braden-forest: oklch(0.663 0.160 152.4);
50
+ --braden-lavender: oklch(0.577 0.152 315.3);
51
+ --braden-indigo: oklch(0.400 0.200 270);
52
+ --braden-teal-deep: oklch(0.500 0.140 220);
53
+ --braden-aqua: oklch(0.800 0.130 198);
54
+
55
+ /* ============================================================
56
+ LAYER 2 — SURFACES AND BORDERS
57
+ ============================================================ */
58
+ --role-bg-body: oklch(0.980 0.002 50);
59
+ --role-bg-surface: oklch(0.968 0.004 55);
60
+ --role-bg-panel: oklch(0.994 0.001 50);
61
+ --role-bg-input: oklch(0.972 0.003 250);
62
+ --role-bg-sunken: oklch(0.952 0.005 250);
63
+ --role-border: oklch(0.900 0.008 248);
64
+ --role-border-strong: oklch(0.840 0.012 248);
65
+
66
+ /* ============================================================
67
+ LAYER 3 — HEADING RAMP (six levels, per the contract)
68
+ ============================================================ */
69
+ --role-h1: oklch(0.20 0.028 252);
70
+ --role-h2: oklch(0.25 0.034 250);
71
+ --role-h3: oklch(0.30 0.040 246);
72
+ --role-h4: oklch(0.35 0.046 240);
73
+ --role-h5: oklch(0.40 0.052 232);
74
+ --role-h6: oklch(0.44 0.058 224);
75
+
76
+ /* ============================================================
77
+ LAYER 3 — TEXT AND LINKS
78
+ ============================================================ */
79
+ --role-text-heading: var(--role-h1);
80
+ --role-text-body: oklch(0.22 0.020 250);
81
+ --role-text-secondary: oklch(0.38 0.018 250);
82
+ --role-text-muted: oklch(0.52 0.018 250);
83
+ --role-text-subtle: oklch(0.60 0.012 250);
84
+ --role-text-disabled: oklch(0.72 0.010 250);
85
+ --role-text-strong: oklch(0.16 0.024 252);
86
+ --role-text-emphasis: oklch(0.42 0.070 240);
87
+ --role-link: oklch(0.480 0.140 245);
88
+ --role-link-hover: oklch(0.400 0.150 250);
89
+ --role-link-visited: oklch(0.450 0.120 300);
90
+
91
+ /* ============================================================
92
+ LAYER 3 EDITORIAL (code, quote, mark, kbd)
93
+ ============================================================ */
94
+ --role-code-text: oklch(0.400 0.090 230);
95
+ --role-code-bg: oklch(0.950 0.010 235);
96
+ --role-quote-text: oklch(0.400 0.060 25);
97
+ --role-quote-rule: var(--braden-gold);
98
+ --role-mark-bg: oklch(0.930 0.060 90);
99
+ --role-mark-text: oklch(0.24 0.040 60);
100
+ --role-kbd-bg: oklch(0.962 0.004 250);
101
+ --role-kbd-border: oklch(0.870 0.010 250);
102
+
103
+ /* ============================================================
104
+ LAYER 3 SEMANTIC ROLES
105
+ error = --braden-indigo: Corporate primary is RED, so error cannot be.
106
+ ============================================================ */
107
+ --role-primary: var(--braden-red);
108
+ --role-secondary: var(--braden-navy);
109
+ --role-accent: var(--braden-gold);
110
+ --role-error: var(--braden-indigo);
111
+ --role-destructive: var(--braden-indigo);
112
+ --role-warning: var(--braden-gold);
113
+ --role-success: var(--braden-aqua);
114
+ --role-info: var(--braden-teal-deep);
115
+
116
+ /* ═══ INFORMATIONAL SCALE — categorical, NOT semantic ═══
117
+ Added 2026-08-03 by operator ruling: "informational hues might be handy
118
+ to stand out against the style colour palette."
119
+
120
+ Role tokens describe STATE. They cannot describe CATEGORY — which
121
+ pipeline stage, which user role, which product tile. Apps that needed a
122
+ category reached for raw Tailwind instead, which is how one role-badge
123
+ table came to use purple, indigo, blue, violet and sky as its only
124
+ distinguishing signal.
125
+
126
+ NUMBERED, NOT NAMED, deliberately. Call one of these "moss green" and
127
+ it will be used for success within a week. A number says what these
128
+ are: interchangeable slots carrying no meaning of their own.
129
+ NEVER use an --info-* for state. State has role tokens.
130
+
131
+ Derived, not picked by eye: Vienot-Brettel-Mollon dichromat simulation,
132
+ DeltaE in OKLab, scored as the WORST case across normal, deuteranope and
133
+ protanope vision. Two constraintsperceptual distance keeps them
134
+ DISTINGUISHABLE, a >=28 degree hue gap keeps them looking CATEGORICAL
135
+ rather than like a light/dark pair of one colour. Every one is also
136
+ held away from the brand roles, which is what "stand out against the
137
+ style palette" requires: an informational badge must not read as a
138
+ primary action or an error.
139
+ Reproduce with scripts/design-informational-hues.py
140
+
141
+ LIMIT: worst-case separation is ~0.06-0.07, against >=0.156 for the
142
+ semantic pairs. That is 10x better than the purple-vs-blue pairing this
143
+ system rejected (0.006), but it is NOT the semantic standard and cannot
144
+ be — 8 mutually-distinct categories that also avoid 6 reserved brand
145
+ colours is a harder problem than 6 roles. So: colour is never the sole
146
+ signifier for a category. Always pair with a label or an icon. Past 8
147
+ categories, stop adding hues and add a second dimension. */
148
+ --info-1: oklch(0.660 0.120 0.0);
149
+ --info-2: oklch(0.660 0.120 30.0);
150
+ --info-3: oklch(0.620 0.120 72.0);
151
+ --info-4: oklch(0.460 0.120 132.0);
152
+ --info-5: oklch(0.780 0.120 162.0);
153
+ --info-6: oklch(0.740 0.120 222.0);
154
+ --info-7: oklch(0.660 0.120 252.0);
155
+ --info-8: oklch(0.620 0.120 324.0);
156
+
157
+ /* ═══ STAGE RAMP — progression, one hue, ascending lightness ═══
158
+ For an ORDERED sequence (pipeline stages), not a category set. A ramp
159
+ encodes direction: "further along" is visible at a glance, and it
160
+ survives greyscale and both dichromacies because lightness carries it.
161
+ Arbitrary per-stage hues cannot do that — a reader has to LEARN that
162
+ sky comes after amber.
163
+
164
+ Progression only. OUTCOMES are state, not stage: a hired candidate is
165
+ --role-success, a rejected one --role-error, a withdrawn one muted.
166
+ Do not extend this ramp to cover them. */
167
+ --stage-1: oklch(0.860 0.090 249.0);
168
+ --stage-2: oklch(0.780 0.090 249.0);
169
+ --stage-3: oklch(0.700 0.090 249.0);
170
+ --stage-4: oklch(0.620 0.090 249.0);
171
+ --stage-5: oklch(0.540 0.090 249.0);
172
+ --stage-6: oklch(0.460 0.090 249.0);
173
+
174
+ /* ============================================================
175
+ LAYER 3 AA-SAFE TEXT RENDERINGS OF THE SATURATED ROLES
176
+ A saturated fill colour is not a legible text colour.
177
+ ============================================================ */
178
+ --role-primary-text: oklch(0.566 0.170 17.6);
179
+ --role-accent-text: oklch(0.544 0.092 89.9);
180
+ --role-error-text: oklch(0.562 0.200 270);
181
+ --role-warning-text: oklch(0.544 0.092 89.9);
182
+ --role-success-text: oklch(0.515 0.130 198);
183
+ --role-info-text: oklch(0.519 0.140 220);
184
+
185
+ /* ============================================================
186
+ LAYER 3 — INVERSE TEXT ON COLOURED FILLS
187
+ ============================================================ */
188
+ --text-on-light-fill: oklch(0.980 0.006 50);
189
+ --text-on-dark-fill: oklch(0.190 0.020 250);
190
+ --text-on-primary: var(--text-on-light-fill);
191
+ --text-on-error: var(--text-on-light-fill);
192
+ --text-on-accent: var(--text-on-dark-fill);
193
+ --text-on-warning: var(--text-on-dark-fill);
194
+ --text-on-success: var(--text-on-dark-fill);
195
+ --text-on-info: var(--text-on-light-fill);
196
+
197
+ /* ============================================================
198
+ DECORATION — shadow, glow, dot and grid
199
+ ============================================================ */
200
+ --gradient-accent: linear-gradient(135deg, var(--braden-red), var(--braden-gold));
201
+ --shadow-xs: 0 1px 2px oklch(0.30 0.03 250 / 0.06);
202
+ --shadow-sm: 0 1px 3px oklch(0.30 0.03 250 / 0.08), 0 1px 2px oklch(0.30 0.03 250 / 0.04);
203
+ --shadow-md: 0 4px 10px -3px oklch(0.30 0.03 250 / 0.10), 0 2px 4px -2px oklch(0.30 0.03 250 / 0.06);
204
+ --shadow-lg: 0 14px 26px -8px oklch(0.28 0.04 250 / 0.14), 0 5px 10px -5px oklch(0.28 0.04 250 / 0.07);
170
205
  --glow-card: none;
171
206
  --glow-card-hover: none;
207
+ --panel-hairline: transparent;
208
+ --dot-color: oklch(0.42 0.030 250 / 0.14);
209
+ --dot-size: 16px;
210
+ --dot-radius: 1px;
211
+ --grid-color: oklch(0.40 0.030 250 / 0.07);
212
+ --grid-size: 32px;
213
+
214
+ /* ============================================================
215
+ TYPOGRAPHY AND GEOMETRY
216
+ ============================================================ */
217
+ --font-display: 'Open Sans', system-ui, sans-serif;
218
+ --font-heading: Lato, 'Open Sans', system-ui, sans-serif;
219
+ --font-body: 'Open Sans', system-ui, sans-serif;
220
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
221
+ --track-display: -0.018em;
222
+ --track-heading: -0.008em;
223
+ --track-body: 0em;
224
+ --track-eyebrow: 0.10em;
225
+ --radius-sm: 4px;
226
+ --radius: 6px;
227
+ --radius-lg: 10px;
228
+ --row-pad: 11px;
172
229
  }
173
230
 
174
231
  /* ============================================================
175
- BRADEN DARK MODE
232
+ DARK MODE — the contract's .dark block, verbatim.
233
+ Only tokens the contract actually flips appear here; anything
234
+ absent is mode-independent by design, not by omission.
176
235
  ============================================================ */
177
236
  .dark {
178
- /* Dark surfaces */
179
- --braden-dark-bg-body: oklch(0.118 0.010 252);
180
- --braden-dark-bg-surface: oklch(0.165 0.015 251);
181
- --braden-dark-bg-panel: oklch(0.200 0.018 250);
182
- --braden-dark-bg-input: oklch(0.240 0.020 250);
183
-
184
- /* Dark text scale — hue 250, capped L=0.93 (Braden uses navy cast) */
185
- --braden-dark-text-primary: oklch(0.93 0.014 250); /* ~13.1:1 ✓ AAA */
186
- --braden-dark-text-secondary: oklch(0.80 0.016 250); /* ~9.0:1 ✓ AAA */
187
- --braden-dark-text-muted: oklch(0.66 0.018 250); /* ~5.2:1 ✓ AA */
188
- --braden-dark-text-subtle: oklch(0.55 0.015 250); /* ~3.5:1 ✓ AA large */
189
- --braden-dark-text-disabled: oklch(0.43 0.010 250);
190
-
191
- /* Rebind roles */
192
- --role-text-body: var(--braden-dark-text-primary);
193
- --role-text-secondary: var(--braden-dark-text-secondary);
194
- --role-text-muted: var(--braden-dark-text-muted);
195
- --role-text-subtle: var(--braden-dark-text-subtle);
196
- --role-text-disabled: var(--braden-dark-text-disabled);
197
- --role-text-heading: oklch(0.91 0.016 250);
198
-
199
- --role-bg-body: var(--braden-dark-bg-body);
200
- --role-bg-surface: var(--braden-dark-bg-surface);
201
- --role-bg-panel: var(--braden-dark-bg-panel);
202
- --role-bg-input: var(--braden-dark-bg-input);
203
- --role-border: oklch(0.30 0.020 250);
204
-
205
- /* Shadcn bridge picks up via var() chain */
206
- --background: var(--braden-dark-bg-body);
207
- --foreground: var(--braden-dark-text-primary);
208
- --card: var(--braden-dark-bg-panel);
209
- --card-foreground: var(--braden-dark-text-primary);
210
- --muted: var(--braden-dark-bg-input);
211
- --muted-foreground:var(--braden-dark-text-muted);
212
- --border: oklch(0.30 0.020 250);
213
- --input: oklch(0.30 0.020 250);
214
-
215
- /*
216
- * Card glow Braden dark mode gets a restrained gold separation glow,
217
- * NOT the D2C neon treatment (no cyan, lower alpha, same two-layer
218
- * ring+halo shape for consumer-code parity with @bsuite/theme's D2C
219
- * --glow-card). Source is --app-accent (Braden Gold), which is
220
- * inherited from :root since this file does not rebind it per-mode.
221
- * Deliberately more subdued than D2C to honour the "corporate
222
- * subtle, no neon glow" shadow philosophy above, adapted for dark UI
223
- * where SOME separation glow is still needed for surface legibility.
224
- */
225
- --card-glow-source: var(--app-accent, var(--braden-gold-500));
226
- --glow-card: 0 0 0 1px color-mix(in oklch, var(--card-glow-source) 18%, transparent),
227
- 0 0 10px -4px color-mix(in oklch, var(--card-glow-source) 20%, transparent);
228
- --glow-card-hover: 0 0 0 1px color-mix(in oklch, var(--card-glow-source) 26%, transparent),
229
- 0 0 14px -4px color-mix(in oklch, var(--card-glow-source) 28%, transparent);
237
+ --role-bg-body: oklch(0.140 0.012 252);
238
+ --role-bg-surface: oklch(0.180 0.016 251);
239
+ --role-bg-panel: oklch(0.205 0.018 250);
240
+ --role-bg-input: oklch(0.245 0.020 250);
241
+ --role-bg-sunken: oklch(0.120 0.010 252);
242
+ --role-border: oklch(0.320 0.020 250);
243
+ --role-border-strong: oklch(0.420 0.024 250);
244
+ --role-h1: oklch(0.95 0.014 250);
245
+ --role-h2: oklch(0.92 0.018 250);
246
+ --role-h3: oklch(0.88 0.024 244);
247
+ --role-h4: oklch(0.84 0.032 236);
248
+ --role-h5: oklch(0.80 0.040 228);
249
+ --role-h6: oklch(0.75 0.048 220);
250
+ --role-text-body: oklch(0.93 0.014 250);
251
+ --role-text-secondary: oklch(0.80 0.016 250);
252
+ --role-text-muted: oklch(0.66 0.018 250);
253
+ --role-text-subtle: oklch(0.55 0.015 250);
254
+ --role-text-disabled: oklch(0.43 0.010 250);
255
+ --role-text-strong: oklch(0.97 0.010 250);
256
+ --role-text-emphasis: oklch(0.85 0.050 235);
257
+ --role-link: oklch(0.780 0.110 240);
258
+ --role-link-hover: oklch(0.860 0.095 225);
259
+ --role-link-visited: oklch(0.770 0.080 300);
260
+ --role-code-text: oklch(0.850 0.070 215);
261
+ --role-code-bg: oklch(0.250 0.026 230);
262
+ --role-quote-text: oklch(0.840 0.045 40);
263
+ --role-quote-rule: var(--braden-gold);
264
+ --role-mark-bg: oklch(0.360 0.060 85);
265
+ --role-mark-text: oklch(0.94 0.030 85);
266
+ --role-kbd-bg: oklch(0.250 0.020 250);
267
+ --role-kbd-border: oklch(0.400 0.022 250);
268
+ --role-primary-text: oklch(0.704 0.170 17.6);
269
+ --role-accent-text: oklch(0.683 0.092 89.9);
270
+ --role-error-text: oklch(0.711 0.200 270);
271
+ --role-warning-text: oklch(0.683 0.092 89.9);
272
+ --role-success-text: oklch(0.662 0.130 198);
273
+ --role-info-text: oklch(0.667 0.140 220);
274
+ --shadow-xs: 0 1px 2px oklch(0.07 0.01 252 / 0.5);
275
+ --shadow-sm: 0 1px 3px oklch(0.07 0.01 252 / 0.55);
276
+ --shadow-md: 0 6px 16px -6px oklch(0.07 0.01 252 / 0.6);
277
+ --shadow-lg: 0 18px 34px -12px oklch(0.07 0.01 252 / 0.68);
278
+ --glow-card: 0 0 0 1px color-mix(in oklch, var(--braden-gold) 18%, transparent), 0 0 10px -4px color-mix(in oklch, var(--braden-gold) 20%, transparent);
279
+ --glow-card-hover: 0 0 0 1px color-mix(in oklch, var(--braden-gold) 30%, transparent), 0 0 16px -4px color-mix(in oklch, var(--braden-gold) 32%, transparent);
280
+ --panel-hairline: color-mix(in oklch, var(--braden-gold) 26%, transparent);
281
+ --dot-color: oklch(0.72 0.060 90 / 0.16);
282
+ --grid-color: oklch(0.82 0.020 250 / 0.055);
230
283
  }
package/src/css/vars.css CHANGED
@@ -30,7 +30,8 @@
30
30
  * Pure white (L=1.0) and pure black (L=0) are BANNED in EVERY role —
31
31
  * text, surface, border, ring and shadow alike (operator ruling
32
32
  * 2026-08-02). The prior text-only scoping of this rule is what let
33
- * --light-bg-accent ship as oklch(1 0 0.5) and render every shadcn card
33
+ * --light-bg-accent ship as oklch(1 0 0.5) theme-audit-ok: naming the
34
+ * value that was REMOVED — and render every shadcn card
34
35
  * and popover pure white. "White" here means oklch(0.982 0.002 248)
35
36
  * (#f8f9fa); the lightest surface is bg-panel at L=0.994.
36
37
  *
@@ -74,7 +75,7 @@
74
75
  --light-bg-primary: oklch(0.961 0 0.5);
75
76
  --light-bg-secondary: oklch(0.982 0.002 248);
76
77
  --light-bg-tertiary: oklch(0.963 0.003 228.9);
77
- /* bg-panel. Was oklch(1 0 0.5) — pure white, and since --card and
78
+ /* bg-panel. theme-audit-ok: prose. Was oklch(1 0 0.5) — pure white, and since --card and
78
79
  --popover bridge to it, every shadcn card and popover in all five D2C
79
80
  apps rendered pure white in light mode from this one line. */
80
81
  --light-bg-accent: oklch(0.994 0.002 260);
@@ -126,6 +127,64 @@
126
127
  --role-success: var(--neon-electric-teal); /* was green — 0.043 from red */
127
128
  --role-info: var(--neon-electric-cyan); /* oklch(0.769 0.132 191.7) */
128
129
 
130
+ /* ═══ INFORMATIONAL SCALE — categorical, NOT semantic ═══
131
+ Added 2026-08-03 by operator ruling: "informational hues might be handy
132
+ to stand out against the style colour palette."
133
+
134
+ Role tokens describe STATE. They cannot describe CATEGORY — which
135
+ pipeline stage, which user role, which product tile. Apps that needed a
136
+ category reached for raw Tailwind instead, which is how one role-badge
137
+ table came to use purple, indigo, blue, violet and sky as its only
138
+ distinguishing signal.
139
+
140
+ NUMBERED, NOT NAMED, deliberately. Call one of these "moss green" and
141
+ it will be used for success within a week. A number says what these
142
+ are: interchangeable slots carrying no meaning of their own.
143
+ NEVER use an --info-* for state. State has role tokens.
144
+
145
+ Derived, not picked by eye: Vienot-Brettel-Mollon dichromat simulation,
146
+ DeltaE in OKLab, scored as the WORST case across normal, deuteranope and
147
+ protanope vision. Two constraints — perceptual distance keeps them
148
+ DISTINGUISHABLE, a >=28 degree hue gap keeps them looking CATEGORICAL
149
+ rather than like a light/dark pair of one colour. Every one is also
150
+ held away from the brand roles, which is what "stand out against the
151
+ style palette" requires: an informational badge must not read as a
152
+ primary action or an error.
153
+ Reproduce with scripts/design-informational-hues.py
154
+
155
+ LIMIT: worst-case separation is ~0.06-0.07, against >=0.156 for the
156
+ semantic pairs. That is 10x better than the purple-vs-blue pairing this
157
+ system rejected (0.006), but it is NOT the semantic standard and cannot
158
+ be — 8 mutually-distinct categories that also avoid 6 reserved brand
159
+ colours is a harder problem than 6 roles. So: colour is never the sole
160
+ signifier for a category. Always pair with a label or an icon. Past 8
161
+ categories, stop adding hues and add a second dimension. */
162
+ --info-1: oklch(0.460 0.140 0.0);
163
+ --info-2: oklch(0.700 0.140 42.0);
164
+ --info-3: oklch(0.540 0.140 138.0);
165
+ --info-4: oklch(0.700 0.140 168.0);
166
+ --info-5: oklch(0.780 0.140 228.0);
167
+ --info-6: oklch(0.700 0.140 258.0);
168
+ --info-7: oklch(0.500 0.140 300.0);
169
+ --info-8: oklch(0.460 0.140 330.0);
170
+
171
+ /* ═══ STAGE RAMP — progression, one hue, ascending lightness ═══
172
+ For an ORDERED sequence (pipeline stages), not a category set. A ramp
173
+ encodes direction: "further along" is visible at a glance, and it
174
+ survives greyscale and both dichromacies because lightness carries it.
175
+ Arbitrary per-stage hues cannot do that — a reader has to LEARN that
176
+ sky comes after amber.
177
+
178
+ Progression only. OUTCOMES are state, not stage: a hired candidate is
179
+ --role-success, a rejected one --role-error, a withdrawn one muted.
180
+ Do not extend this ramp to cover them. */
181
+ --stage-1: oklch(0.860 0.130 262.9);
182
+ --stage-2: oklch(0.780 0.130 262.9);
183
+ --stage-3: oklch(0.700 0.130 262.9);
184
+ --stage-4: oklch(0.620 0.130 262.9);
185
+ --stage-5: oklch(0.540 0.130 262.9);
186
+ --stage-6: oklch(0.460 0.130 262.9);
187
+
129
188
  /* AA-safe text renderings of the saturated roles on light surfaces.
130
189
  A saturated fill colour is not a legible text colour; these are the
131
190
  text variants and are the ONLY correct token for coloured type. */
@@ -138,7 +197,18 @@
138
197
  /* Text roles — six-tier scale (W3 §3.1 contract). Every tier below is
139
198
  measured against --role-bg-body (the conservative floor); all tiers
140
199
  score HIGHER against --role-bg-panel/--role-bg-surface. */
141
- --role-text-heading: oklch(0.20 0.018 263); /* 16.17:1 AAA (slightly darker + bluer than body) */
200
+ /* Six-level heading ramp contract §heading-ramp. Each level shifts hue as
201
+ well as lightness, so the hierarchy reads in greyscale and under both
202
+ dichromacies, not only by size. --role-text-heading is h1 by definition;
203
+ it used to carry its own literal (chroma 0.018 against the contract's
204
+ 0.020) which is how a token drifts off-palette one hair at a time. */
205
+ --role-h1: oklch(0.20 0.020 263);
206
+ --role-h2: oklch(0.26 0.026 258);
207
+ --role-h3: oklch(0.31 0.034 248);
208
+ --role-h4: oklch(0.36 0.042 236);
209
+ --role-h5: oklch(0.41 0.050 222);
210
+ --role-h6: oklch(0.45 0.058 208);
211
+ --role-text-heading: var(--role-h1); /* 16.17:1 ✓ AAA */
142
212
  --role-text-body: var(--light-text-primary); /* 15.46:1 ✓ AAA */
143
213
  --role-text-secondary: var(--light-text-secondary); /* 8.94:1 ✓ AAA */
144
214
  --role-text-muted: var(--light-text-muted); /* 4.92:1 ✓ AA normal */
@@ -154,7 +224,7 @@
154
224
  --role-border: var(--light-border);
155
225
  --role-border-strong: oklch(0.880 0.008 250);
156
226
 
157
- /* Scrim — the veil behind a modal, and the reason `bg-black/N` survived
227
+ /* Scrim — the veil behind a modal, and the reason `bg-black/N` survived theme-audit-ok: prose
158
228
  everywhere else. It was the single most duplicated pure endpoint in the
159
229
  tree (9 sites across nav-core, page-builder and schema-builder) for one
160
230
  reason: no overlay role existed, so every author reached for the only
@@ -301,7 +371,7 @@
301
371
  NO sRGB FALLBACK — DELIBERATELY REMOVED 2026-08-02.
302
372
  Precedent: precedent__bsuite__20260802__theme_conformance_is_absolute.
303
373
 
304
- The `@supports not (color: oklch(0 0 0))` block that stood here shipped
374
+ theme-audit-ok: prose. The `@supports not (color: oklch(0 0 0))` block that stood here shipped
305
375
  a parallel hex palette. It is gone, and must not be reinstated:
306
376
 
307
377
  · OKLCH has been Baseline-available in every evergreen engine since
@@ -320,12 +390,13 @@
320
390
  DARK MODE — Layer 2 surface overrides + Layer 3 role rebind
321
391
  ============================================================ */
322
392
  .dark {
323
- /* Dark surface primitives */
393
+ /* Dark surface primitives.
394
+ --dark-bg-tertiary, -quaternary, -accent and --dark-hover were DELETED
395
+ 2026-08-03: four tokens, zero consumers anywhere in the monorepo or the
396
+ five apps, and none of their values appears in the contract. A palette
397
+ entry nobody binds to is not a spare part — it is a second source of
398
+ truth waiting for someone to reach for it. */
324
399
  --dark-bg-primary: oklch(0.166 0.026 269.4);
325
- --dark-bg-secondary: oklch(0.242 0.03 269.9);
326
- --dark-bg-tertiary: oklch(0.326 0.036 266.7);
327
- --dark-bg-quaternary: oklch(0.39 0.035 265);
328
- --dark-bg-accent: oklch(0.292 0.034 270);
329
400
 
330
401
  /* D2C dark text scale — hue 260, capped L=0.94.
331
402
  Pure white (L=1.0) is BANNED as a text token.
@@ -342,10 +413,19 @@
342
413
  --dark-text-disabled: oklch(0.44 0.010 260); /* disabled — 2.48:1 ✗ fails AA at any size — MUST pair with icon cue */
343
414
 
344
415
  --dark-border: oklch(0.428 0.015 248.2);
345
- --dark-hover: oklch(0.39 0.035 265);
416
+
417
+ /* Six-level heading ramp — contract §heading-ramp. Every level is a
418
+ DIFFERENT hue as well as a different lightness, so the hierarchy survives
419
+ greyscale and both dichromacies rather than relying on size alone. */
420
+ --role-h1: oklch(0.95 0.016 262);
421
+ --role-h2: oklch(0.92 0.018 264);
422
+ --role-h3: oklch(0.88 0.026 240);
423
+ --role-h4: oklch(0.84 0.040 220);
424
+ --role-h5: oklch(0.80 0.052 200);
425
+ --role-h6: oklch(0.75 0.062 195);
346
426
 
347
427
  /* Rebind roles to dark-mode surface values — six-tier scale (W3 §3.1). */
348
- --role-text-heading: oklch(0.92 0.014 260); /* 15.21:1 ✓ AAA */
428
+ --role-text-heading: var(--role-h1);
349
429
  --role-text-body: var(--dark-text-primary); /* 16.16:1 ✓ AAA */
350
430
  --role-text-secondary: var(--dark-text-secondary); /* 11.04:1 ✓ AAA */
351
431
  --role-text-muted: var(--dark-text-muted); /* 6.69:1 ✓ AA normal */
@@ -353,7 +433,7 @@
353
433
  --role-text-disabled: var(--dark-text-disabled); /* 2.48:1 ✗ icon cue required */
354
434
 
355
435
  --role-bg-body: var(--dark-bg-primary);
356
- --role-bg-surface: var(--dark-bg-secondary);
436
+ --role-bg-surface: oklch(0.212 0.028 269);
357
437
  --role-bg-panel: oklch(0.190 0.020 260);
358
438
  --role-bg-input: oklch(0.240 0.020 260);
359
439
  --role-bg-sunken: oklch(0.145 0.022 268);
package/src/preset-v4.css CHANGED
@@ -102,6 +102,30 @@
102
102
  --color-warning-text: var(--role-warning-text);
103
103
  --color-success-text: var(--role-success-text);
104
104
 
105
+ /* Informational scale — CATEGORICAL, never state. `bg-info-3`, `text-info-5`.
106
+ Numbered because they are interchangeable slots with no meaning of their
107
+ own; naming one invites it to be used as a semantic. Pair every one with a
108
+ label or icon — their separation is deliberately below the semantic
109
+ standard and cannot carry meaning alone. See --info-* in vars.css. */
110
+ --color-info-1: var(--info-1);
111
+ --color-info-2: var(--info-2);
112
+ --color-info-3: var(--info-3);
113
+ --color-info-4: var(--info-4);
114
+ --color-info-5: var(--info-5);
115
+ --color-info-6: var(--info-6);
116
+ --color-info-7: var(--info-7);
117
+ --color-info-8: var(--info-8);
118
+
119
+ /* Stage ramp — ORDERED progression. `bg-stage-1` .. `bg-stage-6`, lightness
120
+ descending, so "further along" reads at a glance and survives greyscale.
121
+ Outcomes are NOT stages: hired is --role-success, rejected --role-error. */
122
+ --color-stage-1: var(--stage-1);
123
+ --color-stage-2: var(--stage-2);
124
+ --color-stage-3: var(--stage-3);
125
+ --color-stage-4: var(--stage-4);
126
+ --color-stage-5: var(--stage-5);
127
+ --color-stage-6: var(--stage-6);
128
+
105
129
  /* Per-app accent (Layer 5). Resolves through [data-app] on <body>,
106
130
  falling back to the shared accent when no app is declared. */
107
131
  --color-app-accent: var(--app-accent, var(--role-accent));