@repobit/dex-system-design 0.25.12 → 1.0.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.
Files changed (336) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +131 -8
  3. package/package.json +96 -13
  4. package/schemas/component-manifest.json +3294 -405
  5. package/schemas/manifest-omissions.json +173 -0
  6. package/src/blog/src/breakpoints.js +6 -6
  7. package/src/blog/src/content/blog-atoms-showcase.js +1 -1
  8. package/src/blog/src/content/blog-page-layout.js +2 -2
  9. package/src/blog/src/lib/article-toc.js +7 -7
  10. package/src/blog/src/shell/blog-shell.js +1 -1
  11. package/src/blog/src/tokens/blog.js +2 -2
  12. package/src/blog/src/tokens/core.js +34 -35
  13. package/src/blog/src/tokens/legacy.js +0 -9
  14. package/src/blog/tokens/blog.js +2 -2
  15. package/src/blog/tokens/core.js +34 -35
  16. package/src/blog/tokens/legacy.js +0 -9
  17. package/src/blog/tokens.css +36 -46
  18. package/src/components/accordion/accordion-bg.css.js +83 -59
  19. package/src/components/accordion/accordion-bg.js +29 -10
  20. package/src/components/accordion/accordion-bg.stories.js +67 -67
  21. package/src/components/accordion/accordion-keyboard.js +80 -0
  22. package/src/components/accordion/accordion.css.js +142 -80
  23. package/src/components/accordion/accordion.js +37 -10
  24. package/src/components/accordion/accordion.stories.js +7 -7
  25. package/src/components/agent-endpoints/agent-endpoints.css.js +36 -39
  26. package/src/components/agent-endpoints/agent-endpoints.js +30 -20
  27. package/src/components/agent-endpoints/agent-endpoints.stories.js +1 -1
  28. package/src/components/ai-guardian-demo.stories.js +91 -91
  29. package/src/components/alert/alert.css.js +198 -0
  30. package/src/components/alert/alert.js +103 -0
  31. package/src/components/alert/alert.stories.js +206 -0
  32. package/src/components/anchor/anchor-nav.css.js +213 -101
  33. package/src/components/anchor/anchor-nav.js +26 -40
  34. package/src/components/anchor/anchor.stories.js +22 -12
  35. package/src/components/annotation/annotation.css.js +58 -0
  36. package/src/components/annotation/annotation.js +67 -0
  37. package/src/components/annotation/annotation.stories.js +63 -0
  38. package/src/components/architecture.stories.js +285 -0
  39. package/src/components/atoms.stories.js +342 -0
  40. package/src/components/avatar/avatar.css.js +67 -0
  41. package/src/components/avatar/avatar.js +42 -0
  42. package/src/components/avatar/avatar.stories.js +95 -0
  43. package/src/components/awards/awards-flex.css.js +209 -23
  44. package/src/components/awards/awards-flex.js +66 -7
  45. package/src/components/awards/awards-flex.stories.js +1 -1
  46. package/src/components/awards/{awards-light.stories.js → awards-summary.stories.js} +54 -64
  47. package/src/components/awards/awards.css.js +116 -58
  48. package/src/components/awards/awards.js +102 -28
  49. package/src/components/awards/awards.stories.js +1 -1
  50. package/src/components/back/back.css.js +51 -22
  51. package/src/components/back/back.js +28 -7
  52. package/src/components/back/back.stories.js +9 -3
  53. package/src/components/badge/badge.css.js +158 -46
  54. package/src/components/badge/badge.js +62 -74
  55. package/src/components/badge/badge.stories.js +176 -248
  56. package/src/components/bento-grid/bento-grid.css.js +21 -16
  57. package/src/components/bento-grid/bento-grid.js +16 -4
  58. package/src/components/bento-grid/bento-grid.stories.js +19 -18
  59. package/src/components/bento-grid/bento-item.css.js +52 -28
  60. package/src/components/bento-grid/bento-item.js +2 -2
  61. package/src/components/blockquote/blockquote.css.js +47 -0
  62. package/src/components/blockquote/blockquote.js +29 -0
  63. package/src/components/blockquote/blockquote.stories.js +62 -0
  64. package/src/components/breadcrumb/breadcrumb.css.js +92 -22
  65. package/src/components/breadcrumb/breadcrumb.js +56 -11
  66. package/src/components/button/button.css.js +531 -161
  67. package/src/components/button/button.js +206 -214
  68. package/src/components/button/button.stories.js +322 -174
  69. package/src/components/caption/caption.css.js +21 -0
  70. package/src/components/caption/caption.js +31 -0
  71. package/src/components/caption/caption.stories.js +62 -0
  72. package/src/components/card-features/card-features.css.js +14 -22
  73. package/src/components/card-features/card-features.js +68 -9
  74. package/src/components/card-features/card-features.stories.js +11 -11
  75. package/src/components/cards/card.css.js +60 -81
  76. package/src/components/cards/card.js +56 -25
  77. package/src/components/cards/card.stories.js +39 -39
  78. package/src/components/carousel/carousel.css.js +93 -99
  79. package/src/components/carousel/carousel.js +32 -4
  80. package/src/components/checkbox/checkbox.css.js +111 -93
  81. package/src/components/checkbox/checkbox.js +64 -90
  82. package/src/components/checkbox/checkbox.stories.js +96 -29
  83. package/src/components/code/code.css.js +70 -0
  84. package/src/components/code/code.js +40 -0
  85. package/src/components/code/code.stories.js +73 -0
  86. package/src/components/compare/compare.css.js +179 -43
  87. package/src/components/compare/compare.js +52 -25
  88. package/src/components/compare/compare.stories.js +61 -4
  89. package/src/components/comparison-panel/comparison-panel.css.js +38 -51
  90. package/src/components/comparison-panel/comparison-panel.js +5 -4
  91. package/src/components/comparison-panel/comparison-panel.stories.js +1 -1
  92. package/src/components/compatibility-table/compatibility-table.css.js +180 -80
  93. package/src/components/compatibility-table/compatibility-table.js +37 -9
  94. package/src/components/control-alignment.stories.js +393 -0
  95. package/src/components/cta-banner/cta-banner.css.js +41 -13
  96. package/src/components/cta-banner/cta-banner.js +6 -5
  97. package/src/components/cta-banner/cta-banner.stories.js +43 -98
  98. package/src/components/display/display.css.js +25 -1
  99. package/src/components/display/display.js +46 -13
  100. package/src/components/display/display.stories.js +270 -40
  101. package/src/components/divider/divider-horizontal.js +21 -5
  102. package/src/components/divider/divider-vertical.js +15 -7
  103. package/src/components/divider/divider.css.js +2 -2
  104. package/src/components/divider/divider.stories.js +38 -34
  105. package/src/components/dropdown/dropdown.css.js +93 -126
  106. package/src/components/dropdown/dropdown.js +114 -34
  107. package/src/components/dropdown/dropdown.stories.js +56 -24
  108. package/src/components/features/features.css.js +24 -11
  109. package/src/components/features/features.js +7 -3
  110. package/src/components/features/features.stories.js +37 -37
  111. package/src/components/features-tab/features-tab.css.js +19 -8
  112. package/src/components/features-tab/features-tab.js +6 -5
  113. package/src/components/features-tab/features-tab.stories.js +23 -23
  114. package/src/components/fieldset/fieldset.css.js +30 -0
  115. package/src/components/fieldset/fieldset.js +28 -0
  116. package/src/components/fieldset/fieldset.stories.js +84 -0
  117. package/src/components/footer/footer-links-group.css.js +10 -14
  118. package/src/components/footer/footer-links-group.js +4 -3
  119. package/src/components/footer/footer-lp.css.js +59 -30
  120. package/src/components/footer/footer-lp.js +17 -12
  121. package/src/components/footer/footer-lp.stories.js +63 -75
  122. package/src/components/footer/footer-nav-menu.css.js +26 -4
  123. package/src/components/footer/footer-nav-menu.js +3 -2
  124. package/src/components/footer/footer.css.js +146 -264
  125. package/src/components/footer/footer.js +176 -106
  126. package/src/components/footer/footer.stories.js +9 -9
  127. package/src/components/footer/localeMap.js +1 -1
  128. package/src/components/grid/grid.css.js +1 -1
  129. package/src/components/grid/grid.js +49 -7
  130. package/src/components/grid/grid.stories.js +111 -0
  131. package/src/components/header/header-code.css.js +61 -51
  132. package/src/components/header/header-code.js +28 -14
  133. package/src/components/header/header-code.stories.js +5 -5
  134. package/src/components/header/header.css.js +655 -176
  135. package/src/components/header/header.js +269 -36
  136. package/src/components/header/header.stories.js +841 -18
  137. package/src/components/heading/heading.css.js +14 -17
  138. package/src/components/heading/heading.js +48 -33
  139. package/src/components/heading/heading.stories.js +27 -25
  140. package/src/components/help-text/help-text.css.js +21 -0
  141. package/src/components/help-text/help-text.js +25 -0
  142. package/src/components/help-text/help-text.stories.js +66 -0
  143. package/src/components/highlight/highlight.css.js +86 -64
  144. package/src/components/highlight/highlight.js +18 -27
  145. package/src/components/highlight/highlight.stories.js +14 -3
  146. package/src/components/icons/analysis-icon.js +6 -16
  147. package/src/components/icons/arrow-down-icon.js +6 -19
  148. package/src/components/icons/arrow-icon.js +44 -0
  149. package/src/components/icons/arrow-right-icon.js +30 -23
  150. package/src/components/icons/arrow-up-icon.js +19 -18
  151. package/src/components/icons/bd-ai-icon.js +1 -1
  152. package/src/components/icons/brand-marks.stories.js +279 -0
  153. package/src/components/icons/brand-signature-icon.js +42 -0
  154. package/src/components/icons/check-icon.js +27 -0
  155. package/src/components/icons/circle-info-icon.js +33 -0
  156. package/src/components/icons/close-icon.js +6 -18
  157. package/src/components/icons/family-icon.js +8 -20
  158. package/src/components/icons/globe-icon.js +6 -18
  159. package/src/components/icons/guide-icon.js +6 -18
  160. package/src/components/icons/icon-element.js +99 -0
  161. package/src/components/icons/icons.js +36 -0
  162. package/src/components/icons/icons.stories.js +184 -52
  163. package/src/components/icons/individual-icon.js +6 -18
  164. package/src/components/icons/laurel-icon.js +9 -19
  165. package/src/components/icons/machine-learning-icon.js +6 -17
  166. package/src/components/icons/marks.js +41 -0
  167. package/src/components/icons/minus-icon.js +7 -21
  168. package/src/components/icons/missing-icon.js +52 -0
  169. package/src/components/icons/plus-icon.js +7 -21
  170. package/src/components/icons/quote-icon.js +19 -19
  171. package/src/components/image/image.css.js +72 -25
  172. package/src/components/image/image.js +11 -6
  173. package/src/components/image/image.stories.js +2 -2
  174. package/src/components/input/custom-form.stories.js +214 -62
  175. package/src/components/input/input-clipboard.css.js +51 -144
  176. package/src/components/input/input-clipboard.js +31 -15
  177. package/src/components/input/input.css.js +8 -146
  178. package/src/components/input/input.js +93 -38
  179. package/src/components/install-options/install-options.css.js +84 -77
  180. package/src/components/install-options/install-options.js +33 -30
  181. package/src/components/install-options/install-options.stories.js +3 -3
  182. package/src/components/label/label.css.js +35 -0
  183. package/src/components/label/label.js +38 -0
  184. package/src/components/label/label.stories.js +82 -0
  185. package/src/components/light-carousel/light-carousel.css.js +111 -63
  186. package/src/components/light-carousel/light-carousel.js +85 -11
  187. package/src/components/light-carousel/light-carousel.stories.js +126 -82
  188. package/src/components/link/link.css.js +150 -99
  189. package/src/components/link/link.js +86 -11
  190. package/src/components/link/link.stories.js +124 -30
  191. package/src/components/list/list-item/list-item.css.js +74 -22
  192. package/src/components/list/list-item/list-item.js +32 -10
  193. package/src/components/list/list-item/list-item.stories.js +23 -23
  194. package/src/components/list/list.css.js +8 -2
  195. package/src/components/list/list.js +3 -2
  196. package/src/components/list/list.stories.js +222 -0
  197. package/src/components/modal/modal.css.js +114 -24
  198. package/src/components/modal/modal.js +83 -7
  199. package/src/components/modal/modal.stories.js +9 -9
  200. package/src/components/overview/overview.css.js +19 -14
  201. package/src/components/overview/overview.js +6 -5
  202. package/src/components/overview/overview.stories.js +10 -10
  203. package/src/components/page-switcher/page-switcher.css.js +97 -40
  204. package/src/components/page-switcher/page-switcher.js +28 -5
  205. package/src/components/page-switcher/page-switcher.stories.js +95 -0
  206. package/src/components/paragraph/paragraph.css.js +15 -10
  207. package/src/components/paragraph/paragraph.js +41 -44
  208. package/src/components/paragraph/paragraph.stories.js +56 -21
  209. package/src/components/picture/picture.css.js +103 -44
  210. package/src/components/picture/picture.js +100 -30
  211. package/src/components/picture/picture.stories.js +1 -1
  212. package/src/components/pricing-cards/new-pricing-card.js +3 -3
  213. package/src/components/pricing-cards/new-pricing.css.js +24 -15
  214. package/src/components/pricing-cards/pricing-card-actions.css.js +1 -1
  215. package/src/components/pricing-cards/pricing-card-actions.js +3 -4
  216. package/src/components/pricing-cards/pricing-card-container.css.js +3 -3
  217. package/src/components/pricing-cards/pricing-card-container.js +3 -2
  218. package/src/components/pricing-cards/pricing-card-header.css.js +10 -8
  219. package/src/components/pricing-cards/pricing-card-header.js +6 -7
  220. package/src/components/pricing-cards/pricing-card-pricing.css.js +16 -9
  221. package/src/components/pricing-cards/pricing-card-pricing.js +8 -6
  222. package/src/components/pricing-cards/pricing-card-show-more.css.js +11 -3
  223. package/src/components/pricing-cards/pricing-card-show-more.js +8 -3
  224. package/src/components/pricing-cards/pricing-card.css.js +244 -190
  225. package/src/components/pricing-cards/pricing-card.js +54 -70
  226. package/src/components/pricing-cards/pricing-feature-item.css.js +25 -8
  227. package/src/components/pricing-cards/pricing-feature-item.js +8 -3
  228. package/src/components/process-steps/process-steps.css.js +26 -14
  229. package/src/components/process-steps/process-steps.js +23 -6
  230. package/src/components/process-steps/process-steps.stories.js +1 -1
  231. package/src/components/progress/progress.css.js +80 -0
  232. package/src/components/progress/progress.js +42 -0
  233. package/src/components/progress/progress.stories.js +79 -0
  234. package/src/components/quick-answer/quick-answer.css.js +75 -13
  235. package/src/components/quick-answer/quick-answer.js +8 -3
  236. package/src/components/radio/radio.css.js +92 -128
  237. package/src/components/radio/radio.js +106 -127
  238. package/src/components/radio/radio.stories.js +54 -19
  239. package/src/components/review/review-grid.css.js +5 -6
  240. package/src/components/review/review-grid.js +14 -11
  241. package/src/components/review/review.css.js +11 -6
  242. package/src/components/review/review.js +3 -8
  243. package/src/components/shared/ensemble.css.js +93 -0
  244. package/src/components/shared/form-field.css.js +206 -0
  245. package/src/components/shared/prop-vocabulary.js +48 -0
  246. package/src/components/shared/token-scales.js +306 -0
  247. package/src/components/skeleton/skeleton.css.js +34 -0
  248. package/src/components/skeleton/skeleton.js +32 -0
  249. package/src/components/skeleton/skeleton.stories.js +70 -0
  250. package/src/components/spinner/spinner.css.js +62 -0
  251. package/src/components/spinner/spinner.js +38 -0
  252. package/src/components/spinner/spinner.stories.js +92 -0
  253. package/src/components/spotlight-section/spotlight-section.css.js +14 -13
  254. package/src/components/spotlight-section/spotlight-section.js +42 -39
  255. package/src/components/switch/switch.css.js +103 -0
  256. package/src/components/switch/switch.js +44 -0
  257. package/src/components/switch/switch.stories.js +86 -0
  258. package/src/components/tab-list/content-switcher.stories.js +189 -0
  259. package/src/components/tab-list/tab-list.css.js +250 -0
  260. package/src/components/tab-list/tab-list.js +184 -0
  261. package/src/components/tab-list/tab-list.stories.js +221 -0
  262. package/src/components/tabs/tabs.css.js +14 -123
  263. package/src/components/tabs/tabs.js +16 -19
  264. package/src/components/tabs/tabs.stories.js +95 -95
  265. package/src/components/tag/tag.css.js +54 -0
  266. package/src/components/tag/tag.js +32 -0
  267. package/src/components/tag/tag.stories.js +126 -0
  268. package/src/components/terminal/terminal-message.css.js +164 -60
  269. package/src/components/terminal/terminal-message.js +5 -5
  270. package/src/components/terminal/terminal-message.stories.js +70 -1
  271. package/src/components/termsOfUse/terms.css.js +25 -20
  272. package/src/components/termsOfUse/terms.js +13 -9
  273. package/src/components/termsOfUse/terms.stories.js +1 -1
  274. package/src/components/textarea/textarea.css.js +51 -0
  275. package/src/components/textarea/textarea.js +136 -0
  276. package/src/components/textarea/textarea.stories.js +191 -0
  277. package/src/components/tooltip/tooltip.css.js +75 -0
  278. package/src/components/tooltip/tooltip.js +156 -0
  279. package/src/components/tooltip/tooltip.stories.js +161 -0
  280. package/src/components/trust-panel/trust-panel.css.js +85 -33
  281. package/src/components/trust-panel/trust-panel.js +18 -10
  282. package/src/components/trust-panel/trust-panel.stories.js +1 -1
  283. package/src/components/ultimate-security-demo.stories.js +431 -0
  284. package/src/components/video/video.css.js +17 -0
  285. package/src/components/video/video.js +40 -0
  286. package/src/components/video/video.stories.js +106 -0
  287. package/src/components/video-player/video-player.css.js +67 -11
  288. package/src/components/video-player/video-player.js +35 -4
  289. package/src/components/video-player/video-player.stories.js +1 -1
  290. package/src/components/visual-alignment.stories.js +411 -0
  291. package/src/components/visually-hidden/visually-hidden.css.js +24 -0
  292. package/src/components/visually-hidden/visually-hidden.js +16 -0
  293. package/src/components/visually-hidden/visually-hidden.stories.js +56 -0
  294. package/src/stories/Button.stories.js +17 -17
  295. package/src/stories/Header.stories.js +10 -10
  296. package/src/stories/Page.js +1 -1
  297. package/src/stories/Page.stories.js +6 -6
  298. package/src/tokens/borders.stories.js +339 -0
  299. package/src/tokens/color-responsibilities.stories.js +227 -0
  300. package/src/tokens/dark-derivation.js +309 -0
  301. package/src/tokens/dark-findings.js +197 -0
  302. package/src/tokens/dark-mode.stories.js +207 -0
  303. package/src/tokens/dimension.stories.js +251 -0
  304. package/src/tokens/elevation.stories.js +143 -0
  305. package/src/tokens/focus.stories.js +276 -0
  306. package/src/tokens/fonts.css +20 -20
  307. package/src/tokens/fonts.stories.js +354 -374
  308. package/src/tokens/layout.css +25 -11
  309. package/src/tokens/motion.stories.js +231 -0
  310. package/src/tokens/shape.stories.js +272 -0
  311. package/src/tokens/spacing-rhythm.stories.js +359 -0
  312. package/src/tokens/spacing.stories.js +208 -703
  313. package/src/tokens/story-chrome.js +503 -0
  314. package/src/tokens/surfaces.stories.js +224 -0
  315. package/src/tokens/text-on-surface.stories.js +164 -0
  316. package/src/tokens/token-map.js +220 -0
  317. package/src/tokens/token-map.stories.js +257 -0
  318. package/src/tokens/token-purposes.js +266 -0
  319. package/src/tokens/tokens-grid.stories.js +388 -900
  320. package/src/tokens/tokens.css +1633 -1045
  321. package/src/tokens/tokens.js +1130 -492
  322. package/src/tokens/tokens.stories.js +137 -47
  323. package/src/tokens/type-ceiling-words.js +299 -0
  324. package/src/tokens/typography.stories.js +1003 -965
  325. package/src/tokens/vertical-rhythm.stories.js +224 -0
  326. package/src/components/awards/awards-icon.js +0 -56
  327. package/src/components/awards/awards-light.js +0 -240
  328. package/src/components/banner/banner.js +0 -60
  329. package/src/components/banner/banner.stories.js +0 -51
  330. package/src/components/button/icons.js +0 -24
  331. package/src/components/input/index.js +0 -0
  332. package/src/components/light-carousel/light-carousel-simple.css.js +0 -183
  333. package/src/components/light-carousel/light-carousel-simple.js +0 -74
  334. package/src/tokens/colors.js +0 -121
  335. package/src/tokens/typography.css.js +0 -207
  336. package/src/tokens/typography.js +0 -0
