@cloudgrid-io/ui 0.12.0 → 0.14.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/dist/theme.css CHANGED
@@ -66,6 +66,10 @@
66
66
  --color-page: var(--cg-surface-page);
67
67
  --color-page-alt: var(--cg-surface-page-alt);
68
68
  --color-subtle: var(--cg-surface-subtle);
69
+ /* The neutral raised ground (#151). `subtle` is the lavender tint and `card`
70
+ is white, so before this there was no name for "one step off white, no
71
+ hue" and a raised panel had to flatten or take a cast. */
72
+ --color-raised: var(--cg-surface-raised);
69
73
  --color-control: var(--cg-surface-control);
70
74
  --color-panel: var(--cg-surface-panel);
71
75
  --color-code: var(--cg-surface-code);
@@ -137,6 +141,26 @@
137
141
  --color-expired-fill: var(--cg-expired-fill);
138
142
  --color-danger-fill: var(--cg-danger-fill);
139
143
 
144
+ /* The other two rungs of the chip (#149, #150). `-fill` is the ground, the
145
+ bare name is the dot, `-border` is the hairline and `-strong` is the word.
146
+ Mapped in the same pass as the tokens, for the reason the block above
147
+ gives: a token whose utility lands a release later is a token the first
148
+ consumer reaches for and does not find.
149
+
150
+ The grid is whole as of #154: danger and unhealthy have borders now, so the
151
+ absence this comment used to record is gone. --color-offline-fill is still
152
+ the one deliberate hole, and still for #111's reason. */
153
+ --color-online-border: var(--cg-online-border);
154
+ --color-online-strong: var(--cg-online-strong);
155
+ --color-expired-border: var(--cg-expired-border);
156
+ --color-expired-strong: var(--cg-expired-strong);
157
+ --color-pending-border: var(--cg-pending-border);
158
+ --color-pending-strong: var(--cg-pending-strong);
159
+ --color-unhealthy-border: var(--cg-unhealthy-border);
160
+ --color-unhealthy-strong: var(--cg-unhealthy-strong);
161
+ --color-danger-border: var(--cg-danger-border);
162
+ --color-danger-strong: var(--cg-danger-strong);
163
+
140
164
  /* ---- lines ---- */
141
165
  --color-hairline: var(--cg-border-hairline);
142
166
  --color-line: var(--cg-border-control);
@@ -251,6 +275,50 @@
251
275
  --shadow-hover: var(--cg-shadow-hover);
252
276
  --shadow-panel: var(--cg-shadow-panel);
253
277
  --shadow-focus: var(--cg-ring-focus);
278
+ /* The live dot's halo (#154), paired with --cg-live-halo the way --shadow-focus
279
+ is paired with --cg-focus-ring. */
280
+ --shadow-live: var(--cg-ring-live);
281
+
282
+ /* Tailwind's OWN three rungs, pointed at the brand's (#155).
283
+
284
+ This block is not a rename and not a convenience. Until it existed, the
285
+ elevation of this package's own components was decided by the CONSUMER:
286
+ dropdown-menu is authored with `shadow-md` and `shadow-lg`, select and
287
+ combobox with `shadow-md`, tabs with `shadow-sm`, and theme.css declared
288
+ none of those three names. So in a consumer that declares nothing, every
289
+ library dropdown and select renders Tailwind stock — measured,
290
+ `0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a` for shadow-lg — which
291
+ is far heavier and greyer than anything this brand ships. Nothing errors and
292
+ nothing looks broken; the components are just not on the brand.
293
+
294
+ The console was carrying three lines exactly like these in its own
295
+ globals.css to hold this package's components on this package's elevation.
296
+ A consumer paying to fix the library is the shape #149, #150 and #151 all
297
+ had, and the answer is the same: it belongs in the token file.
298
+
299
+ Forwarded, not repointed. The alternative was to rewrite the four component
300
+ class strings onto `shadow-card` / `shadow-raised`, which would have changed
301
+ the compiled class NAMES and so changed what the console renders today. This
302
+ way the compiled rules are byte-identical to what the console's own forwards
303
+ produce, and every other consumer gets the brand values without knowing this
304
+ was ever a problem. Proof in docs/test-results/.
305
+
306
+ The role mapping is the console's, unchanged: sm and md are the card
307
+ elevation, lg is the overlay lift. sm and md landing on one value is not an
308
+ oversight — this system contains one card elevation, and the console had
309
+ already collapsed its own three-tier ramp onto it.
310
+
311
+ xs, xl and 2xl are deliberately left on Tailwind stock. No component here
312
+ uses them, and forwarding a rung nothing consumes is a value invented to
313
+ complete a grid.
314
+
315
+ KNOWN: a forwarded rung loses Tailwind's --tw-shadow-color fallback, so
316
+ `shadow-red-500/20` stops tinting shadow-md. That is already true of
317
+ shadow-card and shadow-raised, and this palette has no colour a shadow would
318
+ be tinted with. */
319
+ --shadow-sm: var(--cg-shadow-card);
320
+ --shadow-md: var(--cg-shadow-card);
321
+ --shadow-lg: var(--cg-shadow-raised);
254
322
 
255
323
  /* ---- spacing ----
256
324
  --spacing sets the 4px base, so the numeric utilities (p-4, gap-6) line up
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "state": {
55
- "$description": "state: what an entity IS, not what it means editorially.\n\nAdopted from the Console rather than invented, 2026-08-24 (#96). The Console\nhad already chosen and contrast-tested these — --color-live and\n--color-attention — while the brand carried no state colour at all beyond the\ndanger placeholder. Taking its values means the Console's appearance does not\nchange when it starts consuming this package, and the two stop disagreeing\nabout what 'live' looks like.\n\nEach carries per-surface values in surface-panel and surface-ink, for the same\nreason danger does: measured against all three grounds, no single value works.\nThe live green is 1.72:1 on the purple panel, and the brand's own lime is\n1.54:1 on white. A flat value would be invisible somewhere.\n\nNo --cg-offline-fill, deliberately. Measured, the pair does not work: the\nmuted neutral on a neutral tint is 2.90:1, under the 3:1 bar for a dot or a\nborder. Rather than invent a value to satisfy the ratio, an unplugged chip\nuses the existing surface-card ground with border-control, and --cg-offline\nis the dot on that ground, where it measures 3.28:1. Absence of state should\nnot need a colour of its own.\n\npending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a\nnew value. Same amber today, separate name, so the two can diverge later with\na one-line edit here and no change in any consumer. Aliases compile to var(),\nso the flip survives.\n\nunhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and\nit is the one tone here that was searched for rather than adopted. Founder\ndecision: expired is terminal, unhealthy is live-and-fixable, and sharing a\ntoken would mean retuning one silently retunes the other. It is a warmer,\nbrighter gold on the same ramp as the amber rather than a new hue family,\nbecause green/amber/red is the traffic light an operator already reads and\n'serving but degraded' is its middle rung. Its own note carries the numbers\nand the three families that were measured and rejected.",
55
+ "$description": "state: what an entity IS, not what it means editorially.\n\nAdopted from the Console rather than invented, 2026-08-24 (#96). The Console\nhad already chosen and contrast-tested these — --color-live and\n--color-attention — while the brand carried no state colour at all beyond the\ndanger placeholder. Taking its values means the Console's appearance does not\nchange when it starts consuming this package, and the two stop disagreeing\nabout what 'live' looks like.\n\nEach carries per-surface values in surface-panel and surface-ink, for the same\nreason danger does: measured against all three grounds, no single value works.\nThe live green is 1.72:1 on the purple panel, and the brand's own lime is\n1.54:1 on white. A flat value would be invisible somewhere.\n\nNo --cg-offline-fill, deliberately. Measured, the pair does not work: the\nmuted neutral on a neutral tint is 2.90:1, under the 3:1 bar for a dot or a\nborder. Rather than invent a value to satisfy the ratio, an unplugged chip\nuses the existing surface-card ground with border-control, and --cg-offline\nis the dot on that ground, where it measures 3.28:1. Absence of state should\nnot need a colour of its own.\n\npending joined them on 2026-08-24 (#114) as an ALIAS of expired rather than a\nnew value. Same amber today, separate name, so the two can diverge later with\na one-line edit here and no change in any consumer. Aliases compile to var(),\nso the flip survives.\n\nunhealthy joined them on 2026-08-25 (#122) as a REAL value, not an alias, and\nit is the one tone here that was searched for rather than adopted. Founder\ndecision: expired is terminal, unhealthy is live-and-fixable, and sharing a\ntoken would mean retuning one silently retunes the other. It is a warmer,\nbrighter gold on the same ramp as the amber rather than a new hue family,\nbecause green/amber/red is the traffic light an operator already reads and\n'serving but degraded' is its middle rung. Its own note carries the numbers\nand the three families that were measured and rejected.\n\nTHREE RUNGS PER STATUS, from 2026-09-04 (#149, #150). A chip is a fill, a\nhairline around it, a dot and a word, and until now the set carried only the\nfirst two of those four. -border is the hairline and -strong is the word; the\nbare name stays the dot it has always been.\n\n <status>-fill the chip ground\n <status>-border the hairline around it, a pale tint\n <status> the dot. An indicator, so 3:1, never text\n <status>-strong the word on the fill. Text, so 4.5:1\n\nThe values are the Console's, measured and shipped, not new ones: #149 asked\nfor the rung and console#579 had already been forced to leave two bands as\nliterals for the want of it. Only unhealthy-strong and danger-strong are\nderived, each for a stated reason in its own note.\n\nFOUR RUNGS PER STATUS as of 2026-09-05 (#154). --cg-danger-border and\n--cg-unhealthy-border were the two holes #152 left, on the reasoning above: at\nthe time nothing drew either one, so a value would have been invented to complete\na grid rather than to serve a chip. That note said what would change the answer —\n\"add them when something needs them, with the number that made the choice\" — and\nthis is it. Both are DERIVED rather than adopted, because neither has a shipped\nliteral anywhere, and each carries its number in its own note.\n\nThe derivation is one rule, and it is the two adopted borders' own measured\nposition rather than a recipe: a -border is its own tone's hue, at the lightness\nthat reproduces the 1.18-1.19:1 those two hold against their own fills, carrying\nthe mean of their chroma (OKLCH 0.0998) or the most sRGB has at that hue and\nlightness, whichever is smaller. Danger's hue hits the second clause and\nunhealthy's does not; both notes show the sweep.\n\noffline has no -border and no -strong either, and for the reason it has no\nfill: an unplugged chip sits on the card ground with border-control, and its\nword is --cg-text-primary. Absence of state still does not need a colour.\n\nOn the panel and on ink, -border resolves to the surface's own hairline and\n-strong to its own primary text. A dark ground has no darker rung to reach for,\nand the light tints these scopes carry measure 3.03:1 to 3.60:1 on the panel's\nchip fill, which is a dot and not a word. Worth knowing before retuning: on ink\nthe tints DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so a designer who\nwants the word tinted there changes five entries in surface-ink.json. On the\npanel it is not available at any value — white is 4.31:1 on that fill and white\nis the lightest colour there is, so AA on a panel chip needs the FILL to move.",
56
56
  "online": {
57
57
  "$value": "#00a05c",
58
58
  "$type": "color",
@@ -149,6 +149,104 @@
149
149
  "css": "--cg-danger-fill"
150
150
  }
151
151
  }
152
+ },
153
+ "online-border": {
154
+ "$value": "#abefc6",
155
+ "$type": "color",
156
+ "$description": "The hairline around an online chip. From the Console's OK band, where it has\nshipped as #abefc6 on #ecfdf3 (console#579).\n\nIts own rung rather than the tone, and console#579 is why. Both systems carried\n{tone, fill} pairs and no third value, so converting the band meant sending the\nborder AND the word to --cg-online. Measured, that is not a repaint: the word\nfell from 7.55:1 to 3.06:1 and the border went from this pale tint to full\nsaturation. The band stayed a literal instead. This token is what lets it move.\n\n1.19:1 against --cg-online-fill. A hairline is not text and not an indicator, so\nthere is no floor it has to clear; what it has to do is stay pale, which the\ncontrast guardrail holds with a ceiling rather than a floor.",
157
+ "$extensions": {
158
+ "io.cloudgrid": {
159
+ "css": "--cg-online-border"
160
+ }
161
+ }
162
+ },
163
+ "expired-border": {
164
+ "$value": "#fedf89",
165
+ "$type": "color",
166
+ "$description": "The hairline around an expired or pending chip. From the Console's WARN band,\nwhere it has shipped as #fedf89 on #fffaeb (console#579).\n\n1.18:1 against --cg-expired-fill. Same rung and same reasoning as\n--cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is\nthe tone doing the border's job.",
167
+ "$extensions": {
168
+ "io.cloudgrid": {
169
+ "css": "--cg-expired-border"
170
+ }
171
+ }
172
+ },
173
+ "unhealthy-border": {
174
+ "$value": "#eee596",
175
+ "$type": "color",
176
+ "$description": "The hairline around an unhealthy chip. 1.19:1 against --cg-unhealthy-fill and\n1.28:1 on white.\n\nDERIVED, like unhealthy-strong and for the same reason: unhealthy is the one\ntone here the Console never had, so there is no shipped literal to take.\n\nThe rule is the two adopted borders' own position, measured rather than\ninvented. #abefc6 and #fedf89 are not mixes of their tone and their fill — the\nclosest sRGB mix reproduces them at dE2000 5.5 and 13.1 — but they sit in one\nnarrow band: OKLCH L 0.8958 and 0.9112, chroma 0.0886 and 0.1109, and 1.19:1\nand 1.18:1 against their own fills. So a new border is its own tone's hue at the\nlightness that reproduces that ratio, carrying the mean of their chroma (0.0998)\nor the most sRGB has at that hue and lightness, whichever is smaller. Here the\nhue has room: L 0.911, C 0.0998, H 102.17.\n\nKNOWN, measured before it was accepted rather than after: dE2000 6.3 from\nexpired-border, which is far under the 16.5 floor the tones in this palette were\nchosen against. It is not a defect, and the reason is that this rung already\nbehaves this way — expired-fill and unhealthy-fill ship 2.4 apart, so the border\nis MORE separated than the ground it surrounds. 16.5 is a floor for the rungs\nthat carry the signal, the dot and the word, which measure 22.3 and 20.6 for this\npair. At the pale end an amber and a gold are the same colour: swept across the\nwhole hue's usable chroma, the best separation available anywhere in the band is\n11.6, and buying it costs the pale tint that IS this rung's definition. Same\nfinding #122 recorded for the tones on a dark ground.\n\nNot an alias of expired-border, though the two are close enough that it would\nrender almost identically today. #122's whole decision was that unhealthy must\nnot be an alias of expired: a designer retuning one must not silently retune the\nother.",
177
+ "$extensions": {
178
+ "io.cloudgrid": {
179
+ "css": "--cg-unhealthy-border"
180
+ }
181
+ }
182
+ },
183
+ "danger-border": {
184
+ "$value": "#ffd3cd",
185
+ "$type": "color",
186
+ "$description": "The hairline around a danger chip. 1.19:1 against --cg-danger-fill and 1.35:1 on\nwhite.\n\nDERIVED by the rule unhealthy-border's note sets out, and the interesting part is\nwhere the rule runs out. --cg-danger is OKLCH H 27.03, and at the lightness the\nrule asks for, sRGB carries at most C 0.0506 on that hue — half the 0.0998 the\ntwo adopted borders carry. There is no paler red with more chroma; the gamut\nends. So this is the second clause of the rule doing its work: L 0.904 at the\nmost chroma available, which puts the red channel at 255.\n\nMeasured across the band, so the choice is visible:\n\n L 0.896 maxC 0.0551 #ffd0c9 1.22:1 on fill\n L 0.904 maxC 0.0506 #ffd3cd 1.19:1 on fill <- taken\n L 0.912 maxC 0.0461 #ffd7d2 1.16:1 on fill\n\ndE2000 to its neighbours in this rung: 38.6 from online-border, 27.3 from\nunhealthy-border, 25.0 from expired-border. It is the best separated of the four,\nwhich is the right way round — a failed chip is the one a reader must not\nmisread.\n\n#152 declined to add this, and correctly: at the time nothing drew it, and the\n`state` group's note says a value would then be invented to complete a grid\nrather than to serve a chip. That note also says what would change the answer —\n\"add them when something needs them, with the number that made the choice\" — and\nui#154 is that. The number is above.",
187
+ "$extensions": {
188
+ "io.cloudgrid": {
189
+ "css": "--cg-danger-border"
190
+ }
191
+ }
192
+ },
193
+ "pending-border": {
194
+ "$value": "{state.expired-border}",
195
+ "$description": "The hairline around a pending chip, aliased to expired's for the same reason the\ntone and the fill are. Splitting pending from expired later means editing the\nfour pending entries in this file and nothing else.",
196
+ "$extensions": {
197
+ "io.cloudgrid": {
198
+ "css": "--cg-pending-border"
199
+ }
200
+ }
201
+ },
202
+ "online-strong": {
203
+ "$value": "#085d3a",
204
+ "$type": "color",
205
+ "$description": "The WORD on an online chip, and online as running text anywhere. From the\nConsole's OK band. 7.17:1 on --cg-online-fill and 7.96:1 on white.\n\nThis is the rung EntityStatus said the library lacked. --cg-online is 3.40:1 on\nwhite and 3.06:1 on its own fill, which is an indicator and not a foreground, so\nthe component paints the dot with the tone and the word with --cg-text-primary,\nand its own note says a state foreground is a brand decision rather than a\ncomponent's to take. This is that decision, made with a value the Console has\nalready shipped rather than a new one.",
206
+ "$extensions": {
207
+ "io.cloudgrid": {
208
+ "css": "--cg-online-strong"
209
+ }
210
+ }
211
+ },
212
+ "expired-strong": {
213
+ "$value": "#7a3d0a",
214
+ "$type": "color",
215
+ "$description": "The word on an expired chip, and expired as running text. From the Console's\nWARN band. 7.63:1 on --cg-expired-fill and 8.40:1 on white.",
216
+ "$extensions": {
217
+ "io.cloudgrid": {
218
+ "css": "--cg-expired-strong"
219
+ }
220
+ }
221
+ },
222
+ "pending-strong": {
223
+ "$value": "{state.expired-strong}",
224
+ "$description": "The word on a pending chip. Aliased to expired's, like every other pending rung.",
225
+ "$extensions": {
226
+ "io.cloudgrid": {
227
+ "css": "--cg-pending-strong"
228
+ }
229
+ }
230
+ },
231
+ "unhealthy-strong": {
232
+ "$value": "#5d5401",
233
+ "$type": "color",
234
+ "$description": "The word on an unhealthy chip. 7.10:1 on --cg-unhealthy-fill and 7.68:1 on\nwhite.\n\nDERIVED, not adopted. unhealthy is the one tone here the Console never had, so\nthere is no shipped literal to take. This is --cg-unhealthy darkened along its\nown hue and saturation until it reaches the band the two adopted words already\noccupy — 7.17:1 for online and 7.63:1 for expired on their own fills — rather\nthan stopped at the first value that clears 4.5:1, so the words read as one set\ninstead of three matched and one nearly failing.\n\nThe tone is not simply reused because it does not clear the bar: --cg-unhealthy\nmeasures 4.46:1 on this fill. Four hundredths under the floor is under it.\n\ndE2000 to its neighbours in this rung: 20.6 from expired-strong, 23.1 from\nonline-strong. The tightest pair in the whole rung is expired-strong against\ndanger-strong at 19.8, above the 16.5 floor this palette was measured against.",
235
+ "$extensions": {
236
+ "io.cloudgrid": {
237
+ "css": "--cg-unhealthy-strong"
238
+ }
239
+ }
240
+ },
241
+ "danger-strong": {
242
+ "$value": "#c42828",
243
+ "$type": "color",
244
+ "$description": "The word on a danger chip, and danger as running text. 5.02:1 on\n--cg-danger-fill and 5.70:1 on white.\n\n--cg-danger itself is #E53935, which is 4.23:1 on white and 3.72:1 on its own\nfill. It passes as a fill, a border or an icon, where the bar is 3:1, and fails\nas a sentence. The token is deliberately NOT moved to fix that: every fill,\nborder and icon already consuming it would repaint, and the error is that danger\nhad no word rung, not that its tone is wrong.\n\nThe Console's retired --color-error #d42b2b is where this starts and is not\nwhere it lands. It is 5.02:1 on white, but 4.42:1 on --cg-danger-fill — still\nunder the floor on the exact chip that needs it. This is that red darkened along\nits own hue and saturation until it clears 5:1 on the fill, which is the\nsmallest move that holds on both grounds.\n\nKept lighter than the other three words on purpose. It is already the closest\npair in the rung, dE2000 19.8 from expired-strong, and darkening it into their\n7:1 band closes that to 16.6 — a failed chip and an expiring one should not\nconverge.",
245
+ "$extensions": {
246
+ "io.cloudgrid": {
247
+ "css": "--cg-danger-strong"
248
+ }
249
+ }
152
250
  }
153
251
  },
154
252
  "brand-derived": {
@@ -210,6 +308,16 @@
210
308
  }
211
309
  }
212
310
  },
311
+ "neutral-50": {
312
+ "$value": "#f4f6f8",
313
+ "$type": "color",
314
+ "$description": "SOURCE: the Console's --color-canvas, the recessed ground behind a card.\n\nNot a pure grey. It carries a slight blue cast where the rest of this ramp is\nneutral, and that is recorded rather than corrected because the value is what\nlegacy raised panels already render and matching them is the point (#151).",
315
+ "$extensions": {
316
+ "io.cloudgrid": {
317
+ "css": "--cg-neutral-50"
318
+ }
319
+ }
320
+ },
213
321
  "neutral-100": {
214
322
  "$value": "#f1f1f1",
215
323
  "$type": "color",
@@ -304,6 +412,15 @@
304
412
  }
305
413
  }
306
414
  },
415
+ "surface-raised": {
416
+ "$value": "{neutrals.neutral-50}",
417
+ "$description": "A neutral ground one step off white, for a panel that should read as lifted off\nthe page rather than flat on it.\n\n#151: --cg-surface-card is #ffffff and --cg-surface-subtle is the lavender tint,\nso a raised panel had no value-preserving target and every consumer faced the\nsame three-way choice with no right answer — flatten it to white, tint it\nlavender, or pin a literal. This is the fourth answer, and it is the Console's\nown #f4f6f8 rather than a new value.\n\nDeclared at :root only, like --cg-surface-subtle and --cg-surface-control. The\nneutral grounds do not flip: inside a purple panel or on ink, depth is carried\nby --cg-surface-card and the hairline instead. Making it flip is one entry in\neach of the two surface sets.",
418
+ "$extensions": {
419
+ "io.cloudgrid": {
420
+ "css": "--cg-surface-raised"
421
+ }
422
+ }
423
+ },
307
424
  "surface-control": {
308
425
  "$value": "{neutrals.neutral-200}",
309
426
  "$extensions": {
@@ -1082,6 +1199,16 @@
1082
1199
  "css": "--cg-ring-focus"
1083
1200
  }
1084
1201
  }
1202
+ },
1203
+ "ring-live": {
1204
+ "$value": "0 0 0 4px var(--cg-live-halo)",
1205
+ "$description": "The live dot's halo as a ready-made box-shadow, so a surface draws the mark with\none token instead of restating the geometry.\n\nSame pair as --cg-focus-ring and --cg-ring-focus above: the colour lives in the\nindicator group, the shadow that spends it lives here. 4px rather than the focus\nring's 3px because that is the spread the mark ships at — the Console writes\n`box-shadow: 0 0 0 4px var(--color-volt-ring)` at all seven call sites, so the\ngeometry was already one value repeated seven times and is now one token.\n\nA dot larger than 8px wants a larger halo; that is a call site's business, and it\nsets its own box-shadow with --cg-live-halo rather than this rung.",
1206
+ "$extensions": {
1207
+ "io.cloudgrid": {
1208
+ "raw": true,
1209
+ "css": "--cg-ring-live"
1210
+ }
1211
+ }
1085
1212
  }
1086
1213
  },
1087
1214
  "motion": {
@@ -1164,30 +1291,30 @@
1164
1291
  "volt": {
1165
1292
  "$value": "#b6ff2e",
1166
1293
  "$type": "color",
1167
- "$description": "The live/current INDICATOR — the dot in its halo, and the brand mark. Never\ndecorative, never a fill.\n\nMEASURED 1.21:1 on white, which is below every bar there is. It works on the\npanel (4.82:1) and on ink (13.13:1); on the page ground it reads only because\nit is paired with the ring and, in a pill, with volt-ink. Recorded rather than\nsilently corrected: darkening it would change the Console's appearance, which\nis a design decision and not a contrast fix.\n\nNote it is a SECOND lime beside --cg-secondary (#b6de68), 36 dE away. Which one\nthe brand owns is the open question here.\n\nIt is also a second LIVE. state.online (#00a05c) is the same idea in the state\nscale, and EntityStatus (#116) already paints the live dot with it. They are not\ninterchangeable at any bar — online is 3.40:1 on white, volt is 1.21:1 — so a\nsurface must not swap one for the other on the assumption that both mean\n'serving'. Which name the brand keeps for live belongs to the same open question,\nand until it is answered volt is the Console's brand mark and online is the\nentity's state.",
1294
+ "$description": "The live/current INDICATOR — the dot in its halo, and the brand mark. Never\ndecorative, never a fill.\n\nMEASURED 1.21:1 on white, which is below every bar there is. It works on the\npanel (4.82:1) and on ink (13.13:1); on the page ground it reads only because\nit is paired with volt-ink in a pill. Recorded rather than\nsilently corrected: darkening it would change the Console's appearance, which\nis a design decision and not a contrast fix.\n\nNote it is a SECOND lime beside --cg-secondary (#b6de68), 36 dE away. Which one\nthe brand owns is the open question here.\n\nIt is also a second LIVE. state.online (#00a05c) is the same idea in the state\nscale, and EntityStatus (#116) already paints the live dot with it. They are not\ninterchangeable at any bar — online is 3.40:1 on white, volt is 1.21:1 — so a\nsurface must not swap one for the other on the assumption that both mean\n'serving'. Which name the brand keeps for live belongs to the same open question,\nand until it is answered volt is the Console's brand mark and online is the\nentity's state.",
1168
1295
  "$extensions": {
1169
1296
  "io.cloudgrid": {
1170
1297
  "css": "--cg-volt"
1171
1298
  }
1172
1299
  }
1173
1300
  },
1174
- "volt-ring": {
1175
- "$value": "rgba(182, 255, 46, 0.32)",
1301
+ "volt-ink": {
1302
+ "$value": "#243d00",
1176
1303
  "$type": "color",
1177
- "$description": "The halo behind a volt dot. Its job is to make the dot findable on a light ground, which is what carries the 1.21:1 above.",
1304
+ "$description": "Text and glyphs ON volt. 9.95:1 against it — the one part of the volt family that\nis comfortably legible.\n\nThis is NOT the state -ink that EntityStatus (#116) says the library lacks. That\nnote wants a foreground for the WORD on a state chip, and the arrangement it\nshipped instead — dot carries the colour, word is text-ink — already measures\n14.33:1 on online-fill, against 10.86:1 for this olive. volt-ink is tuned to volt\nand belongs to volt. Adding a state -ink is still a brand decision and still\nopen.",
1178
1305
  "$extensions": {
1179
1306
  "io.cloudgrid": {
1180
- "css": "--cg-volt-ring"
1307
+ "css": "--cg-volt-ink"
1181
1308
  }
1182
1309
  }
1183
1310
  },
1184
- "volt-ink": {
1185
- "$value": "#243d00",
1186
- "$type": "color",
1187
- "$description": "Text and glyphs ON volt. 9.95:1 against it — the one part of the volt family that\nis comfortably legible.\n\nThis is NOT the state -ink that EntityStatus (#116) says the library lacks. That\nnote wants a foreground for the WORD on a state chip, and the arrangement it\nshipped instead — dot carries the colour, word is text-ink — already measures\n14.33:1 on online-fill, against 10.86:1 for this olive. volt-ink is tuned to volt\nand belongs to volt. Adding a state -ink is still a brand decision and still\nopen.",
1311
+ "live-halo": {
1312
+ "$value": "color-mix(in srgb, var(--cg-volt) 32%, transparent)",
1313
+ "$description": "The soft ring around a live dot. The other half of the mark --cg-volt's note\ndescribes: \"the dot in its halo\".\n\nDERIVED FROM THE DOT, not written out, and that is the whole reason this exists\nrather than the value being pasted back. Computed it is the Console's own\nrgba(182, 255, 46, 0.32) — 32% of #b6ff2e over white is #e8ffbc, which is what\nships today — but written as a literal it would be a third place #b6ff2e is\nspelled out, and the next volt retune would leave an old-hue halo around a\nnew-hue dot with nothing anywhere going red.\n\nA ROLE name rather than --cg-volt-ring, which #149 purged as volt residue. The\npurge was right about this package (nothing here read it) and the name was the\nresidue: a token named for the hue cannot survive the hue being renamed, and\n--cg-volt's own note records that which lime the brand owns is still open. This\none is deliberately downstream of that question — whatever answers it, the halo\nfollows.\n\nWhat the purge missed is that the Console DOES read it, seven times: .cg-live-dot\nplus StatusChip, DiscoverRails, EngagementHeadline, MyStuffLoopStrip,\nViewsTrendChart and GridStatusBoard, each setting the dot and its box-shadow\ntogether. console#591 kept all three volt names rather than convert two thirds of\none mark, which is what ui#154 reports.\n\nNo contrast obligation of its own: it is a halo, not a foreground, and the dot it\nsurrounds is the signal. Declared at :root only, like --cg-volt itself, because a\nbrand mark is not a state and does not flip per surface.",
1188
1314
  "$extensions": {
1189
1315
  "io.cloudgrid": {
1190
- "css": "--cg-volt-ink"
1316
+ "raw": true,
1317
+ "css": "--cg-live-halo"
1191
1318
  }
1192
1319
  }
1193
1320
  },
@@ -134,7 +134,7 @@
134
134
  }
135
135
  },
136
136
  "state": {
137
- "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.",
137
+ "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.\n\nThe -border and -strong rungs (#149, #150) are the exception to \"measured per surface\": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is.",
138
138
  "online": {
139
139
  "$value": "#5FD99A",
140
140
  "$type": "color",
@@ -232,6 +232,88 @@
232
232
  "css": "--cg-danger-fill"
233
233
  }
234
234
  }
235
+ },
236
+ "online-border": {
237
+ "$value": "{semantic-lines.border-hairline}",
238
+ "$extensions": {
239
+ "io.cloudgrid": {
240
+ "css": "--cg-online-border"
241
+ }
242
+ }
243
+ },
244
+ "expired-border": {
245
+ "$value": "{semantic-lines.border-hairline}",
246
+ "$extensions": {
247
+ "io.cloudgrid": {
248
+ "css": "--cg-expired-border"
249
+ }
250
+ }
251
+ },
252
+ "unhealthy-border": {
253
+ "$value": "{semantic-lines.border-hairline}",
254
+ "$description": "The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot.",
255
+ "$extensions": {
256
+ "io.cloudgrid": {
257
+ "css": "--cg-unhealthy-border"
258
+ }
259
+ }
260
+ },
261
+ "danger-border": {
262
+ "$value": "{semantic-lines.border-hairline}",
263
+ "$description": "The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline.",
264
+ "$extensions": {
265
+ "io.cloudgrid": {
266
+ "css": "--cg-danger-border"
267
+ }
268
+ }
269
+ },
270
+ "pending-border": {
271
+ "$value": "{state.expired-border}",
272
+ "$extensions": {
273
+ "io.cloudgrid": {
274
+ "css": "--cg-pending-border"
275
+ }
276
+ }
277
+ },
278
+ "online-strong": {
279
+ "$value": "{semantic-text.text-primary}",
280
+ "$extensions": {
281
+ "io.cloudgrid": {
282
+ "css": "--cg-online-strong"
283
+ }
284
+ }
285
+ },
286
+ "expired-strong": {
287
+ "$value": "{semantic-text.text-primary}",
288
+ "$extensions": {
289
+ "io.cloudgrid": {
290
+ "css": "--cg-expired-strong"
291
+ }
292
+ }
293
+ },
294
+ "pending-strong": {
295
+ "$value": "{state.expired-strong}",
296
+ "$extensions": {
297
+ "io.cloudgrid": {
298
+ "css": "--cg-pending-strong"
299
+ }
300
+ }
301
+ },
302
+ "unhealthy-strong": {
303
+ "$value": "{semantic-text.text-primary}",
304
+ "$extensions": {
305
+ "io.cloudgrid": {
306
+ "css": "--cg-unhealthy-strong"
307
+ }
308
+ }
309
+ },
310
+ "danger-strong": {
311
+ "$value": "{semantic-text.text-primary}",
312
+ "$extensions": {
313
+ "io.cloudgrid": {
314
+ "css": "--cg-danger-strong"
315
+ }
316
+ }
235
317
  }
236
318
  },
237
319
  "indicator": {
@@ -118,7 +118,7 @@
118
118
  }
119
119
  },
120
120
  "state": {
121
- "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.",
121
+ "$description": "state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.\n\nThe -border and -strong rungs (#149, #150) are the exception to \"measured per surface\": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is.",
122
122
  "online": {
123
123
  "$value": "#C8F5DC",
124
124
  "$type": "color",
@@ -216,6 +216,88 @@
216
216
  "css": "--cg-danger-fill"
217
217
  }
218
218
  }
219
+ },
220
+ "online-border": {
221
+ "$value": "{semantic-lines.border-hairline}",
222
+ "$extensions": {
223
+ "io.cloudgrid": {
224
+ "css": "--cg-online-border"
225
+ }
226
+ }
227
+ },
228
+ "expired-border": {
229
+ "$value": "{semantic-lines.border-hairline}",
230
+ "$extensions": {
231
+ "io.cloudgrid": {
232
+ "css": "--cg-expired-border"
233
+ }
234
+ }
235
+ },
236
+ "unhealthy-border": {
237
+ "$value": "{semantic-lines.border-hairline}",
238
+ "$description": "The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot.",
239
+ "$extensions": {
240
+ "io.cloudgrid": {
241
+ "css": "--cg-unhealthy-border"
242
+ }
243
+ }
244
+ },
245
+ "danger-border": {
246
+ "$value": "{semantic-lines.border-hairline}",
247
+ "$description": "The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline.",
248
+ "$extensions": {
249
+ "io.cloudgrid": {
250
+ "css": "--cg-danger-border"
251
+ }
252
+ }
253
+ },
254
+ "pending-border": {
255
+ "$value": "{state.expired-border}",
256
+ "$extensions": {
257
+ "io.cloudgrid": {
258
+ "css": "--cg-pending-border"
259
+ }
260
+ }
261
+ },
262
+ "online-strong": {
263
+ "$value": "{semantic-text.text-primary}",
264
+ "$extensions": {
265
+ "io.cloudgrid": {
266
+ "css": "--cg-online-strong"
267
+ }
268
+ }
269
+ },
270
+ "expired-strong": {
271
+ "$value": "{semantic-text.text-primary}",
272
+ "$extensions": {
273
+ "io.cloudgrid": {
274
+ "css": "--cg-expired-strong"
275
+ }
276
+ }
277
+ },
278
+ "pending-strong": {
279
+ "$value": "{state.expired-strong}",
280
+ "$extensions": {
281
+ "io.cloudgrid": {
282
+ "css": "--cg-pending-strong"
283
+ }
284
+ }
285
+ },
286
+ "unhealthy-strong": {
287
+ "$value": "{semantic-text.text-primary}",
288
+ "$extensions": {
289
+ "io.cloudgrid": {
290
+ "css": "--cg-unhealthy-strong"
291
+ }
292
+ }
293
+ },
294
+ "danger-strong": {
295
+ "$value": "{semantic-text.text-primary}",
296
+ "$extensions": {
297
+ "io.cloudgrid": {
298
+ "css": "--cg-danger-strong"
299
+ }
300
+ }
219
301
  }
220
302
  },
221
303
  "indicator": {
package/dist/tokens.css CHANGED
@@ -107,7 +107,50 @@
107
107
  brighter gold on the same ramp as the amber rather than a new hue family,
108
108
  because green/amber/red is the traffic light an operator already reads and
109
109
  'serving but degraded' is its middle rung. Its own note carries the numbers
110
- and the three families that were measured and rejected. */
110
+ and the three families that were measured and rejected.
111
+
112
+ THREE RUNGS PER STATUS, from 2026-09-04 (#149, #150). A chip is a fill, a
113
+ hairline around it, a dot and a word, and until now the set carried only the
114
+ first two of those four. -border is the hairline and -strong is the word; the
115
+ bare name stays the dot it has always been.
116
+
117
+ <status>-fill the chip ground
118
+ <status>-border the hairline around it, a pale tint
119
+ <status> the dot. An indicator, so 3:1, never text
120
+ <status>-strong the word on the fill. Text, so 4.5:1
121
+
122
+ The values are the Console's, measured and shipped, not new ones: #149 asked
123
+ for the rung and console#579 had already been forced to leave two bands as
124
+ literals for the want of it. Only unhealthy-strong and danger-strong are
125
+ derived, each for a stated reason in its own note.
126
+
127
+ FOUR RUNGS PER STATUS as of 2026-09-05 (#154). --cg-danger-border and
128
+ --cg-unhealthy-border were the two holes #152 left, on the reasoning above: at
129
+ the time nothing drew either one, so a value would have been invented to complete
130
+ a grid rather than to serve a chip. That note said what would change the answer —
131
+ "add them when something needs them, with the number that made the choice" — and
132
+ this is it. Both are DERIVED rather than adopted, because neither has a shipped
133
+ literal anywhere, and each carries its number in its own note.
134
+
135
+ The derivation is one rule, and it is the two adopted borders' own measured
136
+ position rather than a recipe: a -border is its own tone's hue, at the lightness
137
+ that reproduces the 1.18-1.19:1 those two hold against their own fills, carrying
138
+ the mean of their chroma (OKLCH 0.0998) or the most sRGB has at that hue and
139
+ lightness, whichever is smaller. Danger's hue hits the second clause and
140
+ unhealthy's does not; both notes show the sweep.
141
+
142
+ offline has no -border and no -strong either, and for the reason it has no
143
+ fill: an unplugged chip sits on the card ground with border-control, and its
144
+ word is --cg-text-primary. Absence of state still does not need a colour.
145
+
146
+ On the panel and on ink, -border resolves to the surface's own hairline and
147
+ -strong to its own primary text. A dark ground has no darker rung to reach for,
148
+ and the light tints these scopes carry measure 3.03:1 to 3.60:1 on the panel's
149
+ chip fill, which is a dot and not a word. Worth knowing before retuning: on ink
150
+ the tints DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so a designer who
151
+ wants the word tinted there changes five entries in surface-ink.json. On the
152
+ panel it is not available at any value — white is 4.31:1 on that fill and white
153
+ is the lightest colour there is, so AA on a panel chip needs the FILL to move. */
111
154
  /*
112
155
  Serving. From the Console's --color-live. 3.40:1 on white — clears the 3:1 bar for an indicator, which is all this is ever used as. Never body text on white. */
113
156
  --cg-online: #00a05c;
@@ -162,6 +205,150 @@
162
205
  /*
163
206
  From the Console's --color-error-fill. --cg-danger measures 3.72:1 on it. danger had no fill before; a chip needs one. */
164
207
  --cg-danger-fill: #fceded;
208
+ /*
209
+ The hairline around an online chip. From the Console's OK band, where it has
210
+ shipped as #abefc6 on #ecfdf3 (console#579).
211
+
212
+ Its own rung rather than the tone, and console#579 is why. Both systems carried
213
+ {tone, fill} pairs and no third value, so converting the band meant sending the
214
+ border AND the word to --cg-online. Measured, that is not a repaint: the word
215
+ fell from 7.55:1 to 3.06:1 and the border went from this pale tint to full
216
+ saturation. The band stayed a literal instead. This token is what lets it move.
217
+
218
+ 1.19:1 against --cg-online-fill. A hairline is not text and not an indicator, so
219
+ there is no floor it has to clear; what it has to do is stay pale, which the
220
+ contrast guardrail holds with a ceiling rather than a floor. */
221
+ --cg-online-border: #abefc6;
222
+ /*
223
+ The hairline around an expired or pending chip. From the Console's WARN band,
224
+ where it has shipped as #fedf89 on #fffaeb (console#579).
225
+
226
+ 1.18:1 against --cg-expired-fill. Same rung and same reasoning as
227
+ --cg-online-border: collapsing it onto --cg-expired measured 3.99:1, which is
228
+ the tone doing the border's job. */
229
+ --cg-expired-border: #fedf89;
230
+ /*
231
+ The hairline around an unhealthy chip. 1.19:1 against --cg-unhealthy-fill and
232
+ 1.28:1 on white.
233
+
234
+ DERIVED, like unhealthy-strong and for the same reason: unhealthy is the one
235
+ tone here the Console never had, so there is no shipped literal to take.
236
+
237
+ The rule is the two adopted borders' own position, measured rather than
238
+ invented. #abefc6 and #fedf89 are not mixes of their tone and their fill — the
239
+ closest sRGB mix reproduces them at dE2000 5.5 and 13.1 — but they sit in one
240
+ narrow band: OKLCH L 0.8958 and 0.9112, chroma 0.0886 and 0.1109, and 1.19:1
241
+ and 1.18:1 against their own fills. So a new border is its own tone's hue at the
242
+ lightness that reproduces that ratio, carrying the mean of their chroma (0.0998)
243
+ or the most sRGB has at that hue and lightness, whichever is smaller. Here the
244
+ hue has room: L 0.911, C 0.0998, H 102.17.
245
+
246
+ KNOWN, measured before it was accepted rather than after: dE2000 6.3 from
247
+ expired-border, which is far under the 16.5 floor the tones in this palette were
248
+ chosen against. It is not a defect, and the reason is that this rung already
249
+ behaves this way — expired-fill and unhealthy-fill ship 2.4 apart, so the border
250
+ is MORE separated than the ground it surrounds. 16.5 is a floor for the rungs
251
+ that carry the signal, the dot and the word, which measure 22.3 and 20.6 for this
252
+ pair. At the pale end an amber and a gold are the same colour: swept across the
253
+ whole hue's usable chroma, the best separation available anywhere in the band is
254
+ 11.6, and buying it costs the pale tint that IS this rung's definition. Same
255
+ finding #122 recorded for the tones on a dark ground.
256
+
257
+ Not an alias of expired-border, though the two are close enough that it would
258
+ render almost identically today. #122's whole decision was that unhealthy must
259
+ not be an alias of expired: a designer retuning one must not silently retune the
260
+ other. */
261
+ --cg-unhealthy-border: #eee596;
262
+ /*
263
+ The hairline around a danger chip. 1.19:1 against --cg-danger-fill and 1.35:1 on
264
+ white.
265
+
266
+ DERIVED by the rule unhealthy-border's note sets out, and the interesting part is
267
+ where the rule runs out. --cg-danger is OKLCH H 27.03, and at the lightness the
268
+ rule asks for, sRGB carries at most C 0.0506 on that hue — half the 0.0998 the
269
+ two adopted borders carry. There is no paler red with more chroma; the gamut
270
+ ends. So this is the second clause of the rule doing its work: L 0.904 at the
271
+ most chroma available, which puts the red channel at 255.
272
+
273
+ Measured across the band, so the choice is visible:
274
+
275
+ L 0.896 maxC 0.0551 #ffd0c9 1.22:1 on fill
276
+ L 0.904 maxC 0.0506 #ffd3cd 1.19:1 on fill <- taken
277
+ L 0.912 maxC 0.0461 #ffd7d2 1.16:1 on fill
278
+
279
+ dE2000 to its neighbours in this rung: 38.6 from online-border, 27.3 from
280
+ unhealthy-border, 25.0 from expired-border. It is the best separated of the four,
281
+ which is the right way round — a failed chip is the one a reader must not
282
+ misread.
283
+
284
+ #152 declined to add this, and correctly: at the time nothing drew it, and the
285
+ `state` group's note says a value would then be invented to complete a grid
286
+ rather than to serve a chip. That note also says what would change the answer —
287
+ "add them when something needs them, with the number that made the choice" — and
288
+ ui#154 is that. The number is above. */
289
+ --cg-danger-border: #ffd3cd;
290
+ /*
291
+ The hairline around a pending chip, aliased to expired's for the same reason the
292
+ tone and the fill are. Splitting pending from expired later means editing the
293
+ four pending entries in this file and nothing else. */
294
+ --cg-pending-border: var(--cg-expired-border);
295
+ /*
296
+ The WORD on an online chip, and online as running text anywhere. From the
297
+ Console's OK band. 7.17:1 on --cg-online-fill and 7.96:1 on white.
298
+
299
+ This is the rung EntityStatus said the library lacked. --cg-online is 3.40:1 on
300
+ white and 3.06:1 on its own fill, which is an indicator and not a foreground, so
301
+ the component paints the dot with the tone and the word with --cg-text-primary,
302
+ and its own note says a state foreground is a brand decision rather than a
303
+ component's to take. This is that decision, made with a value the Console has
304
+ already shipped rather than a new one. */
305
+ --cg-online-strong: #085d3a;
306
+ /*
307
+ The word on an expired chip, and expired as running text. From the Console's
308
+ WARN band. 7.63:1 on --cg-expired-fill and 8.40:1 on white. */
309
+ --cg-expired-strong: #7a3d0a;
310
+ /*
311
+ The word on a pending chip. Aliased to expired's, like every other pending rung. */
312
+ --cg-pending-strong: var(--cg-expired-strong);
313
+ /*
314
+ The word on an unhealthy chip. 7.10:1 on --cg-unhealthy-fill and 7.68:1 on
315
+ white.
316
+
317
+ DERIVED, not adopted. unhealthy is the one tone here the Console never had, so
318
+ there is no shipped literal to take. This is --cg-unhealthy darkened along its
319
+ own hue and saturation until it reaches the band the two adopted words already
320
+ occupy — 7.17:1 for online and 7.63:1 for expired on their own fills — rather
321
+ than stopped at the first value that clears 4.5:1, so the words read as one set
322
+ instead of three matched and one nearly failing.
323
+
324
+ The tone is not simply reused because it does not clear the bar: --cg-unhealthy
325
+ measures 4.46:1 on this fill. Four hundredths under the floor is under it.
326
+
327
+ dE2000 to its neighbours in this rung: 20.6 from expired-strong, 23.1 from
328
+ online-strong. The tightest pair in the whole rung is expired-strong against
329
+ danger-strong at 19.8, above the 16.5 floor this palette was measured against. */
330
+ --cg-unhealthy-strong: #5d5401;
331
+ /*
332
+ The word on a danger chip, and danger as running text. 5.02:1 on
333
+ --cg-danger-fill and 5.70:1 on white.
334
+
335
+ --cg-danger itself is #E53935, which is 4.23:1 on white and 3.72:1 on its own
336
+ fill. It passes as a fill, a border or an icon, where the bar is 3:1, and fails
337
+ as a sentence. The token is deliberately NOT moved to fix that: every fill,
338
+ border and icon already consuming it would repaint, and the error is that danger
339
+ had no word rung, not that its tone is wrong.
340
+
341
+ The Console's retired --color-error #d42b2b is where this starts and is not
342
+ where it lands. It is 5.02:1 on white, but 4.42:1 on --cg-danger-fill — still
343
+ under the floor on the exact chip that needs it. This is that red darkened along
344
+ its own hue and saturation until it clears 5:1 on the fill, which is the
345
+ smallest move that holds on both grounds.
346
+
347
+ Kept lighter than the other three words on purpose. It is already the closest
348
+ pair in the rung, dE2000 19.8 from expired-strong, and darkening it into their
349
+ 7:1 band closes that to 16.6 — a failed chip and an expiring one should not
350
+ converge. */
351
+ --cg-danger-strong: #c42828;
165
352
 
166
353
  /* ---- brand-derived ----
167
354
  brand derived (interaction only) */
@@ -174,6 +361,13 @@
174
361
  --cg-neutral-0: #ffffff;
175
362
  /* SOURCE: alternate page wash */
176
363
  --cg-lavender-50: #f2f1f7;
364
+ /*
365
+ SOURCE: the Console's --color-canvas, the recessed ground behind a card.
366
+
367
+ Not a pure grey. It carries a slight blue cast where the rest of this ramp is
368
+ neutral, and that is recorded rather than corrected because the value is what
369
+ legacy raised panels already render and matching them is the point (#151). */
370
+ --cg-neutral-50: #f4f6f8;
177
371
  /* SOURCE: hairline / card border */
178
372
  --cg-neutral-100: #f1f1f1;
179
373
  /* SOURCE: control border, inactive pill fill */
@@ -192,6 +386,21 @@
192
386
  --cg-surface-page-alt: var(--cg-lavender-50);
193
387
  --cg-surface-card: var(--cg-neutral-0);
194
388
  --cg-surface-subtle: var(--cg-lavender-50);
389
+ /*
390
+ A neutral ground one step off white, for a panel that should read as lifted off
391
+ the page rather than flat on it.
392
+
393
+ #151: --cg-surface-card is #ffffff and --cg-surface-subtle is the lavender tint,
394
+ so a raised panel had no value-preserving target and every consumer faced the
395
+ same three-way choice with no right answer — flatten it to white, tint it
396
+ lavender, or pin a literal. This is the fourth answer, and it is the Console's
397
+ own #f4f6f8 rather than a new value.
398
+
399
+ Declared at :root only, like --cg-surface-subtle and --cg-surface-control. The
400
+ neutral grounds do not flip: inside a purple panel or on ink, depth is carried
401
+ by --cg-surface-card and the hairline instead. Making it flip is one entry in
402
+ each of the two surface sets. */
403
+ --cg-surface-raised: var(--cg-neutral-50);
195
404
  --cg-surface-control: var(--cg-neutral-200);
196
405
  --cg-surface-panel: var(--cg-primary);
197
406
  --cg-surface-code: var(--cg-ink);
@@ -326,6 +535,19 @@
326
535
  --cg-shadow-hover: 0 8px 24px rgba(34, 34, 34, 0.08);
327
536
  --cg-shadow-panel: 0 10px 40px rgba(90, 71, 240, 0.18);
328
537
  --cg-ring-focus: 0 0 0 3px var(--cg-focus-ring);
538
+ /*
539
+ The live dot's halo as a ready-made box-shadow, so a surface draws the mark with
540
+ one token instead of restating the geometry.
541
+
542
+ Same pair as --cg-focus-ring and --cg-ring-focus above: the colour lives in the
543
+ indicator group, the shadow that spends it lives here. 4px rather than the focus
544
+ ring's 3px because that is the spread the mark ships at — the Console writes
545
+ `box-shadow: 0 0 0 4px var(--color-volt-ring)` at all seven call sites, so the
546
+ geometry was already one value repeated seven times and is now one token.
547
+
548
+ A dot larger than 8px wants a larger halo; that is a call site's business, and it
549
+ sets its own box-shadow with --cg-live-halo rather than this rung. */
550
+ --cg-ring-live: 0 0 0 4px var(--cg-live-halo);
329
551
 
330
552
  /* ---- motion ----
331
553
  motion: small and quick, nothing slides in */
@@ -353,7 +575,7 @@
353
575
 
354
576
  MEASURED 1.21:1 on white, which is below every bar there is. It works on the
355
577
  panel (4.82:1) and on ink (13.13:1); on the page ground it reads only because
356
- it is paired with the ring and, in a pill, with volt-ink. Recorded rather than
578
+ it is paired with volt-ink in a pill. Recorded rather than
357
579
  silently corrected: darkening it would change the Console's appearance, which
358
580
  is a design decision and not a contrast fix.
359
581
 
@@ -368,9 +590,6 @@
368
590
  and until it is answered volt is the Console's brand mark and online is the
369
591
  entity's state. */
370
592
  --cg-volt: #b6ff2e;
371
- /*
372
- The halo behind a volt dot. Its job is to make the dot findable on a light ground, which is what carries the 1.21:1 above. */
373
- --cg-volt-ring: rgba(182, 255, 46, 0.32);
374
593
  /*
375
594
  Text and glyphs ON volt. 9.95:1 against it — the one part of the volt family that
376
595
  is comfortably legible.
@@ -382,6 +601,34 @@
382
601
  and belongs to volt. Adding a state -ink is still a brand decision and still
383
602
  open. */
384
603
  --cg-volt-ink: #243d00;
604
+ /*
605
+ The soft ring around a live dot. The other half of the mark --cg-volt's note
606
+ describes: "the dot in its halo".
607
+
608
+ DERIVED FROM THE DOT, not written out, and that is the whole reason this exists
609
+ rather than the value being pasted back. Computed it is the Console's own
610
+ rgba(182, 255, 46, 0.32) — 32% of #b6ff2e over white is #e8ffbc, which is what
611
+ ships today — but written as a literal it would be a third place #b6ff2e is
612
+ spelled out, and the next volt retune would leave an old-hue halo around a
613
+ new-hue dot with nothing anywhere going red.
614
+
615
+ A ROLE name rather than --cg-volt-ring, which #149 purged as volt residue. The
616
+ purge was right about this package (nothing here read it) and the name was the
617
+ residue: a token named for the hue cannot survive the hue being renamed, and
618
+ --cg-volt's own note records that which lime the brand owns is still open. This
619
+ one is deliberately downstream of that question — whatever answers it, the halo
620
+ follows.
621
+
622
+ What the purge missed is that the Console DOES read it, seven times: .cg-live-dot
623
+ plus StatusChip, DiscoverRails, EngagementHeadline, MyStuffLoopStrip,
624
+ ViewsTrendChart and GridStatusBoard, each setting the dot and its box-shadow
625
+ together. console#591 kept all three volt names rather than convert two thirds of
626
+ one mark, which is what ui#154 reports.
627
+
628
+ No contrast obligation of its own: it is a halo, not a foreground, and the dot it
629
+ surrounds is the signal. Declared at :root only, like --cg-volt itself, because a
630
+ brand mark is not a state and does not flip per surface. */
631
+ --cg-live-halo: color-mix(in srgb, var(--cg-volt) 32%, transparent);
385
632
  /*
386
633
  The wash behind a modal or overlay. Uses the BRAND ink (#222222) rather than the
387
634
  Console's (#16181d) — a scrim sits above content and belongs to the system, not
@@ -423,7 +670,9 @@
423
670
  --cg-danger: #FFDAD6;
424
671
 
425
672
  /* ---- state ----
426
- state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground. */
673
+ state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.
674
+
675
+ The -border and -strong rungs (#149, #150) are the exception to "measured per surface": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is. */
427
676
  /*
428
677
  4.88:1 on the purple. The source green is 1.72:1 here — invisible, same class as the danger override. */
429
678
  --cg-online: #C8F5DC;
@@ -458,6 +707,20 @@
458
707
  /*
459
708
  Translucent white rather than a tint; the panel foreground measures 3.35:1 on it. */
460
709
  --cg-danger-fill: rgba(255, 255, 255, 0.16);
710
+ --cg-online-border: var(--cg-border-hairline);
711
+ --cg-expired-border: var(--cg-border-hairline);
712
+ /*
713
+ The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot. */
714
+ --cg-unhealthy-border: var(--cg-border-hairline);
715
+ /*
716
+ The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline. */
717
+ --cg-danger-border: var(--cg-border-hairline);
718
+ --cg-pending-border: var(--cg-expired-border);
719
+ --cg-online-strong: var(--cg-text-primary);
720
+ --cg-expired-strong: var(--cg-text-primary);
721
+ --cg-pending-strong: var(--cg-expired-strong);
722
+ --cg-unhealthy-strong: var(--cg-text-primary);
723
+ --cg-danger-strong: var(--cg-text-primary);
461
724
 
462
725
  /* ---- indicator ----
463
726
  indicator + chrome: only hover flips. A dark wash is invisible on a dark ground. */
@@ -496,7 +759,9 @@
496
759
  --cg-danger: #FF8A80;
497
760
 
498
761
  /* ---- state ----
499
- state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground. */
762
+ state: the per-surface values for online/offline/expired. Same necessity as danger — measured, not assumed. pending is an alias of expired (#114), declared here so it flips rather than freezing to the base scope. unhealthy (#122) is a real value rather than an alias, and needs the same per-surface treatment for the same reason every other tone here does: its base gold is unreadable on this ground.
763
+
764
+ The -border and -strong rungs (#149, #150) are the exception to "measured per surface": they resolve to this surface's own hairline and its own primary text rather than to a tint of the tone. A dark ground has no darker rung, and the tints above are tuned to be dots — on the panel's chip fill they measure 3.03:1 to 3.60:1, which is an indicator and not a word. On ink they DO clear 4.5:1 on the ink fill (5.48:1 to 8.92:1), so tinting the word there is available and is five edits in this file; on the panel it is not available at any value, because white measures 4.31:1 on that fill and white is the lightest colour there is. */
500
765
  /*
501
766
  9.00:1 on #222. Ink has headroom, so this stays a recognisable green rather than washing to mint. */
502
767
  --cg-online: #5FD99A;
@@ -523,6 +788,20 @@
523
788
  --cg-unhealthy-fill: rgba(255, 255, 255, 0.08);
524
789
  /* Translucent white; the ink foreground measures 5.45:1 on it. */
525
790
  --cg-danger-fill: rgba(255, 255, 255, 0.08);
791
+ --cg-online-border: var(--cg-border-hairline);
792
+ --cg-expired-border: var(--cg-border-hairline);
793
+ /*
794
+ The hairline around an unhealthy chip on this surface. The surface's own hairline, for the reason online-border and expired-border take it: a dark ground has no paler rung of the tone to reach for, and the light tint this scope carries is a dot. */
795
+ --cg-unhealthy-border: var(--cg-border-hairline);
796
+ /*
797
+ The hairline around a danger chip on this surface. Same rule as every other -border here: the surface's own hairline. */
798
+ --cg-danger-border: var(--cg-border-hairline);
799
+ --cg-pending-border: var(--cg-expired-border);
800
+ --cg-online-strong: var(--cg-text-primary);
801
+ --cg-expired-strong: var(--cg-text-primary);
802
+ --cg-pending-strong: var(--cg-expired-strong);
803
+ --cg-unhealthy-strong: var(--cg-text-primary);
804
+ --cg-danger-strong: var(--cg-text-primary);
526
805
 
527
806
  /* ---- indicator ----
528
807
  indicator + chrome: only hover flips. A dark wash is invisible on a dark ground. */
package/dist/tokens.json CHANGED
@@ -18,12 +18,23 @@
18
18
  "--cg-pending-fill": "var(--cg-expired-fill)",
19
19
  "--cg-unhealthy-fill": "#fdf6df",
20
20
  "--cg-danger-fill": "#fceded",
21
+ "--cg-online-border": "#abefc6",
22
+ "--cg-expired-border": "#fedf89",
23
+ "--cg-unhealthy-border": "#eee596",
24
+ "--cg-danger-border": "#ffd3cd",
25
+ "--cg-pending-border": "var(--cg-expired-border)",
26
+ "--cg-online-strong": "#085d3a",
27
+ "--cg-expired-strong": "#7a3d0a",
28
+ "--cg-pending-strong": "var(--cg-expired-strong)",
29
+ "--cg-unhealthy-strong": "#5d5401",
30
+ "--cg-danger-strong": "#c42828",
21
31
  "--cg-primary-press": "#4b39db",
22
32
  "--cg-primary-tint": "#eeecfe",
23
33
  "--cg-secondary-press": "#a6cf57",
24
34
  "--cg-tertiary-press": "#4dd8e0",
25
35
  "--cg-neutral-0": "#ffffff",
26
36
  "--cg-lavender-50": "#f2f1f7",
37
+ "--cg-neutral-50": "#f4f6f8",
27
38
  "--cg-neutral-100": "#f1f1f1",
28
39
  "--cg-neutral-200": "#e3e3e3",
29
40
  "--cg-neutral-400": "#acacac",
@@ -34,6 +45,7 @@
34
45
  "--cg-surface-page-alt": "var(--cg-lavender-50)",
35
46
  "--cg-surface-card": "var(--cg-neutral-0)",
36
47
  "--cg-surface-subtle": "var(--cg-lavender-50)",
48
+ "--cg-surface-raised": "var(--cg-neutral-50)",
37
49
  "--cg-surface-control": "var(--cg-neutral-200)",
38
50
  "--cg-surface-panel": "var(--cg-primary)",
39
51
  "--cg-surface-code": "var(--cg-ink)",
@@ -118,6 +130,7 @@
118
130
  "--cg-shadow-hover": "0 8px 24px rgba(34, 34, 34, 0.08)",
119
131
  "--cg-shadow-panel": "0 10px 40px rgba(90, 71, 240, 0.18)",
120
132
  "--cg-ring-focus": "0 0 0 3px var(--cg-focus-ring)",
133
+ "--cg-ring-live": "0 0 0 4px var(--cg-live-halo)",
121
134
  "--cg-duration-fast": "120ms",
122
135
  "--cg-duration": "180ms",
123
136
  "--cg-duration-slow": "280ms",
@@ -127,8 +140,8 @@
127
140
  "--cg-lift-hover": "translateY(-1px)",
128
141
  "--cg-press-scale": "0.985",
129
142
  "--cg-volt": "#b6ff2e",
130
- "--cg-volt-ring": "rgba(182, 255, 46, 0.32)",
131
143
  "--cg-volt-ink": "#243d00",
144
+ "--cg-live-halo": "color-mix(in srgb, var(--cg-volt) 32%, transparent)",
132
145
  "--cg-scrim": "rgba(34, 34, 34, 0.4)",
133
146
  "--cg-hover": "rgba(34, 34, 34, 0.045)"
134
147
  },
@@ -155,6 +168,16 @@
155
168
  "--cg-pending-fill": "var(--cg-expired-fill)",
156
169
  "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.16)",
157
170
  "--cg-danger-fill": "rgba(255, 255, 255, 0.16)",
171
+ "--cg-online-border": "var(--cg-border-hairline)",
172
+ "--cg-expired-border": "var(--cg-border-hairline)",
173
+ "--cg-unhealthy-border": "var(--cg-border-hairline)",
174
+ "--cg-danger-border": "var(--cg-border-hairline)",
175
+ "--cg-pending-border": "var(--cg-expired-border)",
176
+ "--cg-online-strong": "var(--cg-text-primary)",
177
+ "--cg-expired-strong": "var(--cg-text-primary)",
178
+ "--cg-pending-strong": "var(--cg-expired-strong)",
179
+ "--cg-unhealthy-strong": "var(--cg-text-primary)",
180
+ "--cg-danger-strong": "var(--cg-text-primary)",
158
181
  "--cg-hover": "rgba(255, 255, 255, 0.10)"
159
182
  },
160
183
  "ink": {
@@ -182,6 +205,16 @@
182
205
  "--cg-pending-fill": "var(--cg-expired-fill)",
183
206
  "--cg-unhealthy-fill": "rgba(255, 255, 255, 0.08)",
184
207
  "--cg-danger-fill": "rgba(255, 255, 255, 0.08)",
208
+ "--cg-online-border": "var(--cg-border-hairline)",
209
+ "--cg-expired-border": "var(--cg-border-hairline)",
210
+ "--cg-unhealthy-border": "var(--cg-border-hairline)",
211
+ "--cg-danger-border": "var(--cg-border-hairline)",
212
+ "--cg-pending-border": "var(--cg-expired-border)",
213
+ "--cg-online-strong": "var(--cg-text-primary)",
214
+ "--cg-expired-strong": "var(--cg-text-primary)",
215
+ "--cg-pending-strong": "var(--cg-expired-strong)",
216
+ "--cg-unhealthy-strong": "var(--cg-text-primary)",
217
+ "--cg-danger-strong": "var(--cg-text-primary)",
185
218
  "--cg-hover": "rgba(255, 255, 255, 0.06)"
186
219
  }
187
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/ui",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "CloudGrid design system: React components, brand design tokens, Tailwind theme, self-hosted fonts.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",