@@ -0,0 +1,227 @@
1
+ import { html } from 'lit';
2
+ import { contrast, docsPage, refSection, resolve } from './story-chrome.js';
3
+
4
+ export default {
5
+ title: 'Foundations/Color/Responsibilities',
6
+ tags : ['autodocs']
7
+ };
8
+
9
+ /* Measured as the page renders, so a palette change moves the numbers. */
10
+ const ratio = (a, b) => {
11
+ try {
12
+ return Math.round(contrast(a, b) * 100) / 100;
13
+ } catch {
14
+ return null;
15
+ }
16
+ };
17
+
18
+ const chain = (primitive, role, component, ui) => html`
19
+ <div style="display:flex;flex-wrap:wrap;align-items:center;gap:var(--spacing-8);font-family:var(--typography-fontFamily-mono);font-size:var(--typography-fontSize-xs)">
20
+ <code>${primitive}</code><span style="color:var(--color-text-muted)">→</span>
21
+ <code>${role}</code><span style="color:var(--color-text-muted)">→</span>
22
+ <code>${component}</code><span style="color:var(--color-text-muted)">→</span>
23
+ <span>${ui}</span>
24
+ </div>`;
25
+
26
+ const OWNERS = [
27
+ ['Text hierarchy', '--color-text-*', 'primary, secondary, muted, subtle, disabled, inverse, muted-inverse'],
28
+ ['Surfaces', '--color-surface-*', 'default, container, subtle, muted, brand, inverse, info'],
29
+ ['Borders', '--color-border-*', 'container, subtle, default, strong, brand, disabled, container-inverse'],
30
+ ['Actions', '--color-action-*', 'primary, -hover, -active; accent ramp for conversion; on-fill, the ink on any of them'],
31
+ ['Status / feedback', '--status-{tone}-*', 'text, surface, surface-strong (not brand), border, accent, fill, on-fill'],
32
+ ['Focus', '--focus-outline-*', 'one ring, never a border role'],
33
+ ['Indicators', '--indicator-*', 'what marks the current item: the tab and nav underline, and the carousel dot']
34
+ ];
35
+
36
+ export const Responsibilities = () =>
37
+ docsPage({
38
+ title: 'Colour responsibilities',
39
+ lede : `Which family owns which job, and how a colour gets from the palette to the screen.
40
+ Foundations/Surfaces answers "what does this surface mean"; this page answers the
41
+ question one level up — which vocabulary should you be asking at all.`,
42
+ note: html`<b>Do not choose a token because its numeric colour is closest to what you
43
+ want. Choose the role whose responsibility matches the UI.</b><br />
44
+ Fourteen primitives in this system serve more than one live role, and
45
+ <code>#006dff</code> alone serves six. A value tells you nothing about a job.`,
46
+ children: html`
47
+ ${refSection({
48
+ name : 'The chain',
49
+ meta : 'primitive → responsibility → component → rendered UI',
50
+ children: html`
51
+ <p style="max-width:70ch">
52
+ Every colour that carries meaning should travel the whole chain. A component reaching
53
+ straight past the middle is not a style error — it is a missing sentence about what
54
+ the colour is <i>for</i>, and it is why the same job ends up spelled four ways.
55
+ </p>
56
+ <div style="display:grid;gap:var(--spacing-12);background:var(--color-surface-muted);padding:var(--spacing-24);border-radius:var(--radius-lg);overflow-x:auto">
57
+ ${chain('--color-blue-500', '--color-action-primary', '--button-primary-background-default', 'a primary button')}
58
+ ${chain('--color-neutral-0', '--color-text-inverse', '(used directly)', 'footer text on the masthead')}
59
+ ${chain('--color-green-800', '--status-success-text', '(used directly)', 'a "supported" table cell')}
60
+ ${chain('--color-neutral-300', '--color-text-muted-inverse', '--bd-terminalmsg-subdued', 'terminal dim text')}
61
+ </div>
62
+ <p style="max-width:70ch;margin-top:var(--spacing-24)">
63
+ The middle step is the only one that survives a redesign. Primitives get retuned and
64
+ components get rewritten; the responsibility is the thing that stays true.
65
+ </p>`
66
+ })}
67
+
68
+ ${refSection({
69
+ name : 'Who owns what',
70
+ meta : `${OWNERS.length} families`,
71
+ children: html`
72
+ <div class="table-scroll">
73
+ <table>
74
+ <caption>Each family answers one question. Reaching into another family's
75
+ vocabulary is how a status colour ends up meaning "blue".</caption>
76
+ <thead><tr><th scope="col">Responsibility</th><th scope="col">Family</th><th scope="col">Roles</th></tr></thead>
77
+ <tbody>
78
+ ${OWNERS.map(([r, f, roles]) => html`
79
+ <tr><td><b>${r}</b></td><td><code>${f}</code></td><td>${roles}</td></tr>`)}
80
+ </tbody>
81
+ </table>
82
+ </div>`
83
+ })}
84
+
85
+ ${refSection({
86
+ name : 'General vs status',
87
+ meta : 'two systems, one seam',
88
+ children: html`
89
+ <p style="max-width:70ch">
90
+ <b>The general <code>--color-*</code> family owns neutral hierarchy, surfaces and
91
+ actions. The <code>--status-*</code> family owns feedback state.</b> That split was
92
+ not designed; it is what adoption settled on, and measuring it is how it became a
93
+ rule. The feedback members of the general family —
94
+ <code>--color-surface-error</code>, <code>-success</code>, <code>-warning</code> —
95
+ have no component consumers at all, while their status counterparts have two or
96
+ three each.
97
+ </p>
98
+ <p style="max-width:70ch">
99
+ <b>They are not deleted, and value equality is not the reason.</b>
100
+ <code>--color-surface-info</code> and <code>--status-info-surface</code> both resolve
101
+ to the same tint, and consolidating them would be wrong: <code>bd-highlight</code>
102
+ uses the first one for decorative emphasis, which is not feedback. Two roles that
103
+ happen to share a value are still two roles.
104
+ </p>
105
+ <p style="max-width:70ch">
106
+ <code>--color-text-success</code> and <code>--color-text-warning</code> look unused
107
+ in CSS and are not: <code>shared/token-scales.js</code> exposes them as
108
+ <code>color="success"</code> and <code>color="warning"</code> on every
109
+ <code>bd-h</code> and <code>bd-p</code>. Everything in <code>tokens.css</code> also
110
+ ships to consuming pages, so an unused-looking token may simply have consumers this
111
+ repository cannot see.
112
+ </p>`
113
+ })}
114
+
115
+ ${refSection({
116
+ name : 'Action vs brand',
117
+ meta : 'a ramp, not a colour',
118
+ children: html`
119
+ <p style="max-width:70ch">
120
+ <code>--color-action-primary</code> → <code>-hover</code> → <code>-active</code> is a
121
+ state ramp. Brand blue is an identity. They overlap in value at the first step and
122
+ diverge immediately after, which is why "use the brand colour" is not an answer to
123
+ "what should hover look like".
124
+ </p>
125
+ <p style="max-width:70ch">
126
+ <b>Let the state pick the token.</b> There is no hover-blue role and there should not
127
+ be one — the question is what the element is doing, not what colour it is going.
128
+ <code>bd-link</code>'s resting colour is <code>--color-text-brand</code> rather than
129
+ <code>--color-action-primary</code> deliberately: blue-500 measures
130
+ ${ratio(resolve('--color-action-primary'), resolve('--color-surface-default')) ?? '—'}:1
131
+ on white and fails on any tint, so links start one step down the ramp.
132
+ </p>`
133
+ })}
134
+
135
+ ${refSection({
136
+ name : 'Subtle, muted, disabled',
137
+ meta : 'two are hierarchy, one is a state',
138
+ children: html`
139
+ <div class="table-scroll">
140
+ <table>
141
+ <caption>Measured on --color-surface-default as this page renders</caption>
142
+ <thead><tr><th scope="col">Role</th><th scope="col">Kind</th><th scope="col">On white</th><th scope="col">Use it for</th></tr></thead>
143
+ <tbody>
144
+ ${[
145
+ ['--color-text-secondary', 'hierarchy', 'supporting copy under a title'],
146
+ ['--color-text-muted', 'hierarchy', 'metadata, captions, timestamps'],
147
+ ['--color-text-subtle', 'hierarchy', 'the quietest readable step'],
148
+ ['--color-text-disabled', 'STATE', 'a control the user cannot operate']
149
+ ].map(([t, kind, use]) => html`
150
+ <tr>
151
+ <td><code>${t.replace('--color-text-', '')}</code></td>
152
+ <td>${kind === 'STATE' ? html`<b>${kind}</b>` : kind}</td>
153
+ <td class="num">${ratio(resolve(t), resolve('--color-surface-default')) ?? '—'}</td>
154
+ <td>${use}</td>
155
+ </tr>`)}
156
+ </tbody>
157
+ </table>
158
+ </div>
159
+ <p style="max-width:70ch">
160
+ <b>Disabled is a state, not a step on the hierarchy.</b> Reaching for a hierarchy role
161
+ to paint a disabled control makes disabled content indistinguishable from ordinary
162
+ quiet content, and it is what <code>bd-checkbox</code>, <code>bd-radio</code> and
163
+ <code>bd-switch</code> currently do. They are left alone on purpose: the value they
164
+ chose is the <i>more</i> legible one, and correcting the name at the cost of contrast
165
+ would be the wrong trade. That is a design decision about
166
+ <code>--color-text-disabled</code>'s value, recorded rather than made here.
167
+ </p>`
168
+ })}
169
+
170
+ ${refSection({
171
+ name : 'Inverse',
172
+ meta : 'two steps, and why not three',
173
+ children: html`
174
+ <p style="max-width:70ch">
175
+ On an inverse surface only two text roles exist:
176
+ <code>--color-text-inverse</code> for high emphasis and
177
+ <code>--color-text-muted-inverse</code> for everything subordinate. The light-side
178
+ hierarchy cannot be reused — <code>-secondary</code>, <code>-muted</code> and
179
+ <code>-subtle</code> measure
180
+ ${ratio(resolve('--color-text-secondary'), resolve('--color-surface-inverse')) ?? '—'},
181
+ ${ratio(resolve('--color-text-muted'), resolve('--color-surface-inverse')) ?? '—'} and
182
+ ${ratio(resolve('--color-text-subtle'), resolve('--color-surface-inverse')) ?? '—'}
183
+ against <code>--color-surface-inverse</code>.
184
+ </p>
185
+ <div style="background:var(--color-surface-inverse);padding:var(--spacing-24);border-radius:var(--radius-lg);max-width:52ch">
186
+ <p style="margin:0 0 var(--spacing-8);color:var(--color-text-inverse)">
187
+ High emphasis — <code>--color-text-inverse</code>,
188
+ ${ratio(resolve('--color-text-inverse'), resolve('--color-surface-inverse')) ?? '—'}:1
189
+ </p>
190
+ <p style="margin:0;color:var(--color-text-muted-inverse);font-size:var(--typography-fontSize-sm)">
191
+ Subordinate — <code>--color-text-muted-inverse</code>,
192
+ ${ratio(resolve('--color-text-muted-inverse'), resolve('--color-surface-inverse')) ?? '—'}:1
193
+ </p>
194
+ </div>
195
+ <p style="max-width:70ch;margin-top:var(--spacing-16)">
196
+ There is no third step, and the reason is worth keeping: one component wanted one, and
197
+ measurement showed its value was a contrast failure rather than a tier. A
198
+ responsibility needs independent consumers <i>and</i> a value that survives the
199
+ grounds it will be used on. Do not build inverse counterparts for symmetry — build
200
+ them when two components have already built them separately.
201
+ </p>`
202
+ })}
203
+
204
+ ${refSection({
205
+ name : 'When a primitive is the right answer',
206
+ meta : 'not every colour has a responsibility',
207
+ children: html`
208
+ <p style="max-width:70ch">
209
+ Zero primitive references is not the goal. A primitive is right when the value
210
+ genuinely has no semantic job:
211
+ </p>
212
+ <ul style="max-width:70ch">
213
+ <li><b>Artwork.</b> The terminal's window dots are red, amber and green because that
214
+ is what a terminal looks like. They are not status.</li>
215
+ <li><b>Illustration and brand marks.</b> A logo's colours belong to the logo.</li>
216
+ <li><b>Product-specific comparison colours.</b> The compare bars name competitors, not
217
+ states.</li>
218
+ <li><b>A deliberate local visual API</b> a consuming page is meant to set.</li>
219
+ </ul>
220
+ <p style="max-width:70ch">
221
+ The test is not "is this a token" but <b>"does this colour communicate something the
222
+ system has a word for"</b>. If it does, use the word. If it does not, say so in a
223
+ comment so the next person does not have to work it out.
224
+ </p>`
225
+ })}
226
+ `
227
+ });
@@ -0,0 +1,309 @@
1
+ /**
2
+ * A dark value for every semantic colour role, derived from the light theme's
3
+ * own measurements. This is the method behind the 🔬 Dark mode study page in
4
+ * the Figma foundation file, and the data behind Foundations/Theming/Dark mode.
5
+ *
6
+ * Code has no dark theme. These are proposals, computed from tokens.css each
7
+ * time they are read, so they follow the palette instead of describing it.
8
+ *
9
+ * The method is the point: a dark value is chosen to KEEP what the light value
10
+ * measures, not to look like a dark theme.
11
+ *
12
+ * text keeps its contrast against the ground it is read on: the step
13
+ * whose ratio is nearest the light ratio, never below 4.5 where
14
+ * light clears 4.5, with the hierarchy order kept.
15
+ * grounds keep their tonal step from the page, matched by a step LIGHTER
16
+ * than the dark page (on dark, a container is one step lighter).
17
+ * edges keep their ratio class against the page: under 3:1 stays
18
+ * decorative, 3:1 and over stays a boundary.
19
+ * status tints sit at the dark end of the tone's ramp; ink is the
20
+ * nearest-to-light step that clears 4.5 on the tint; accent clears
21
+ * 3:1 on the page AND on its own tint; a fill keeps its light pair
22
+ * unless it would vanish into the dark page, then the chip inverts.
23
+ * action fills are self-contained, so they keep their light value, and
24
+ * their ink (--color-action-on-fill) stays white. The label
25
+ * clears 4.5:1 in every state; a RESTING fill also clears 3:1 on
26
+ * the page, kept by choice. Hover and press are exempt from the
27
+ * page bar: WCAG 1.4.11 treats a hover treatment as supplemental,
28
+ * and a button with a text label needs no boundary at all. On the
29
+ * dark page no ramp could keep it anyway -- label-on-fill times
30
+ * fill-on-page is 19.45 for every fill, so both bars leave a band
31
+ * only 1.44:1 wide (decision 4, decided 2026-10-02).
32
+ *
33
+ * A role is measured on every ground code draws it on, not only the page --
34
+ * the first version measured accent and brand text on the page alone, and the
35
+ * Alert atom is what showed it. Only existing primitives are used: where no
36
+ * step meets a rule, the nearest is returned and the row fails, because adding
37
+ * a primitive is its own decision.
38
+ *
39
+ * Not a story file: Storybook only globs *.stories.js. Pure, so it runs under
40
+ * Node too -- the parity check against the original script did.
41
+ */
42
+
43
+ const RAMP = /^--color-(blue|red|green|yellow|neutral)-(\d+)$/;
44
+
45
+ export const blank = text => text.replace(/\/\*[\s\S]*?\*\//g, '');
46
+
47
+ /** name -> declared value, first declaration wins. */
48
+ export function declarations(cssText) {
49
+ const out = new Map();
50
+ for (const m of blank(cssText).matchAll(/(?:^|[\s;{])(--[A-Za-z0-9-]+)\s*:\s*([^;]*);/g)) {
51
+ if (!out.has(m[1])) out.set(m[1], m[2]);
52
+ }
53
+ return out;
54
+ }
55
+
56
+ export function luminance(hex) {
57
+ const c = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
58
+ .map(x => (x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4));
59
+ return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
60
+ }
61
+
62
+ export function contrast(a, b) {
63
+ const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
64
+ return (x + 0.05) / (y + 0.05);
65
+ }
66
+
67
+ const f2 = n => n.toFixed(2);
68
+ const f3 = n => n.toFixed(3);
69
+
70
+ export function derive(cssText) {
71
+ const decl = declarations(cssText);
72
+ const RAMPS = {};
73
+ for (const [k, v] of decl) {
74
+ const m = k.match(RAMP);
75
+ if (m) (RAMPS[m[1]] ??= {})[Number(m[2])] = v.trim().toLowerCase();
76
+ }
77
+ const prim = ref => {
78
+ const m = ref.trim().match(/^var\(\s*--color-(blue|red|green|yellow|neutral)-(\d+)\s*\)$/);
79
+ return m ? [m[1], Number(m[2])] : null;
80
+ };
81
+ const hexof = p => RAMPS[p[0]][p[1]];
82
+ const steps = ramp => Object.keys(RAMPS[ramp]).map(Number)
83
+ .sort((a, b) => a - b);
84
+ const lightOf = t => (decl.has(t) ? prim(decl.get(t)) : null);
85
+ const same = (a, b) => a && b && a[0] === b[0] && a[1] === b[1];
86
+ const lum = luminance, ratio = contrast;
87
+
88
+ function nearest(ramp, ground, target, { floor = null, lighterThan = null, exclude = [], also = [] } = {}) {
89
+ let best = null;
90
+ for (const s of steps(ramp)) {
91
+ if (exclude.some(e => same(e, [ramp, s]))) continue;
92
+ const h = RAMPS[ramp][s];
93
+ if (lighterThan && lum(h) <= lum(lighterThan)) continue;
94
+ const r = ratio(h, ground);
95
+ if (floor && (r < floor || also.some(g => ratio(h, g) < floor))) continue;
96
+ const d = Math.abs(r - target);
97
+ if (best === null || d < best[0]) best = [d, [ramp, s]];
98
+ }
99
+ return best && best[1];
100
+ }
101
+
102
+ const rows = [];
103
+ const row = (token, dark, against, againstLight, againstDark, rule, need = null) => {
104
+ const lp = lightOf(token);
105
+ const lightRatio = lp ? ratio(hexof(lp), againstLight) : null;
106
+ const darkRatio = dark ? ratio(hexof(dark), againstDark) : null;
107
+ const ok = need === null ? null : darkRatio !== null && darkRatio >= need;
108
+ rows.push({ token, light: lp, lightRatio, dark, darkRatio, against, rule, need, ok });
109
+ return dark;
110
+ };
111
+
112
+ const WHITE = RAMPS.neutral[0];
113
+ const L = {};
114
+ for (const t of decl.keys()) { const p = lightOf(t); if (p) L[t] = hexof(p); }
115
+
116
+ // ---------------------------------------------------------------- grounds --
117
+ const PAGE = ['neutral', 950]; // near-black, so a lighter container step exists
118
+ const page = hexof(PAGE);
119
+ row('--color-surface-default', PAGE, 'itself', L['--color-surface-default'], page,
120
+ 'the dark page: near-black rather than pure black, so a lighter container step exists above it');
121
+ // in light order, each further from the page than the last: a ground that
122
+ // lands on its neighbour's step has lost the step it exists for. Two grounds
123
+ // that share a value in light (muted and disabled) share it in dark.
124
+ const groundOf = {};
125
+ let prevGround = PAGE;
126
+ for (const t of ['--color-surface-container', '--color-surface-subtle', '--color-surface-muted', '--color-surface-disabled', '--color-surface-pressed']) {
127
+ if (!L[t]) continue;
128
+ const twin = Object.keys(groundOf).find(k => L[k] === L[t]);
129
+ const target = ratio(L[t], L['--color-surface-default']);
130
+ const d = twin ? groundOf[twin] : nearest('neutral', page, target, { lighterThan: hexof(prevGround) });
131
+ groundOf[t] = d;
132
+ prevGround = d;
133
+ row(t, d, 'surface-default', L['--color-surface-default'], page,
134
+ twin ? `shares ${twin.slice(16)}'s value in light, so shares it in dark` : `keeps its tonal step from the page (light ${f3(target)}), further out than the ground before it`);
135
+ }
136
+ row('--color-surface-inverse', ['neutral', 50], 'surface-default', L['--color-surface-default'], page,
137
+ 'flips: on a dark theme the inverse ground is the light one (Foundations/Surfaces)');
138
+ row('--color-surface-brand', lightOf('--color-surface-brand'), 'surface-default', L['--color-surface-default'], page,
139
+ 'follows the brand, not the neutral ramp: unchanged');
140
+
141
+ // ------------------------------------------------------------------- text --
142
+ let prev = null;
143
+ for (const t of ['--color-text-primary', '--color-text-secondary', '--color-text-muted', '--color-text-subtle']) {
144
+ const target = ratio(L[t], WHITE);
145
+ const floor = target >= 4.5 ? 4.5 : null;
146
+ let d = nearest('neutral', page, target, { floor, lighterThan: page });
147
+ if (prev && lum(hexof(d)) >= lum(hexof(prev))) { // keep the hierarchy strictly ordered
148
+ const cands = steps('neutral').filter(s => lum(RAMPS.neutral[s]) < lum(hexof(prev)) && (!floor || ratio(RAMPS.neutral[s], page) >= floor));
149
+ if (cands.length) d = ['neutral', Math.min(...cands)];
150
+ else { // nothing clears the floor: the nearest, and the row fails
151
+ const darker = steps('neutral').filter(s => lum(RAMPS.neutral[s]) < lum(hexof(prev)) && lum(RAMPS.neutral[s]) > lum(page));
152
+ d = ['neutral', darker.reduce((a, s) => (Math.abs(ratio(RAMPS.neutral[s], page) - target) < Math.abs(ratio(RAMPS.neutral[a], page) - target) ? s : a))];
153
+ }
154
+ }
155
+ prev = d;
156
+ row(t, d, 'surface-default', WHITE, page, `keeps its contrast on the page (light ${f2(target)}:1)`, floor);
157
+ }
158
+ {
159
+ const t = '--color-text-disabled';
160
+ const target = ratio(L[t], WHITE);
161
+ row(t, nearest('neutral', page, target, { lighterThan: page }), 'surface-default', WHITE, page,
162
+ `STATE, exempt from the minimum: keeps its light ratio (${f2(target)}:1)`);
163
+ }
164
+ const invDark = hexof(['neutral', 50]);
165
+ for (const t of ['--color-text-inverse', '--color-text-muted-inverse']) {
166
+ const target = ratio(L[t], L['--color-surface-inverse']);
167
+ row(t, nearest('neutral', invDark, target, { floor: 4.5 }), 'surface-inverse', L['--color-surface-inverse'], invDark,
168
+ `keeps its contrast on the inverse ground (light ${f2(target)}:1)`, 4.5);
169
+ }
170
+ // feedback pairs: tint first, then the ink measured on it
171
+ for (const [tone, ramp] of [['success', 'green'], ['warning', 'yellow'], ['error', 'red'], ['info', 'blue']]) {
172
+ const st = `--color-surface-${tone}`;
173
+ const step = ratio(L[st], L['--color-surface-default']);
174
+ const tint = nearest(ramp, page, step, { lighterThan: page });
175
+ row(st, tint, 'surface-default', L['--color-surface-default'], page, `a tint one step off the page, like light (${f3(step)})`);
176
+ const tt = `--color-text-${tone}`;
177
+ if (L[tt]) {
178
+ const target = ratio(L[tt], L[st]);
179
+ row(tt, nearest(ramp, hexof(tint), target, { floor: 4.5 }), `surface-${tone}`, L[st], hexof(tint),
180
+ `keeps its contrast on its paired tint (light ${f2(target)}:1)`, 4.5);
181
+ }
182
+ }
183
+
184
+ // ------------------------------------------------------------------ edges --
185
+ for (const t of ['--color-border-subtle', '--color-border-default', '--color-border-strong', '--color-border-disabled', '--color-border-container']) {
186
+ const target = ratio(L[t], WHITE);
187
+ const floor = target >= 3 ? 3 : null;
188
+ row(t, nearest('neutral', page, target, { floor, lighterThan: page }), 'surface-default', WHITE, page,
189
+ `keeps its ratio class on the page (light ${f2(target)}:1)`, floor);
190
+ }
191
+ {
192
+ const t = '--color-border-brand';
193
+ const target = ratio(L[t], WHITE);
194
+ row(t, nearest('blue', page, target, { floor: 3 }), 'surface-default', WHITE, page, `keeps its boundary contrast (light ${f2(target)}:1)`, 3);
195
+ }
196
+
197
+ // ----------------------------------------------------------------- action --
198
+ const FILLS = ['--color-action-primary', '--color-action-primary-hover', '--color-action-primary-active',
199
+ '--color-action-accent', '--color-action-accent-hover', '--color-action-accent-active'];
200
+ const isState = t => /-(hover|active)$/.test(t);
201
+ const fillRow = (t, lp, dk, label, ground, lead) => {
202
+ const state = isState(t);
203
+ rows.push({ token : t, light : lp, lightRatio : ratio(WHITE, hexof(lp)), dark : dk, darkRatio : label, against : 'white label',
204
+ rule : `${lead}: the white label keeps ${f2(label)}:1; against the dark page it measures ${f2(ground)}:1${state ? ', a state, so supplemental' : ', a resting fill, kept at 3:1'}`,
205
+ need : 4.5, ok : label >= 4.5 && (state || ground >= 3), groundRatio: ground, state });
206
+ };
207
+ for (const t of FILLS) {
208
+ const lp = lightOf(t);
209
+ fillRow(t, lp, lp, ratio(WHITE, hexof(lp)), ratio(hexof(lp), page), 'self-contained fill');
210
+ }
211
+
212
+ // the neutral fill cannot keep its light value: neutral-950 IS the dark page.
213
+ // Rest takes the step that clears 3:1 on the page and keeps the white label
214
+ // at 4.5; hover and press step away from the label's side of the ramp, the
215
+ // direction light's primary and accent ramps already go (decision 4).
216
+ if (decl.has('--color-action-secondary')) {
217
+ const usable = steps('neutral').filter(s => ratio(WHITE, RAMPS.neutral[s]) >= 4.5);
218
+ const rest = usable.filter(s => ratio(RAMPS.neutral[s], page) >= 3).reduce((a, s) => (ratio(RAMPS.neutral[s], page) > ratio(RAMPS.neutral[a], page) ? s : a), usable[0]);
219
+ const after = usable.filter(s => s > rest);
220
+ const pick = { '--color-action-secondary': rest, '--color-action-secondary-hover': after[0], '--color-action-secondary-active': after[1] };
221
+ for (const [t, step] of Object.entries(pick)) {
222
+ if (!decl.has(t) || step == null) continue;
223
+ const hex = RAMPS.neutral[step];
224
+ fillRow(t, lightOf(t), ['neutral', step], ratio(WHITE, hex), ratio(hex, page), 'the neutral fill moves off the dark page');
225
+ }
226
+ }
227
+
228
+ // ----------------------------------------------------------------- status --
229
+ const TINTS = {};
230
+ const TONES = { neutral: 'neutral', brand: 'blue', info: 'blue', success: 'green', warning: 'yellow', error: 'red' };
231
+ for (const [tone, ramp] of Object.entries(TONES)) {
232
+ const p = `--status-${tone}-`;
233
+ const hasStrong = decl.has(`${p}surface-strong`);
234
+ const sTarget = ratio(L[`${p}surface`], WHITE);
235
+ const surf = nearest(ramp, page, sTarget, { lighterThan: page });
236
+ row(`${p}surface`, surf, 'surface-default', WHITE, page, `the light tint: one step off the page, like light (${f3(sTarget)})`);
237
+ let strong = null;
238
+ if (hasStrong) {
239
+ const ssTarget = ratio(L[`${p}surface-strong`], WHITE);
240
+ strong = nearest(ramp, page, ssTarget, { lighterThan: hexof(surf), exclude: [surf] });
241
+ row(`${p}surface-strong`, strong, 'surface-default', WHITE, page,
242
+ `the heavier tint: further from the page than -surface, like light (${f3(ssTarget)})`);
243
+ }
244
+ const inkGround = hexof(strong || surf);
245
+ const inkOn = hasStrong ? 'surface-strong' : 'surface';
246
+ const tTarget = ratio(L[`${p}text`], L[p + inkOn]);
247
+ row(`${p}text`, nearest(ramp, inkGround, tTarget, { floor: 4.5 }), inkOn, L[p + inkOn], inkGround,
248
+ `ink on the heavier tint a badge uses, keeping light (${f2(tTarget)}:1)`, 4.5);
249
+ const bTarget = ratio(L[`${p}border`], L[`${p}surface`]);
250
+ row(`${p}border`, nearest(ramp, hexof(surf), bTarget, { lighterThan: hexof(surf) }), 'surface', L[`${p}surface`], hexof(surf),
251
+ `the tag hairline on its own tint, keeping light (${f2(bTarget)}:1)`);
252
+ TINTS[tone] = hexof(surf);
253
+ const aTarget = ratio(L[`${p}accent`], WHITE);
254
+ const accent = nearest(ramp, page, aTarget, { floor: 3, also: [hexof(surf)] });
255
+ row(`${p}accent`, accent, 'surface-default', WHITE, page,
256
+ `the badge dot on the page AND the alert icon on its own tint: clears 3:1 on both (${f2(ratio(hexof(accent), hexof(surf)))}:1 on the tint; light ${f2(aTarget)}:1 on the page)`, 3);
257
+ let fill = lightOf(`${p}fill`), onfill = lightOf(`${p}on-fill`), why = 'self-contained chip: unchanged';
258
+ if (ratio(hexof(fill), page) < 3) { // would vanish into the page: invert the chip
259
+ fill = nearest(ramp, page, ratio(L[`${p}fill`], WHITE), { floor: 3 });
260
+ onfill = ['neutral', steps('neutral').reduce((a, s) => (ratio(RAMPS.neutral[s], hexof(fill)) > ratio(RAMPS.neutral[a], hexof(fill)) ? s : a))];
261
+ why = 'the light fill vanishes into the dark page, so the chip inverts';
262
+ }
263
+ row(`${p}fill`, fill, 'surface-default', WHITE, page, why, 3);
264
+ row(`${p}on-fill`, onfill, 'fill', L[`${p}fill`], hexof(fill), 'ink on the fill', 4.5);
265
+ }
266
+
267
+ // ------------------------------------------------- brand text, every ground --
268
+ const tints = Object.values(TINTS);
269
+ const brandTarget = ratio(L['--color-text-brand'], WHITE);
270
+ const brand = nearest('blue', page, brandTarget, { floor: 4.5, also: tints });
271
+ const brandWorst = Math.min(...tints.map(g => ratio(hexof(brand), g)));
272
+ row('--color-text-brand', brand, 'surface-default', WHITE, page,
273
+ `keeps its contrast on the page (light ${f2(brandTarget)}:1) AND clears 4.5:1 on every status tint, where an alert sets its action link (${f2(brandWorst)}:1 on the weakest)`, 4.5);
274
+
275
+ // ------------------------------------------- roles added by 591c695a --
276
+ if (decl.has('--color-text-brand-hover')) {
277
+ const sep = ratio(L['--color-text-brand-hover'], L['--color-text-brand']);
278
+ const cands = steps('blue').filter(s => lum(RAMPS.blue[s]) > lum(hexof(brand)) && [page, ...tints].every(g => ratio(RAMPS.blue[s], g) >= 4.5));
279
+ const hover = ['blue', cands.reduce((a, s) => (Math.abs(ratio(RAMPS.blue[s], hexof(brand)) - sep) < Math.abs(ratio(RAMPS.blue[a], hexof(brand)) - sep) ? s : a))];
280
+ row('--color-text-brand-hover', hover, 'surface-default', WHITE, page,
281
+ `one step past the rest colour, away from the page as in light (${f2(ratio(hexof(hover), hexof(brand)))}:1 from it; light ${f2(sep)})`, 4.5);
282
+ }
283
+ if (decl.has('--color-action-on-fill')) {
284
+ const worst = Math.min(...FILLS.map(t => ratio(WHITE, hexof(lightOf(t)))));
285
+ rows.push({ token : '--color-action-on-fill', light : lightOf('--color-action-on-fill'), lightRatio: worst, dark : lightOf('--color-action-on-fill'), darkRatio : worst,
286
+ against : 'action fills', rule : `white in both themes: the fills do not change, so neither does their ink (weakest ${f2(worst)}:1)`, need : 4.5, ok : worst >= 4.5 });
287
+ }
288
+
289
+ // ------------------------------------------ the media frame, decision 15 --
290
+ // fixed in every theme: a letterbox and a terminal are dark whatever the page
291
+ // is, and their ink is fixed with them. Measured on the frame, not the page.
292
+ if (decl.has('--color-surface-media')) {
293
+ const frame = lightOf('--color-surface-media');
294
+ row('--color-surface-media', frame, 'surface-default', L['--color-surface-default'], page,
295
+ 'fixed: the frame around media is dark in every theme, so it keeps its light value');
296
+ for (const t of ['--color-text-on-media', '--color-text-on-media-muted']) {
297
+ if (!decl.has(t)) continue;
298
+ row(t, lightOf(t), 'surface-media', hexof(frame), hexof(frame),
299
+ `fixed with its ground: ${f2(ratio(L[t], hexof(frame)))}:1 on the frame in either theme`, 4.5);
300
+ }
301
+ }
302
+
303
+ const name = p => (p ? `${p[0]}/${p[1]}` : null);
304
+ return {
305
+ page : hexof(PAGE),
306
+ ramps: RAMPS,
307
+ rows : rows.map(r => ({ ...r, light: name(r.light), dark: name(r.dark), lightHex: r.light ? hexof(r.light) : null, darkHex: r.dark ? hexof(r.dark) : null }))
308
+ };
309
+ }