@eifi1/ui-kit 0.51.3 → 0.51.5

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 (780) hide show
  1. package/README.md +6 -6
  2. package/dist/account/account-settings-labels.js.map +1 -1
  3. package/dist/account/account-settings.js +11 -9
  4. package/dist/account/account-settings.js.map +1 -1
  5. package/dist/account/delete-account-setting.js +56 -65
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting-labels.js.map +1 -1
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/passkeys-setting.js.map +1 -1
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.js +1 -3
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/admin-action-log.js.map +1 -1
  14. package/dist/admin/admin-area-labels.js +4 -1
  15. package/dist/admin/admin-area-labels.js.map +1 -1
  16. package/dist/admin/admin-overview.js +19 -11
  17. package/dist/admin/admin-overview.js.map +1 -1
  18. package/dist/admin/admin-sections.js.map +1 -1
  19. package/dist/admin/admin-tool-links.d.ts +1 -1
  20. package/dist/admin/admin-tool-links.js +7 -1
  21. package/dist/admin/admin-tool-links.js.map +1 -1
  22. package/dist/admin/invitations-panel.js +2 -13
  23. package/dist/admin/invitations-panel.js.map +1 -1
  24. package/dist/admin/mail-test-card.js.map +1 -1
  25. package/dist/admin/reviewer-scope-editor.js +4 -11
  26. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  27. package/dist/admin/role-select.js.map +1 -1
  28. package/dist/admin/user-roster.js +1 -9
  29. package/dist/admin/user-roster.js.map +1 -1
  30. package/dist/auth/accept-invitation.js.map +1 -1
  31. package/dist/auth/auth-errors.js.map +1 -1
  32. package/dist/auth/forgot-password-form.js +23 -31
  33. package/dist/auth/forgot-password-form.js.map +1 -1
  34. package/dist/auth/register-form.js.map +1 -1
  35. package/dist/auth/reset-password-form.js.map +1 -1
  36. package/dist/auth/sign-in-form.js +13 -5
  37. package/dist/auth/sign-in-form.js.map +1 -1
  38. package/dist/auth/verify-email.js +1 -8
  39. package/dist/auth/verify-email.js.map +1 -1
  40. package/dist/billing/billing-banner.js.map +1 -1
  41. package/dist/billing/billing-labels.js.map +1 -1
  42. package/dist/billing/checkout-processing.js.map +1 -1
  43. package/dist/billing/plan-card.js +91 -85
  44. package/dist/billing/plan-card.js.map +1 -1
  45. package/dist/billing/plan-limit-toast.js.map +1 -1
  46. package/dist/billing/plan-limit.js.map +1 -1
  47. package/dist/billing/plan-price.js.map +1 -1
  48. package/dist/billing/plan-switch.js.map +1 -1
  49. package/dist/billing/subscription-actions.js.map +1 -1
  50. package/dist/bin/eifi1-pay-page.mjs +6 -1
  51. package/dist/components/account-chips.js.map +1 -1
  52. package/dist/components/alert-banner.js +1 -5
  53. package/dist/components/alert-banner.js.map +1 -1
  54. package/dist/components/amount-input.js +29 -12
  55. package/dist/components/amount-input.js.map +1 -1
  56. package/dist/components/authed-image.js.map +1 -1
  57. package/dist/components/autocomplete.js.map +1 -1
  58. package/dist/components/bulk-action-bar.js +1 -4
  59. package/dist/components/bulk-action-bar.js.map +1 -1
  60. package/dist/components/button-group.js.map +1 -1
  61. package/dist/components/button-look.js.map +1 -1
  62. package/dist/components/button.js +1 -7
  63. package/dist/components/button.js.map +1 -1
  64. package/dist/components/calculator.js +5 -5
  65. package/dist/components/calculator.js.map +1 -1
  66. package/dist/components/calendar-heatmap.js +4 -10
  67. package/dist/components/calendar-heatmap.js.map +1 -1
  68. package/dist/components/card.js +1 -8
  69. package/dist/components/card.js.map +1 -1
  70. package/dist/components/chart-zoom-core.js +3 -17
  71. package/dist/components/chart-zoom-core.js.map +1 -1
  72. package/dist/components/chart.d.ts +1 -1
  73. package/dist/components/chart.js +4 -22
  74. package/dist/components/chart.js.map +1 -1
  75. package/dist/components/checkbox-group.js +1 -4
  76. package/dist/components/checkbox-group.js.map +1 -1
  77. package/dist/components/chip-input.js +4 -1
  78. package/dist/components/chip-input.js.map +1 -1
  79. package/dist/components/chip.js +10 -5
  80. package/dist/components/chip.js.map +1 -1
  81. package/dist/components/choice-card.js +58 -74
  82. package/dist/components/choice-card.js.map +1 -1
  83. package/dist/components/column-mapper-labels.js.map +1 -1
  84. package/dist/components/column-mapper.js +167 -227
  85. package/dist/components/column-mapper.js.map +1 -1
  86. package/dist/components/combobox-core.js +5 -9
  87. package/dist/components/combobox-core.js.map +1 -1
  88. package/dist/components/combobox.js +1 -4
  89. package/dist/components/combobox.js.map +1 -1
  90. package/dist/components/confirm-dialog.js.map +1 -1
  91. package/dist/components/copy-button.js.map +1 -1
  92. package/dist/components/country-select.d.ts +5 -4
  93. package/dist/components/country-select.js +3 -19
  94. package/dist/components/country-select.js.map +1 -1
  95. package/dist/components/currency-option-list.js +13 -11
  96. package/dist/components/currency-option-list.js.map +1 -1
  97. package/dist/components/currency-select.js +1 -6
  98. package/dist/components/currency-select.js.map +1 -1
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.js.map +1 -1
  101. package/dist/components/data-table-column-settings.js.map +1 -1
  102. package/dist/components/data-table-filter-popover.js +2 -18
  103. package/dist/components/data-table-filter-popover.js.map +1 -1
  104. package/dist/components/data-table-filters.js.map +1 -1
  105. package/dist/components/data-table-fit.js +7 -5
  106. package/dist/components/data-table-fit.js.map +1 -1
  107. package/dist/components/data-table-groups.js +3 -1
  108. package/dist/components/data-table-groups.js.map +1 -1
  109. package/dist/components/data-table-head.js +49 -57
  110. package/dist/components/data-table-head.js.map +1 -1
  111. package/dist/components/data-table-mobile.js +31 -44
  112. package/dist/components/data-table-mobile.js.map +1 -1
  113. package/dist/components/data-table-pagination.js.map +1 -1
  114. package/dist/components/data-table-row-actions.js +19 -16
  115. package/dist/components/data-table-row-actions.js.map +1 -1
  116. package/dist/components/data-table-row.js.map +1 -1
  117. package/dist/components/data-table-shared.js.map +1 -1
  118. package/dist/components/data-table-sort.js.map +1 -1
  119. package/dist/components/data-table-virtual.js +4 -1
  120. package/dist/components/data-table-virtual.js.map +1 -1
  121. package/dist/components/data-table.d.ts +8 -0
  122. package/dist/components/data-table.js +30 -39
  123. package/dist/components/data-table.js.map +1 -1
  124. package/dist/components/date-picker.js +1 -6
  125. package/dist/components/date-picker.js.map +1 -1
  126. package/dist/components/date-range-picker.js.map +1 -1
  127. package/dist/components/description-list.js +10 -10
  128. package/dist/components/description-list.js.map +1 -1
  129. package/dist/components/dialog-frame.js +9 -2
  130. package/dist/components/dialog-frame.js.map +1 -1
  131. package/dist/components/disclosure.js +17 -27
  132. package/dist/components/disclosure.js.map +1 -1
  133. package/dist/components/dropdown.js +25 -35
  134. package/dist/components/dropdown.js.map +1 -1
  135. package/dist/components/empty-state.js.map +1 -1
  136. package/dist/components/entity-combobox.js.map +1 -1
  137. package/dist/components/error-boundary.js.map +1 -1
  138. package/dist/components/field-base.js +5 -5
  139. package/dist/components/field-base.js.map +1 -1
  140. package/dist/components/field-parts.d.ts +1 -1
  141. package/dist/components/field-parts.js +10 -14
  142. package/dist/components/field-parts.js.map +1 -1
  143. package/dist/components/field-sync.js +8 -10
  144. package/dist/components/field-sync.js.map +1 -1
  145. package/dist/components/file-button.js.map +1 -1
  146. package/dist/components/file-dropzone.js.map +1 -1
  147. package/dist/components/floating-panel.js +4 -1
  148. package/dist/components/floating-panel.js.map +1 -1
  149. package/dist/components/focus-ring.js.map +1 -1
  150. package/dist/components/form-actions.js.map +1 -1
  151. package/dist/components/grouped-picker.js +11 -12
  152. package/dist/components/grouped-picker.js.map +1 -1
  153. package/dist/components/hover-menu.js +1 -4
  154. package/dist/components/hover-menu.js.map +1 -1
  155. package/dist/components/iban-input-labels.js.map +1 -1
  156. package/dist/components/icon-button.js +4 -1
  157. package/dist/components/icon-button.js.map +1 -1
  158. package/dist/components/icon-picker.js +6 -10
  159. package/dist/components/icon-picker.js.map +1 -1
  160. package/dist/components/image-grid.js +41 -50
  161. package/dist/components/image-grid.js.map +1 -1
  162. package/dist/components/inline-edit-field.js.map +1 -1
  163. package/dist/components/inline-entity-combobox.js +6 -12
  164. package/dist/components/inline-entity-combobox.js.map +1 -1
  165. package/dist/components/input.js +14 -2
  166. package/dist/components/input.js.map +1 -1
  167. package/dist/components/jump-calendar.js +1 -7
  168. package/dist/components/jump-calendar.js.map +1 -1
  169. package/dist/components/legal-labels.js +8 -2
  170. package/dist/components/legal-labels.js.map +1 -1
  171. package/dist/components/legal-page.d.ts +18 -1
  172. package/dist/components/legal-page.js +27 -2
  173. package/dist/components/legal-page.js.map +1 -1
  174. package/dist/components/legal.d.ts +40 -5
  175. package/dist/components/legal.js +41 -5
  176. package/dist/components/legal.js.map +1 -1
  177. package/dist/components/lightbox.js.map +1 -1
  178. package/dist/components/line-items.js +26 -36
  179. package/dist/components/line-items.js.map +1 -1
  180. package/dist/components/list-labels.js +3 -1
  181. package/dist/components/list-labels.js.map +1 -1
  182. package/dist/components/list.d.ts +18 -1
  183. package/dist/components/list.js +54 -13
  184. package/dist/components/list.js.map +1 -1
  185. package/dist/components/measured-grid.js +29 -53
  186. package/dist/components/measured-grid.js.map +1 -1
  187. package/dist/components/menu-item.js.map +1 -1
  188. package/dist/components/mini-calendar.js +13 -31
  189. package/dist/components/mini-calendar.js.map +1 -1
  190. package/dist/components/money-field.js.map +1 -1
  191. package/dist/components/month-picker.js +10 -21
  192. package/dist/components/month-picker.js.map +1 -1
  193. package/dist/components/multi-entity-combobox.js +3 -16
  194. package/dist/components/multi-entity-combobox.js.map +1 -1
  195. package/dist/components/multi-select.js.map +1 -1
  196. package/dist/components/nav-pills.js +10 -1
  197. package/dist/components/nav-pills.js.map +1 -1
  198. package/dist/components/number-field.js.map +1 -1
  199. package/dist/components/number-input.js +1 -7
  200. package/dist/components/number-input.js.map +1 -1
  201. package/dist/components/numpad-sheet.js +1 -4
  202. package/dist/components/numpad-sheet.js.map +1 -1
  203. package/dist/components/one-time-code-input.js +1 -7
  204. package/dist/components/one-time-code-input.js.map +1 -1
  205. package/dist/components/page-contents.js +13 -22
  206. package/dist/components/page-contents.js.map +1 -1
  207. package/dist/components/page-header.js +1 -11
  208. package/dist/components/page-header.js.map +1 -1
  209. package/dist/components/password-strength-labels.js.map +1 -1
  210. package/dist/components/password-strength.js +2 -8
  211. package/dist/components/password-strength.js.map +1 -1
  212. package/dist/components/phone-input.js +42 -57
  213. package/dist/components/phone-input.js.map +1 -1
  214. package/dist/components/picker-sheet.js.map +1 -1
  215. package/dist/components/pie-chart.js +1 -8
  216. package/dist/components/pie-chart.js.map +1 -1
  217. package/dist/components/popover.js.map +1 -1
  218. package/dist/components/progress-bar.js +16 -9
  219. package/dist/components/progress-bar.js.map +1 -1
  220. package/dist/components/qr-code.js +1 -8
  221. package/dist/components/qr-code.js.map +1 -1
  222. package/dist/components/row-actions.js +5 -1
  223. package/dist/components/row-actions.js.map +1 -1
  224. package/dist/components/search-field.js.map +1 -1
  225. package/dist/components/select.js +28 -38
  226. package/dist/components/select.js.map +1 -1
  227. package/dist/components/separator.js +1 -5
  228. package/dist/components/separator.js.map +1 -1
  229. package/dist/components/series-chart-axes.js +4 -1
  230. package/dist/components/series-chart-axes.js.map +1 -1
  231. package/dist/components/series-chart-budget.js.map +1 -1
  232. package/dist/components/series-chart-ticks.js.map +1 -1
  233. package/dist/components/series-chart.js +29 -45
  234. package/dist/components/series-chart.js.map +1 -1
  235. package/dist/components/settings-fields.js +21 -17
  236. package/dist/components/settings-fields.js.map +1 -1
  237. package/dist/components/share-card.js +4 -9
  238. package/dist/components/share-card.js.map +1 -1
  239. package/dist/components/sign-chip.js.map +1 -1
  240. package/dist/components/signature-pad.js +4 -13
  241. package/dist/components/signature-pad.js.map +1 -1
  242. package/dist/components/signed-amount.js +12 -5
  243. package/dist/components/signed-amount.js.map +1 -1
  244. package/dist/components/skeleton.js +2 -17
  245. package/dist/components/skeleton.js.map +1 -1
  246. package/dist/components/slider.js +13 -36
  247. package/dist/components/slider.js.map +1 -1
  248. package/dist/components/sparkline.js.map +1 -1
  249. package/dist/components/stat-tile.js +14 -10
  250. package/dist/components/stat-tile.js.map +1 -1
  251. package/dist/components/swatch-picker.js +14 -4
  252. package/dist/components/swatch-picker.js.map +1 -1
  253. package/dist/components/swipeable-row.d.ts +1 -1
  254. package/dist/components/swipeable-row.js +1 -8
  255. package/dist/components/swipeable-row.js.map +1 -1
  256. package/dist/components/table-skeleton.js +4 -1
  257. package/dist/components/table-skeleton.js.map +1 -1
  258. package/dist/components/table.js +12 -9
  259. package/dist/components/table.js.map +1 -1
  260. package/dist/components/tabs.js +5 -24
  261. package/dist/components/tabs.js.map +1 -1
  262. package/dist/components/text-link.js.map +1 -1
  263. package/dist/components/text.js.map +1 -1
  264. package/dist/components/tile-radio.js +1 -7
  265. package/dist/components/tile-radio.js.map +1 -1
  266. package/dist/components/time-input.js.map +1 -1
  267. package/dist/components/toast.js.map +1 -1
  268. package/dist/components/toggle-group.js +6 -1
  269. package/dist/components/toggle-group.js.map +1 -1
  270. package/dist/components/toggle-legend.js +2 -11
  271. package/dist/components/toggle-legend.js.map +1 -1
  272. package/dist/components/tooltip.js +2 -22
  273. package/dist/components/tooltip.js.map +1 -1
  274. package/dist/components/translation-review-editor.js +1 -4
  275. package/dist/components/translation-review-editor.js.map +1 -1
  276. package/dist/components/translation-review-parts.d.ts +1 -1
  277. package/dist/components/translation-review-parts.js +7 -1
  278. package/dist/components/translation-review-parts.js.map +1 -1
  279. package/dist/components/translation-review.js +18 -7
  280. package/dist/components/translation-review.js.map +1 -1
  281. package/dist/components/tree-view.js +14 -10
  282. package/dist/components/tree-view.js.map +1 -1
  283. package/dist/components/treemap.js +1 -13
  284. package/dist/components/treemap.js.map +1 -1
  285. package/dist/components/trigger-aria.js.map +1 -1
  286. package/dist/components/use-table-state.js +14 -11
  287. package/dist/components/use-table-state.js.map +1 -1
  288. package/dist/components/user-avatar.js +3 -26
  289. package/dist/components/user-avatar.js.map +1 -1
  290. package/dist/demo/demo-start.js +8 -4
  291. package/dist/demo/demo-start.js.map +1 -1
  292. package/dist/eslint/index.js +1 -1
  293. package/dist/feedback/feedback-attachment-labels.js.map +1 -1
  294. package/dist/feedback/feedback-attachment.js +1 -8
  295. package/dist/feedback/feedback-attachment.js.map +1 -1
  296. package/dist/feedback/feedback-capture.d.ts +1 -1
  297. package/dist/feedback/feedback-capture.js +3 -1
  298. package/dist/feedback/feedback-capture.js.map +1 -1
  299. package/dist/feedback/feedback-comment-thread.js +27 -13
  300. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  301. package/dist/feedback/feedback-context.js +1 -4
  302. package/dist/feedback/feedback-context.js.map +1 -1
  303. package/dist/feedback/feedback-dialog.js.map +1 -1
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-public-list.js +7 -1
  306. package/dist/feedback/feedback-public-list.js.map +1 -1
  307. package/dist/feedback/feedback-record.js +1 -5
  308. package/dist/feedback/feedback-record.js.map +1 -1
  309. package/dist/feedback/feedback-replies.js +25 -10
  310. package/dist/feedback/feedback-replies.js.map +1 -1
  311. package/dist/feedback/feedback-row-detail.js.map +1 -1
  312. package/dist/feedback/feedback-status-undo.js.map +1 -1
  313. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  314. package/dist/feedback/feedback-table.d.ts +3 -1
  315. package/dist/feedback/feedback-table.js +22 -5
  316. package/dist/feedback/feedback-table.js.map +1 -1
  317. package/dist/feedback/feedback-thread.js +2 -13
  318. package/dist/feedback/feedback-thread.js.map +1 -1
  319. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  320. package/dist/feedback/feedback-vote-button.js.map +1 -1
  321. package/dist/hooks/use-anchored-rect.js.map +1 -1
  322. package/dist/hooks/use-document-title.js.map +1 -1
  323. package/dist/hooks/use-file-drop.js.map +1 -1
  324. package/dist/hooks/use-file-picker-labels.js.map +1 -1
  325. package/dist/hooks/use-file-picker.js.map +1 -1
  326. package/dist/hooks/use-focus-trap.js.map +1 -1
  327. package/dist/hooks/use-overlay-history.js +10 -5
  328. package/dist/hooks/use-overlay-history.js.map +1 -1
  329. package/dist/hooks/use-row-swipe.d.ts +1 -1
  330. package/dist/hooks/use-row-swipe.js +1 -6
  331. package/dist/hooks/use-row-swipe.js.map +1 -1
  332. package/dist/i18n/defaults.js +1 -4
  333. package/dist/i18n/defaults.js.map +1 -1
  334. package/dist/i18n/german.js +15 -4
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.js +2 -8
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.js.map +1 -1
  339. package/dist/i18n/locales/en.js +19 -4
  340. package/dist/i18n/locales/en.js.map +2 -2
  341. package/dist/i18n/locales/es.js +15 -4
  342. package/dist/i18n/locales/es.js.map +1 -1
  343. package/dist/i18n/locales/fr.js +15 -4
  344. package/dist/i18n/locales/fr.js.map +1 -1
  345. package/dist/i18n/locales/hu.js +15 -4
  346. package/dist/i18n/locales/hu.js.map +1 -1
  347. package/dist/i18n/locales/it.js +15 -4
  348. package/dist/i18n/locales/it.js.map +1 -1
  349. package/dist/i18n/locales/zh.js +11 -3
  350. package/dist/i18n/locales/zh.js.map +1 -1
  351. package/dist/i18n/plurals.js.map +1 -1
  352. package/dist/i18n/review.js +15 -7
  353. package/dist/i18n/review.js.map +1 -1
  354. package/dist/i18n/shared-labels.js.map +1 -1
  355. package/dist/index.d.ts +17 -17
  356. package/dist/index.js +40 -23
  357. package/dist/index.js.map +1 -1
  358. package/dist/landing/access.js +1 -4
  359. package/dist/landing/access.js.map +1 -1
  360. package/dist/landing/landing-sections.js +35 -38
  361. package/dist/landing/landing-sections.js.map +1 -1
  362. package/dist/landing/page-seo.js.map +1 -1
  363. package/dist/lib/calc.js.map +1 -1
  364. package/dist/lib/clipboard-minus.js.map +1 -1
  365. package/dist/lib/dates.js.map +1 -1
  366. package/dist/lib/decimal-marks.js.map +1 -1
  367. package/dist/lib/device-settings.js +2 -1
  368. package/dist/lib/device-settings.js.map +1 -1
  369. package/dist/lib/direction.js.map +1 -1
  370. package/dist/lib/iban.js.map +1 -1
  371. package/dist/lib/logger.js +1 -5
  372. package/dist/lib/logger.js.map +1 -1
  373. package/dist/lib/person-name.js.map +1 -1
  374. package/dist/lib/phone.js.map +1 -1
  375. package/dist/lib/qr-encode.js +344 -8
  376. package/dist/lib/qr-encode.js.map +1 -1
  377. package/dist/lib/session-refresh.js.map +1 -1
  378. package/dist/lib/table-text.js +15 -3
  379. package/dist/lib/table-text.js.map +1 -1
  380. package/dist/lib/text-match.d.ts +20 -0
  381. package/dist/lib/text-match.js +11 -1
  382. package/dist/lib/text-match.js.map +1 -1
  383. package/dist/lib/translation-review.js.map +1 -1
  384. package/dist/notifications/notification-inbox.js +63 -44
  385. package/dist/notifications/notification-inbox.js.map +1 -1
  386. package/dist/notifications/notification-parts.js.map +1 -1
  387. package/dist/notifications/notification-types-setting.js.map +1 -1
  388. package/dist/notifications/notifications-labels.js.map +1 -1
  389. package/dist/notifications/push-device-setting.js +6 -1
  390. package/dist/notifications/push-device-setting.js.map +1 -1
  391. package/dist/notifications/push-devices-setting.js.map +1 -1
  392. package/dist/notifications/push-worker.js.map +1 -1
  393. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  394. package/dist/notifications/use-push-device.js +4 -1
  395. package/dist/notifications/use-push-device.js.map +1 -1
  396. package/dist/offline/pwa-update-prompt.d.ts +1 -1
  397. package/dist/offline/pwa-update-prompt.js +7 -1
  398. package/dist/offline/pwa-update-prompt.js.map +1 -1
  399. package/dist/offline/query-persistence.js.map +1 -1
  400. package/dist/offline.d.ts +1 -1
  401. package/dist/offline.js +6 -1
  402. package/dist/offline.js.map +1 -1
  403. package/dist/pay/pay.css +8 -1
  404. package/dist/rhf/fields.d.ts +1 -2
  405. package/dist/rhf/fields.js +1 -10
  406. package/dist/rhf/fields.js.map +1 -1
  407. package/dist/rhf/form-core.js +1 -4
  408. package/dist/rhf/form-core.js.map +1 -1
  409. package/dist/rhf/line-items.js.map +1 -1
  410. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  411. package/dist/search/command-palette.js +71 -62
  412. package/dist/search/command-palette.js.map +1 -1
  413. package/dist/search/global-search.js +34 -5
  414. package/dist/search/global-search.js.map +1 -1
  415. package/dist/search/search-index.js.map +1 -1
  416. package/dist/settings/device-reset-setting.js +1 -6
  417. package/dist/settings/device-reset-setting.js.map +1 -1
  418. package/dist/settings/settings-heading.js.map +1 -1
  419. package/dist/settings/settings-layout.js +9 -20
  420. package/dist/settings/settings-layout.js.map +1 -1
  421. package/dist/settings/use-settings-focus.d.ts +7 -0
  422. package/dist/settings/use-settings-focus.js +2 -0
  423. package/dist/settings/use-settings-focus.js.map +1 -1
  424. package/dist/settings/use-settings-route.js.map +1 -1
  425. package/dist/shell/app-shell.js +62 -31
  426. package/dist/shell/app-shell.js.map +1 -1
  427. package/dist/shell/auth-layout.js +1 -10
  428. package/dist/shell/auth-layout.js.map +1 -1
  429. package/dist/shell/dev-menu.d.ts +5 -0
  430. package/dist/shell/dev-menu.js +14 -7
  431. package/dist/shell/dev-menu.js.map +1 -1
  432. package/dist/shell/option-switcher-menu.js.map +1 -1
  433. package/dist/shell/sync-status-indicator.js +7 -6
  434. package/dist/shell/sync-status-indicator.js.map +1 -1
  435. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  436. package/dist/shell/topbar-action-menu.d.ts +14 -1
  437. package/dist/shell/topbar-action-menu.js +85 -34
  438. package/dist/shell/topbar-action-menu.js.map +1 -1
  439. package/dist/shell/topbar-controls.js +1 -10
  440. package/dist/shell/topbar-controls.js.map +1 -1
  441. package/dist/shell/tour-menu.js +9 -1
  442. package/dist/shell/tour-menu.js.map +1 -1
  443. package/dist/shell.d.ts +1 -1
  444. package/dist/shell.js +2 -1
  445. package/dist/shell.js.map +1 -1
  446. package/dist/sync/db.js.map +1 -1
  447. package/dist/sync/engine.js +24 -6
  448. package/dist/sync/engine.js.map +1 -1
  449. package/dist/sync.d.ts +2 -2
  450. package/dist/sync.js +7 -1
  451. package/dist/sync.js.map +1 -1
  452. package/dist/testing/css-variables.js.map +1 -1
  453. package/dist/testing/index.d.ts +1 -1
  454. package/dist/testing/index.js +1 -3
  455. package/dist/testing/index.js.map +1 -1
  456. package/dist/testing/react.js.map +1 -1
  457. package/dist/testing/viewport.js +7 -4
  458. package/dist/testing/viewport.js.map +1 -1
  459. package/dist/theme/contrast-tokens.js +13 -2
  460. package/dist/theme/contrast-tokens.js.map +1 -1
  461. package/dist/theme/palette-derive.d.ts +1 -1
  462. package/dist/theme/palette-derive.js +18 -29
  463. package/dist/theme/palette-derive.js.map +1 -1
  464. package/dist/theme/palette-presets.js.map +1 -1
  465. package/dist/theme/palette-store.js.map +1 -1
  466. package/dist/theme/text-size.js.map +1 -1
  467. package/dist/theme/theme-store.js +1 -3
  468. package/dist/theme/theme-store.js.map +1 -1
  469. package/dist/tour/tour-catalogue.js.map +1 -1
  470. package/dist/tour/tour-dock.js +5 -1
  471. package/dist/tour/tour-dock.js.map +1 -1
  472. package/dist/tour/tour-engine.js +1 -4
  473. package/dist/tour/tour-engine.js.map +1 -1
  474. package/dist/tour/tour.js +21 -12
  475. package/dist/tour/tour.js.map +1 -1
  476. package/dist/tour/use-tour-progress.js.map +1 -1
  477. package/dist/tour/use-tour-url-sync.js.map +1 -1
  478. package/dist/wizard/stepper-nav.js +3 -24
  479. package/dist/wizard/stepper-nav.js.map +1 -1
  480. package/dist/wizard/types.js.map +1 -1
  481. package/dist/wizard/use-wizard.js +24 -28
  482. package/dist/wizard/use-wizard.js.map +1 -1
  483. package/dist/wizard/validation.js.map +1 -1
  484. package/dist/wizard/wizard-context.js.map +1 -1
  485. package/dist/wizard/wizard-step.d.ts +1 -1
  486. package/dist/wizard/wizard-step.js +1 -8
  487. package/dist/wizard/wizard-step.js.map +1 -1
  488. package/dist/wizard/wizard-summary.js.map +1 -1
  489. package/package.json +5 -2
  490. package/src/account/account-settings-labels.ts +1 -2
  491. package/src/account/account-settings.tsx +11 -17
  492. package/src/account/delete-account-setting.tsx +1 -6
  493. package/src/account/email-change-setting-labels.ts +1 -2
  494. package/src/account/email-change-setting.tsx +3 -1
  495. package/src/account/passkeys-setting.tsx +1 -3
  496. package/src/account/sessions-setting.tsx +4 -2
  497. package/src/admin/admin-action-confirm.tsx +4 -7
  498. package/src/admin/admin-action-log.tsx +29 -29
  499. package/src/admin/admin-area-labels.ts +4 -1
  500. package/src/admin/admin-overview.tsx +8 -2
  501. package/src/admin/admin-sections.tsx +3 -1
  502. package/src/admin/admin-tool-links.tsx +7 -1
  503. package/src/admin/invitations-panel.tsx +5 -11
  504. package/src/admin/mail-test-card.tsx +1 -2
  505. package/src/admin/reviewer-scope-editor.tsx +4 -9
  506. package/src/admin/role-select.tsx +4 -2
  507. package/src/admin/user-roster.tsx +2 -11
  508. package/src/auth/accept-invitation.tsx +1 -7
  509. package/src/auth/auth-errors.ts +1 -6
  510. package/src/auth/forgot-password-form.tsx +2 -7
  511. package/src/auth/register-form.tsx +7 -8
  512. package/src/auth/reset-password-form.tsx +2 -5
  513. package/src/auth/sign-in-form.tsx +15 -14
  514. package/src/auth/verify-email.tsx +2 -7
  515. package/src/billing/billing-banner.tsx +3 -7
  516. package/src/billing/billing-labels.ts +6 -4
  517. package/src/billing/checkout-processing.ts +0 -1
  518. package/src/billing/plan-card.tsx +9 -8
  519. package/src/billing/plan-limit-toast.tsx +1 -2
  520. package/src/billing/plan-limit.ts +1 -2
  521. package/src/billing/plan-price.ts +1 -4
  522. package/src/billing/plan-switch.tsx +1 -2
  523. package/src/billing/subscription-actions.tsx +5 -2
  524. package/src/components/account-chips.tsx +1 -7
  525. package/src/components/alert-banner.tsx +2 -8
  526. package/src/components/amount-input.tsx +206 -178
  527. package/src/components/authed-image.tsx +7 -4
  528. package/src/components/autocomplete.tsx +7 -9
  529. package/src/components/bulk-action-bar.tsx +2 -7
  530. package/src/components/button-group.tsx +1 -2
  531. package/src/components/button-look.ts +6 -17
  532. package/src/components/button.tsx +1 -4
  533. package/src/components/calculator.tsx +6 -9
  534. package/src/components/calendar-heatmap.tsx +5 -10
  535. package/src/components/card.tsx +9 -13
  536. package/src/components/chart-zoom-core.tsx +6 -33
  537. package/src/components/chart.tsx +8 -31
  538. package/src/components/checkbox-group.tsx +1 -4
  539. package/src/components/chip-input.tsx +6 -6
  540. package/src/components/chip.tsx +14 -26
  541. package/src/components/choice-card.tsx +4 -18
  542. package/src/components/column-mapper-labels.ts +3 -10
  543. package/src/components/column-mapper.tsx +33 -122
  544. package/src/components/combobox-core.tsx +17 -47
  545. package/src/components/combobox.tsx +2 -6
  546. package/src/components/confirm-dialog.tsx +1 -3
  547. package/src/components/copy-button.tsx +2 -4
  548. package/src/components/country-select.tsx +9 -21
  549. package/src/components/currency-option-list.tsx +16 -18
  550. package/src/components/currency-select.tsx +2 -7
  551. package/src/components/danger-confirm.tsx +8 -9
  552. package/src/components/data-table-cells.tsx +1 -2
  553. package/src/components/data-table-column-settings.tsx +1 -3
  554. package/src/components/data-table-filter-popover.tsx +5 -19
  555. package/src/components/data-table-filters.ts +1 -6
  556. package/src/components/data-table-fit.ts +7 -7
  557. package/src/components/data-table-groups.tsx +3 -1
  558. package/src/components/data-table-head.tsx +32 -46
  559. package/src/components/data-table-mobile.tsx +99 -108
  560. package/src/components/data-table-pagination.tsx +2 -6
  561. package/src/components/data-table-row-actions.tsx +25 -24
  562. package/src/components/data-table-row.tsx +16 -17
  563. package/src/components/data-table-shared.tsx +10 -16
  564. package/src/components/data-table-sort.ts +1 -2
  565. package/src/components/data-table-virtual.ts +9 -4
  566. package/src/components/data-table.tsx +497 -501
  567. package/src/components/date-picker.tsx +1 -7
  568. package/src/components/date-range-picker.tsx +3 -13
  569. package/src/components/description-list.tsx +7 -13
  570. package/src/components/dialog-frame.tsx +12 -3
  571. package/src/components/disclosure.tsx +8 -12
  572. package/src/components/dropdown.tsx +3 -11
  573. package/src/components/empty-state.tsx +1 -3
  574. package/src/components/entity-combobox.tsx +2 -6
  575. package/src/components/error-boundary.tsx +7 -7
  576. package/src/components/field-base.tsx +8 -14
  577. package/src/components/field-parts.tsx +12 -23
  578. package/src/components/field-sync.tsx +7 -11
  579. package/src/components/file-button.tsx +7 -3
  580. package/src/components/file-dropzone.tsx +7 -12
  581. package/src/components/floating-panel.tsx +44 -51
  582. package/src/components/focus-ring.ts +1 -2
  583. package/src/components/form-actions.tsx +1 -4
  584. package/src/components/grouped-picker.tsx +21 -16
  585. package/src/components/hover-menu.tsx +1 -4
  586. package/src/components/iban-input-labels.ts +1 -2
  587. package/src/components/icon-button.tsx +46 -48
  588. package/src/components/icon-picker.tsx +9 -20
  589. package/src/components/image-grid.tsx +1 -6
  590. package/src/components/inline-edit-field.tsx +1 -5
  591. package/src/components/inline-entity-combobox.tsx +11 -25
  592. package/src/components/input.tsx +14 -2
  593. package/src/components/jump-calendar.tsx +2 -6
  594. package/src/components/legal-labels.ts +8 -3
  595. package/src/components/legal-page.tsx +56 -6
  596. package/src/components/legal.tsx +83 -10
  597. package/src/components/lightbox.tsx +1 -3
  598. package/src/components/line-items.tsx +7 -15
  599. package/src/components/list-labels.ts +2 -0
  600. package/src/components/list.tsx +81 -27
  601. package/src/components/measured-grid.tsx +8 -30
  602. package/src/components/menu-item.tsx +1 -3
  603. package/src/components/mini-calendar.tsx +129 -159
  604. package/src/components/money-field.tsx +4 -2
  605. package/src/components/month-picker.tsx +14 -21
  606. package/src/components/multi-entity-combobox.tsx +6 -26
  607. package/src/components/multi-select.tsx +5 -9
  608. package/src/components/nav-pills.tsx +10 -2
  609. package/src/components/number-field.tsx +18 -15
  610. package/src/components/number-input.tsx +3 -19
  611. package/src/components/numpad-sheet.tsx +9 -13
  612. package/src/components/one-time-code-input.tsx +6 -11
  613. package/src/components/page-contents.tsx +2 -9
  614. package/src/components/page-header.tsx +5 -10
  615. package/src/components/password-strength-labels.ts +1 -2
  616. package/src/components/password-strength.tsx +6 -9
  617. package/src/components/phone-input.tsx +2 -13
  618. package/src/components/picker-sheet.tsx +2 -5
  619. package/src/components/pie-chart.tsx +6 -12
  620. package/src/components/popover.tsx +2 -1
  621. package/src/components/progress-bar.tsx +16 -25
  622. package/src/components/qr-code.tsx +2 -9
  623. package/src/components/row-actions.tsx +9 -1
  624. package/src/components/search-field.tsx +4 -5
  625. package/src/components/select.tsx +2 -10
  626. package/src/components/separator.tsx +1 -5
  627. package/src/components/series-chart-axes.ts +5 -5
  628. package/src/components/series-chart-budget.ts +1 -4
  629. package/src/components/series-chart-ticks.ts +1 -4
  630. package/src/components/series-chart.tsx +43 -83
  631. package/src/components/settings-fields.tsx +20 -15
  632. package/src/components/share-card.tsx +6 -14
  633. package/src/components/sign-chip.tsx +16 -16
  634. package/src/components/signature-pad.tsx +21 -13
  635. package/src/components/signed-amount.tsx +6 -14
  636. package/src/components/skeleton.tsx +2 -13
  637. package/src/components/slider.tsx +9 -20
  638. package/src/components/sparkline.tsx +2 -13
  639. package/src/components/stat-tile.tsx +13 -24
  640. package/src/components/swatch-picker.tsx +8 -3
  641. package/src/components/swipeable-row.tsx +2 -11
  642. package/src/components/table-skeleton.tsx +4 -1
  643. package/src/components/table.tsx +12 -8
  644. package/src/components/tabs.tsx +24 -39
  645. package/src/components/text-link.tsx +6 -6
  646. package/src/components/text.tsx +1 -2
  647. package/src/components/tile-radio.tsx +6 -19
  648. package/src/components/time-input.tsx +4 -5
  649. package/src/components/toast.tsx +3 -12
  650. package/src/components/toggle-group.tsx +9 -9
  651. package/src/components/toggle-legend.tsx +4 -16
  652. package/src/components/tooltip.tsx +9 -48
  653. package/src/components/translation-review-editor.tsx +4 -4
  654. package/src/components/translation-review-parts.tsx +7 -1
  655. package/src/components/translation-review.tsx +15 -16
  656. package/src/components/tree-view.tsx +17 -20
  657. package/src/components/treemap.tsx +3 -15
  658. package/src/components/trigger-aria.ts +1 -4
  659. package/src/components/use-table-state.ts +16 -17
  660. package/src/components/user-avatar.tsx +4 -21
  661. package/src/demo/demo-start.tsx +8 -4
  662. package/src/feedback/feedback-attachment-labels.ts +2 -1
  663. package/src/feedback/feedback-attachment.tsx +16 -29
  664. package/src/feedback/feedback-capture.ts +3 -1
  665. package/src/feedback/feedback-comment-thread.tsx +9 -6
  666. package/src/feedback/feedback-context.tsx +1 -4
  667. package/src/feedback/feedback-dialog.tsx +3 -4
  668. package/src/feedback/feedback-inbox.tsx +3 -9
  669. package/src/feedback/feedback-public-list.tsx +7 -1
  670. package/src/feedback/feedback-record.ts +6 -14
  671. package/src/feedback/feedback-replies.tsx +10 -5
  672. package/src/feedback/feedback-row-detail.tsx +2 -5
  673. package/src/feedback/feedback-status-undo.ts +1 -4
  674. package/src/feedback/feedback-submit-dialog.tsx +4 -4
  675. package/src/feedback/feedback-table.tsx +29 -9
  676. package/src/feedback/feedback-thread.tsx +4 -16
  677. package/src/feedback/feedback-vocabulary.ts +72 -82
  678. package/src/feedback/feedback-vote-button.tsx +9 -8
  679. package/src/hooks/use-anchored-rect.ts +1 -4
  680. package/src/hooks/use-document-title.ts +3 -1
  681. package/src/hooks/use-file-drop.ts +1 -3
  682. package/src/hooks/use-file-picker-labels.ts +1 -2
  683. package/src/hooks/use-file-picker.tsx +1 -5
  684. package/src/hooks/use-focus-trap.ts +1 -2
  685. package/src/hooks/use-overlay-history.ts +10 -5
  686. package/src/hooks/use-row-swipe.ts +1 -6
  687. package/src/i18n/defaults.ts +1 -4
  688. package/src/i18n/german.ts +42 -50
  689. package/src/i18n/kit-labels.tsx +5 -20
  690. package/src/i18n/languages.ts +2 -8
  691. package/src/i18n/locales/en.ts +15 -31
  692. package/src/i18n/locales/es.ts +44 -50
  693. package/src/i18n/locales/fr.ts +43 -52
  694. package/src/i18n/locales/hu.ts +35 -38
  695. package/src/i18n/locales/it.ts +44 -53
  696. package/src/i18n/locales/zh.ts +23 -33
  697. package/src/i18n/plurals.ts +4 -1
  698. package/src/i18n/review.ts +20 -7
  699. package/src/i18n/shared-labels.ts +1 -2
  700. package/src/index.ts +107 -32
  701. package/src/landing/access.ts +2 -7
  702. package/src/landing/landing-sections.tsx +14 -15
  703. package/src/landing/page-seo.ts +1 -3
  704. package/src/lib/calc.ts +4 -1
  705. package/src/lib/clipboard-minus.ts +4 -1
  706. package/src/lib/dates.ts +2 -10
  707. package/src/lib/decimal-marks.ts +1 -3
  708. package/src/lib/device-settings.ts +2 -1
  709. package/src/lib/direction.ts +1 -3
  710. package/src/lib/iban.ts +89 -9
  711. package/src/lib/logger.ts +1 -5
  712. package/src/lib/person-name.ts +1 -3
  713. package/src/lib/phone.ts +5 -8
  714. package/src/lib/qr-encode.ts +32 -8
  715. package/src/lib/session-refresh.ts +5 -6
  716. package/src/lib/table-text.ts +24 -6
  717. package/src/lib/text-match.ts +43 -1
  718. package/src/lib/translation-review.ts +1 -3
  719. package/src/notifications/notification-inbox.tsx +17 -4
  720. package/src/notifications/notification-parts.tsx +1 -2
  721. package/src/notifications/notification-types-setting.tsx +19 -18
  722. package/src/notifications/notifications-labels.ts +1 -2
  723. package/src/notifications/push-device-setting.tsx +7 -1
  724. package/src/notifications/push-devices-setting.tsx +7 -3
  725. package/src/notifications/push-worker.ts +10 -2
  726. package/src/notifications/quiet-hours-setting.tsx +4 -1
  727. package/src/notifications/use-push-device.ts +4 -1
  728. package/src/offline/pwa-update-prompt.tsx +7 -1
  729. package/src/offline/query-persistence.ts +9 -6
  730. package/src/offline.ts +6 -1
  731. package/src/rhf/fields.tsx +50 -19
  732. package/src/rhf/form-core.tsx +2 -6
  733. package/src/rhf/line-items.tsx +5 -7
  734. package/src/rhf/use-rhf-wizard-step.ts +3 -12
  735. package/src/search/command-palette.tsx +15 -23
  736. package/src/search/global-search.tsx +38 -8
  737. package/src/search/search-index.ts +5 -1
  738. package/src/settings/device-reset-setting.tsx +5 -8
  739. package/src/settings/settings-heading.tsx +3 -1
  740. package/src/settings/settings-layout.tsx +6 -13
  741. package/src/settings/use-settings-focus.ts +8 -0
  742. package/src/settings/use-settings-route.ts +4 -3
  743. package/src/shell/app-shell.tsx +48 -24
  744. package/src/shell/auth-layout.tsx +1 -6
  745. package/src/shell/dev-menu.tsx +18 -6
  746. package/src/shell/option-switcher-menu.tsx +1 -3
  747. package/src/shell/sync-status-indicator.tsx +14 -12
  748. package/src/shell/top-bar-parts-labels.ts +1 -3
  749. package/src/shell/topbar-action-menu.tsx +130 -45
  750. package/src/shell/topbar-controls.tsx +5 -19
  751. package/src/shell/tour-menu.tsx +9 -1
  752. package/src/shell.ts +1 -1
  753. package/src/sync/db.ts +5 -1
  754. package/src/sync/engine.ts +44 -12
  755. package/src/sync.ts +16 -2
  756. package/src/testing/css-variables.ts +1 -2
  757. package/src/testing/index.ts +1 -6
  758. package/src/testing/react.ts +1 -2
  759. package/src/testing/viewport.ts +9 -9
  760. package/src/theme/contrast-tokens.ts +13 -2
  761. package/src/theme/palette-derive.ts +24 -50
  762. package/src/theme/palette-presets.ts +0 -1
  763. package/src/theme/palette-store.ts +1 -4
  764. package/src/theme/text-size.ts +1 -1
  765. package/src/theme/theme-store.ts +2 -6
  766. package/src/tour/tour-catalogue.tsx +4 -2
  767. package/src/tour/tour-dock.ts +5 -1
  768. package/src/tour/tour-engine.ts +2 -6
  769. package/src/tour/tour.tsx +28 -12
  770. package/src/tour/use-tour-progress.ts +8 -2
  771. package/src/tour/use-tour-url-sync.ts +3 -1
  772. package/src/vite-env.d.ts +1 -4
  773. package/src/wizard/stepper-nav.tsx +10 -35
  774. package/src/wizard/types.ts +1 -4
  775. package/src/wizard/use-wizard.ts +28 -43
  776. package/src/wizard/validation.ts +1 -5
  777. package/src/wizard/wizard-context.tsx +2 -6
  778. package/src/wizard/wizard-step.tsx +1 -8
  779. package/src/wizard/wizard-summary.tsx +2 -6
  780. package/tokens.css +393 -152
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/theme/contrast-tokens.ts"],"sourcesContent":["import { contrast, hexToOklch, luminance, oklchToHex, oklchToRgb, parseHex, rgbToOklch, solveLightness, toHex } from \"./color\";\nimport type { TokenSet } from \"./palette-presets\";\n\n/**\n * The \"More contrast\" step on a token set (docs/text-size-harmonization.md §5, §10.5).\n * INTERNAL — `applyTokenSet` and the contrast layer share it; it is not re-exported.\n *\n * The step happens HERE, in JS, because the tokens it changes are written inline on\n * `<html>` by `applyTokenSet`: an inline value beats any `[data-contrast=more]` rule and\n * any `@media (prefers-contrast: more)` block, so a stylesheet cannot do it (§10.5, the\n * keksdose review). `--text-placeholder` is a CSS-derived token, and it is written inline\n * here too rather than redefined per attribute in tokens.css.\n *\n * What moves (§5):\n * - `--text-muted` becomes the set's `--text-secondary`;\n * - `--text-secondary` moves halfway to `--text-primary`;\n * - `--text-placeholder` becomes the set's `--text-muted` (4.6–5.5:1 on every shipped\n * preset, where the CSS formula gives 2.4–2.8:1) — the order primary > secondary >\n * muted > placeholder survives the step;\n * - `--border` moves toward `--text-primary` until it clears 3:1 against the worst of the\n * three surfaces (WCAG 1.4.11, a field's boundary), and never less far than\n * `--border-strong`'s 45 % — so every border is at least as strong as a \"strong\" one was.\n * `--border-strong` stays derived in tokens.css and grows with it;\n * - in a LIGHT set only (0.36, docs/colour-roles-harmonization.md §13): the well\n * (`--bg-surface-2`) becomes {@link WELL_INK_MORE} of `--text-primary` mixed into the\n * card, and each of the four money colours that falls under 4.5:1 on that well is\n * solved darker at its own hue and chroma. The border above is solved against the\n * stepped well. A dark set's well stands 1.11–1.13:1 from its card already, and its\n * money has no room: nothing of it steps.\n *\n * What does NOT move here, because it is not inline: the light status, hue and field-sync\n * literals and `--bg-active` step in tokens.css's `:root[data-contrast=\"more\"]:not(.dark)`\n * block, solved over these stepped wells.\n */\n\n/** The custom properties a contrast step writes. The last five only for a light set. */\nexport const CONTRAST_STEPPED_VARS = [\n \"--text-secondary\",\n \"--text-muted\",\n \"--text-placeholder\",\n \"--border\",\n \"--bg-surface-2\",\n \"--money-income\",\n \"--money-expense\",\n \"--money-net\",\n \"--money-neutral\",\n] as const;\n\n/** The light well under More contrast and the money that reads on it (§13.4). */\nexport interface WellStep {\n bgSurface2: string;\n moneyIncome: string;\n moneyExpense: string;\n moneyNet: string;\n moneyNeutral: string;\n}\n\nexport interface ContrastStep {\n textSecondary: string;\n textMuted: string;\n textPlaceholder: string;\n border: string;\n /** The stepped well and money, or null for a dark set (which does not step them). */\n well: WellStep | null;\n}\n\ninterface Lab {\n l: number;\n a: number;\n b: number;\n}\n\nfunction toLab(hex: string): Lab | null {\n const rgb = parseHex(hex);\n if (!rgb) return null;\n const { l, c, h } = rgbToOklch(rgb);\n const rad = (h * Math.PI) / 180;\n return { l, a: c * Math.cos(rad), b: c * Math.sin(rad) };\n}\n\n/** `color-mix(in oklab, a p, b)` as a hex — the same formula tokens.css's derived roles\n * use, so a stepped value sits where the stylesheet would have put it. A colour that is\n * not hex (a consumer's own token set) comes back unmixed. The deriver mixes with it\n * too (`--bg-hover`, `--brand-muted`, `--brand-bg`, `--brand-bg-hover`). */\nexport function mix(a: string, b: string, p: number): string {\n const A = toLab(a);\n const B = toLab(b);\n if (!A || !B) return a;\n const l = A.l * p + B.l * (1 - p);\n const x = A.a * p + B.a * (1 - p);\n const y = A.b * p + B.b * (1 - p);\n const c = Math.hypot(x, y);\n const h = c < 1e-6 ? 0 : ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;\n return toHex(oklchToRgb({ l, c, h }));\n}\n\n/**\n * `--bg-hover` in tokens.css: this share of `--text-primary` mixed into `--bg-surface`.\n *\n * 5.5 %, down from 7 % in 0.33 (docs/colour-roles-harmonization.md §12.10): a hovered row\n * on a card is the darkest fill (in dark, the lightest) that text sits on, and at 7 % the\n * light status colours, the money colours and Ink/Moss/Plum's muted text fell to\n * 4.25–4.49:1 on it. The status literals are fixed, the same in every preset, so the\n * hover moved instead: text wins over the hover's step. At 5.5 % the step from the card\n * is still 1.09–1.13:1, above the 1.08:1 the kit's hover rule asks (§12.2). The deriver\n * solves the money colours against this fill, and tokens-css-audit.test.ts measures\n * every text role on it in every shipped preset, so the two cannot part.\n */\nexport const HOVER_INK = 0.055;\n\n/** The card's hover fill (`--bg-hover`) for `t`, as tokens.css mixes it. */\nexport function hoverFill(t: Pick<TokenSet, \"textPrimary\" | \"bgSurface\">): string {\n return mix(t.textPrimary, t.bgSurface, HOVER_INK);\n}\n\n/**\n * The light well under More contrast: this share of `--text-primary` mixed into the card,\n * `--bg-hover`'s formula at a larger share (0.36, docs/colour-roles-harmonization.md §13).\n *\n * In light the well is the page (§5.3), 1.09–1.11:1 from the card. At 10 % it stands\n * 1.19–1.24:1 from it, about twice that. The rule it is held to: **the stepped well stands\n * at least 1.17:1 from the card**, two of the hover rule's 1.08 steps; 9 % is the least\n * that clears it in every shipped preset, with no room. The step cannot ship alone: on a\n * 10 % well money, status and two hues read 4.07–4.28:1, so the money is solved with it\n * here and the status and hue literals in tokens.css.\n */\nexport const WELL_INK_MORE = 0.1;\n\n/** The least the stepped light well stands from the card (§13.3, decision 2). */\nexport const WELL_STEP_MIN = 1.17;\n\n/** True when `t` is a light set: its body ink darker than its card. */\nfunction isLightSet(t: Pick<TokenSet, \"textPrimary\" | \"bgSurface\">): boolean | null {\n const ink = parseHex(t.textPrimary);\n const card = parseHex(t.bgSurface);\n if (!ink || !card) return null;\n return luminance(ink) < luminance(card);\n}\n\n/** Money as text: WCAG 1.4.3 AA. */\nconst MONEY_TARGET = 4.5;\n\n/** `color` as it is when it clears 4.5:1 on `well`, else solved darker at its own hue and\n * chroma until it does — measured on the hex (`solveLightness`). */\nfunction moneyOn(color: string, well: string): string {\n if (contrast(color, well) >= MONEY_TARGET) return color;\n const lch = hexToOklch(color);\n const bg = parseHex(well);\n if (!lch || !bg) return color;\n const l = solveLightness(MONEY_TARGET, bg, lch.h, lch.c, \"darker\");\n return oklchToHex({ l, c: lch.c, h: lch.h });\n}\n\n/** The light well under More contrast for `t`, or null for a dark set (or one whose ink or\n * card is not hex, which cannot be mixed). The deriver solves the brand against it. */\nexport function steppedWellFill(t: Pick<TokenSet, \"textPrimary\" | \"bgSurface\">): string | null {\n return isLightSet(t) ? mix(t.textPrimary, t.bgSurface, WELL_INK_MORE) : null;\n}\n\n/** The stepped well and money of a light set, or null (see {@link steppedWellFill}). */\nfunction wellStep(t: TokenSet): WellStep | null {\n const well = steppedWellFill(t);\n if (!well) return null;\n return {\n bgSurface2: well,\n moneyIncome: moneyOn(t.moneyIncome, well),\n moneyExpense: moneyOn(t.moneyExpense, well),\n moneyNet: moneyOn(t.moneyNet, well),\n moneyNeutral: moneyOn(t.moneyNeutral, well),\n };\n}\n\n/**\n * `--brand-muted` in tokens.css: this share of `--brand` mixed into `--text-primary`.\n *\n * 62 %, down from 70 % in 0.33 (docs/colour-roles-harmonization.md §12.11): the brand as\n * text sits on its soft chip HOVERED too (`--brand-bg-hover` — ButtonGroupLink's current\n * page, a selected day), and at 70 % it read 4.08–4.42:1 there in Default and Moss light\n * and in Ink, Moss, Plum and Contrast dark. The brand could not move instead: the dark\n * brands would have flipped their white label to the dark ink. Nor could the chip's hover\n * lighten: by the time the text cleared 4.5:1 the hover sat 1.00:1 from the chip it\n * hovers. So the text moves toward the ink, as `--bg-hover` moved for its text.\n */\nexport const BRAND_MUTED_SHARE = 0.62;\n/** `--brand-bg` and `--brand-bg-hover` in tokens.css: these shares of `--brand` mixed\n * into `--bg-surface`. */\nexport const BRAND_BG_SHARE = 0.14;\nexport const BRAND_BG_HOVER_SHARE = 0.24;\n\n/** `--brand-muted`, `--brand-bg` and `--brand-bg-hover` for `t`, as tokens.css mixes them.\n * tokens-css-audit.test.ts holds the three shares equal to the stylesheet's. */\nexport function brandSoft(t: Pick<TokenSet, \"brand\" | \"textPrimary\" | \"bgSurface\">): {\n brandMuted: string;\n brandBg: string;\n brandBgHover: string;\n} {\n return {\n brandMuted: mix(t.brand, t.textPrimary, BRAND_MUTED_SHARE),\n brandBg: mix(t.brand, t.bgSurface, BRAND_BG_SHARE),\n brandBgHover: mix(t.brand, t.bgSurface, BRAND_BG_HOVER_SHARE),\n };\n}\n\n/** WCAG 1.4.11 for a component's boundary. */\nconst BORDER_TARGET = 3;\n/** `--border-strong` in tokens.css: 55 % border, 45 % text. */\nconst BORDER_STRONG_SHARE = 0.45;\n\n/** The worst of the three surfaces; `well`, when the step has one, in place of the set's. */\nfunction worstContrast(color: string, t: TokenSet, well: string): number {\n return Math.min(contrast(color, t.bgPage), contrast(color, t.bgSurface), contrast(color, well));\n}\n\nfunction steppedBorder(t: TokenSet, well: WellStep | null): string {\n // The share of `--text-primary` in the border: at least --border-strong's, more until\n // 3:1. Monotonic in the share (the mix walks from the border to the text, away from\n // every surface), so a bisection finds the least that clears it. Against the stepped\n // light well (§13.4), which is the darkest surface under More contrast.\n const surface2 = well?.bgSurface2 ?? t.bgSurface2;\n let lo = BORDER_STRONG_SHARE;\n if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t, surface2) >= BORDER_TARGET) return mix(t.border, t.textPrimary, 1 - lo);\n let hi = 1;\n for (let i = 0; i < 16; i++) {\n const mid = (lo + hi) / 2;\n if (worstContrast(mix(t.border, t.textPrimary, 1 - mid), t, surface2) >= BORDER_TARGET) hi = mid;\n else lo = mid;\n }\n return mix(t.border, t.textPrimary, 1 - hi);\n}\n\n/** The stepped values for `t`: the four text and line tokens, and for a light set the\n * well and money (`well`, null in dark). Pure. */\nexport function contrastStep(t: TokenSet): ContrastStep {\n const well = wellStep(t);\n return {\n textSecondary: mix(t.textSecondary, t.textPrimary, 0.5),\n textMuted: t.textSecondary,\n textPlaceholder: t.textMuted,\n border: steppedBorder(t, well),\n well,\n };\n}\n\n/* ── The token set last written to an element ───────────────────────────── */\n\nconst lastWritten = new WeakMap<HTMLElement, TokenSet>();\n\n/** Remember the set `applyTokenSet` wrote, so a contrast change can re-apply it. */\nexport function rememberTokenSet(el: HTMLElement, t: TokenSet): void {\n lastWritten.set(el, t);\n}\n\n/** The set `applyTokenSet` last wrote to `el`, or undefined when nothing did — an app\n * without the palette layer, whose base tokens come from tokens.css. */\nexport function rememberedTokenSet(el: HTMLElement): TokenSet | undefined {\n return lastWritten.get(el);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/theme/contrast-tokens.ts"],"sourcesContent":["import {\n contrast,\n hexToOklch,\n luminance,\n oklchToHex,\n oklchToRgb,\n parseHex,\n rgbToOklch,\n solveLightness,\n toHex,\n} from \"./color\";\nimport type { TokenSet } from \"./palette-presets\";\n\n/**\n * The \"More contrast\" step on a token set (docs/text-size-harmonization.md §5, §10.5).\n * INTERNAL — `applyTokenSet` and the contrast layer share it; it is not re-exported.\n *\n * The step happens HERE, in JS, because the tokens it changes are written inline on\n * `<html>` by `applyTokenSet`: an inline value beats any `[data-contrast=more]` rule and\n * any `@media (prefers-contrast: more)` block, so a stylesheet cannot do it (§10.5, the\n * keksdose review). `--text-placeholder` is a CSS-derived token, and it is written inline\n * here too rather than redefined per attribute in tokens.css.\n *\n * What moves (§5):\n * - `--text-muted` becomes the set's `--text-secondary`;\n * - `--text-secondary` moves halfway to `--text-primary`;\n * - `--text-placeholder` becomes the set's `--text-muted` (4.6–5.5:1 on every shipped\n * preset, where the CSS formula gives 2.4–2.8:1) — the order primary > secondary >\n * muted > placeholder survives the step;\n * - `--border` moves toward `--text-primary` until it clears 3:1 against the worst of the\n * three surfaces (WCAG 1.4.11, a field's boundary), and never less far than\n * `--border-strong`'s 45 % — so every border is at least as strong as a \"strong\" one was.\n * `--border-strong` stays derived in tokens.css and grows with it;\n * - in a LIGHT set only (0.36, docs/colour-roles-harmonization.md §13): the well\n * (`--bg-surface-2`) becomes {@link WELL_INK_MORE} of `--text-primary` mixed into the\n * card, and each of the four money colours that falls under 4.5:1 on that well is\n * solved darker at its own hue and chroma. The border above is solved against the\n * stepped well. A dark set's well stands 1.11–1.13:1 from its card already, and its\n * money has no room: nothing of it steps.\n *\n * What does NOT move here, because it is not inline: the light status, hue and field-sync\n * literals and `--bg-active` step in tokens.css's `:root[data-contrast=\"more\"]:not(.dark)`\n * block, solved over these stepped wells.\n */\n\n/** The custom properties a contrast step writes. The last five only for a light set. */\nexport const CONTRAST_STEPPED_VARS = [\n \"--text-secondary\",\n \"--text-muted\",\n \"--text-placeholder\",\n \"--border\",\n \"--bg-surface-2\",\n \"--money-income\",\n \"--money-expense\",\n \"--money-net\",\n \"--money-neutral\",\n] as const;\n\n/** The light well under More contrast and the money that reads on it (§13.4). */\nexport interface WellStep {\n bgSurface2: string;\n moneyIncome: string;\n moneyExpense: string;\n moneyNet: string;\n moneyNeutral: string;\n}\n\nexport interface ContrastStep {\n textSecondary: string;\n textMuted: string;\n textPlaceholder: string;\n border: string;\n /** The stepped well and money, or null for a dark set (which does not step them). */\n well: WellStep | null;\n}\n\ninterface Lab {\n l: number;\n a: number;\n b: number;\n}\n\nfunction toLab(hex: string): Lab | null {\n const rgb = parseHex(hex);\n if (!rgb) return null;\n const { l, c, h } = rgbToOklch(rgb);\n const rad = (h * Math.PI) / 180;\n return { l, a: c * Math.cos(rad), b: c * Math.sin(rad) };\n}\n\n/** `color-mix(in oklab, a p, b)` as a hex — the same formula tokens.css's derived roles\n * use, so a stepped value sits where the stylesheet would have put it. A colour that is\n * not hex (a consumer's own token set) comes back unmixed. The deriver mixes with it\n * too (`--bg-hover`, `--brand-muted`, `--brand-bg`, `--brand-bg-hover`). */\nexport function mix(a: string, b: string, p: number): string {\n const A = toLab(a);\n const B = toLab(b);\n if (!A || !B) return a;\n const l = A.l * p + B.l * (1 - p);\n const x = A.a * p + B.a * (1 - p);\n const y = A.b * p + B.b * (1 - p);\n const c = Math.hypot(x, y);\n const h = c < 1e-6 ? 0 : ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;\n return toHex(oklchToRgb({ l, c, h }));\n}\n\n/**\n * `--bg-hover` in tokens.css: this share of `--text-primary` mixed into `--bg-surface`.\n *\n * 5.5 %, down from 7 % in 0.33 (docs/colour-roles-harmonization.md §12.10): a hovered row\n * on a card is the darkest fill (in dark, the lightest) that text sits on, and at 7 % the\n * light status colours, the money colours and Ink/Moss/Plum's muted text fell to\n * 4.25–4.49:1 on it. The status literals are fixed, the same in every preset, so the\n * hover moved instead: text wins over the hover's step. At 5.5 % the step from the card\n * is still 1.09–1.13:1, above the 1.08:1 the kit's hover rule asks (§12.2). The deriver\n * solves the money colours against this fill, and tokens-css-audit.test.ts measures\n * every text role on it in every shipped preset, so the two cannot part.\n */\nexport const HOVER_INK = 0.055;\n\n/** The card's hover fill (`--bg-hover`) for `t`, as tokens.css mixes it. */\nexport function hoverFill(t: Pick<TokenSet, \"textPrimary\" | \"bgSurface\">): string {\n return mix(t.textPrimary, t.bgSurface, HOVER_INK);\n}\n\n/**\n * The light well under More contrast: this share of `--text-primary` mixed into the card,\n * `--bg-hover`'s formula at a larger share (0.36, docs/colour-roles-harmonization.md §13).\n *\n * In light the well is the page (§5.3), 1.09–1.11:1 from the card. At 10 % it stands\n * 1.19–1.24:1 from it, about twice that. The rule it is held to: **the stepped well stands\n * at least 1.17:1 from the card**, two of the hover rule's 1.08 steps; 9 % is the least\n * that clears it in every shipped preset, with no room. The step cannot ship alone: on a\n * 10 % well money, status and two hues read 4.07–4.28:1, so the money is solved with it\n * here and the status and hue literals in tokens.css.\n */\nexport const WELL_INK_MORE = 0.1;\n\n/** The least the stepped light well stands from the card (§13.3, decision 2). */\nexport const WELL_STEP_MIN = 1.17;\n\n/** True when `t` is a light set: its body ink darker than its card. */\nfunction isLightSet(t: Pick<TokenSet, \"textPrimary\" | \"bgSurface\">): boolean | null {\n const ink = parseHex(t.textPrimary);\n const card = parseHex(t.bgSurface);\n if (!ink || !card) return null;\n return luminance(ink) < luminance(card);\n}\n\n/** Money as text: WCAG 1.4.3 AA. */\nconst MONEY_TARGET = 4.5;\n\n/** `color` as it is when it clears 4.5:1 on `well`, else solved darker at its own hue and\n * chroma until it does — measured on the hex (`solveLightness`). */\nfunction moneyOn(color: string, well: string): string {\n if (contrast(color, well) >= MONEY_TARGET) return color;\n const lch = hexToOklch(color);\n const bg = parseHex(well);\n if (!lch || !bg) return color;\n const l = solveLightness(MONEY_TARGET, bg, lch.h, lch.c, \"darker\");\n return oklchToHex({ l, c: lch.c, h: lch.h });\n}\n\n/** The light well under More contrast for `t`, or null for a dark set (or one whose ink or\n * card is not hex, which cannot be mixed). The deriver solves the brand against it. */\nexport function steppedWellFill(t: Pick<TokenSet, \"textPrimary\" | \"bgSurface\">): string | null {\n return isLightSet(t) ? mix(t.textPrimary, t.bgSurface, WELL_INK_MORE) : null;\n}\n\n/** The stepped well and money of a light set, or null (see {@link steppedWellFill}). */\nfunction wellStep(t: TokenSet): WellStep | null {\n const well = steppedWellFill(t);\n if (!well) return null;\n return {\n bgSurface2: well,\n moneyIncome: moneyOn(t.moneyIncome, well),\n moneyExpense: moneyOn(t.moneyExpense, well),\n moneyNet: moneyOn(t.moneyNet, well),\n moneyNeutral: moneyOn(t.moneyNeutral, well),\n };\n}\n\n/**\n * `--brand-muted` in tokens.css: this share of `--brand` mixed into `--text-primary`.\n *\n * 62 %, down from 70 % in 0.33 (docs/colour-roles-harmonization.md §12.11): the brand as\n * text sits on its soft chip HOVERED too (`--brand-bg-hover` — ButtonGroupLink's current\n * page, a selected day), and at 70 % it read 4.08–4.42:1 there in Default and Moss light\n * and in Ink, Moss, Plum and Contrast dark. The brand could not move instead: the dark\n * brands would have flipped their white label to the dark ink. Nor could the chip's hover\n * lighten: by the time the text cleared 4.5:1 the hover sat 1.00:1 from the chip it\n * hovers. So the text moves toward the ink, as `--bg-hover` moved for its text.\n */\nexport const BRAND_MUTED_SHARE = 0.62;\n/** `--brand-bg` and `--brand-bg-hover` in tokens.css: these shares of `--brand` mixed\n * into `--bg-surface`. */\nexport const BRAND_BG_SHARE = 0.14;\nexport const BRAND_BG_HOVER_SHARE = 0.24;\n\n/** `--brand-muted`, `--brand-bg` and `--brand-bg-hover` for `t`, as tokens.css mixes them.\n * tokens-css-audit.test.ts holds the three shares equal to the stylesheet's. */\nexport function brandSoft(t: Pick<TokenSet, \"brand\" | \"textPrimary\" | \"bgSurface\">): {\n brandMuted: string;\n brandBg: string;\n brandBgHover: string;\n} {\n return {\n brandMuted: mix(t.brand, t.textPrimary, BRAND_MUTED_SHARE),\n brandBg: mix(t.brand, t.bgSurface, BRAND_BG_SHARE),\n brandBgHover: mix(t.brand, t.bgSurface, BRAND_BG_HOVER_SHARE),\n };\n}\n\n/** WCAG 1.4.11 for a component's boundary. */\nconst BORDER_TARGET = 3;\n/** `--border-strong` in tokens.css: 55 % border, 45 % text. */\nconst BORDER_STRONG_SHARE = 0.45;\n\n/** The worst of the three surfaces; `well`, when the step has one, in place of the set's. */\nfunction worstContrast(color: string, t: TokenSet, well: string): number {\n return Math.min(contrast(color, t.bgPage), contrast(color, t.bgSurface), contrast(color, well));\n}\n\nfunction steppedBorder(t: TokenSet, well: WellStep | null): string {\n // The share of `--text-primary` in the border: at least --border-strong's, more until\n // 3:1. Monotonic in the share (the mix walks from the border to the text, away from\n // every surface), so a bisection finds the least that clears it. Against the stepped\n // light well (§13.4), which is the darkest surface under More contrast.\n const surface2 = well?.bgSurface2 ?? t.bgSurface2;\n let lo = BORDER_STRONG_SHARE;\n if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t, surface2) >= BORDER_TARGET)\n return mix(t.border, t.textPrimary, 1 - lo);\n let hi = 1;\n for (let i = 0; i < 16; i++) {\n const mid = (lo + hi) / 2;\n if (worstContrast(mix(t.border, t.textPrimary, 1 - mid), t, surface2) >= BORDER_TARGET) hi = mid;\n else lo = mid;\n }\n return mix(t.border, t.textPrimary, 1 - hi);\n}\n\n/** The stepped values for `t`: the four text and line tokens, and for a light set the\n * well and money (`well`, null in dark). Pure. */\nexport function contrastStep(t: TokenSet): ContrastStep {\n const well = wellStep(t);\n return {\n textSecondary: mix(t.textSecondary, t.textPrimary, 0.5),\n textMuted: t.textSecondary,\n textPlaceholder: t.textMuted,\n border: steppedBorder(t, well),\n well,\n };\n}\n\n/* ── The token set last written to an element ───────────────────────────── */\n\nconst lastWritten = new WeakMap<HTMLElement, TokenSet>();\n\n/** Remember the set `applyTokenSet` wrote, so a contrast change can re-apply it. */\nexport function rememberTokenSet(el: HTMLElement, t: TokenSet): void {\n lastWritten.set(el, t);\n}\n\n/** The set `applyTokenSet` last wrote to `el`, or undefined when nothing did — an app\n * without the palette layer, whose base tokens come from tokens.css. */\nexport function rememberedTokenSet(el: HTMLElement): TokenSet | undefined {\n return lastWritten.get(el);\n}\n"],"mappings":"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","names":[]}
@@ -92,7 +92,7 @@ export interface DerivedPalette {
92
92
  /** Compromises the deriver had to make, in plain words. Never silent. */
93
93
  warnings: string[];
94
94
  }
95
- export declare function derivePalette({ anchors, mode, surfaceTint, textContrast, }: DeriveOptions): DerivedPalette;
95
+ export declare function derivePalette({ anchors, mode, surfaceTint, textContrast }: DeriveOptions): DerivedPalette;
96
96
  /**
97
97
  * Measure every pair that has a requirement, and say which ones fail.
98
98
  *
@@ -92,12 +92,7 @@ function tintOn(hue, chroma, surface, mode) {
92
92
  const l = mode === "light" ? s.l - 0.055 : s.l + 0.055;
93
93
  return oklchToHex({ l, c: Math.min(chroma * 0.45, 0.06), h: hue });
94
94
  }
95
- function derivePalette({
96
- anchors,
97
- mode,
98
- surfaceTint = 0.55,
99
- textContrast
100
- }) {
95
+ function derivePalette({ anchors, mode, surfaceTint = 0.55, textContrast }) {
101
96
  const warnings = [];
102
97
  const brand = hexToOklch(anchors.brand);
103
98
  if (!brand) {
@@ -194,20 +189,8 @@ function derivePalette({
194
189
  const hue = semanticHue[role];
195
190
  const chroma = semanticChroma[role];
196
191
  const bg = tintOn(hue, chroma, surfaces.bgSurface, mode);
197
- const onSurface = solveLightness(
198
- 4.5,
199
- parseHex(worstText),
200
- hue,
201
- chroma,
202
- mode === "light" ? "darker" : "lighter"
203
- );
204
- const onTint = solveLightness(
205
- 4.5,
206
- parseHex(bg),
207
- hue,
208
- chroma,
209
- mode === "light" ? "darker" : "lighter"
210
- );
192
+ const onSurface = solveLightness(4.5, parseHex(worstText), hue, chroma, mode === "light" ? "darker" : "lighter");
193
+ const onTint = solveLightness(4.5, parseHex(bg), hue, chroma, mode === "light" ? "darker" : "lighter");
211
194
  const l = mode === "light" ? Math.min(onSurface, onTint) : Math.max(onSurface, onTint);
212
195
  return oklchToHex({ l, c: chroma, h: hue });
213
196
  };
@@ -266,7 +249,13 @@ function derivePalette({
266
249
  const moneyExpense = money(70, 4.5);
267
250
  const moneyNet = money(300, 4.5);
268
251
  const neutralChroma = 0.012 * weight;
269
- const neutralFloor = solveLightness(4.5, parseHex(worstText), brand.h, neutralChroma, mode === "light" ? "darker" : "lighter");
252
+ const neutralFloor = solveLightness(
253
+ 4.5,
254
+ parseHex(worstText),
255
+ brand.h,
256
+ neutralChroma,
257
+ mode === "light" ? "darker" : "lighter"
258
+ );
270
259
  const moneyNeutral = oklchToHex({
271
260
  l: mode === "light" ? Math.min(0.52, neutralFloor) : Math.max(0.72, neutralFloor),
272
261
  c: neutralChroma,
@@ -350,7 +339,13 @@ function auditPalette(set, semantic, options = {}) {
350
339
  else add(`border on ${name}`, t.border, bg, 1.1, "house rule: a hairline must be findable");
351
340
  }
352
341
  if (step?.well) {
353
- add("bgSurface2 on bgSurface", t.bgSurface2, t.bgSurface, WELL_STEP_MIN, "house rule: the well stands two hover steps from the card");
342
+ add(
343
+ "bgSurface2 on bgSurface",
344
+ t.bgSurface2,
345
+ t.bgSurface,
346
+ WELL_STEP_MIN,
347
+ "house rule: the well stands two hover steps from the card"
348
+ );
354
349
  }
355
350
  for (const [name, bg] of textGrounds) {
356
351
  add(`textPrimary on ${name}`, t.textPrimary, bg, 7, "WCAG 1.4.6 AAA body text");
@@ -387,13 +382,7 @@ function auditPalette(set, semantic, options = {}) {
387
382
  add("warning on warningBg", semantic.warning, semantic.warningBg, 4.5, "WCAG 1.4.3 \u2014 badge text");
388
383
  add("info on infoBg", semantic.info, semantic.infoBg, 4.5, "WCAG 1.4.3 \u2014 badge text");
389
384
  add("success on successBg", semantic.success, semantic.successBg, 4.5, "WCAG 1.4.3 \u2014 badge text");
390
- add(
391
- "dangerContrast on danger",
392
- semantic.dangerContrast,
393
- semantic.danger,
394
- 4.5,
395
- "WCAG 1.4.3 AA body text"
396
- );
385
+ add("dangerContrast on danger", semantic.dangerContrast, semantic.danger, 4.5, "WCAG 1.4.3 AA body text");
397
386
  }
398
387
  const failures = checks.filter((c) => !c.passes);
399
388
  return { checks, failures, passes: failures.length === 0 };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/theme/palette-derive.ts"],"sourcesContent":["import type { ThemeMode } from \"./theme-store\";\nimport type { TokenSet } from \"./palette-presets\";\nimport type { HeatStops } from \"./chart-palette\";\nimport {\n contrast,\n deltaE,\n toHex,\n hexToOklch,\n oklchToHex,\n parseHex,\n rgbToOklch,\n simulateCvd,\n solveLightness,\n} from \"./color\";\nimport type { CvdType, Oklch } from \"./color\";\nimport type { ContrastLevel } from \"./contrast\";\nimport { brandSoft, contrastStep, hoverFill, steppedWellFill, WELL_STEP_MIN } from \"./contrast-tokens\";\n\n/**\n * Build a whole palette from one brand colour, plus any number of colours pinned by\n * hand, with every contrast ratio SOLVED rather than chosen and then hoped for.\n *\n * The presets this package shipped were assembled by eye and annotated with the ratios\n * they were believed to hold — `tokens.css` records \"7.0:1 and 4.8:1\", and\n * `palette-presets.ts` records \"2.67:1 … lifted they clear it at 3.30:1\". Nothing\n * computed any of them, and the audit found four of the nine default light chart hues\n * below the 3:1 that WCAG 1.4.11 requires of a graphical object. A comment is not a\n * test. Everything here is derived to a target and then measured, and\n * {@link auditPalette} returns the measurements so a caller can refuse a palette that\n * does not hold up.\n *\n * WHAT IS DERIVED AND WHAT IS NOT. The UI palette and the CATEGORICAL CHART RAMP solve\n * different problems and are deliberately kept apart. A UI colour needs contrast against\n * one known surface and carries a fixed meaning; a chart colour needs to stay separable\n * from eight unknown siblings, at roughly equal salience, with no implied order.\n * Optimising a set for one makes it worse at the other, which is why `deriveTokenSet`\n * leaves `chart` alone unless you explicitly ask for {@link deriveChartRamp}.\n */\n\n/* ── Inputs ───────────────────────────────────────────────────────────────── */\n\n/** A role whose hue may be pinned by the caller instead of chosen by the deriver. */\nexport type AnchorRole = \"brand\" | \"accent\" | \"danger\" | \"warning\" | \"success\" | \"info\";\n\nexport type PaletteAnchors = { brand: string } & Partial<Record<Exclude<AnchorRole, \"brand\">, string>>;\n\nexport interface DeriveOptions {\n anchors: PaletteAnchors;\n mode: ThemeMode;\n /**\n * How strongly the surfaces carry the brand's hue, 0–1 of the maximum useful tint.\n * 0 gives neutral greys; the shipped default is a visible warmth, because a page that\n * is very slightly the brand's hue reads as considered where a pure grey reads as\n * unstyled. Above ~0.5 the surfaces start competing with the content.\n */\n surfaceTint?: number;\n /** Contrast targets for the three text roles against the WORST surface. */\n textContrast?: { primary?: number; secondary?: number; muted?: number };\n}\n\n/* ── Outputs ──────────────────────────────────────────────────────────────── */\n\n/** The semantic layer, which lives in tokens.css rather than in `TokenSet`. */\nexport interface SemanticTokens {\n danger: string;\n dangerHover: string;\n dangerContrast: string;\n dangerBorder: string;\n /** The error line (invalid field, danger banner, destructive focus ring): as loud as\n * `dangerBorder` was before 0.15.5, so it keeps 3:1 against the surface. */\n dangerBorderStrong: string;\n dangerBg: string;\n warning: string;\n warningBorder: string;\n warningBg: string;\n info: string;\n infoBorder: string;\n infoBg: string;\n success: string;\n successBorder: string;\n successBg: string;\n /** Present only when a `accent` (secondary brand) anchor was supplied. */\n accent?: string;\n accentContrast?: string;\n accentBg?: string;\n}\n\nexport interface ContrastCheck {\n /** e.g. \"textMuted on bgPage\". */\n pair: string;\n ratio: number;\n /** What WCAG asks of this pair. */\n required: number;\n /** Which rule the requirement comes from, for a reader who wants to argue with it. */\n rule: string;\n passes: boolean;\n}\n\nexport interface ContrastReport {\n checks: ContrastCheck[];\n failures: ContrastCheck[];\n passes: boolean;\n}\n\nexport interface DerivedPalette {\n tokens: TokenSet;\n semantic: SemanticTokens;\n /** {@link auditPalette} at both contrast levels: the More-contrast checks carry a\n * \" (More contrast)\" suffix (0.36). */\n audit: ContrastReport;\n /** Compromises the deriver had to make, in plain words. Never silent. */\n warnings: string[];\n}\n\n/* ── The hue homes ────────────────────────────────────────────────────────── */\n\n/**\n * Where each semantic role wants to sit, and how far it may be pushed.\n *\n * A semantic colour is a convention before it is an aesthetic: danger is red because\n * every other interface the user has ever used made it red, and a \"danger\" button in\n * the brand's teal is a worse button however well it matches. So each role has a home\n * hue and a narrow licence to move — enough to get out of the brand's way when the\n * brand happens to live in the same neighbourhood, not enough to stop meaning what it\n * means. If it cannot get far enough, that is a warning, not a silent compromise.\n */\nconst HUE_HOME: Record<Exclude<AnchorRole, \"brand\" | \"accent\">, { hue: number; slack: number }> = {\n danger: { hue: 27, slack: 14 },\n warning: { hue: 75, slack: 16 },\n success: { hue: 150, slack: 20 },\n info: { hue: 240, slack: 22 },\n};\n\n/** Circular distance between two hue angles, 0–180. */\nfunction hueGap(a: number, b: number): number {\n const d = Math.abs(((a - b) % 360) + 360) % 360;\n return d > 180 ? 360 - d : d;\n}\n\n/**\n * Nudge `home` within `slack` to maximise the distance from every taken hue.\n *\n * Sampled rather than solved: the objective is a min-of-distances over a handful of\n * points on a short arc, which is piecewise-linear with kinks wherever the nearest\n * neighbour changes. Gradient methods stall on those kinks; 2° steps over a ±22° arc is\n * 23 evaluations of some trigonometry and cannot stall.\n */\nfunction placeHue(home: number, slack: number, taken: number[]): number {\n let best = home;\n let bestGap = Math.min(...taken.map((t) => hueGap(home, t)), 360);\n for (let d = -slack; d <= slack; d += 2) {\n const h = (home + d + 360) % 360;\n const gap = Math.min(...taken.map((t) => hueGap(h, t)), 360);\n // Ties go to the hue nearest home: a role should move only as far as it must.\n if (gap > bestGap + 1e-9) {\n best = h;\n bestGap = gap;\n }\n }\n return best;\n}\n\n/* ── Derivation ───────────────────────────────────────────────────────────── */\n\nconst SURFACE_L = {\n // Light: the PAGE is slightly darker than the cards on it, so a card reads as raised\n // without needing a shadow to say so. Matches the shipped presets' relationship.\n //\n // The light WELL is the page itself (0.33, docs/colour-roles-harmonization.md §5.3,\n // k26). Between the card and the page it sat 1.02–1.05:1 from the card it is inset\n // in, which is no well at all; at the page's lightness it stands out from the card\n // exactly as far as the card does from the page, and nothing re-solves, because the\n // page is already the surface every text role is solved against. Dark keeps its\n // well a step lighter than the card (1.11–1.13:1).\n light: { page: 0.912, surface: 0.945, surface2: 0.912 },\n dark: { page: 0.158, surface: 0.206, surface2: 0.249 },\n} as const;\n\n/**\n * What a hairline has to clear against the surface it divides.\n *\n * WCAG has nothing to say here — a decorative border is not a graphical object that\n * conveys information — so this is a house rule, and it is deliberately low. A border\n * that clears 3:1 is a RULE, not a hairline: it draws the eye to the box instead of to\n * what is in it. 1.5:1 is the point at which the edge is reliably findable without\n * becoming part of the composition.\n */\nconst BORDER_CONTRAST = 1.5;\n\n/** The most tint a surface can carry before it stops reading as a neutral ground. */\nconst MAX_SURFACE_CHROMA = 0.034;\n\n/**\n * The brand chroma from which the surfaces and the greys carry the brand's hue in full;\n * below it they carry it in proportion, down to none for a grey (round 0.34 BUG-8). A\n * grey has no hue — OKLCH reports 0, which is pink — and at full tint #808080 painted\n * every surface #eddde1.\n */\nconst FULL_HUE_CHROMA = 0.06;\n\n/** How much of the brand's hue the neutrals may carry: 0 for a grey, 1 from {@link FULL_HUE_CHROMA}. */\nfunction hueWeight(brand: Oklch): number {\n return Math.min(1, brand.c / FULL_HUE_CHROMA);\n}\n\nfunction surfaceSet(hue: number, mode: ThemeMode, tint: number, weight: number) {\n const c = MAX_SURFACE_CHROMA * Math.max(0, Math.min(1, tint)) * weight;\n const L = SURFACE_L[mode];\n const bgPage = oklchToHex({ l: L.page, c, h: hue });\n const bgSurface = oklchToHex({ l: L.surface, c, h: hue });\n const bgSurface2 = oklchToHex({ l: L.surface2, c, h: hue });\n // Solved, not guessed: a fixed lightness that looks right against a pale warm ground\n // disappears against a deep cold one, and both are reachable from the same `hue`\n // input. The border carries a little more chroma than the surfaces because at these\n // lightnesses a perfectly neutral hairline on a tinted ground reads as a wrong grey.\n const borderChroma = c * 1.6;\n // Solved against the surface the hairline is HARDEST to see on, not against one of\n // the three. Solving against `bgSurface` alone left it at 1.36:1 on `bgPage`, so the\n // target and the audit disagreed about the same border.\n const hardest = [bgPage, bgSurface, bgSurface2].reduce((a, b) =>\n contrast(a, mode === \"light\" ? \"#000000\" : \"#ffffff\") <\n contrast(b, mode === \"light\" ? \"#000000\" : \"#ffffff\")\n ? a\n : b,\n );\n const borderL = solveLightness(\n BORDER_CONTRAST,\n parseHex(hardest)!,\n hue,\n borderChroma,\n mode === \"light\" ? \"darker\" : \"lighter\",\n );\n return {\n bgPage,\n bgSurface,\n bgSurface2,\n border: oklchToHex({ l: borderL, c: borderChroma, h: hue }),\n };\n}\n\n/** Solve a colour at this hue that clears `target` against `against`, keeping chroma. */\nfunction solveAgainst(hue: number, chroma: number, target: number, against: string, mode: ThemeMode): string {\n const bg = parseHex(against)!;\n const l = solveLightness(target, bg, hue, chroma, mode === \"light\" ? \"darker\" : \"lighter\");\n return oklchToHex({ l, c: chroma, h: hue });\n}\n\n/** Whichever of near-white / near-black reads better ON `fill`. */\nfunction contrastOn(fill: string): string {\n const light = \"#ffffff\";\n const dark = \"#12100e\";\n return contrast(fill, light) >= contrast(fill, dark) ? light : dark;\n}\n\n/** A soft tint of `hue` sitting on the surface — a badge fill, not a button fill. */\nfunction tintOn(hue: number, chroma: number, surface: string, mode: ThemeMode): string {\n const s = hexToOklch(surface)!;\n // Toward the colour, but only far enough to be seen: these back TEXT of the same hue,\n // and a strong tint leaves the text nowhere to go.\n const l = mode === \"light\" ? s.l - 0.055 : s.l + 0.055;\n return oklchToHex({ l, c: Math.min(chroma * 0.45, 0.06), h: hue });\n}\n\nexport function derivePalette({\n anchors,\n mode,\n surfaceTint = 0.55,\n textContrast,\n}: DeriveOptions): DerivedPalette {\n const warnings: string[] = [];\n const brand = hexToOklch(anchors.brand);\n if (!brand) {\n throw new Error(\n `derivePalette: brand \"${anchors.brand}\" is not a #rgb or #rrggbb colour. ` +\n `Anchors must be hex — a named colour or an rgba() string cannot be converted.`,\n );\n }\n\n const targets = { primary: 10, secondary: 7, muted: 4.6, ...textContrast };\n\n // ── Surfaces, from the brand's hue.\n const weight = hueWeight(brand);\n const surfaces = surfaceSet(brand.h, mode, surfaceTint, weight);\n // Every text role is solved against the WORST of the three, so a token is legible\n // wherever it lands. Solving against `bgSurface` alone is the mistake that makes a\n // muted label fail on the page background it also sits on.\n const worstSurface = [surfaces.bgPage, surfaces.bgSurface, surfaces.bgSurface2].reduce((a, b) =>\n contrast(a, mode === \"light\" ? \"#000000\" : \"#ffffff\") <\n contrast(b, mode === \"light\" ? \"#000000\" : \"#ffffff\")\n ? a\n : b,\n );\n\n // ── Text. A trace of the brand hue keeps the greys from reading cold on a warm page.\n const textChroma = 0.014 * weight;\n const textPrimary = solveAgainst(brand.h, textChroma, targets.primary, worstSurface, mode);\n const textSecondary = solveAgainst(brand.h, textChroma, targets.secondary, worstSurface, mode);\n const textMuted = solveAgainst(brand.h, textChroma, targets.muted, worstSurface, mode);\n\n // The darkest fill text sits on (the lightest, in dark): the worst surface, or a\n // hovered row on the card (`--bg-hover`), which in light is a shade darker than the\n // page. The colours that are solved exactly to 4.5:1 — money, the semantic text — are\n // solved against this, so they hold on a hovered row too (0.33, §12.10). The text\n // roles above keep their margin (4.6:1 and up) and need no second solve.\n const hover = hoverFill({ textPrimary, bgSurface: surfaces.bgSurface });\n const worstText = [worstSurface, hover].reduce((a, b) =>\n contrast(a, mode === \"light\" ? \"#000000\" : \"#ffffff\") <\n contrast(b, mode === \"light\" ? \"#000000\" : \"#ffffff\")\n ? a\n : b,\n );\n\n // ── Brand. Keep the hue the caller asked for; move only the lightness, and only as\n // far as 3:1 against the worst surface — WCAG 1.4.11, since a brand-filled control is\n // a graphical object whose boundary has to be perceivable.\n //\n // In light the worst surface includes More contrast's well (0.36, colour-roles §13.4):\n // 10 % ink into the card, darker than the page. The step is applied, not derived, but\n // the brand is not stepped, so both brand solves measure there too and a derived\n // palette holds at both levels.\n const moreWell = steppedWellFill({ textPrimary, bgSurface: surfaces.bgSurface });\n const brandGround = moreWell\n ? [worstSurface, moreWell].reduce((a, b) => (contrast(a, \"#000000\") < contrast(b, \"#000000\") ? a : b))\n : worstSurface;\n const brandChroma = Math.max(brand.c, 0.06);\n const brandRatio = contrast(anchors.brand, brandGround);\n // NORMALISED, never the caller's raw string. `parseHex` accepts \"4f46e5\" and \"#abc\",\n // and passing either through unchanged put a value in the TokenSet that is not a CSS\n // colour — it then fails silently at paint time, three layers from here.\n let brandHex =\n brandRatio >= 3\n ? toHex(parseHex(anchors.brand)!)\n : solveAgainst(brand.h, brandChroma, 3, brandGround, mode);\n if (brandRatio < 3) {\n warnings.push(\n `brand ${anchors.brand} sits at ${brandRatio.toFixed(2)}:1 against the ${mode} surfaces, ` +\n `below the 3:1 WCAG 1.4.11 asks of a control boundary. Lightness was adjusted to ` +\n `${brandHex}; hue and chroma are unchanged.`,\n );\n }\n // `--brand-muted` is the brand AS TEXT — a soft brand chip's label, an outline chip, a\n // current link — and tokens.css derives it: 62 % brand into the ink. A brand solved\n // to 3:1 left it at 4.43–4.51:1 in three dark presets (0.33, §5.5), so it gets the\n // text floor here: the brand moves away from the surfaces, a step at a time, until its\n // muted form clears 4.5:1 on the worst fill and on its own soft chip, at rest\n // (`--brand-bg`) and hovered (`--brand-bg-hover`, the harder of the two: §12.11).\n const brandMutedWorst = (hex: string) => {\n const soft = brandSoft({ brand: hex, textPrimary, bgSurface: surfaces.bgSurface });\n return Math.min(\n contrast(soft.brandMuted, worstText),\n contrast(soft.brandMuted, soft.brandBg),\n contrast(soft.brandMuted, soft.brandBgHover),\n // …and on More contrast's light well, where a link button reads it (§13.3).\n moreWell ? contrast(soft.brandMuted, moreWell) : Infinity,\n );\n };\n for (let i = 0; i < 40 && brandMutedWorst(brandHex) < 4.5; i++) {\n const lch = hexToOklch(brandHex)!;\n brandHex = oklchToHex({ ...lch, l: mode === \"light\" ? lch.l - 0.005 : lch.l + 0.005 });\n }\n if (brandMutedWorst(brandHex) < 4.5) {\n warnings.push(\n `the brand as text (--brand-muted) reaches only ${brandMutedWorst(brandHex).toFixed(2)}:1 ` +\n `against the ${mode} surfaces (needs 4.5:1).`,\n );\n }\n const brandLch = hexToOklch(brandHex)!;\n const brandContrast = contrastOn(brandHex);\n const brandHover = oklchToHex({\n ...brandLch,\n // Hover moves AWAY from the colour of the text on it (0.33, §5.5): darker under\n // white, lighter under a dark ink. It used to move away from the page, which in dark\n // lightened a fill that carries WHITE text, toward its own label: 4.07:1.\n l: brandContrast === \"#ffffff\" ? brandLch.l - 0.06 : brandLch.l + 0.06,\n });\n const onBrand = contrast(brandHex, brandContrast);\n if (onBrand < 4.5) {\n warnings.push(\n `text on the brand fill reaches only ${onBrand.toFixed(2)}:1 (needs 4.5:1). ` +\n `A mid-lightness, high-chroma brand cannot carry legible text at any ink; ` +\n `use it as an accent and keep primary buttons on a darker or lighter variant.`,\n );\n }\n\n // ── Semantic roles. Anchors are honoured exactly; the rest are placed away from the\n // brand and from each other, within each role's licence to move.\n const taken: number[] = [brandLch.h];\n const accentLch = anchors.accent ? hexToOklch(anchors.accent) : null;\n if (anchors.accent && !accentLch) {\n warnings.push(`accent \"${anchors.accent}\" is not a hex colour and was ignored.`);\n }\n if (accentLch) taken.push(accentLch.h);\n\n const semanticHue = {} as Record<Exclude<AnchorRole, \"brand\" | \"accent\">, number>;\n const semanticChroma = {} as Record<Exclude<AnchorRole, \"brand\" | \"accent\">, number>;\n for (const role of [\"danger\", \"warning\", \"success\", \"info\"] as const) {\n const pinned = anchors[role] ? hexToOklch(anchors[role]!) : null;\n if (anchors[role] && !pinned) {\n warnings.push(`${role} \"${anchors[role]}\" is not a hex colour and was ignored.`);\n }\n const home = HUE_HOME[role];\n const hue = pinned ? pinned.h : placeHue(home.hue, home.slack, taken);\n if (!pinned) {\n const gap = Math.min(...taken.map((t) => hueGap(hue, t)));\n if (gap < 20) {\n warnings.push(\n `${role} could only reach ${gap.toFixed(0)}° from the nearest pinned hue, which is ` +\n `too close to tell apart at a glance. Pin ${role} explicitly, or move the brand.`,\n );\n }\n }\n semanticHue[role] = hue;\n semanticChroma[role] = pinned ? Math.max(pinned.c, 0.08) : 0.13;\n taken.push(hue);\n }\n\n /**\n * A semantic colour has two jobs and therefore two backgrounds: plain text on the\n * page, and the label inside its own tinted badge. The badge is the harder of the\n * two — it is a step toward the colour — so solving only against the surface produced\n * a badge at 4.1:1 while the loose text passed, which is the failure mode that is\n * hardest to notice because nine tenths of the uses look fine.\n */\n const sem = (role: Exclude<AnchorRole, \"brand\" | \"accent\">) => {\n const hue = semanticHue[role];\n const chroma = semanticChroma[role];\n const bg = tintOn(hue, chroma, surfaces.bgSurface, mode);\n const onSurface = solveLightness(\n 4.5,\n parseHex(worstText)!,\n hue,\n chroma,\n mode === \"light\" ? \"darker\" : \"lighter\",\n );\n const onTint = solveLightness(\n 4.5,\n parseHex(bg)!,\n hue,\n chroma,\n mode === \"light\" ? \"darker\" : \"lighter\",\n );\n const l = mode === \"light\" ? Math.min(onSurface, onTint) : Math.max(onSurface, onTint);\n return oklchToHex({ l, c: chroma, h: hue });\n };\n\n const danger = sem(\"danger\");\n const dangerLch = hexToOklch(danger)!;\n const semantic: SemanticTokens = {\n danger,\n dangerHover: oklchToHex({\n ...dangerLch,\n l: mode === \"light\" ? dangerLch.l - 0.06 : dangerLch.l + 0.06,\n }),\n dangerContrast: contrastOn(danger),\n // The tone border follows its siblings' lightness in dark mode (warning 0.44, info\n // and success 0.45) — a soft frame; the strong line keeps the old, louder recipe.\n dangerBorder: oklchToHex({\n l: mode === \"light\" ? dangerLch.l + 0.22 : 0.45,\n c: semanticChroma.danger,\n h: semanticHue.danger,\n }),\n dangerBorderStrong: oklchToHex({\n l: mode === \"light\" ? dangerLch.l + 0.22 : dangerLch.l - 0.22,\n c: semanticChroma.danger,\n h: semanticHue.danger,\n }),\n dangerBg: tintOn(semanticHue.danger, semanticChroma.danger, surfaces.bgSurface, mode),\n warning: sem(\"warning\"),\n warningBorder: oklchToHex({\n l: mode === \"light\" ? 0.82 : 0.44,\n c: semanticChroma.warning,\n h: semanticHue.warning,\n }),\n warningBg: tintOn(semanticHue.warning, semanticChroma.warning, surfaces.bgSurface, mode),\n info: sem(\"info\"),\n infoBorder: oklchToHex({\n l: mode === \"light\" ? 0.8 : 0.45,\n c: semanticChroma.info,\n h: semanticHue.info,\n }),\n infoBg: tintOn(semanticHue.info, semanticChroma.info, surfaces.bgSurface, mode),\n success: sem(\"success\"),\n successBorder: oklchToHex({\n l: mode === \"light\" ? 0.81 : 0.45,\n c: semanticChroma.success,\n h: semanticHue.success,\n }),\n successBg: tintOn(semanticHue.success, semanticChroma.success, surfaces.bgSurface, mode),\n };\n\n if (accentLch) {\n const accent = solveAgainst(accentLch.h, Math.max(accentLch.c, 0.06), 3, worstSurface, mode);\n semantic.accent = accent;\n semantic.accentContrast = contrastOn(accent);\n semantic.accentBg = tintOn(accentLch.h, Math.max(accentLch.c, 0.06), surfaces.bgSurface, mode);\n }\n\n // ── Money. A THIRD system again: these are data colours with fixed meanings, and the\n // one rule that outranks everything is that income and expense must never be the\n // red/green pair — which is why the shipped presets use teal/amber/violet. Derived\n // here at fixed hues rather than from the brand, for the same reason `--danger` is:\n // a consumer who rebrands does not thereby change what \"money out\" looks like.\n const money = (hue: number, target: number) => solveAgainst(hue, 0.12, target, worstText, mode);\n const moneyIncome = money(175, 4.5);\n const moneyExpense = money(70, 4.5);\n const moneyNet = money(300, 4.5);\n // \"No money has moved\": a quiet grey at a fixed lightness, but printed as TEXT (the\n // numpad's sign, a future row's figure), so it gets the text floor too (0.33). The\n // fixed lightness left it at 4.24:1 on four light pages.\n const neutralChroma = 0.012 * weight;\n const neutralFloor = solveLightness(4.5, parseHex(worstText)!, brand.h, neutralChroma, mode === \"light\" ? \"darker\" : \"lighter\");\n const moneyNeutral = oklchToHex({\n l: mode === \"light\" ? Math.min(0.52, neutralFloor) : Math.max(0.72, neutralFloor),\n c: neutralChroma,\n h: brand.h,\n });\n\n const tokens: TokenSet = {\n ...surfaces,\n textPrimary,\n textSecondary,\n textMuted,\n brand: brandHex,\n brandHover,\n brandContrast,\n moneyIncome,\n moneyExpense,\n moneyNet,\n moneyNeutral,\n // Left alone. See the note at the top of this file about why the categorical ramp is\n // not derived from the brand unless the caller asks.\n //\n // ⚠️ EMPTY, not absent: a caller who hands this straight to `applyTokenSet` keeps\n // whatever `--chart-1…9` were already on the element. That is deliberate — the ramp\n // is a separate decision and clearing it would leave charts unpainted — but it means\n // a derived palette applied over another preset is a MIX until you also supply a\n // ramp. `deriveChartRamp` is how you supply one.\n chart: [],\n heat: deriveHeat(moneyIncome, moneyExpense, surfaces, mode),\n };\n\n // Audited at both contrast levels (0.36, colour-roles §13.5): More contrast steps the\n // text, the border and, in light, the well and money of this set. The derived semantic\n // colours are audited at Standard only: they are solved for the standard grounds, and\n // only tokens.css's shipped literals step for the darker well.\n const standard = auditPalette(tokens, semantic);\n const more = auditPalette(tokens, undefined, { level: \"more\" });\n const checks = [...standard.checks, ...more.checks];\n const failures = checks.filter((c) => !c.passes);\n return { tokens, semantic, audit: { checks, failures, passes: failures.length === 0 }, warnings };\n}\n\n/**\n * Heatmap stops, reusing the money pair.\n *\n * `under` is the good direction and `over` the bad one, at the SAME hues as\n * `moneyIncome`/`moneyExpense` — so a heatmap cell and the figure printed beside it\n * agree about which way is which, rather than each inventing its own convention.\n */\nfunction deriveHeat(\n income: string,\n expense: string,\n surfaces: { bgSurface2: string },\n mode: ThemeMode,\n): HeatStops {\n const exp = hexToOklch(expense)!;\n return {\n neutral: surfaces.bgSurface2,\n under: income,\n over: expense,\n seqLow: oklchToHex({ l: mode === \"light\" ? 0.93 : 0.26, c: 0.05, h: exp.h }),\n seqHigh: expense,\n empty: mode === \"light\" ? \"rgba(120,110,95,0.14)\" : \"rgba(160,170,190,0.10)\",\n };\n}\n\n/* ── Audit ────────────────────────────────────────────────────────────────── */\n\n/**\n * Measure every pair that has a requirement, and say which ones fail.\n *\n * This is the part the shipped presets were missing. `derivePalette` runs it on the way\n * out, and the preset test runs it over everything the package ships, so a palette that\n * does not hold up cannot be merged with a comment claiming that it does.\n *\n * **`level: \"more\"`** (0.36, docs/colour-roles-harmonization.md §13.5) measures the set as\n * More contrast paints it: the stepped secondary, muted and border, and in a light set the\n * stepped well (as `bgSurface2`) and money — the same pairs, named with a \" (More\n * contrast)\" suffix. Two things change with the level: the border is a field's boundary\n * there, held to WCAG 1.4.11's 3:1 rather than the hairline's 1.1, and a light set's well\n * must stand {@link WELL_STEP_MIN}:1 from the card. Pass `semantic` with tokens.css's More\n * literals for a light set (the status colours step there); the status text is then\n * measured on the stepped well too.\n */\nexport function auditPalette(\n set: TokenSet,\n semantic?: SemanticTokens,\n options: { level?: ContrastLevel } = {},\n): ContrastReport {\n const more = options.level === \"more\";\n const step = more ? contrastStep(set) : null;\n // The set as it paints at this level: what `applyTokenSet` writes for it.\n const t: TokenSet = step\n ? {\n ...set,\n textSecondary: step.textSecondary,\n textMuted: step.textMuted,\n border: step.border,\n ...(step.well && {\n bgSurface2: step.well.bgSurface2,\n moneyIncome: step.well.moneyIncome,\n moneyExpense: step.well.moneyExpense,\n moneyNet: step.well.moneyNet,\n moneyNeutral: step.well.moneyNeutral,\n }),\n }\n : set;\n const suffix = more ? \" (More contrast)\" : \"\";\n const checks: ContrastCheck[] = [];\n const surfaces: Array<[string, string]> = [\n [\"bgPage\", t.bgPage],\n [\"bgSurface\", t.bgSurface],\n [\"bgSurface2\", t.bgSurface2],\n ];\n // A hovered row on the card (`--bg-hover`) carries the same text as the card, and in\n // light it is the darkest fill that text sits on (0.33, §12.10). Text is measured on\n // it; the brand fill and the hairline, which are not text, are not.\n const hover = hoverFill(t);\n const textGrounds: Array<[string, string]> = [...surfaces, [\"bgHover\", hover]];\n // `--brand-muted`, `--brand-bg` and `--brand-bg-hover`, mixed as tokens.css mixes them.\n const { brandMuted, brandBg, brandBgHover } = brandSoft(t);\n\n // The 0.05 of slack is for a pair with a color-mix on one side, and only for that: the\n // browser mixes in float and paints the result, where this mixes and rounds to hex, so\n // the two can differ by a hair. Hex against hex is what the page paints, measured\n // exactly — the slack used to cover those too, and passed the Contrast preset's dark\n // brand fill at 2.998:1 (round 0.34 BUG-8).\n const MIXED = /\\b(bgHover|brandMuted|brandBg|brandBgHover)\\b/;\n const add = (pair: string, fg: string, bg: string, required: number, rule: string) => {\n const ratio = contrast(fg, bg);\n const slack = MIXED.test(pair) ? 0.05 : 0;\n checks.push({ pair: pair + suffix, ratio, required, rule, passes: ratio >= required - slack });\n };\n\n for (const [name, bg] of surfaces) {\n add(`brand on ${name}`, t.brand, bg, 3, \"WCAG 1.4.11 graphical object\");\n if (more) add(`border on ${name}`, t.border, bg, 3, \"WCAG 1.4.11 — under More contrast a border bounds a field\");\n else add(`border on ${name}`, t.border, bg, 1.1, \"house rule: a hairline must be findable\");\n }\n // The stepped light well, a hex `applyTokenSet` writes (§13.3, decision 2).\n if (step?.well) {\n add(\"bgSurface2 on bgSurface\", t.bgSurface2, t.bgSurface, WELL_STEP_MIN, \"house rule: the well stands two hover steps from the card\");\n }\n for (const [name, bg] of textGrounds) {\n add(`textPrimary on ${name}`, t.textPrimary, bg, 7, \"WCAG 1.4.6 AAA body text\");\n add(`textSecondary on ${name}`, t.textSecondary, bg, 4.5, \"WCAG 1.4.3 AA body text\");\n add(`textMuted on ${name}`, t.textMuted, bg, 4.5, \"WCAG 1.4.3 AA body text\");\n add(`brandMuted on ${name}`, brandMuted, bg, 4.5, \"WCAG 1.4.3 — the brand as text\");\n for (const [key, hex] of [\n [\"moneyIncome\", t.moneyIncome],\n [\"moneyExpense\", t.moneyExpense],\n [\"moneyNet\", t.moneyNet],\n // Not a figure's colour but still printed as one (the numpad's sign, a future\n // row's amount) — measured since 0.33, when it was found at 4.24:1.\n [\"moneyNeutral\", t.moneyNeutral],\n ] as const) {\n add(`${key} on ${name}`, hex, bg, 4.5, \"WCAG 1.4.3 — these are rendered as TEXT\");\n }\n }\n\n add(\"brandContrast on brand\", t.brandContrast, t.brand, 4.5, \"WCAG 1.4.3 AA body text\");\n // A hovered brand button carries the same label (0.33): the derived dark presets\n // lightened the hover toward their white label and left it at 4.07:1.\n add(\"brandContrast on brandHover\", t.brandContrast, t.brandHover, 4.5, \"WCAG 1.4.3 AA body text\");\n add(\"brandMuted on brandBg\", brandMuted, brandBg, 4.5, \"WCAG 1.4.3 — a soft brand chip's label\");\n // The same label on the chip hovered (§12.11): a current link, a selected day. At 70 %\n // brand the muted text read 4.08–4.42:1 here (Default and Moss light; Ink, Moss, Plum\n // and Contrast dark).\n add(\"brandMuted on brandBgHover\", brandMuted, brandBgHover, 4.5, \"WCAG 1.4.3 — a hovered soft brand chip's label\");\n\n if (semantic) {\n for (const [name, fg] of [\n [\"danger\", semantic.danger],\n [\"warning\", semantic.warning],\n [\"info\", semantic.info],\n [\"success\", semantic.success],\n ] as const) {\n add(`${name} on bgSurface`, fg, t.bgSurface, 4.5, \"WCAG 1.4.3 — rendered as text\");\n add(`${name} on bgHover`, fg, hover, 4.5, \"WCAG 1.4.3 — rendered as text, on a hovered row\");\n // The darker light well the More literals were solved for (§13.3).\n if (step?.well) add(`${name} on bgSurface2`, fg, t.bgSurface2, 4.5, \"WCAG 1.4.3 — rendered as text, on the well\");\n }\n add(\"danger on dangerBg\", semantic.danger, semantic.dangerBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"warning on warningBg\", semantic.warning, semantic.warningBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"info on infoBg\", semantic.info, semantic.infoBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"success on successBg\", semantic.success, semantic.successBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\n \"dangerContrast on danger\",\n semantic.dangerContrast,\n semantic.danger,\n 4.5,\n \"WCAG 1.4.3 AA body text\",\n );\n }\n\n const failures = checks.filter((c) => !c.passes);\n return { checks, failures, passes: failures.length === 0 };\n}\n\n/* ── Categorical chart ramp ───────────────────────────────────────────────── */\n\nexport interface ChartRampReport {\n colors: string[];\n /** Series pairs that are too close under normal vision or any simulated CVD. */\n collisions: Array<{ a: number; b: number; under: \"normal\" | CvdType; distance: number }>;\n /** Series that do not clear 3:1 against a surface they may be drawn on. */\n lowContrast: Array<{ index: number; surface: string; ratio: number }>;\n /** The worst pairwise separation, across normal vision and all three dichromacies. */\n minSeparation: number;\n /** The worst contrast any series reaches against any surface it was measured on. */\n minContrast: number;\n passes: boolean;\n}\n\n/**\n * The floor for pairwise separation in OKLab, across normal vision and each dichromacy.\n *\n * CALIBRATED, not chosen. Paul Tol's \"Muted\" set — which this package ships and which is\n * a well-regarded CVD-safe palette — bottoms out at 0.059 in its light variant and 0.032\n * in the lightened dark one. A threshold above 0.059 would fail a palette that is known\n * to work, which is worse than having no check at all: an audit that cries wolf gets\n * switched off. 0.05 sits just under the known-good case.\n */\nconst MIN_SEPARATION = 0.05;\n\n/**\n * **The categorical trade-off, measured.** At a SINGLE lightness, the most series that\n * can satisfy both the 3:1 fill contrast of WCAG 1.4.11 and {@link MIN_SEPARATION} under\n * dichromacy is FOUR. Searched exhaustively over starting hue and lightness:\n *\n * n=4 best separation 0.065 — both satisfiable\n * n=5 best separation 0.041 — not satisfiable\n * n=9 best separation 0.015 — not satisfiable\n *\n * The reason is structural: dichromacy collapses the red-green axis, so hue alone stops\n * distinguishing colours once you need more than a handful, and the remaining channel is\n * LIGHTNESS — the very thing a uniform-contrast ramp holds constant. Paul Tol's set\n * varies lightness deliberately and pays for it in contrast; that is not an oversight in\n * their palette, it is the only currency left.\n *\n * So above four series this deriver varies lightness too, and reports what it gave up\n * rather than pretending both constraints were met.\n *\n * NOTE ON WHAT THIS BOUND IS. It describes the PROBLEM, not a guarantee about\n * {@link deriveChartRamp}. That function fixes lightness and starts the hue wheel at the\n * brand's own hue rather than searching for the best start, so it reaches the bound only\n * where the brand happens to sit well: sweeping 72 brand hues against the default light\n * surfaces, 7 of 72 produced a passing 4-series ramp (12 of 72 on dark), and none passed\n * at 5 or 9. Read the report it returns; do not assume a pass.\n */\nconst SINGLE_LIGHTNESS_MAX_SERIES = 4;\n\n/**\n * A categorical ramp of `count` hues, evenly spaced and equal in perceived lightness,\n * then MEASURED for separability — including under each dichromacy.\n *\n * Offered but not used by default, and the distinction matters. Evenly spaced hues at\n * one lightness are a reasonable generic answer; Paul Tol's \"Muted\" set, which this\n * package ships, is a better specific one, because it was optimised against real\n * confusion lines rather than derived from a formula. So this exists for a consumer who\n * wants their charts to follow their brand and is willing to read the report — not as\n * a silent upgrade to the default.\n */\nexport function deriveChartRamp(\n brandHex: string,\n mode: ThemeMode,\n surfaces: string[],\n count = 9,\n): ChartRampReport {\n const brand = hexToOklch(brandHex) ?? { l: 0.6, c: 0.12, h: 264 };\n const base = mode === \"light\" ? 0.58 : 0.74;\n const c = Math.max(0.09, Math.min(brand.c, 0.15));\n // Up to four series, one lightness: a scale that varies lightness implies rank, and a\n // categorical one must not. Beyond four, lightness is the only channel left that\n // survives dichromacy — see SINGLE_LIGHTNESS_MAX_SERIES — so it alternates in a\n // three-step cycle, which keeps ADJACENT series (the ones a legend puts side by side)\n // at different lightnesses as well as different hues.\n const vary = count > SINGLE_LIGHTNESS_MAX_SERIES;\n const offsets = [0, mode === \"light\" ? -0.1 : 0.1, mode === \"light\" ? 0.08 : -0.08];\n const colors = Array.from({ length: count }, (_, i) =>\n oklchToHex({\n l: vary ? base + offsets[i % offsets.length] : base,\n c,\n h: (brand.h + (360 / count) * i) % 360,\n }),\n );\n\n return measureRamp(colors, surfaces);\n}\n\n/** Audit a ramp somebody else chose — the shipped Paul Tol set, say. */\nexport function auditChartRamp(colors: string[], surfaces: string[]): ChartRampReport {\n return measureRamp(colors, surfaces);\n}\n\nfunction measureRamp(colors: string[], surfaces: string[]): ChartRampReport {\n const collisions: ChartRampReport[\"collisions\"] = [];\n const rgbs = colors.map((hex) => parseHex(hex)).filter((v): v is NonNullable<typeof v> => !!v);\n let minSeparation = Infinity;\n for (let i = 0; i < rgbs.length; i++) {\n for (let j = i + 1; j < rgbs.length; j++) {\n const normal = deltaE(rgbs[i], rgbs[j]);\n minSeparation = Math.min(minSeparation, normal);\n if (normal < MIN_SEPARATION) {\n collisions.push({ a: i, b: j, under: \"normal\", distance: normal });\n }\n for (const type of [\"protanopia\", \"deuteranopia\", \"tritanopia\"] as const) {\n const d = deltaE(simulateCvd(rgbs[i], type), simulateCvd(rgbs[j], type));\n minSeparation = Math.min(minSeparation, d);\n if (d < MIN_SEPARATION) collisions.push({ a: i, b: j, under: type, distance: d });\n }\n }\n }\n\n const lowContrast: ChartRampReport[\"lowContrast\"] = [];\n let minContrast = Infinity;\n colors.forEach((hex, index) => {\n for (const surface of surfaces) {\n const ratio = contrast(hex, surface);\n minContrast = Math.min(minContrast, ratio);\n // WCAG 1.4.11: a graphical object carrying meaning needs 3:1 against what is\n // adjacent to it. A bar the same value as the card behind it has no boundary.\n if (ratio < 3) lowContrast.push({ index, surface, ratio });\n }\n });\n\n return {\n colors,\n collisions,\n lowContrast,\n minSeparation: Number.isFinite(minSeparation) ? minSeparation : 0,\n minContrast: Number.isFinite(minContrast) ? minContrast : 0,\n passes: collisions.length === 0 && lowContrast.length === 0,\n };\n}\n\n/** Convenience: the OKLCH of a hex, for a caller inspecting an anchor. */\nexport function describe(hex: string): Oklch | null {\n const rgb = parseHex(hex);\n return rgb ? rgbToOklch(rgb) : null;\n}\n"],"mappings":";AAGA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,WAAW,cAAc,WAAW,iBAAiB,qBAAqB;AA8GnF,MAAM,WAA4F;AAAA,EAChG,QAAQ,EAAE,KAAK,IAAI,OAAO,GAAG;AAAA,EAC7B,SAAS,EAAE,KAAK,IAAI,OAAO,GAAG;AAAA,EAC9B,SAAS,EAAE,KAAK,KAAK,OAAO,GAAG;AAAA,EAC/B,MAAM,EAAE,KAAK,KAAK,OAAO,GAAG;AAC9B;AAGA,SAAS,OAAO,GAAW,GAAmB;AAC5C,QAAM,IAAI,KAAK,KAAM,IAAI,KAAK,MAAO,GAAG,IAAI;AAC5C,SAAO,IAAI,MAAM,MAAM,IAAI;AAC7B;AAUA,SAAS,SAAS,MAAc,OAAe,OAAyB;AACtE,MAAI,OAAO;AACX,MAAI,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC,GAAG,GAAG;AAChE,WAAS,IAAI,CAAC,OAAO,KAAK,OAAO,KAAK,GAAG;AACvC,UAAM,KAAK,OAAO,IAAI,OAAO;AAC7B,UAAM,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,GAAG;AAE3D,QAAI,MAAM,UAAU,MAAM;AACxB,aAAO;AACP,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAIA,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUhB,OAAO,EAAE,MAAM,OAAO,SAAS,OAAO,UAAU,MAAM;AAAA,EACtD,MAAM,EAAE,MAAM,OAAO,SAAS,OAAO,UAAU,MAAM;AACvD;AAWA,MAAM,kBAAkB;AAGxB,MAAM,qBAAqB;AAQ3B,MAAM,kBAAkB;AAGxB,SAAS,UAAU,OAAsB;AACvC,SAAO,KAAK,IAAI,GAAG,MAAM,IAAI,eAAe;AAC9C;AAEA,SAAS,WAAW,KAAa,MAAiB,MAAc,QAAgB;AAC9E,QAAM,IAAI,qBAAqB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC,IAAI;AAChE,QAAM,IAAI,UAAU,IAAI;AACxB,QAAM,SAAS,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,CAAC;AAClD,QAAM,YAAY,WAAW,EAAE,GAAG,EAAE,SAAS,GAAG,GAAG,IAAI,CAAC;AACxD,QAAM,aAAa,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,IAAI,CAAC;AAK1D,QAAM,eAAe,IAAI;AAIzB,QAAM,UAAU,CAAC,QAAQ,WAAW,UAAU,EAAE;AAAA,IAAO,CAAC,GAAG,MACzD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IACpD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IAChD,IACA;AAAA,EACN;AACA,QAAM,UAAU;AAAA,IACd;AAAA,IACA,SAAS,OAAO;AAAA,IAChB;AAAA,IACA;AAAA,IACA,SAAS,UAAU,WAAW;AAAA,EAChC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,WAAW,EAAE,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CAAC;AAAA,EAC5D;AACF;AAGA,SAAS,aAAa,KAAa,QAAgB,QAAgB,SAAiB,MAAyB;AAC3G,QAAM,KAAK,SAAS,OAAO;AAC3B,QAAM,IAAI,eAAe,QAAQ,IAAI,KAAK,QAAQ,SAAS,UAAU,WAAW,SAAS;AACzF,SAAO,WAAW,EAAE,GAAG,GAAG,QAAQ,GAAG,IAAI,CAAC;AAC5C;AAGA,SAAS,WAAW,MAAsB;AACxC,QAAM,QAAQ;AACd,QAAM,OAAO;AACb,SAAO,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,IAAI,IAAI,QAAQ;AACjE;AAGA,SAAS,OAAO,KAAa,QAAgB,SAAiB,MAAyB;AACrF,QAAM,IAAI,WAAW,OAAO;AAG5B,QAAM,IAAI,SAAS,UAAU,EAAE,IAAI,QAAQ,EAAE,IAAI;AACjD,SAAO,WAAW,EAAE,GAAG,GAAG,KAAK,IAAI,SAAS,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC;AACnE;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAkC;AAChC,QAAM,WAAqB,CAAC;AAC5B,QAAM,QAAQ,WAAW,QAAQ,KAAK;AACtC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,yBAAyB,QAAQ,KAAK;AAAA,IAExC;AAAA,EACF;AAEA,QAAM,UAAU,EAAE,SAAS,IAAI,WAAW,GAAG,OAAO,KAAK,GAAG,aAAa;AAGzE,QAAM,SAAS,UAAU,KAAK;AAC9B,QAAM,WAAW,WAAW,MAAM,GAAG,MAAM,aAAa,MAAM;AAI9D,QAAM,eAAe,CAAC,SAAS,QAAQ,SAAS,WAAW,SAAS,UAAU,EAAE;AAAA,IAAO,CAAC,GAAG,MACzF,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IACpD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IAChD,IACA;AAAA,EACN;AAGA,QAAM,aAAa,QAAQ;AAC3B,QAAM,cAAc,aAAa,MAAM,GAAG,YAAY,QAAQ,SAAS,cAAc,IAAI;AACzF,QAAM,gBAAgB,aAAa,MAAM,GAAG,YAAY,QAAQ,WAAW,cAAc,IAAI;AAC7F,QAAM,YAAY,aAAa,MAAM,GAAG,YAAY,QAAQ,OAAO,cAAc,IAAI;AAOrF,QAAM,QAAQ,UAAU,EAAE,aAAa,WAAW,SAAS,UAAU,CAAC;AACtE,QAAM,YAAY,CAAC,cAAc,KAAK,EAAE;AAAA,IAAO,CAAC,GAAG,MACjD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IACpD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IAChD,IACA;AAAA,EACN;AAUA,QAAM,WAAW,gBAAgB,EAAE,aAAa,WAAW,SAAS,UAAU,CAAC;AAC/E,QAAM,cAAc,WAChB,CAAC,cAAc,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAO,SAAS,GAAG,SAAS,IAAI,SAAS,GAAG,SAAS,IAAI,IAAI,CAAE,IACnG;AACJ,QAAM,cAAc,KAAK,IAAI,MAAM,GAAG,IAAI;AAC1C,QAAM,aAAa,SAAS,QAAQ,OAAO,WAAW;AAItD,MAAI,WACF,cAAc,IACV,MAAM,SAAS,QAAQ,KAAK,CAAE,IAC9B,aAAa,MAAM,GAAG,aAAa,GAAG,aAAa,IAAI;AAC7D,MAAI,aAAa,GAAG;AAClB,aAAS;AAAA,MACP,SAAS,QAAQ,KAAK,YAAY,WAAW,QAAQ,CAAC,CAAC,kBAAkB,IAAI,8FAExE,QAAQ;AAAA,IACf;AAAA,EACF;AAOA,QAAM,kBAAkB,CAAC,QAAgB;AACvC,UAAM,OAAO,UAAU,EAAE,OAAO,KAAK,aAAa,WAAW,SAAS,UAAU,CAAC;AACjF,WAAO,KAAK;AAAA,MACV,SAAS,KAAK,YAAY,SAAS;AAAA,MACnC,SAAS,KAAK,YAAY,KAAK,OAAO;AAAA,MACtC,SAAS,KAAK,YAAY,KAAK,YAAY;AAAA;AAAA,MAE3C,WAAW,SAAS,KAAK,YAAY,QAAQ,IAAI;AAAA,IACnD;AAAA,EACF;AACA,WAAS,IAAI,GAAG,IAAI,MAAM,gBAAgB,QAAQ,IAAI,KAAK,KAAK;AAC9D,UAAM,MAAM,WAAW,QAAQ;AAC/B,eAAW,WAAW,EAAE,GAAG,KAAK,GAAG,SAAS,UAAU,IAAI,IAAI,OAAQ,IAAI,IAAI,KAAM,CAAC;AAAA,EACvF;AACA,MAAI,gBAAgB,QAAQ,IAAI,KAAK;AACnC,aAAS;AAAA,MACP,kDAAkD,gBAAgB,QAAQ,EAAE,QAAQ,CAAC,CAAC,kBACrE,IAAI;AAAA,IACvB;AAAA,EACF;AACA,QAAM,WAAW,WAAW,QAAQ;AACpC,QAAM,gBAAgB,WAAW,QAAQ;AACzC,QAAM,aAAa,WAAW;AAAA,IAC5B,GAAG;AAAA;AAAA;AAAA;AAAA,IAIH,GAAG,kBAAkB,YAAY,SAAS,IAAI,OAAO,SAAS,IAAI;AAAA,EACpE,CAAC;AACD,QAAM,UAAU,SAAS,UAAU,aAAa;AAChD,MAAI,UAAU,KAAK;AACjB,aAAS;AAAA,MACP,uCAAuC,QAAQ,QAAQ,CAAC,CAAC;AAAA,IAG3D;AAAA,EACF;AAIA,QAAM,QAAkB,CAAC,SAAS,CAAC;AACnC,QAAM,YAAY,QAAQ,SAAS,WAAW,QAAQ,MAAM,IAAI;AAChE,MAAI,QAAQ,UAAU,CAAC,WAAW;AAChC,aAAS,KAAK,WAAW,QAAQ,MAAM,wCAAwC;AAAA,EACjF;AACA,MAAI,UAAW,OAAM,KAAK,UAAU,CAAC;AAErC,QAAM,cAAc,CAAC;AACrB,QAAM,iBAAiB,CAAC;AACxB,aAAW,QAAQ,CAAC,UAAU,WAAW,WAAW,MAAM,GAAY;AACpE,UAAM,SAAS,QAAQ,IAAI,IAAI,WAAW,QAAQ,IAAI,CAAE,IAAI;AAC5D,QAAI,QAAQ,IAAI,KAAK,CAAC,QAAQ;AAC5B,eAAS,KAAK,GAAG,IAAI,KAAK,QAAQ,IAAI,CAAC,wCAAwC;AAAA,IACjF;AACA,UAAM,OAAO,SAAS,IAAI;AAC1B,UAAM,MAAM,SAAS,OAAO,IAAI,SAAS,KAAK,KAAK,KAAK,OAAO,KAAK;AACpE,QAAI,CAAC,QAAQ;AACX,YAAM,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC,CAAC;AACxD,UAAI,MAAM,IAAI;AACZ,iBAAS;AAAA,UACP,GAAG,IAAI,qBAAqB,IAAI,QAAQ,CAAC,CAAC,uFACI,IAAI;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AACA,gBAAY,IAAI,IAAI;AACpB,mBAAe,IAAI,IAAI,SAAS,KAAK,IAAI,OAAO,GAAG,IAAI,IAAI;AAC3D,UAAM,KAAK,GAAG;AAAA,EAChB;AASA,QAAM,MAAM,CAAC,SAAkD;AAC7D,UAAM,MAAM,YAAY,IAAI;AAC5B,UAAM,SAAS,eAAe,IAAI;AAClC,UAAM,KAAK,OAAO,KAAK,QAAQ,SAAS,WAAW,IAAI;AACvD,UAAM,YAAY;AAAA,MAChB;AAAA,MACA,SAAS,SAAS;AAAA,MAClB;AAAA,MACA;AAAA,MACA,SAAS,UAAU,WAAW;AAAA,IAChC;AACA,UAAM,SAAS;AAAA,MACb;AAAA,MACA,SAAS,EAAE;AAAA,MACX;AAAA,MACA;AAAA,MACA,SAAS,UAAU,WAAW;AAAA,IAChC;AACA,UAAM,IAAI,SAAS,UAAU,KAAK,IAAI,WAAW,MAAM,IAAI,KAAK,IAAI,WAAW,MAAM;AACrF,WAAO,WAAW,EAAE,GAAG,GAAG,QAAQ,GAAG,IAAI,CAAC;AAAA,EAC5C;AAEA,QAAM,SAAS,IAAI,QAAQ;AAC3B,QAAM,YAAY,WAAW,MAAM;AACnC,QAAM,WAA2B;AAAA,IAC/B;AAAA,IACA,aAAa,WAAW;AAAA,MACtB,GAAG;AAAA,MACH,GAAG,SAAS,UAAU,UAAU,IAAI,OAAO,UAAU,IAAI;AAAA,IAC3D,CAAC;AAAA,IACD,gBAAgB,WAAW,MAAM;AAAA;AAAA;AAAA,IAGjC,cAAc,WAAW;AAAA,MACvB,GAAG,SAAS,UAAU,UAAU,IAAI,OAAO;AAAA,MAC3C,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,oBAAoB,WAAW;AAAA,MAC7B,GAAG,SAAS,UAAU,UAAU,IAAI,OAAO,UAAU,IAAI;AAAA,MACzD,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,UAAU,OAAO,YAAY,QAAQ,eAAe,QAAQ,SAAS,WAAW,IAAI;AAAA,IACpF,SAAS,IAAI,SAAS;AAAA,IACtB,eAAe,WAAW;AAAA,MACxB,GAAG,SAAS,UAAU,OAAO;AAAA,MAC7B,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,WAAW,OAAO,YAAY,SAAS,eAAe,SAAS,SAAS,WAAW,IAAI;AAAA,IACvF,MAAM,IAAI,MAAM;AAAA,IAChB,YAAY,WAAW;AAAA,MACrB,GAAG,SAAS,UAAU,MAAM;AAAA,MAC5B,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,QAAQ,OAAO,YAAY,MAAM,eAAe,MAAM,SAAS,WAAW,IAAI;AAAA,IAC9E,SAAS,IAAI,SAAS;AAAA,IACtB,eAAe,WAAW;AAAA,MACxB,GAAG,SAAS,UAAU,OAAO;AAAA,MAC7B,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,WAAW,OAAO,YAAY,SAAS,eAAe,SAAS,SAAS,WAAW,IAAI;AAAA,EACzF;AAEA,MAAI,WAAW;AACb,UAAM,SAAS,aAAa,UAAU,GAAG,KAAK,IAAI,UAAU,GAAG,IAAI,GAAG,GAAG,cAAc,IAAI;AAC3F,aAAS,SAAS;AAClB,aAAS,iBAAiB,WAAW,MAAM;AAC3C,aAAS,WAAW,OAAO,UAAU,GAAG,KAAK,IAAI,UAAU,GAAG,IAAI,GAAG,SAAS,WAAW,IAAI;AAAA,EAC/F;AAOA,QAAM,QAAQ,CAAC,KAAa,WAAmB,aAAa,KAAK,MAAM,QAAQ,WAAW,IAAI;AAC9F,QAAM,cAAc,MAAM,KAAK,GAAG;AAClC,QAAM,eAAe,MAAM,IAAI,GAAG;AAClC,QAAM,WAAW,MAAM,KAAK,GAAG;AAI/B,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,eAAe,eAAe,KAAK,SAAS,SAAS,GAAI,MAAM,GAAG,eAAe,SAAS,UAAU,WAAW,SAAS;AAC9H,QAAM,eAAe,WAAW;AAAA,IAC9B,GAAG,SAAS,UAAU,KAAK,IAAI,MAAM,YAAY,IAAI,KAAK,IAAI,MAAM,YAAY;AAAA,IAChF,GAAG;AAAA,IACH,GAAG,MAAM;AAAA,EACX,CAAC;AAED,QAAM,SAAmB;AAAA,IACvB,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,OAAO,CAAC;AAAA,IACR,MAAM,WAAW,aAAa,cAAc,UAAU,IAAI;AAAA,EAC5D;AAMA,QAAM,WAAW,aAAa,QAAQ,QAAQ;AAC9C,QAAM,OAAO,aAAa,QAAQ,QAAW,EAAE,OAAO,OAAO,CAAC;AAC9D,QAAM,SAAS,CAAC,GAAG,SAAS,QAAQ,GAAG,KAAK,MAAM;AAClD,QAAM,WAAW,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM;AAC/C,SAAO,EAAE,QAAQ,UAAU,OAAO,EAAE,QAAQ,UAAU,QAAQ,SAAS,WAAW,EAAE,GAAG,SAAS;AAClG;AASA,SAAS,WACP,QACA,SACA,UACA,MACW;AACX,QAAM,MAAM,WAAW,OAAO;AAC9B,SAAO;AAAA,IACL,SAAS,SAAS;AAAA,IAClB,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ,WAAW,EAAE,GAAG,SAAS,UAAU,OAAO,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC;AAAA,IAC3E,SAAS;AAAA,IACT,OAAO,SAAS,UAAU,0BAA0B;AAAA,EACtD;AACF;AAoBO,SAAS,aACd,KACA,UACA,UAAqC,CAAC,GACtB;AAChB,QAAM,OAAO,QAAQ,UAAU;AAC/B,QAAM,OAAO,OAAO,aAAa,GAAG,IAAI;AAExC,QAAM,IAAc,OAChB;AAAA,IACE,GAAG;AAAA,IACH,eAAe,KAAK;AAAA,IACpB,WAAW,KAAK;AAAA,IAChB,QAAQ,KAAK;AAAA,IACb,GAAI,KAAK,QAAQ;AAAA,MACf,YAAY,KAAK,KAAK;AAAA,MACtB,aAAa,KAAK,KAAK;AAAA,MACvB,cAAc,KAAK,KAAK;AAAA,MACxB,UAAU,KAAK,KAAK;AAAA,MACpB,cAAc,KAAK,KAAK;AAAA,IAC1B;AAAA,EACF,IACA;AACJ,QAAM,SAAS,OAAO,qBAAqB;AAC3C,QAAM,SAA0B,CAAC;AACjC,QAAM,WAAoC;AAAA,IACxC,CAAC,UAAU,EAAE,MAAM;AAAA,IACnB,CAAC,aAAa,EAAE,SAAS;AAAA,IACzB,CAAC,cAAc,EAAE,UAAU;AAAA,EAC7B;AAIA,QAAM,QAAQ,UAAU,CAAC;AACzB,QAAM,cAAuC,CAAC,GAAG,UAAU,CAAC,WAAW,KAAK,CAAC;AAE7E,QAAM,EAAE,YAAY,SAAS,aAAa,IAAI,UAAU,CAAC;AAOzD,QAAM,QAAQ;AACd,QAAM,MAAM,CAAC,MAAc,IAAY,IAAY,UAAkB,SAAiB;AACpF,UAAM,QAAQ,SAAS,IAAI,EAAE;AAC7B,UAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO;AACxC,WAAO,KAAK,EAAE,MAAM,OAAO,QAAQ,OAAO,UAAU,MAAM,QAAQ,SAAS,WAAW,MAAM,CAAC;AAAA,EAC/F;AAEA,aAAW,CAAC,MAAM,EAAE,KAAK,UAAU;AACjC,QAAI,YAAY,IAAI,IAAI,EAAE,OAAO,IAAI,GAAG,8BAA8B;AACtE,QAAI,KAAM,KAAI,aAAa,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,gEAA2D;AAAA,QAC1G,KAAI,aAAa,IAAI,IAAI,EAAE,QAAQ,IAAI,KAAK,yCAAyC;AAAA,EAC5F;AAEA,MAAI,MAAM,MAAM;AACd,QAAI,2BAA2B,EAAE,YAAY,EAAE,WAAW,eAAe,2DAA2D;AAAA,EACtI;AACA,aAAW,CAAC,MAAM,EAAE,KAAK,aAAa;AACpC,QAAI,kBAAkB,IAAI,IAAI,EAAE,aAAa,IAAI,GAAG,0BAA0B;AAC9E,QAAI,oBAAoB,IAAI,IAAI,EAAE,eAAe,IAAI,KAAK,yBAAyB;AACnF,QAAI,gBAAgB,IAAI,IAAI,EAAE,WAAW,IAAI,KAAK,yBAAyB;AAC3E,QAAI,iBAAiB,IAAI,IAAI,YAAY,IAAI,KAAK,qCAAgC;AAClF,eAAW,CAAC,KAAK,GAAG,KAAK;AAAA,MACvB,CAAC,eAAe,EAAE,WAAW;AAAA,MAC7B,CAAC,gBAAgB,EAAE,YAAY;AAAA,MAC/B,CAAC,YAAY,EAAE,QAAQ;AAAA;AAAA;AAAA,MAGvB,CAAC,gBAAgB,EAAE,YAAY;AAAA,IACjC,GAAY;AACV,UAAI,GAAG,GAAG,OAAO,IAAI,IAAI,KAAK,IAAI,KAAK,8CAAyC;AAAA,IAClF;AAAA,EACF;AAEA,MAAI,0BAA0B,EAAE,eAAe,EAAE,OAAO,KAAK,yBAAyB;AAGtF,MAAI,+BAA+B,EAAE,eAAe,EAAE,YAAY,KAAK,yBAAyB;AAChG,MAAI,yBAAyB,YAAY,SAAS,KAAK,6CAAwC;AAI/F,MAAI,8BAA8B,YAAY,cAAc,KAAK,qDAAgD;AAEjH,MAAI,UAAU;AACZ,eAAW,CAAC,MAAM,EAAE,KAAK;AAAA,MACvB,CAAC,UAAU,SAAS,MAAM;AAAA,MAC1B,CAAC,WAAW,SAAS,OAAO;AAAA,MAC5B,CAAC,QAAQ,SAAS,IAAI;AAAA,MACtB,CAAC,WAAW,SAAS,OAAO;AAAA,IAC9B,GAAY;AACV,UAAI,GAAG,IAAI,iBAAiB,IAAI,EAAE,WAAW,KAAK,oCAA+B;AACjF,UAAI,GAAG,IAAI,eAAe,IAAI,OAAO,KAAK,sDAAiD;AAE3F,UAAI,MAAM,KAAM,KAAI,GAAG,IAAI,kBAAkB,IAAI,EAAE,YAAY,KAAK,iDAA4C;AAAA,IAClH;AACA,QAAI,sBAAsB,SAAS,QAAQ,SAAS,UAAU,KAAK,8BAAyB;AAC5F,QAAI,wBAAwB,SAAS,SAAS,SAAS,WAAW,KAAK,8BAAyB;AAChG,QAAI,kBAAkB,SAAS,MAAM,SAAS,QAAQ,KAAK,8BAAyB;AACpF,QAAI,wBAAwB,SAAS,SAAS,SAAS,WAAW,KAAK,8BAAyB;AAChG;AAAA,MACE;AAAA,MACA,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM;AAC/C,SAAO,EAAE,QAAQ,UAAU,QAAQ,SAAS,WAAW,EAAE;AAC3D;AA0BA,MAAM,iBAAiB;AA2BvB,MAAM,8BAA8B;AAa7B,SAAS,gBACd,UACA,MACA,UACA,QAAQ,GACS;AACjB,QAAM,QAAQ,WAAW,QAAQ,KAAK,EAAE,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI;AAChE,QAAM,OAAO,SAAS,UAAU,OAAO;AACvC,QAAM,IAAI,KAAK,IAAI,MAAM,KAAK,IAAI,MAAM,GAAG,IAAI,CAAC;AAMhD,QAAM,OAAO,QAAQ;AACrB,QAAM,UAAU,CAAC,GAAG,SAAS,UAAU,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK;AAClF,QAAM,SAAS,MAAM;AAAA,IAAK,EAAE,QAAQ,MAAM;AAAA,IAAG,CAAC,GAAG,MAC/C,WAAW;AAAA,MACT,GAAG,OAAO,OAAO,QAAQ,IAAI,QAAQ,MAAM,IAAI;AAAA,MAC/C;AAAA,MACA,IAAI,MAAM,IAAK,MAAM,QAAS,KAAK;AAAA,IACrC,CAAC;AAAA,EACH;AAEA,SAAO,YAAY,QAAQ,QAAQ;AACrC;AAGO,SAAS,eAAe,QAAkB,UAAqC;AACpF,SAAO,YAAY,QAAQ,QAAQ;AACrC;AAEA,SAAS,YAAY,QAAkB,UAAqC;AAC1E,QAAM,aAA4C,CAAC;AACnD,QAAM,OAAO,OAAO,IAAI,CAAC,QAAQ,SAAS,GAAG,CAAC,EAAE,OAAO,CAAC,MAAkC,CAAC,CAAC,CAAC;AAC7F,MAAI,gBAAgB;AACpB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,aAAS,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACxC,YAAM,SAAS,OAAO,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;AACtC,sBAAgB,KAAK,IAAI,eAAe,MAAM;AAC9C,UAAI,SAAS,gBAAgB;AAC3B,mBAAW,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,UAAU,UAAU,OAAO,CAAC;AAAA,MACnE;AACA,iBAAW,QAAQ,CAAC,cAAc,gBAAgB,YAAY,GAAY;AACxE,cAAM,IAAI,OAAO,YAAY,KAAK,CAAC,GAAG,IAAI,GAAG,YAAY,KAAK,CAAC,GAAG,IAAI,CAAC;AACvE,wBAAgB,KAAK,IAAI,eAAe,CAAC;AACzC,YAAI,IAAI,eAAgB,YAAW,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,MAAM,UAAU,EAAE,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAA8C,CAAC;AACrD,MAAI,cAAc;AAClB,SAAO,QAAQ,CAAC,KAAK,UAAU;AAC7B,eAAW,WAAW,UAAU;AAC9B,YAAM,QAAQ,SAAS,KAAK,OAAO;AACnC,oBAAc,KAAK,IAAI,aAAa,KAAK;AAGzC,UAAI,QAAQ,EAAG,aAAY,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC;AAAA,IAC3D;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,OAAO,SAAS,aAAa,IAAI,gBAAgB;AAAA,IAChE,aAAa,OAAO,SAAS,WAAW,IAAI,cAAc;AAAA,IAC1D,QAAQ,WAAW,WAAW,KAAK,YAAY,WAAW;AAAA,EAC5D;AACF;AAGO,SAAS,SAAS,KAA2B;AAClD,QAAM,MAAM,SAAS,GAAG;AACxB,SAAO,MAAM,WAAW,GAAG,IAAI;AACjC;","names":[]}
1
+ {"version":3,"sources":["../../src/theme/palette-derive.ts"],"sourcesContent":["import type { ThemeMode } from \"./theme-store\";\nimport type { TokenSet } from \"./palette-presets\";\nimport type { HeatStops } from \"./chart-palette\";\nimport {\n contrast,\n deltaE,\n toHex,\n hexToOklch,\n oklchToHex,\n parseHex,\n rgbToOklch,\n simulateCvd,\n solveLightness,\n} from \"./color\";\nimport type { CvdType, Oklch } from \"./color\";\nimport type { ContrastLevel } from \"./contrast\";\nimport { brandSoft, contrastStep, hoverFill, steppedWellFill, WELL_STEP_MIN } from \"./contrast-tokens\";\n\n/**\n * Build a whole palette from one brand colour, plus any number of colours pinned by\n * hand, with every contrast ratio SOLVED rather than chosen and then hoped for.\n *\n * The presets this package shipped were assembled by eye and annotated with the ratios\n * they were believed to hold — `tokens.css` records \"7.0:1 and 4.8:1\", and\n * `palette-presets.ts` records \"2.67:1 … lifted they clear it at 3.30:1\". Nothing\n * computed any of them, and the audit found four of the nine default light chart hues\n * below the 3:1 that WCAG 1.4.11 requires of a graphical object. A comment is not a\n * test. Everything here is derived to a target and then measured, and\n * {@link auditPalette} returns the measurements so a caller can refuse a palette that\n * does not hold up.\n *\n * WHAT IS DERIVED AND WHAT IS NOT. The UI palette and the CATEGORICAL CHART RAMP solve\n * different problems and are deliberately kept apart. A UI colour needs contrast against\n * one known surface and carries a fixed meaning; a chart colour needs to stay separable\n * from eight unknown siblings, at roughly equal salience, with no implied order.\n * Optimising a set for one makes it worse at the other, which is why `deriveTokenSet`\n * leaves `chart` alone unless you explicitly ask for {@link deriveChartRamp}.\n */\n\n/* ── Inputs ───────────────────────────────────────────────────────────────── */\n\n/** A role whose hue may be pinned by the caller instead of chosen by the deriver. */\nexport type AnchorRole = \"brand\" | \"accent\" | \"danger\" | \"warning\" | \"success\" | \"info\";\n\nexport type PaletteAnchors = { brand: string } & Partial<Record<Exclude<AnchorRole, \"brand\">, string>>;\n\nexport interface DeriveOptions {\n anchors: PaletteAnchors;\n mode: ThemeMode;\n /**\n * How strongly the surfaces carry the brand's hue, 0–1 of the maximum useful tint.\n * 0 gives neutral greys; the shipped default is a visible warmth, because a page that\n * is very slightly the brand's hue reads as considered where a pure grey reads as\n * unstyled. Above ~0.5 the surfaces start competing with the content.\n */\n surfaceTint?: number;\n /** Contrast targets for the three text roles against the WORST surface. */\n textContrast?: { primary?: number; secondary?: number; muted?: number };\n}\n\n/* ── Outputs ──────────────────────────────────────────────────────────────── */\n\n/** The semantic layer, which lives in tokens.css rather than in `TokenSet`. */\nexport interface SemanticTokens {\n danger: string;\n dangerHover: string;\n dangerContrast: string;\n dangerBorder: string;\n /** The error line (invalid field, danger banner, destructive focus ring): as loud as\n * `dangerBorder` was before 0.15.5, so it keeps 3:1 against the surface. */\n dangerBorderStrong: string;\n dangerBg: string;\n warning: string;\n warningBorder: string;\n warningBg: string;\n info: string;\n infoBorder: string;\n infoBg: string;\n success: string;\n successBorder: string;\n successBg: string;\n /** Present only when a `accent` (secondary brand) anchor was supplied. */\n accent?: string;\n accentContrast?: string;\n accentBg?: string;\n}\n\nexport interface ContrastCheck {\n /** e.g. \"textMuted on bgPage\". */\n pair: string;\n ratio: number;\n /** What WCAG asks of this pair. */\n required: number;\n /** Which rule the requirement comes from, for a reader who wants to argue with it. */\n rule: string;\n passes: boolean;\n}\n\nexport interface ContrastReport {\n checks: ContrastCheck[];\n failures: ContrastCheck[];\n passes: boolean;\n}\n\nexport interface DerivedPalette {\n tokens: TokenSet;\n semantic: SemanticTokens;\n /** {@link auditPalette} at both contrast levels: the More-contrast checks carry a\n * \" (More contrast)\" suffix (0.36). */\n audit: ContrastReport;\n /** Compromises the deriver had to make, in plain words. Never silent. */\n warnings: string[];\n}\n\n/* ── The hue homes ────────────────────────────────────────────────────────── */\n\n/**\n * Where each semantic role wants to sit, and how far it may be pushed.\n *\n * A semantic colour is a convention before it is an aesthetic: danger is red because\n * every other interface the user has ever used made it red, and a \"danger\" button in\n * the brand's teal is a worse button however well it matches. So each role has a home\n * hue and a narrow licence to move — enough to get out of the brand's way when the\n * brand happens to live in the same neighbourhood, not enough to stop meaning what it\n * means. If it cannot get far enough, that is a warning, not a silent compromise.\n */\nconst HUE_HOME: Record<Exclude<AnchorRole, \"brand\" | \"accent\">, { hue: number; slack: number }> = {\n danger: { hue: 27, slack: 14 },\n warning: { hue: 75, slack: 16 },\n success: { hue: 150, slack: 20 },\n info: { hue: 240, slack: 22 },\n};\n\n/** Circular distance between two hue angles, 0–180. */\nfunction hueGap(a: number, b: number): number {\n const d = Math.abs(((a - b) % 360) + 360) % 360;\n return d > 180 ? 360 - d : d;\n}\n\n/**\n * Nudge `home` within `slack` to maximise the distance from every taken hue.\n *\n * Sampled rather than solved: the objective is a min-of-distances over a handful of\n * points on a short arc, which is piecewise-linear with kinks wherever the nearest\n * neighbour changes. Gradient methods stall on those kinks; 2° steps over a ±22° arc is\n * 23 evaluations of some trigonometry and cannot stall.\n */\nfunction placeHue(home: number, slack: number, taken: number[]): number {\n let best = home;\n let bestGap = Math.min(...taken.map((t) => hueGap(home, t)), 360);\n for (let d = -slack; d <= slack; d += 2) {\n const h = (home + d + 360) % 360;\n const gap = Math.min(...taken.map((t) => hueGap(h, t)), 360);\n // Ties go to the hue nearest home: a role should move only as far as it must.\n if (gap > bestGap + 1e-9) {\n best = h;\n bestGap = gap;\n }\n }\n return best;\n}\n\n/* ── Derivation ───────────────────────────────────────────────────────────── */\n\nconst SURFACE_L = {\n // Light: the PAGE is slightly darker than the cards on it, so a card reads as raised\n // without needing a shadow to say so. Matches the shipped presets' relationship.\n //\n // The light WELL is the page itself (0.33, docs/colour-roles-harmonization.md §5.3,\n // k26). Between the card and the page it sat 1.02–1.05:1 from the card it is inset\n // in, which is no well at all; at the page's lightness it stands out from the card\n // exactly as far as the card does from the page, and nothing re-solves, because the\n // page is already the surface every text role is solved against. Dark keeps its\n // well a step lighter than the card (1.11–1.13:1).\n light: { page: 0.912, surface: 0.945, surface2: 0.912 },\n dark: { page: 0.158, surface: 0.206, surface2: 0.249 },\n} as const;\n\n/**\n * What a hairline has to clear against the surface it divides.\n *\n * WCAG has nothing to say here — a decorative border is not a graphical object that\n * conveys information — so this is a house rule, and it is deliberately low. A border\n * that clears 3:1 is a RULE, not a hairline: it draws the eye to the box instead of to\n * what is in it. 1.5:1 is the point at which the edge is reliably findable without\n * becoming part of the composition.\n */\nconst BORDER_CONTRAST = 1.5;\n\n/** The most tint a surface can carry before it stops reading as a neutral ground. */\nconst MAX_SURFACE_CHROMA = 0.034;\n\n/**\n * The brand chroma from which the surfaces and the greys carry the brand's hue in full;\n * below it they carry it in proportion, down to none for a grey (round 0.34 BUG-8). A\n * grey has no hue — OKLCH reports 0, which is pink — and at full tint #808080 painted\n * every surface #eddde1.\n */\nconst FULL_HUE_CHROMA = 0.06;\n\n/** How much of the brand's hue the neutrals may carry: 0 for a grey, 1 from {@link FULL_HUE_CHROMA}. */\nfunction hueWeight(brand: Oklch): number {\n return Math.min(1, brand.c / FULL_HUE_CHROMA);\n}\n\nfunction surfaceSet(hue: number, mode: ThemeMode, tint: number, weight: number) {\n const c = MAX_SURFACE_CHROMA * Math.max(0, Math.min(1, tint)) * weight;\n const L = SURFACE_L[mode];\n const bgPage = oklchToHex({ l: L.page, c, h: hue });\n const bgSurface = oklchToHex({ l: L.surface, c, h: hue });\n const bgSurface2 = oklchToHex({ l: L.surface2, c, h: hue });\n // Solved, not guessed: a fixed lightness that looks right against a pale warm ground\n // disappears against a deep cold one, and both are reachable from the same `hue`\n // input. The border carries a little more chroma than the surfaces because at these\n // lightnesses a perfectly neutral hairline on a tinted ground reads as a wrong grey.\n const borderChroma = c * 1.6;\n // Solved against the surface the hairline is HARDEST to see on, not against one of\n // the three. Solving against `bgSurface` alone left it at 1.36:1 on `bgPage`, so the\n // target and the audit disagreed about the same border.\n const hardest = [bgPage, bgSurface, bgSurface2].reduce((a, b) =>\n contrast(a, mode === \"light\" ? \"#000000\" : \"#ffffff\") < contrast(b, mode === \"light\" ? \"#000000\" : \"#ffffff\")\n ? a\n : b,\n );\n const borderL = solveLightness(\n BORDER_CONTRAST,\n parseHex(hardest)!,\n hue,\n borderChroma,\n mode === \"light\" ? \"darker\" : \"lighter\",\n );\n return {\n bgPage,\n bgSurface,\n bgSurface2,\n border: oklchToHex({ l: borderL, c: borderChroma, h: hue }),\n };\n}\n\n/** Solve a colour at this hue that clears `target` against `against`, keeping chroma. */\nfunction solveAgainst(hue: number, chroma: number, target: number, against: string, mode: ThemeMode): string {\n const bg = parseHex(against)!;\n const l = solveLightness(target, bg, hue, chroma, mode === \"light\" ? \"darker\" : \"lighter\");\n return oklchToHex({ l, c: chroma, h: hue });\n}\n\n/** Whichever of near-white / near-black reads better ON `fill`. */\nfunction contrastOn(fill: string): string {\n const light = \"#ffffff\";\n const dark = \"#12100e\";\n return contrast(fill, light) >= contrast(fill, dark) ? light : dark;\n}\n\n/** A soft tint of `hue` sitting on the surface — a badge fill, not a button fill. */\nfunction tintOn(hue: number, chroma: number, surface: string, mode: ThemeMode): string {\n const s = hexToOklch(surface)!;\n // Toward the colour, but only far enough to be seen: these back TEXT of the same hue,\n // and a strong tint leaves the text nowhere to go.\n const l = mode === \"light\" ? s.l - 0.055 : s.l + 0.055;\n return oklchToHex({ l, c: Math.min(chroma * 0.45, 0.06), h: hue });\n}\n\nexport function derivePalette({ anchors, mode, surfaceTint = 0.55, textContrast }: DeriveOptions): DerivedPalette {\n const warnings: string[] = [];\n const brand = hexToOklch(anchors.brand);\n if (!brand) {\n throw new Error(\n `derivePalette: brand \"${anchors.brand}\" is not a #rgb or #rrggbb colour. ` +\n `Anchors must be hex — a named colour or an rgba() string cannot be converted.`,\n );\n }\n\n const targets = { primary: 10, secondary: 7, muted: 4.6, ...textContrast };\n\n // ── Surfaces, from the brand's hue.\n const weight = hueWeight(brand);\n const surfaces = surfaceSet(brand.h, mode, surfaceTint, weight);\n // Every text role is solved against the WORST of the three, so a token is legible\n // wherever it lands. Solving against `bgSurface` alone is the mistake that makes a\n // muted label fail on the page background it also sits on.\n const worstSurface = [surfaces.bgPage, surfaces.bgSurface, surfaces.bgSurface2].reduce((a, b) =>\n contrast(a, mode === \"light\" ? \"#000000\" : \"#ffffff\") < contrast(b, mode === \"light\" ? \"#000000\" : \"#ffffff\")\n ? a\n : b,\n );\n\n // ── Text. A trace of the brand hue keeps the greys from reading cold on a warm page.\n const textChroma = 0.014 * weight;\n const textPrimary = solveAgainst(brand.h, textChroma, targets.primary, worstSurface, mode);\n const textSecondary = solveAgainst(brand.h, textChroma, targets.secondary, worstSurface, mode);\n const textMuted = solveAgainst(brand.h, textChroma, targets.muted, worstSurface, mode);\n\n // The darkest fill text sits on (the lightest, in dark): the worst surface, or a\n // hovered row on the card (`--bg-hover`), which in light is a shade darker than the\n // page. The colours that are solved exactly to 4.5:1 — money, the semantic text — are\n // solved against this, so they hold on a hovered row too (0.33, §12.10). The text\n // roles above keep their margin (4.6:1 and up) and need no second solve.\n const hover = hoverFill({ textPrimary, bgSurface: surfaces.bgSurface });\n const worstText = [worstSurface, hover].reduce((a, b) =>\n contrast(a, mode === \"light\" ? \"#000000\" : \"#ffffff\") < contrast(b, mode === \"light\" ? \"#000000\" : \"#ffffff\")\n ? a\n : b,\n );\n\n // ── Brand. Keep the hue the caller asked for; move only the lightness, and only as\n // far as 3:1 against the worst surface — WCAG 1.4.11, since a brand-filled control is\n // a graphical object whose boundary has to be perceivable.\n //\n // In light the worst surface includes More contrast's well (0.36, colour-roles §13.4):\n // 10 % ink into the card, darker than the page. The step is applied, not derived, but\n // the brand is not stepped, so both brand solves measure there too and a derived\n // palette holds at both levels.\n const moreWell = steppedWellFill({ textPrimary, bgSurface: surfaces.bgSurface });\n const brandGround = moreWell\n ? [worstSurface, moreWell].reduce((a, b) => (contrast(a, \"#000000\") < contrast(b, \"#000000\") ? a : b))\n : worstSurface;\n const brandChroma = Math.max(brand.c, 0.06);\n const brandRatio = contrast(anchors.brand, brandGround);\n // NORMALISED, never the caller's raw string. `parseHex` accepts \"4f46e5\" and \"#abc\",\n // and passing either through unchanged put a value in the TokenSet that is not a CSS\n // colour — it then fails silently at paint time, three layers from here.\n let brandHex =\n brandRatio >= 3 ? toHex(parseHex(anchors.brand)!) : solveAgainst(brand.h, brandChroma, 3, brandGround, mode);\n if (brandRatio < 3) {\n warnings.push(\n `brand ${anchors.brand} sits at ${brandRatio.toFixed(2)}:1 against the ${mode} surfaces, ` +\n `below the 3:1 WCAG 1.4.11 asks of a control boundary. Lightness was adjusted to ` +\n `${brandHex}; hue and chroma are unchanged.`,\n );\n }\n // `--brand-muted` is the brand AS TEXT — a soft brand chip's label, an outline chip, a\n // current link — and tokens.css derives it: 62 % brand into the ink. A brand solved\n // to 3:1 left it at 4.43–4.51:1 in three dark presets (0.33, §5.5), so it gets the\n // text floor here: the brand moves away from the surfaces, a step at a time, until its\n // muted form clears 4.5:1 on the worst fill and on its own soft chip, at rest\n // (`--brand-bg`) and hovered (`--brand-bg-hover`, the harder of the two: §12.11).\n const brandMutedWorst = (hex: string) => {\n const soft = brandSoft({ brand: hex, textPrimary, bgSurface: surfaces.bgSurface });\n return Math.min(\n contrast(soft.brandMuted, worstText),\n contrast(soft.brandMuted, soft.brandBg),\n contrast(soft.brandMuted, soft.brandBgHover),\n // …and on More contrast's light well, where a link button reads it (§13.3).\n moreWell ? contrast(soft.brandMuted, moreWell) : Infinity,\n );\n };\n for (let i = 0; i < 40 && brandMutedWorst(brandHex) < 4.5; i++) {\n const lch = hexToOklch(brandHex)!;\n brandHex = oklchToHex({ ...lch, l: mode === \"light\" ? lch.l - 0.005 : lch.l + 0.005 });\n }\n if (brandMutedWorst(brandHex) < 4.5) {\n warnings.push(\n `the brand as text (--brand-muted) reaches only ${brandMutedWorst(brandHex).toFixed(2)}:1 ` +\n `against the ${mode} surfaces (needs 4.5:1).`,\n );\n }\n const brandLch = hexToOklch(brandHex)!;\n const brandContrast = contrastOn(brandHex);\n const brandHover = oklchToHex({\n ...brandLch,\n // Hover moves AWAY from the colour of the text on it (0.33, §5.5): darker under\n // white, lighter under a dark ink. It used to move away from the page, which in dark\n // lightened a fill that carries WHITE text, toward its own label: 4.07:1.\n l: brandContrast === \"#ffffff\" ? brandLch.l - 0.06 : brandLch.l + 0.06,\n });\n const onBrand = contrast(brandHex, brandContrast);\n if (onBrand < 4.5) {\n warnings.push(\n `text on the brand fill reaches only ${onBrand.toFixed(2)}:1 (needs 4.5:1). ` +\n `A mid-lightness, high-chroma brand cannot carry legible text at any ink; ` +\n `use it as an accent and keep primary buttons on a darker or lighter variant.`,\n );\n }\n\n // ── Semantic roles. Anchors are honoured exactly; the rest are placed away from the\n // brand and from each other, within each role's licence to move.\n const taken: number[] = [brandLch.h];\n const accentLch = anchors.accent ? hexToOklch(anchors.accent) : null;\n if (anchors.accent && !accentLch) {\n warnings.push(`accent \"${anchors.accent}\" is not a hex colour and was ignored.`);\n }\n if (accentLch) taken.push(accentLch.h);\n\n const semanticHue = {} as Record<Exclude<AnchorRole, \"brand\" | \"accent\">, number>;\n const semanticChroma = {} as Record<Exclude<AnchorRole, \"brand\" | \"accent\">, number>;\n for (const role of [\"danger\", \"warning\", \"success\", \"info\"] as const) {\n const pinned = anchors[role] ? hexToOklch(anchors[role]!) : null;\n if (anchors[role] && !pinned) {\n warnings.push(`${role} \"${anchors[role]}\" is not a hex colour and was ignored.`);\n }\n const home = HUE_HOME[role];\n const hue = pinned ? pinned.h : placeHue(home.hue, home.slack, taken);\n if (!pinned) {\n const gap = Math.min(...taken.map((t) => hueGap(hue, t)));\n if (gap < 20) {\n warnings.push(\n `${role} could only reach ${gap.toFixed(0)}° from the nearest pinned hue, which is ` +\n `too close to tell apart at a glance. Pin ${role} explicitly, or move the brand.`,\n );\n }\n }\n semanticHue[role] = hue;\n semanticChroma[role] = pinned ? Math.max(pinned.c, 0.08) : 0.13;\n taken.push(hue);\n }\n\n /**\n * A semantic colour has two jobs and therefore two backgrounds: plain text on the\n * page, and the label inside its own tinted badge. The badge is the harder of the\n * two — it is a step toward the colour — so solving only against the surface produced\n * a badge at 4.1:1 while the loose text passed, which is the failure mode that is\n * hardest to notice because nine tenths of the uses look fine.\n */\n const sem = (role: Exclude<AnchorRole, \"brand\" | \"accent\">) => {\n const hue = semanticHue[role];\n const chroma = semanticChroma[role];\n const bg = tintOn(hue, chroma, surfaces.bgSurface, mode);\n const onSurface = solveLightness(4.5, parseHex(worstText)!, hue, chroma, mode === \"light\" ? \"darker\" : \"lighter\");\n const onTint = solveLightness(4.5, parseHex(bg)!, hue, chroma, mode === \"light\" ? \"darker\" : \"lighter\");\n const l = mode === \"light\" ? Math.min(onSurface, onTint) : Math.max(onSurface, onTint);\n return oklchToHex({ l, c: chroma, h: hue });\n };\n\n const danger = sem(\"danger\");\n const dangerLch = hexToOklch(danger)!;\n const semantic: SemanticTokens = {\n danger,\n dangerHover: oklchToHex({\n ...dangerLch,\n l: mode === \"light\" ? dangerLch.l - 0.06 : dangerLch.l + 0.06,\n }),\n dangerContrast: contrastOn(danger),\n // The tone border follows its siblings' lightness in dark mode (warning 0.44, info\n // and success 0.45) — a soft frame; the strong line keeps the old, louder recipe.\n dangerBorder: oklchToHex({\n l: mode === \"light\" ? dangerLch.l + 0.22 : 0.45,\n c: semanticChroma.danger,\n h: semanticHue.danger,\n }),\n dangerBorderStrong: oklchToHex({\n l: mode === \"light\" ? dangerLch.l + 0.22 : dangerLch.l - 0.22,\n c: semanticChroma.danger,\n h: semanticHue.danger,\n }),\n dangerBg: tintOn(semanticHue.danger, semanticChroma.danger, surfaces.bgSurface, mode),\n warning: sem(\"warning\"),\n warningBorder: oklchToHex({\n l: mode === \"light\" ? 0.82 : 0.44,\n c: semanticChroma.warning,\n h: semanticHue.warning,\n }),\n warningBg: tintOn(semanticHue.warning, semanticChroma.warning, surfaces.bgSurface, mode),\n info: sem(\"info\"),\n infoBorder: oklchToHex({\n l: mode === \"light\" ? 0.8 : 0.45,\n c: semanticChroma.info,\n h: semanticHue.info,\n }),\n infoBg: tintOn(semanticHue.info, semanticChroma.info, surfaces.bgSurface, mode),\n success: sem(\"success\"),\n successBorder: oklchToHex({\n l: mode === \"light\" ? 0.81 : 0.45,\n c: semanticChroma.success,\n h: semanticHue.success,\n }),\n successBg: tintOn(semanticHue.success, semanticChroma.success, surfaces.bgSurface, mode),\n };\n\n if (accentLch) {\n const accent = solveAgainst(accentLch.h, Math.max(accentLch.c, 0.06), 3, worstSurface, mode);\n semantic.accent = accent;\n semantic.accentContrast = contrastOn(accent);\n semantic.accentBg = tintOn(accentLch.h, Math.max(accentLch.c, 0.06), surfaces.bgSurface, mode);\n }\n\n // ── Money. A THIRD system again: these are data colours with fixed meanings, and the\n // one rule that outranks everything is that income and expense must never be the\n // red/green pair — which is why the shipped presets use teal/amber/violet. Derived\n // here at fixed hues rather than from the brand, for the same reason `--danger` is:\n // a consumer who rebrands does not thereby change what \"money out\" looks like.\n const money = (hue: number, target: number) => solveAgainst(hue, 0.12, target, worstText, mode);\n const moneyIncome = money(175, 4.5);\n const moneyExpense = money(70, 4.5);\n const moneyNet = money(300, 4.5);\n // \"No money has moved\": a quiet grey at a fixed lightness, but printed as TEXT (the\n // numpad's sign, a future row's figure), so it gets the text floor too (0.33). The\n // fixed lightness left it at 4.24:1 on four light pages.\n const neutralChroma = 0.012 * weight;\n const neutralFloor = solveLightness(\n 4.5,\n parseHex(worstText)!,\n brand.h,\n neutralChroma,\n mode === \"light\" ? \"darker\" : \"lighter\",\n );\n const moneyNeutral = oklchToHex({\n l: mode === \"light\" ? Math.min(0.52, neutralFloor) : Math.max(0.72, neutralFloor),\n c: neutralChroma,\n h: brand.h,\n });\n\n const tokens: TokenSet = {\n ...surfaces,\n textPrimary,\n textSecondary,\n textMuted,\n brand: brandHex,\n brandHover,\n brandContrast,\n moneyIncome,\n moneyExpense,\n moneyNet,\n moneyNeutral,\n // Left alone. See the note at the top of this file about why the categorical ramp is\n // not derived from the brand unless the caller asks.\n //\n // ⚠️ EMPTY, not absent: a caller who hands this straight to `applyTokenSet` keeps\n // whatever `--chart-1…9` were already on the element. That is deliberate — the ramp\n // is a separate decision and clearing it would leave charts unpainted — but it means\n // a derived palette applied over another preset is a MIX until you also supply a\n // ramp. `deriveChartRamp` is how you supply one.\n chart: [],\n heat: deriveHeat(moneyIncome, moneyExpense, surfaces, mode),\n };\n\n // Audited at both contrast levels (0.36, colour-roles §13.5): More contrast steps the\n // text, the border and, in light, the well and money of this set. The derived semantic\n // colours are audited at Standard only: they are solved for the standard grounds, and\n // only tokens.css's shipped literals step for the darker well.\n const standard = auditPalette(tokens, semantic);\n const more = auditPalette(tokens, undefined, { level: \"more\" });\n const checks = [...standard.checks, ...more.checks];\n const failures = checks.filter((c) => !c.passes);\n return { tokens, semantic, audit: { checks, failures, passes: failures.length === 0 }, warnings };\n}\n\n/**\n * Heatmap stops, reusing the money pair.\n *\n * `under` is the good direction and `over` the bad one, at the SAME hues as\n * `moneyIncome`/`moneyExpense` — so a heatmap cell and the figure printed beside it\n * agree about which way is which, rather than each inventing its own convention.\n */\nfunction deriveHeat(income: string, expense: string, surfaces: { bgSurface2: string }, mode: ThemeMode): HeatStops {\n const exp = hexToOklch(expense)!;\n return {\n neutral: surfaces.bgSurface2,\n under: income,\n over: expense,\n seqLow: oklchToHex({ l: mode === \"light\" ? 0.93 : 0.26, c: 0.05, h: exp.h }),\n seqHigh: expense,\n empty: mode === \"light\" ? \"rgba(120,110,95,0.14)\" : \"rgba(160,170,190,0.10)\",\n };\n}\n\n/* ── Audit ────────────────────────────────────────────────────────────────── */\n\n/**\n * Measure every pair that has a requirement, and say which ones fail.\n *\n * This is the part the shipped presets were missing. `derivePalette` runs it on the way\n * out, and the preset test runs it over everything the package ships, so a palette that\n * does not hold up cannot be merged with a comment claiming that it does.\n *\n * **`level: \"more\"`** (0.36, docs/colour-roles-harmonization.md §13.5) measures the set as\n * More contrast paints it: the stepped secondary, muted and border, and in a light set the\n * stepped well (as `bgSurface2`) and money — the same pairs, named with a \" (More\n * contrast)\" suffix. Two things change with the level: the border is a field's boundary\n * there, held to WCAG 1.4.11's 3:1 rather than the hairline's 1.1, and a light set's well\n * must stand {@link WELL_STEP_MIN}:1 from the card. Pass `semantic` with tokens.css's More\n * literals for a light set (the status colours step there); the status text is then\n * measured on the stepped well too.\n */\nexport function auditPalette(\n set: TokenSet,\n semantic?: SemanticTokens,\n options: { level?: ContrastLevel } = {},\n): ContrastReport {\n const more = options.level === \"more\";\n const step = more ? contrastStep(set) : null;\n // The set as it paints at this level: what `applyTokenSet` writes for it.\n const t: TokenSet = step\n ? {\n ...set,\n textSecondary: step.textSecondary,\n textMuted: step.textMuted,\n border: step.border,\n ...(step.well && {\n bgSurface2: step.well.bgSurface2,\n moneyIncome: step.well.moneyIncome,\n moneyExpense: step.well.moneyExpense,\n moneyNet: step.well.moneyNet,\n moneyNeutral: step.well.moneyNeutral,\n }),\n }\n : set;\n const suffix = more ? \" (More contrast)\" : \"\";\n const checks: ContrastCheck[] = [];\n const surfaces: Array<[string, string]> = [\n [\"bgPage\", t.bgPage],\n [\"bgSurface\", t.bgSurface],\n [\"bgSurface2\", t.bgSurface2],\n ];\n // A hovered row on the card (`--bg-hover`) carries the same text as the card, and in\n // light it is the darkest fill that text sits on (0.33, §12.10). Text is measured on\n // it; the brand fill and the hairline, which are not text, are not.\n const hover = hoverFill(t);\n const textGrounds: Array<[string, string]> = [...surfaces, [\"bgHover\", hover]];\n // `--brand-muted`, `--brand-bg` and `--brand-bg-hover`, mixed as tokens.css mixes them.\n const { brandMuted, brandBg, brandBgHover } = brandSoft(t);\n\n // The 0.05 of slack is for a pair with a color-mix on one side, and only for that: the\n // browser mixes in float and paints the result, where this mixes and rounds to hex, so\n // the two can differ by a hair. Hex against hex is what the page paints, measured\n // exactly — the slack used to cover those too, and passed the Contrast preset's dark\n // brand fill at 2.998:1 (round 0.34 BUG-8).\n const MIXED = /\\b(bgHover|brandMuted|brandBg|brandBgHover)\\b/;\n const add = (pair: string, fg: string, bg: string, required: number, rule: string) => {\n const ratio = contrast(fg, bg);\n const slack = MIXED.test(pair) ? 0.05 : 0;\n checks.push({ pair: pair + suffix, ratio, required, rule, passes: ratio >= required - slack });\n };\n\n for (const [name, bg] of surfaces) {\n add(`brand on ${name}`, t.brand, bg, 3, \"WCAG 1.4.11 graphical object\");\n if (more) add(`border on ${name}`, t.border, bg, 3, \"WCAG 1.4.11 — under More contrast a border bounds a field\");\n else add(`border on ${name}`, t.border, bg, 1.1, \"house rule: a hairline must be findable\");\n }\n // The stepped light well, a hex `applyTokenSet` writes (§13.3, decision 2).\n if (step?.well) {\n add(\n \"bgSurface2 on bgSurface\",\n t.bgSurface2,\n t.bgSurface,\n WELL_STEP_MIN,\n \"house rule: the well stands two hover steps from the card\",\n );\n }\n for (const [name, bg] of textGrounds) {\n add(`textPrimary on ${name}`, t.textPrimary, bg, 7, \"WCAG 1.4.6 AAA body text\");\n add(`textSecondary on ${name}`, t.textSecondary, bg, 4.5, \"WCAG 1.4.3 AA body text\");\n add(`textMuted on ${name}`, t.textMuted, bg, 4.5, \"WCAG 1.4.3 AA body text\");\n add(`brandMuted on ${name}`, brandMuted, bg, 4.5, \"WCAG 1.4.3 — the brand as text\");\n for (const [key, hex] of [\n [\"moneyIncome\", t.moneyIncome],\n [\"moneyExpense\", t.moneyExpense],\n [\"moneyNet\", t.moneyNet],\n // Not a figure's colour but still printed as one (the numpad's sign, a future\n // row's amount) — measured since 0.33, when it was found at 4.24:1.\n [\"moneyNeutral\", t.moneyNeutral],\n ] as const) {\n add(`${key} on ${name}`, hex, bg, 4.5, \"WCAG 1.4.3 — these are rendered as TEXT\");\n }\n }\n\n add(\"brandContrast on brand\", t.brandContrast, t.brand, 4.5, \"WCAG 1.4.3 AA body text\");\n // A hovered brand button carries the same label (0.33): the derived dark presets\n // lightened the hover toward their white label and left it at 4.07:1.\n add(\"brandContrast on brandHover\", t.brandContrast, t.brandHover, 4.5, \"WCAG 1.4.3 AA body text\");\n add(\"brandMuted on brandBg\", brandMuted, brandBg, 4.5, \"WCAG 1.4.3 — a soft brand chip's label\");\n // The same label on the chip hovered (§12.11): a current link, a selected day. At 70 %\n // brand the muted text read 4.08–4.42:1 here (Default and Moss light; Ink, Moss, Plum\n // and Contrast dark).\n add(\"brandMuted on brandBgHover\", brandMuted, brandBgHover, 4.5, \"WCAG 1.4.3 — a hovered soft brand chip's label\");\n\n if (semantic) {\n for (const [name, fg] of [\n [\"danger\", semantic.danger],\n [\"warning\", semantic.warning],\n [\"info\", semantic.info],\n [\"success\", semantic.success],\n ] as const) {\n add(`${name} on bgSurface`, fg, t.bgSurface, 4.5, \"WCAG 1.4.3 — rendered as text\");\n add(`${name} on bgHover`, fg, hover, 4.5, \"WCAG 1.4.3 — rendered as text, on a hovered row\");\n // The darker light well the More literals were solved for (§13.3).\n if (step?.well) add(`${name} on bgSurface2`, fg, t.bgSurface2, 4.5, \"WCAG 1.4.3 — rendered as text, on the well\");\n }\n add(\"danger on dangerBg\", semantic.danger, semantic.dangerBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"warning on warningBg\", semantic.warning, semantic.warningBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"info on infoBg\", semantic.info, semantic.infoBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"success on successBg\", semantic.success, semantic.successBg, 4.5, \"WCAG 1.4.3 — badge text\");\n add(\"dangerContrast on danger\", semantic.dangerContrast, semantic.danger, 4.5, \"WCAG 1.4.3 AA body text\");\n }\n\n const failures = checks.filter((c) => !c.passes);\n return { checks, failures, passes: failures.length === 0 };\n}\n\n/* ── Categorical chart ramp ───────────────────────────────────────────────── */\n\nexport interface ChartRampReport {\n colors: string[];\n /** Series pairs that are too close under normal vision or any simulated CVD. */\n collisions: Array<{ a: number; b: number; under: \"normal\" | CvdType; distance: number }>;\n /** Series that do not clear 3:1 against a surface they may be drawn on. */\n lowContrast: Array<{ index: number; surface: string; ratio: number }>;\n /** The worst pairwise separation, across normal vision and all three dichromacies. */\n minSeparation: number;\n /** The worst contrast any series reaches against any surface it was measured on. */\n minContrast: number;\n passes: boolean;\n}\n\n/**\n * The floor for pairwise separation in OKLab, across normal vision and each dichromacy.\n *\n * CALIBRATED, not chosen. Paul Tol's \"Muted\" set — which this package ships and which is\n * a well-regarded CVD-safe palette — bottoms out at 0.059 in its light variant and 0.032\n * in the lightened dark one. A threshold above 0.059 would fail a palette that is known\n * to work, which is worse than having no check at all: an audit that cries wolf gets\n * switched off. 0.05 sits just under the known-good case.\n */\nconst MIN_SEPARATION = 0.05;\n\n/**\n * **The categorical trade-off, measured.** At a SINGLE lightness, the most series that\n * can satisfy both the 3:1 fill contrast of WCAG 1.4.11 and {@link MIN_SEPARATION} under\n * dichromacy is FOUR. Searched exhaustively over starting hue and lightness:\n *\n * n=4 best separation 0.065 — both satisfiable\n * n=5 best separation 0.041 — not satisfiable\n * n=9 best separation 0.015 — not satisfiable\n *\n * The reason is structural: dichromacy collapses the red-green axis, so hue alone stops\n * distinguishing colours once you need more than a handful, and the remaining channel is\n * LIGHTNESS — the very thing a uniform-contrast ramp holds constant. Paul Tol's set\n * varies lightness deliberately and pays for it in contrast; that is not an oversight in\n * their palette, it is the only currency left.\n *\n * So above four series this deriver varies lightness too, and reports what it gave up\n * rather than pretending both constraints were met.\n *\n * NOTE ON WHAT THIS BOUND IS. It describes the PROBLEM, not a guarantee about\n * {@link deriveChartRamp}. That function fixes lightness and starts the hue wheel at the\n * brand's own hue rather than searching for the best start, so it reaches the bound only\n * where the brand happens to sit well: sweeping 72 brand hues against the default light\n * surfaces, 7 of 72 produced a passing 4-series ramp (12 of 72 on dark), and none passed\n * at 5 or 9. Read the report it returns; do not assume a pass.\n */\nconst SINGLE_LIGHTNESS_MAX_SERIES = 4;\n\n/**\n * A categorical ramp of `count` hues, evenly spaced and equal in perceived lightness,\n * then MEASURED for separability — including under each dichromacy.\n *\n * Offered but not used by default, and the distinction matters. Evenly spaced hues at\n * one lightness are a reasonable generic answer; Paul Tol's \"Muted\" set, which this\n * package ships, is a better specific one, because it was optimised against real\n * confusion lines rather than derived from a formula. So this exists for a consumer who\n * wants their charts to follow their brand and is willing to read the report — not as\n * a silent upgrade to the default.\n */\nexport function deriveChartRamp(brandHex: string, mode: ThemeMode, surfaces: string[], count = 9): ChartRampReport {\n const brand = hexToOklch(brandHex) ?? { l: 0.6, c: 0.12, h: 264 };\n const base = mode === \"light\" ? 0.58 : 0.74;\n const c = Math.max(0.09, Math.min(brand.c, 0.15));\n // Up to four series, one lightness: a scale that varies lightness implies rank, and a\n // categorical one must not. Beyond four, lightness is the only channel left that\n // survives dichromacy — see SINGLE_LIGHTNESS_MAX_SERIES — so it alternates in a\n // three-step cycle, which keeps ADJACENT series (the ones a legend puts side by side)\n // at different lightnesses as well as different hues.\n const vary = count > SINGLE_LIGHTNESS_MAX_SERIES;\n const offsets = [0, mode === \"light\" ? -0.1 : 0.1, mode === \"light\" ? 0.08 : -0.08];\n const colors = Array.from({ length: count }, (_, i) =>\n oklchToHex({\n l: vary ? base + offsets[i % offsets.length] : base,\n c,\n h: (brand.h + (360 / count) * i) % 360,\n }),\n );\n\n return measureRamp(colors, surfaces);\n}\n\n/** Audit a ramp somebody else chose — the shipped Paul Tol set, say. */\nexport function auditChartRamp(colors: string[], surfaces: string[]): ChartRampReport {\n return measureRamp(colors, surfaces);\n}\n\nfunction measureRamp(colors: string[], surfaces: string[]): ChartRampReport {\n const collisions: ChartRampReport[\"collisions\"] = [];\n const rgbs = colors.map((hex) => parseHex(hex)).filter((v): v is NonNullable<typeof v> => !!v);\n let minSeparation = Infinity;\n for (let i = 0; i < rgbs.length; i++) {\n for (let j = i + 1; j < rgbs.length; j++) {\n const normal = deltaE(rgbs[i], rgbs[j]);\n minSeparation = Math.min(minSeparation, normal);\n if (normal < MIN_SEPARATION) {\n collisions.push({ a: i, b: j, under: \"normal\", distance: normal });\n }\n for (const type of [\"protanopia\", \"deuteranopia\", \"tritanopia\"] as const) {\n const d = deltaE(simulateCvd(rgbs[i], type), simulateCvd(rgbs[j], type));\n minSeparation = Math.min(minSeparation, d);\n if (d < MIN_SEPARATION) collisions.push({ a: i, b: j, under: type, distance: d });\n }\n }\n }\n\n const lowContrast: ChartRampReport[\"lowContrast\"] = [];\n let minContrast = Infinity;\n colors.forEach((hex, index) => {\n for (const surface of surfaces) {\n const ratio = contrast(hex, surface);\n minContrast = Math.min(minContrast, ratio);\n // WCAG 1.4.11: a graphical object carrying meaning needs 3:1 against what is\n // adjacent to it. A bar the same value as the card behind it has no boundary.\n if (ratio < 3) lowContrast.push({ index, surface, ratio });\n }\n });\n\n return {\n colors,\n collisions,\n lowContrast,\n minSeparation: Number.isFinite(minSeparation) ? minSeparation : 0,\n minContrast: Number.isFinite(minContrast) ? minContrast : 0,\n passes: collisions.length === 0 && lowContrast.length === 0,\n };\n}\n\n/** Convenience: the OKLCH of a hex, for a caller inspecting an anchor. */\nexport function describe(hex: string): Oklch | null {\n const rgb = parseHex(hex);\n return rgb ? rgbToOklch(rgb) : null;\n}\n"],"mappings":";AAGA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,WAAW,cAAc,WAAW,iBAAiB,qBAAqB;AA8GnF,MAAM,WAA4F;AAAA,EAChG,QAAQ,EAAE,KAAK,IAAI,OAAO,GAAG;AAAA,EAC7B,SAAS,EAAE,KAAK,IAAI,OAAO,GAAG;AAAA,EAC9B,SAAS,EAAE,KAAK,KAAK,OAAO,GAAG;AAAA,EAC/B,MAAM,EAAE,KAAK,KAAK,OAAO,GAAG;AAC9B;AAGA,SAAS,OAAO,GAAW,GAAmB;AAC5C,QAAM,IAAI,KAAK,KAAM,IAAI,KAAK,MAAO,GAAG,IAAI;AAC5C,SAAO,IAAI,MAAM,MAAM,IAAI;AAC7B;AAUA,SAAS,SAAS,MAAc,OAAe,OAAyB;AACtE,MAAI,OAAO;AACX,MAAI,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC,GAAG,GAAG;AAChE,WAAS,IAAI,CAAC,OAAO,KAAK,OAAO,KAAK,GAAG;AACvC,UAAM,KAAK,OAAO,IAAI,OAAO;AAC7B,UAAM,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,GAAG;AAE3D,QAAI,MAAM,UAAU,MAAM;AACxB,aAAO;AACP,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAIA,MAAM,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUhB,OAAO,EAAE,MAAM,OAAO,SAAS,OAAO,UAAU,MAAM;AAAA,EACtD,MAAM,EAAE,MAAM,OAAO,SAAS,OAAO,UAAU,MAAM;AACvD;AAWA,MAAM,kBAAkB;AAGxB,MAAM,qBAAqB;AAQ3B,MAAM,kBAAkB;AAGxB,SAAS,UAAU,OAAsB;AACvC,SAAO,KAAK,IAAI,GAAG,MAAM,IAAI,eAAe;AAC9C;AAEA,SAAS,WAAW,KAAa,MAAiB,MAAc,QAAgB;AAC9E,QAAM,IAAI,qBAAqB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC,IAAI;AAChE,QAAM,IAAI,UAAU,IAAI;AACxB,QAAM,SAAS,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,CAAC;AAClD,QAAM,YAAY,WAAW,EAAE,GAAG,EAAE,SAAS,GAAG,GAAG,IAAI,CAAC;AACxD,QAAM,aAAa,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,GAAG,IAAI,CAAC;AAK1D,QAAM,eAAe,IAAI;AAIzB,QAAM,UAAU,CAAC,QAAQ,WAAW,UAAU,EAAE;AAAA,IAAO,CAAC,GAAG,MACzD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IAAI,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IACxG,IACA;AAAA,EACN;AACA,QAAM,UAAU;AAAA,IACd;AAAA,IACA,SAAS,OAAO;AAAA,IAChB;AAAA,IACA;AAAA,IACA,SAAS,UAAU,WAAW;AAAA,EAChC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,WAAW,EAAE,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CAAC;AAAA,EAC5D;AACF;AAGA,SAAS,aAAa,KAAa,QAAgB,QAAgB,SAAiB,MAAyB;AAC3G,QAAM,KAAK,SAAS,OAAO;AAC3B,QAAM,IAAI,eAAe,QAAQ,IAAI,KAAK,QAAQ,SAAS,UAAU,WAAW,SAAS;AACzF,SAAO,WAAW,EAAE,GAAG,GAAG,QAAQ,GAAG,IAAI,CAAC;AAC5C;AAGA,SAAS,WAAW,MAAsB;AACxC,QAAM,QAAQ;AACd,QAAM,OAAO;AACb,SAAO,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,IAAI,IAAI,QAAQ;AACjE;AAGA,SAAS,OAAO,KAAa,QAAgB,SAAiB,MAAyB;AACrF,QAAM,IAAI,WAAW,OAAO;AAG5B,QAAM,IAAI,SAAS,UAAU,EAAE,IAAI,QAAQ,EAAE,IAAI;AACjD,SAAO,WAAW,EAAE,GAAG,GAAG,KAAK,IAAI,SAAS,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC;AACnE;AAEO,SAAS,cAAc,EAAE,SAAS,MAAM,cAAc,MAAM,aAAa,GAAkC;AAChH,QAAM,WAAqB,CAAC;AAC5B,QAAM,QAAQ,WAAW,QAAQ,KAAK;AACtC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,yBAAyB,QAAQ,KAAK;AAAA,IAExC;AAAA,EACF;AAEA,QAAM,UAAU,EAAE,SAAS,IAAI,WAAW,GAAG,OAAO,KAAK,GAAG,aAAa;AAGzE,QAAM,SAAS,UAAU,KAAK;AAC9B,QAAM,WAAW,WAAW,MAAM,GAAG,MAAM,aAAa,MAAM;AAI9D,QAAM,eAAe,CAAC,SAAS,QAAQ,SAAS,WAAW,SAAS,UAAU,EAAE;AAAA,IAAO,CAAC,GAAG,MACzF,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IAAI,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IACxG,IACA;AAAA,EACN;AAGA,QAAM,aAAa,QAAQ;AAC3B,QAAM,cAAc,aAAa,MAAM,GAAG,YAAY,QAAQ,SAAS,cAAc,IAAI;AACzF,QAAM,gBAAgB,aAAa,MAAM,GAAG,YAAY,QAAQ,WAAW,cAAc,IAAI;AAC7F,QAAM,YAAY,aAAa,MAAM,GAAG,YAAY,QAAQ,OAAO,cAAc,IAAI;AAOrF,QAAM,QAAQ,UAAU,EAAE,aAAa,WAAW,SAAS,UAAU,CAAC;AACtE,QAAM,YAAY,CAAC,cAAc,KAAK,EAAE;AAAA,IAAO,CAAC,GAAG,MACjD,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IAAI,SAAS,GAAG,SAAS,UAAU,YAAY,SAAS,IACxG,IACA;AAAA,EACN;AAUA,QAAM,WAAW,gBAAgB,EAAE,aAAa,WAAW,SAAS,UAAU,CAAC;AAC/E,QAAM,cAAc,WAChB,CAAC,cAAc,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAO,SAAS,GAAG,SAAS,IAAI,SAAS,GAAG,SAAS,IAAI,IAAI,CAAE,IACnG;AACJ,QAAM,cAAc,KAAK,IAAI,MAAM,GAAG,IAAI;AAC1C,QAAM,aAAa,SAAS,QAAQ,OAAO,WAAW;AAItD,MAAI,WACF,cAAc,IAAI,MAAM,SAAS,QAAQ,KAAK,CAAE,IAAI,aAAa,MAAM,GAAG,aAAa,GAAG,aAAa,IAAI;AAC7G,MAAI,aAAa,GAAG;AAClB,aAAS;AAAA,MACP,SAAS,QAAQ,KAAK,YAAY,WAAW,QAAQ,CAAC,CAAC,kBAAkB,IAAI,8FAExE,QAAQ;AAAA,IACf;AAAA,EACF;AAOA,QAAM,kBAAkB,CAAC,QAAgB;AACvC,UAAM,OAAO,UAAU,EAAE,OAAO,KAAK,aAAa,WAAW,SAAS,UAAU,CAAC;AACjF,WAAO,KAAK;AAAA,MACV,SAAS,KAAK,YAAY,SAAS;AAAA,MACnC,SAAS,KAAK,YAAY,KAAK,OAAO;AAAA,MACtC,SAAS,KAAK,YAAY,KAAK,YAAY;AAAA;AAAA,MAE3C,WAAW,SAAS,KAAK,YAAY,QAAQ,IAAI;AAAA,IACnD;AAAA,EACF;AACA,WAAS,IAAI,GAAG,IAAI,MAAM,gBAAgB,QAAQ,IAAI,KAAK,KAAK;AAC9D,UAAM,MAAM,WAAW,QAAQ;AAC/B,eAAW,WAAW,EAAE,GAAG,KAAK,GAAG,SAAS,UAAU,IAAI,IAAI,OAAQ,IAAI,IAAI,KAAM,CAAC;AAAA,EACvF;AACA,MAAI,gBAAgB,QAAQ,IAAI,KAAK;AACnC,aAAS;AAAA,MACP,kDAAkD,gBAAgB,QAAQ,EAAE,QAAQ,CAAC,CAAC,kBACrE,IAAI;AAAA,IACvB;AAAA,EACF;AACA,QAAM,WAAW,WAAW,QAAQ;AACpC,QAAM,gBAAgB,WAAW,QAAQ;AACzC,QAAM,aAAa,WAAW;AAAA,IAC5B,GAAG;AAAA;AAAA;AAAA;AAAA,IAIH,GAAG,kBAAkB,YAAY,SAAS,IAAI,OAAO,SAAS,IAAI;AAAA,EACpE,CAAC;AACD,QAAM,UAAU,SAAS,UAAU,aAAa;AAChD,MAAI,UAAU,KAAK;AACjB,aAAS;AAAA,MACP,uCAAuC,QAAQ,QAAQ,CAAC,CAAC;AAAA,IAG3D;AAAA,EACF;AAIA,QAAM,QAAkB,CAAC,SAAS,CAAC;AACnC,QAAM,YAAY,QAAQ,SAAS,WAAW,QAAQ,MAAM,IAAI;AAChE,MAAI,QAAQ,UAAU,CAAC,WAAW;AAChC,aAAS,KAAK,WAAW,QAAQ,MAAM,wCAAwC;AAAA,EACjF;AACA,MAAI,UAAW,OAAM,KAAK,UAAU,CAAC;AAErC,QAAM,cAAc,CAAC;AACrB,QAAM,iBAAiB,CAAC;AACxB,aAAW,QAAQ,CAAC,UAAU,WAAW,WAAW,MAAM,GAAY;AACpE,UAAM,SAAS,QAAQ,IAAI,IAAI,WAAW,QAAQ,IAAI,CAAE,IAAI;AAC5D,QAAI,QAAQ,IAAI,KAAK,CAAC,QAAQ;AAC5B,eAAS,KAAK,GAAG,IAAI,KAAK,QAAQ,IAAI,CAAC,wCAAwC;AAAA,IACjF;AACA,UAAM,OAAO,SAAS,IAAI;AAC1B,UAAM,MAAM,SAAS,OAAO,IAAI,SAAS,KAAK,KAAK,KAAK,OAAO,KAAK;AACpE,QAAI,CAAC,QAAQ;AACX,YAAM,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC,CAAC;AACxD,UAAI,MAAM,IAAI;AACZ,iBAAS;AAAA,UACP,GAAG,IAAI,qBAAqB,IAAI,QAAQ,CAAC,CAAC,uFACI,IAAI;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AACA,gBAAY,IAAI,IAAI;AACpB,mBAAe,IAAI,IAAI,SAAS,KAAK,IAAI,OAAO,GAAG,IAAI,IAAI;AAC3D,UAAM,KAAK,GAAG;AAAA,EAChB;AASA,QAAM,MAAM,CAAC,SAAkD;AAC7D,UAAM,MAAM,YAAY,IAAI;AAC5B,UAAM,SAAS,eAAe,IAAI;AAClC,UAAM,KAAK,OAAO,KAAK,QAAQ,SAAS,WAAW,IAAI;AACvD,UAAM,YAAY,eAAe,KAAK,SAAS,SAAS,GAAI,KAAK,QAAQ,SAAS,UAAU,WAAW,SAAS;AAChH,UAAM,SAAS,eAAe,KAAK,SAAS,EAAE,GAAI,KAAK,QAAQ,SAAS,UAAU,WAAW,SAAS;AACtG,UAAM,IAAI,SAAS,UAAU,KAAK,IAAI,WAAW,MAAM,IAAI,KAAK,IAAI,WAAW,MAAM;AACrF,WAAO,WAAW,EAAE,GAAG,GAAG,QAAQ,GAAG,IAAI,CAAC;AAAA,EAC5C;AAEA,QAAM,SAAS,IAAI,QAAQ;AAC3B,QAAM,YAAY,WAAW,MAAM;AACnC,QAAM,WAA2B;AAAA,IAC/B;AAAA,IACA,aAAa,WAAW;AAAA,MACtB,GAAG;AAAA,MACH,GAAG,SAAS,UAAU,UAAU,IAAI,OAAO,UAAU,IAAI;AAAA,IAC3D,CAAC;AAAA,IACD,gBAAgB,WAAW,MAAM;AAAA;AAAA;AAAA,IAGjC,cAAc,WAAW;AAAA,MACvB,GAAG,SAAS,UAAU,UAAU,IAAI,OAAO;AAAA,MAC3C,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,oBAAoB,WAAW;AAAA,MAC7B,GAAG,SAAS,UAAU,UAAU,IAAI,OAAO,UAAU,IAAI;AAAA,MACzD,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,UAAU,OAAO,YAAY,QAAQ,eAAe,QAAQ,SAAS,WAAW,IAAI;AAAA,IACpF,SAAS,IAAI,SAAS;AAAA,IACtB,eAAe,WAAW;AAAA,MACxB,GAAG,SAAS,UAAU,OAAO;AAAA,MAC7B,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,WAAW,OAAO,YAAY,SAAS,eAAe,SAAS,SAAS,WAAW,IAAI;AAAA,IACvF,MAAM,IAAI,MAAM;AAAA,IAChB,YAAY,WAAW;AAAA,MACrB,GAAG,SAAS,UAAU,MAAM;AAAA,MAC5B,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,QAAQ,OAAO,YAAY,MAAM,eAAe,MAAM,SAAS,WAAW,IAAI;AAAA,IAC9E,SAAS,IAAI,SAAS;AAAA,IACtB,eAAe,WAAW;AAAA,MACxB,GAAG,SAAS,UAAU,OAAO;AAAA,MAC7B,GAAG,eAAe;AAAA,MAClB,GAAG,YAAY;AAAA,IACjB,CAAC;AAAA,IACD,WAAW,OAAO,YAAY,SAAS,eAAe,SAAS,SAAS,WAAW,IAAI;AAAA,EACzF;AAEA,MAAI,WAAW;AACb,UAAM,SAAS,aAAa,UAAU,GAAG,KAAK,IAAI,UAAU,GAAG,IAAI,GAAG,GAAG,cAAc,IAAI;AAC3F,aAAS,SAAS;AAClB,aAAS,iBAAiB,WAAW,MAAM;AAC3C,aAAS,WAAW,OAAO,UAAU,GAAG,KAAK,IAAI,UAAU,GAAG,IAAI,GAAG,SAAS,WAAW,IAAI;AAAA,EAC/F;AAOA,QAAM,QAAQ,CAAC,KAAa,WAAmB,aAAa,KAAK,MAAM,QAAQ,WAAW,IAAI;AAC9F,QAAM,cAAc,MAAM,KAAK,GAAG;AAClC,QAAM,eAAe,MAAM,IAAI,GAAG;AAClC,QAAM,WAAW,MAAM,KAAK,GAAG;AAI/B,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,eAAe;AAAA,IACnB;AAAA,IACA,SAAS,SAAS;AAAA,IAClB,MAAM;AAAA,IACN;AAAA,IACA,SAAS,UAAU,WAAW;AAAA,EAChC;AACA,QAAM,eAAe,WAAW;AAAA,IAC9B,GAAG,SAAS,UAAU,KAAK,IAAI,MAAM,YAAY,IAAI,KAAK,IAAI,MAAM,YAAY;AAAA,IAChF,GAAG;AAAA,IACH,GAAG,MAAM;AAAA,EACX,CAAC;AAED,QAAM,SAAmB;AAAA,IACvB,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,OAAO,CAAC;AAAA,IACR,MAAM,WAAW,aAAa,cAAc,UAAU,IAAI;AAAA,EAC5D;AAMA,QAAM,WAAW,aAAa,QAAQ,QAAQ;AAC9C,QAAM,OAAO,aAAa,QAAQ,QAAW,EAAE,OAAO,OAAO,CAAC;AAC9D,QAAM,SAAS,CAAC,GAAG,SAAS,QAAQ,GAAG,KAAK,MAAM;AAClD,QAAM,WAAW,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM;AAC/C,SAAO,EAAE,QAAQ,UAAU,OAAO,EAAE,QAAQ,UAAU,QAAQ,SAAS,WAAW,EAAE,GAAG,SAAS;AAClG;AASA,SAAS,WAAW,QAAgB,SAAiB,UAAkC,MAA4B;AACjH,QAAM,MAAM,WAAW,OAAO;AAC9B,SAAO;AAAA,IACL,SAAS,SAAS;AAAA,IAClB,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ,WAAW,EAAE,GAAG,SAAS,UAAU,OAAO,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC;AAAA,IAC3E,SAAS;AAAA,IACT,OAAO,SAAS,UAAU,0BAA0B;AAAA,EACtD;AACF;AAoBO,SAAS,aACd,KACA,UACA,UAAqC,CAAC,GACtB;AAChB,QAAM,OAAO,QAAQ,UAAU;AAC/B,QAAM,OAAO,OAAO,aAAa,GAAG,IAAI;AAExC,QAAM,IAAc,OAChB;AAAA,IACE,GAAG;AAAA,IACH,eAAe,KAAK;AAAA,IACpB,WAAW,KAAK;AAAA,IAChB,QAAQ,KAAK;AAAA,IACb,GAAI,KAAK,QAAQ;AAAA,MACf,YAAY,KAAK,KAAK;AAAA,MACtB,aAAa,KAAK,KAAK;AAAA,MACvB,cAAc,KAAK,KAAK;AAAA,MACxB,UAAU,KAAK,KAAK;AAAA,MACpB,cAAc,KAAK,KAAK;AAAA,IAC1B;AAAA,EACF,IACA;AACJ,QAAM,SAAS,OAAO,qBAAqB;AAC3C,QAAM,SAA0B,CAAC;AACjC,QAAM,WAAoC;AAAA,IACxC,CAAC,UAAU,EAAE,MAAM;AAAA,IACnB,CAAC,aAAa,EAAE,SAAS;AAAA,IACzB,CAAC,cAAc,EAAE,UAAU;AAAA,EAC7B;AAIA,QAAM,QAAQ,UAAU,CAAC;AACzB,QAAM,cAAuC,CAAC,GAAG,UAAU,CAAC,WAAW,KAAK,CAAC;AAE7E,QAAM,EAAE,YAAY,SAAS,aAAa,IAAI,UAAU,CAAC;AAOzD,QAAM,QAAQ;AACd,QAAM,MAAM,CAAC,MAAc,IAAY,IAAY,UAAkB,SAAiB;AACpF,UAAM,QAAQ,SAAS,IAAI,EAAE;AAC7B,UAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO;AACxC,WAAO,KAAK,EAAE,MAAM,OAAO,QAAQ,OAAO,UAAU,MAAM,QAAQ,SAAS,WAAW,MAAM,CAAC;AAAA,EAC/F;AAEA,aAAW,CAAC,MAAM,EAAE,KAAK,UAAU;AACjC,QAAI,YAAY,IAAI,IAAI,EAAE,OAAO,IAAI,GAAG,8BAA8B;AACtE,QAAI,KAAM,KAAI,aAAa,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,gEAA2D;AAAA,QAC1G,KAAI,aAAa,IAAI,IAAI,EAAE,QAAQ,IAAI,KAAK,yCAAyC;AAAA,EAC5F;AAEA,MAAI,MAAM,MAAM;AACd;AAAA,MACE;AAAA,MACA,EAAE;AAAA,MACF,EAAE;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,aAAW,CAAC,MAAM,EAAE,KAAK,aAAa;AACpC,QAAI,kBAAkB,IAAI,IAAI,EAAE,aAAa,IAAI,GAAG,0BAA0B;AAC9E,QAAI,oBAAoB,IAAI,IAAI,EAAE,eAAe,IAAI,KAAK,yBAAyB;AACnF,QAAI,gBAAgB,IAAI,IAAI,EAAE,WAAW,IAAI,KAAK,yBAAyB;AAC3E,QAAI,iBAAiB,IAAI,IAAI,YAAY,IAAI,KAAK,qCAAgC;AAClF,eAAW,CAAC,KAAK,GAAG,KAAK;AAAA,MACvB,CAAC,eAAe,EAAE,WAAW;AAAA,MAC7B,CAAC,gBAAgB,EAAE,YAAY;AAAA,MAC/B,CAAC,YAAY,EAAE,QAAQ;AAAA;AAAA;AAAA,MAGvB,CAAC,gBAAgB,EAAE,YAAY;AAAA,IACjC,GAAY;AACV,UAAI,GAAG,GAAG,OAAO,IAAI,IAAI,KAAK,IAAI,KAAK,8CAAyC;AAAA,IAClF;AAAA,EACF;AAEA,MAAI,0BAA0B,EAAE,eAAe,EAAE,OAAO,KAAK,yBAAyB;AAGtF,MAAI,+BAA+B,EAAE,eAAe,EAAE,YAAY,KAAK,yBAAyB;AAChG,MAAI,yBAAyB,YAAY,SAAS,KAAK,6CAAwC;AAI/F,MAAI,8BAA8B,YAAY,cAAc,KAAK,qDAAgD;AAEjH,MAAI,UAAU;AACZ,eAAW,CAAC,MAAM,EAAE,KAAK;AAAA,MACvB,CAAC,UAAU,SAAS,MAAM;AAAA,MAC1B,CAAC,WAAW,SAAS,OAAO;AAAA,MAC5B,CAAC,QAAQ,SAAS,IAAI;AAAA,MACtB,CAAC,WAAW,SAAS,OAAO;AAAA,IAC9B,GAAY;AACV,UAAI,GAAG,IAAI,iBAAiB,IAAI,EAAE,WAAW,KAAK,oCAA+B;AACjF,UAAI,GAAG,IAAI,eAAe,IAAI,OAAO,KAAK,sDAAiD;AAE3F,UAAI,MAAM,KAAM,KAAI,GAAG,IAAI,kBAAkB,IAAI,EAAE,YAAY,KAAK,iDAA4C;AAAA,IAClH;AACA,QAAI,sBAAsB,SAAS,QAAQ,SAAS,UAAU,KAAK,8BAAyB;AAC5F,QAAI,wBAAwB,SAAS,SAAS,SAAS,WAAW,KAAK,8BAAyB;AAChG,QAAI,kBAAkB,SAAS,MAAM,SAAS,QAAQ,KAAK,8BAAyB;AACpF,QAAI,wBAAwB,SAAS,SAAS,SAAS,WAAW,KAAK,8BAAyB;AAChG,QAAI,4BAA4B,SAAS,gBAAgB,SAAS,QAAQ,KAAK,yBAAyB;AAAA,EAC1G;AAEA,QAAM,WAAW,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM;AAC/C,SAAO,EAAE,QAAQ,UAAU,QAAQ,SAAS,WAAW,EAAE;AAC3D;AA0BA,MAAM,iBAAiB;AA2BvB,MAAM,8BAA8B;AAa7B,SAAS,gBAAgB,UAAkB,MAAiB,UAAoB,QAAQ,GAAoB;AACjH,QAAM,QAAQ,WAAW,QAAQ,KAAK,EAAE,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI;AAChE,QAAM,OAAO,SAAS,UAAU,OAAO;AACvC,QAAM,IAAI,KAAK,IAAI,MAAM,KAAK,IAAI,MAAM,GAAG,IAAI,CAAC;AAMhD,QAAM,OAAO,QAAQ;AACrB,QAAM,UAAU,CAAC,GAAG,SAAS,UAAU,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK;AAClF,QAAM,SAAS,MAAM;AAAA,IAAK,EAAE,QAAQ,MAAM;AAAA,IAAG,CAAC,GAAG,MAC/C,WAAW;AAAA,MACT,GAAG,OAAO,OAAO,QAAQ,IAAI,QAAQ,MAAM,IAAI;AAAA,MAC/C;AAAA,MACA,IAAI,MAAM,IAAK,MAAM,QAAS,KAAK;AAAA,IACrC,CAAC;AAAA,EACH;AAEA,SAAO,YAAY,QAAQ,QAAQ;AACrC;AAGO,SAAS,eAAe,QAAkB,UAAqC;AACpF,SAAO,YAAY,QAAQ,QAAQ;AACrC;AAEA,SAAS,YAAY,QAAkB,UAAqC;AAC1E,QAAM,aAA4C,CAAC;AACnD,QAAM,OAAO,OAAO,IAAI,CAAC,QAAQ,SAAS,GAAG,CAAC,EAAE,OAAO,CAAC,MAAkC,CAAC,CAAC,CAAC;AAC7F,MAAI,gBAAgB;AACpB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,aAAS,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACxC,YAAM,SAAS,OAAO,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;AACtC,sBAAgB,KAAK,IAAI,eAAe,MAAM;AAC9C,UAAI,SAAS,gBAAgB;AAC3B,mBAAW,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,UAAU,UAAU,OAAO,CAAC;AAAA,MACnE;AACA,iBAAW,QAAQ,CAAC,cAAc,gBAAgB,YAAY,GAAY;AACxE,cAAM,IAAI,OAAO,YAAY,KAAK,CAAC,GAAG,IAAI,GAAG,YAAY,KAAK,CAAC,GAAG,IAAI,CAAC;AACvE,wBAAgB,KAAK,IAAI,eAAe,CAAC;AACzC,YAAI,IAAI,eAAgB,YAAW,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,OAAO,MAAM,UAAU,EAAE,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAA8C,CAAC;AACrD,MAAI,cAAc;AAClB,SAAO,QAAQ,CAAC,KAAK,UAAU;AAC7B,eAAW,WAAW,UAAU;AAC9B,YAAM,QAAQ,SAAS,KAAK,OAAO;AACnC,oBAAc,KAAK,IAAI,aAAa,KAAK;AAGzC,UAAI,QAAQ,EAAG,aAAY,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC;AAAA,IAC3D;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,OAAO,SAAS,aAAa,IAAI,gBAAgB;AAAA,IAChE,aAAa,OAAO,SAAS,WAAW,IAAI,cAAc;AAAA,IAC1D,QAAQ,WAAW,WAAW,KAAK,YAAY,WAAW;AAAA,EAC5D;AACF;AAGO,SAAS,SAAS,KAA2B;AAClD,QAAM,MAAM,SAAS,GAAG;AACxB,SAAO,MAAM,WAAW,GAAG,IAAI;AACjC;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/theme/palette-presets.ts"],"sourcesContent":["// Appearance presets — the single source of truth for the \"palette switcher\"\n// (feedback #307). Each preset is a full set of design-token values for BOTH\n// light and dark, covering the page/surface backgrounds, the brand accent, the\n// semantic money trio and the categorical chart palette (anyplot's eight \"Imprint\"\n// hues by default since 0.49, Paul Tol's nine in `PAUL_TOL_PRESET`), plus the\n// heatmap stops. A preset is applied at runtime by writing these as CSS custom\n// properties onto <html> (see palette-store.ts), which instantly re-skins every\n// token-driven surface and chart. The two heatmaps and the treemap interpolate\n// concrete hex in JS, so they read the same values from here via the hooks in\n// palette-store.\n//\n// The default preset here mirrors the :root/.dark blocks in tokens.css exactly. The\n// five hand-picked alternatives of #307 (`ALTERNATIVE_PRESETS`, a reference bank that\n// `presetById` never resolved) were removed in 0.35 (refactor audit 2026-10, API-6c;\n// deprecated in 0.34): `PALETTES` is what an app offers, and `derivePalette` makes a\n// palette of its own.\n\nimport type { HeatStops } from \"./chart-palette\";\nimport { contrastStep, rememberTokenSet } from \"./contrast-tokens\";\n\nexport interface TokenSet {\n // Backgrounds & chrome\n bgPage: string;\n bgSurface: string;\n bgSurface2: string;\n border: string;\n textPrimary: string;\n // The two quieter text roles. Derived per preset by blending `textPrimary` toward\n // that preset's own surfaces until the contrast against the WORST of bgPage /\n // bgSurface / bgSurface2 hits 7:1 and 4.8:1 — so both clear WCAG AA for normal text\n // (4.5:1) on every surface they can land on, and both inherit the preset's warmth\n // instead of importing a cool grey from Tailwind's palette.\n textSecondary: string;\n textMuted: string;\n // Interactive brand accent\n brand: string;\n brandHover: string;\n brandContrast: string;\n // Semantic money (never red/green — CVD-safe, paired with +/- cues elsewhere)\n moneyIncome: string;\n moneyExpense: string;\n moneyNet: string;\n moneyNeutral: string;\n // Categorical chart series, written as `--chart-1…9`. anyplot's eight \"Imprint\" hues by\n // default (0.49), Paul Tol \"Muted\"'s nine in `PAUL_TOL_PRESET`. `paletteFor` cycles\n // eight; a shorter ramp is repeated to fill `--chart-9` (see `applyTokenSet`).\n chart: string[];\n // Heatmap interpolation stops (variance diverging + spending sequential)\n heat: HeatStops;\n}\n\nexport interface PalettePreset {\n id: string;\n name: string;\n blurb: string;\n light: TokenSet;\n dark: TokenSet;\n}\n\n// ── \"Imprint\" chart hues (feedback #119; the default since 0.49) ─────────────\n// anyplot's categorical palette (https://anyplot.ai/palette), by a colleague of\n// Marcel's. 8 hues + an amber semantic anchor, derived from Wong 2011 by a\n// Petroff-style max-min ΔE search in CAM02-UCS with Machado 2009 CVD simulation —\n// the same lineage as Paul Tol \"Muted\", one generation on, and tuned for exactly the\n// warm cream paper our light theme uses.\n//\n// 0.49 (Marcel, 2026-10-22): the DEFAULT, in anyplot's slot order, eight slots — amber\n// #DDCC77 left the pool, it is anyplot's semantic warning anchor, not a series colour.\n// The SAME hues in both themes: 0.48 lifted matte red and rose for the dark ground\n// (#BF413E, #A25083), which made the dark chart a different palette from the light one.\n// anyplot answers a hue under 3:1 with a 1px ink edge instead, and so does the kit now\n// (`chartEdge`, drawn by SeriesChart's bars and areas and by Treemap's tiles).\n//\n// Measured on our surfaces (CIEDE2000, Machado severity-100, 8 like-for-like slots on\n// the cream light surface #f6ecd9) — neither palette dominates:\n// Paul Tol Muted min-contrast 1.38 · worst-CVD ΔE00 11.8 · normal ΔE00 16.1\n// imprint min-contrast 1.95 · worst-CVD ΔE00 7.2 · normal ΔE00 22.2\n// i.e. imprint is punchier and has no near-invisible pale slot (Tol's #88ccee sits at\n// 1.5:1 on cream), while Tol separates better under colour-vision deficiency — which is\n// why `paletteFor` offers anyplot's CVD-optimal order (`{ order: \"cvd\" }`).\nconst IMPRINT_HUES = [\n \"#009e73\", // brand green — first series, always\n \"#c475fd\", // lavender — creative\n \"#4467a3\", // blue — cool / info\n \"#bd8233\", // ochre — earth / commodity\n \"#ae3030\", // matte red — bad / loss / error\n \"#2abccd\", // cyan — sky / tech-cool\n \"#954477\", // rose — wellness / health\n \"#99b314\", // lime — growth / nature\n];\n\n/** anyplot's scales (0.49): sequential green → blue, diverging blue (under) ↔ paper or\n * ink (0) ↔ matte red (over). `chart-palette.ts`'s `HEATMAP_HEX` holds the same. */\nconst ANYPLOT_HEAT: Record<\"light\" | \"dark\", HeatStops> = {\n light: {\n neutral: \"#faf8f1\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#1a1a17\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n};\n\n// The shipped default (feedback #328): SOLAR-DUSK warm light + INDIGO-NOIR deep\n// dark surfaces — the two curated presets the user picked per mode — but with a\n// SINGLE unified brand accent (indigo) across both modes rather than each\n// preset's own accent (the user asked for the brand accent to stay the same).\n// So the light block is Solar Dusk with its teal brand overridden to indigo, and\n// the dark block is Indigo Noir (already indigo). The money trio\n// (income/expense/net) drives the chart bars and stays palette-driven so those\n// semantic colours adapt to each mode's contrast (best practice — feedback #328).\n//\n// 0.49: its chart ramp and heat stops are anyplot's (\"Imprint\", above) — the money\n// trio stays the kit's own (Marcel, 2026-10-22: chart income/expense/net/neutral keep\n// the money tokens, never anyplot's green/red). The 0.48 default, the same surfaces\n// with Paul Tol's ramp, is `PAUL_TOL_PRESET`.\nexport const DEFAULT_PRESET: PalettePreset = {\n id: \"default\",\n name: \"Imprint\",\n blurb: \"anyplot.ai chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n // Solar Dusk (light) surfaces, with the brand unified to indigo (see above).\n bgPage: \"#ede3cf\",\n bgSurface: \"#f6ecd9\",\n // The well is the page in light (0.33, k26): #f3ead6 stood 1.02:1 from the card it\n // is inset in. Every light preset does the same; see SURFACE_L in palette-derive.ts.\n bgSurface2: \"#ede3cf\",\n border: \"#e2d6bd\",\n textPrimary: \"#463f33\",\n textSecondary: \"#50493c\",\n textMuted: \"#696153\",\n brand: \"#4f46e5\",\n brandHover: \"#4338ca\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#0b6650\",\n moneyExpense: \"#905902\",\n moneyNet: \"#7a4fa3\",\n moneyNeutral: \"#6b6453\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.light,\n },\n dark: {\n // Indigo Noir (dark)\n bgPage: \"#0e0f1c\",\n bgSurface: \"#181a2c\",\n bgSurface2: \"#21243a\",\n border: \"#2e3252\",\n textPrimary: \"#e6e8f4\",\n textSecondary: \"#adafbe\",\n textMuted: \"#8d90a0\",\n brand: \"#8b93f8\",\n brandHover: \"#aab1fb\",\n brandContrast: \"#16182b\",\n moneyIncome: \"#22c3b6\",\n // Amber-400 (#fbbf24) sat at OKLab L 0.84 while every other dark token lives\n // at 0.68–0.74, so outflows outshouted the inflows they are paired with. Same\n // hue and chroma, pulled to L 0.785 (dev#434). The heat stops below keep the\n // brighter amber on purpose: a gradient endpoint is meant to be the loudest\n // thing in its scale, and it is a fill, not a figure.\n moneyExpense: \"#e4b035\",\n moneyNet: \"#b39aef\",\n moneyNeutral: \"#9aa1bd\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.dark,\n },\n};\n\n\n/**\n * The 0.48 default: the same surfaces, brand and money, with Paul Tol's \"Muted\" nine\n * as the chart ramp and the teal/amber heat stops that went with it. Selectable, no\n * longer the default (0.49). `paletteFor` cycles eight, so its ninth hue (#aa4499) is\n * reachable only as `var(--chart-9)`.\n */\nexport const PAUL_TOL_PRESET: PalettePreset = {\n id: \"paul-tol\",\n name: \"Paul Tol muted\",\n blurb: \"Paul Tol's CVD-safe chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n ...DEFAULT_PRESET.light,\n chart: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n heat: {\n neutral: \"#f1e7d2\",\n under: \"#0b6650\",\n over: \"#9c6418\",\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n },\n dark: {\n ...DEFAULT_PRESET.dark,\n chart: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n heat: {\n neutral: \"#363a5a\",\n under: \"#22c3b6\",\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\",\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n },\n};\n\n// The full-theme switcher from #307 stayed retired (feedback #328) — the default\n// is still the single source of truth for surfaces, brand and money (its five\n// alternatives are gone, see the header). What IS selectable is\n// the categorical chart ramp (feedback #119), so Imprint (the default) and Paul Tol are\n// the live set; presetById falls back to the default for any stale persisted id.\n/**\n * The categorical chart ramp, shared by every derived preset below.\n *\n * The default's ramp (anyplot's eight since 0.49, Paul Tol's \"Muted\" before), and\n * deliberately NOT re-derived per preset. A\n * categorical ramp solves a different problem from a UI palette — separability among\n * nine unknown siblings, rather than contrast against one known surface — and a set\n * optimised against real dichromat confusion lines beats one spaced evenly by formula.\n * `auditChartRamp` in palette-derive.ts measures it: 0.059 minimum separation in the\n * light variant, which is the number MIN_SEPARATION was calibrated under.\n *\n * It pays for that in contrast: four of the nine light hues sit below the 3:1 WCAG\n * 1.4.11 asks of a filled shape. That is not fixable by darkening them — see\n * SINGLE_LIGHTNESS_MAX_SERIES — so a chart with more than four series must carry a\n * second channel (a direct label, a pattern, a legend adjacent to the mark).\n */\nconst KIT_RAMP = {\n light: DEFAULT_PRESET.light.chart,\n dark: DEFAULT_PRESET.dark.chart,\n} as const;\n\n/**\n * Presets DERIVED rather than picked, by `derivePalette` from one brand colour each.\n *\n * Every text role here was solved to a contrast target against the worst of the three\n * surfaces it can land on, every semantic colour against both the page and its own badge\n * tint, and the whole set is re-checked by `__tests__/palette-audit.test.ts` (\"derived\n * presets\") — which re-runs the deriver and fails if these committed values drift from\n * what it now produces.\n *\n * They are committed rather than derived at import because deriving eight token sets\n * costs ~34ms, and charging every consumer that at startup for palettes most of them\n * will never select is the wrong trade. The test is what keeps the two in agreement.\n *\n * Regenerated in 0.34 (round 0.34 BUG-8): the solve now measures the hex it ships rather\n * than the float before rounding, and the audit no longer lets a hex pair pass 0.05 under\n * its threshold. 52 values moved one step (66 counting the heat stops that repeat them);\n * Contrast's neutrals lost half their slate tint, its brand having half the chroma at\n * which the neutrals carry a hue in full.\n */\nexport const DERIVED_PRESETS: PalettePreset[] = [\n {\n id: \"ink\",\n name: \"Ink\",\n blurb: \"Cool blue-grey ground · solved for AAA body text\",\n light: {\n bgPage: \"#dce2ef\",\n bgSurface: \"#e6edfa\",\n bgSurface2: \"#dce2ef\",\n border: \"#b0bace\",\n textPrimary: \"#2e3139\",\n textSecondary: \"#454850\",\n textMuted: \"#5f636c\",\n brand: \"#2f5fd0\",\n brandHover: \"#1f4cbc\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#61646c\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#dce2ef\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#090d15\",\n bgSurface: \"#131720\",\n bgSurface2: \"#1d212b\",\n border: \"#363e4f\",\n textPrimary: \"#c8ccd6\",\n textSecondary: \"#a7abb5\",\n textMuted: \"#858a92\",\n brand: \"#3466d7\",\n brandHover: \"#2353c3\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a1a5ac\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1d212b\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"moss\",\n name: \"Moss\",\n blurb: \"Green ground · solved for AAA body text\",\n light: {\n bgPage: \"#d8e6dd\",\n bgSurface: \"#e3f1e8\",\n bgSurface2: \"#d8e6dd\",\n border: \"#abbfb2\",\n textPrimary: \"#2c332e\",\n textSecondary: \"#424b45\",\n textMuted: \"#5d6660\",\n brand: \"#2f7a52\",\n brandHover: \"#196842\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#5f6662\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#d8e6dd\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#060f0a\",\n bgSurface: \"#101a14\",\n bgSurface2: \"#1a241e\",\n border: \"#314337\",\n textPrimary: \"#c6cfc9\",\n textSecondary: \"#a5aea8\",\n textMuted: \"#838c86\",\n brand: \"#317b53\",\n brandHover: \"#1c6943\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b87c2a\",\n moneyNet: \"#977ac9\",\n moneyNeutral: \"#9fa7a2\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1a241e\",\n under: \"#009a80\",\n over: \"#b87c2a\",\n seqLow: \"#331f05\",\n seqHigh: \"#b87c2a\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"plum\",\n name: \"Plum\",\n blurb: \"Violet ground · solved for AAA body text\",\n light: {\n bgPage: \"#e7dfea\",\n bgSurface: \"#f2e9f5\",\n bgSurface2: \"#e7dfea\",\n border: \"#c2b5c6\",\n textPrimary: \"#352f36\",\n textSecondary: \"#4c464e\",\n textMuted: \"#676169\",\n brand: \"#7b3f8f\",\n brandHover: \"#692d7d\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7355a2\",\n moneyNeutral: \"#676269\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e7dfea\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#100b12\",\n bgSurface: \"#1b151d\",\n bgSurface2: \"#251f27\",\n border: \"#453b48\",\n textPrimary: \"#d0cad2\",\n textSecondary: \"#afa9b1\",\n textMuted: \"#8d8790\",\n brand: \"#9154a6\",\n brandHover: \"#7f4293\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009980\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a8a2aa\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#251f27\",\n under: \"#009980\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"contrast\",\n name: \"High contrast\",\n blurb: \"Near-neutral ground · text solved to 13:1 / 8.5:1 / 5.5:1\",\n light: {\n bgPage: \"#e1e2e3\",\n bgSurface: \"#ecedee\",\n bgSurface2: \"#e1e2e3\",\n border: \"#b8babc\",\n textPrimary: \"#1b1d21\",\n textSecondary: \"#3a3c40\",\n textMuted: \"#55585c\",\n brand: \"#1f2937\",\n brandHover: \"#111b28\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7255a1\",\n moneyNeutral: \"#616467\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e1e2e3\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#0c0d0e\",\n bgSurface: \"#171718\",\n bgSurface2: \"#212122\",\n border: \"#3d3e3f\",\n textPrimary: \"#e4e8ec\",\n textSecondary: \"#b9bdc1\",\n textMuted: \"#95979c\",\n brand: \"#556c8e\",\n brandHover: \"#455b7c\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a2a5a8\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#212122\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n];\n\nexport const PALETTES: PalettePreset[] = [DEFAULT_PRESET, PAUL_TOL_PRESET, ...DERIVED_PRESETS];\n\nexport function presetById(id: string): PalettePreset {\n return PALETTES.find((p) => p.id === id) ?? DEFAULT_PRESET;\n}\n\n/**\n * Write a token set onto an element's inline style as CSS custom properties.\n *\n * This is the only writer, and there is no un-writer: `presetById` falls back to\n * `DEFAULT_PRESET` and this then writes the full set, so returning to the default\n * APPLIES it rather than clearing anything. A `clearTokenSet` and the hand-kept\n * 24-name `TOKEN_VARS` list it looped over used to live here, both exported, both\n * called by nobody in either consumer — a second copy of these property names with\n * nothing comparing the two, so adding a token here and forgetting it there would\n * have been silent and only a dead function would have noticed. The list is gone;\n * the test beside this asserts instead that every field of a `TokenSet` reaches the\n * element, which is what that list was standing in for.\n *\n * **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`\n * the text and border tokens are written one step stronger — in a light set the well and\n * the four money colours too (0.36, docs/colour-roles-harmonization.md §13) — and `--text-placeholder` —\n * otherwise derived in tokens.css — is written inline too; without it that inline\n * placeholder is removed again, so the stylesheet's formula applies. The step is made\n * HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.\n *\n * `contrast` defaults to the element's own `data-contrast=\"more\"`, which the contrast\n * layer (`applyContrast`, `useApplyContrast`) sets first. That is how the palette and the\n * contrast cooperate without knowing each other: a palette change re-applies through\n * here and keeps the contrast, and a contrast change re-applies the set remembered here.\n */\nexport function applyTokenSet(\n el: HTMLElement,\n t: TokenSet,\n contrast: boolean = el.getAttribute(\"data-contrast\") === \"more\",\n): void {\n rememberTokenSet(el, t);\n const step = contrast ? contrastStep(t) : null;\n el.style.setProperty(\"--bg-page\", t.bgPage);\n el.style.setProperty(\"--bg-surface\", t.bgSurface);\n // The light well and money step under More contrast (0.36, colour-roles §13); in dark\n // and at Standard `well` is null and the set's own are written.\n el.style.setProperty(\"--bg-surface-2\", step?.well?.bgSurface2 ?? t.bgSurface2);\n el.style.setProperty(\"--border\", step?.border ?? t.border);\n el.style.setProperty(\"--text-primary\", t.textPrimary);\n el.style.setProperty(\"--text-secondary\", step?.textSecondary ?? t.textSecondary);\n el.style.setProperty(\"--text-muted\", step?.textMuted ?? t.textMuted);\n if (step) el.style.setProperty(\"--text-placeholder\", step.textPlaceholder);\n else el.style.removeProperty(\"--text-placeholder\");\n el.style.setProperty(\"--brand\", t.brand);\n el.style.setProperty(\"--brand-hover\", t.brandHover);\n el.style.setProperty(\"--brand-contrast\", t.brandContrast);\n el.style.setProperty(\"--money-income\", step?.well?.moneyIncome ?? t.moneyIncome);\n el.style.setProperty(\"--money-expense\", step?.well?.moneyExpense ?? t.moneyExpense);\n el.style.setProperty(\"--money-net\", step?.well?.moneyNet ?? t.moneyNet);\n el.style.setProperty(\"--money-neutral\", step?.well?.moneyNeutral ?? t.moneyNeutral);\n // All nine `--chart-N`, a shorter ramp repeated: Imprint's eight write `--chart-9` as\n // slot 0 — what `paletteFor(8)` names — so a host addressing `var(--chart-9)` directly\n // follows the cycle, and switching away from Paul Tol's nine leaves no stale ninth hue.\n // An empty ramp (a `derivePalette` set) leaves whatever was there, deliberately.\n if (t.chart.length > 0) {\n for (let i = 0; i < 9; i++) el.style.setProperty(`--chart-${i + 1}`, t.chart[i % t.chart.length]);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/theme/palette-presets.ts"],"sourcesContent":["// Appearance presets — the single source of truth for the \"palette switcher\"\n// (feedback #307). Each preset is a full set of design-token values for BOTH\n// light and dark, covering the page/surface backgrounds, the brand accent, the\n// semantic money trio and the categorical chart palette (anyplot's eight \"Imprint\"\n// hues by default since 0.49, Paul Tol's nine in `PAUL_TOL_PRESET`), plus the\n// heatmap stops. A preset is applied at runtime by writing these as CSS custom\n// properties onto <html> (see palette-store.ts), which instantly re-skins every\n// token-driven surface and chart. The two heatmaps and the treemap interpolate\n// concrete hex in JS, so they read the same values from here via the hooks in\n// palette-store.\n//\n// The default preset here mirrors the :root/.dark blocks in tokens.css exactly. The\n// five hand-picked alternatives of #307 (`ALTERNATIVE_PRESETS`, a reference bank that\n// `presetById` never resolved) were removed in 0.35 (refactor audit 2026-10, API-6c;\n// deprecated in 0.34): `PALETTES` is what an app offers, and `derivePalette` makes a\n// palette of its own.\n\nimport type { HeatStops } from \"./chart-palette\";\nimport { contrastStep, rememberTokenSet } from \"./contrast-tokens\";\n\nexport interface TokenSet {\n // Backgrounds & chrome\n bgPage: string;\n bgSurface: string;\n bgSurface2: string;\n border: string;\n textPrimary: string;\n // The two quieter text roles. Derived per preset by blending `textPrimary` toward\n // that preset's own surfaces until the contrast against the WORST of bgPage /\n // bgSurface / bgSurface2 hits 7:1 and 4.8:1 — so both clear WCAG AA for normal text\n // (4.5:1) on every surface they can land on, and both inherit the preset's warmth\n // instead of importing a cool grey from Tailwind's palette.\n textSecondary: string;\n textMuted: string;\n // Interactive brand accent\n brand: string;\n brandHover: string;\n brandContrast: string;\n // Semantic money (never red/green — CVD-safe, paired with +/- cues elsewhere)\n moneyIncome: string;\n moneyExpense: string;\n moneyNet: string;\n moneyNeutral: string;\n // Categorical chart series, written as `--chart-1…9`. anyplot's eight \"Imprint\" hues by\n // default (0.49), Paul Tol \"Muted\"'s nine in `PAUL_TOL_PRESET`. `paletteFor` cycles\n // eight; a shorter ramp is repeated to fill `--chart-9` (see `applyTokenSet`).\n chart: string[];\n // Heatmap interpolation stops (variance diverging + spending sequential)\n heat: HeatStops;\n}\n\nexport interface PalettePreset {\n id: string;\n name: string;\n blurb: string;\n light: TokenSet;\n dark: TokenSet;\n}\n\n// ── \"Imprint\" chart hues (feedback #119; the default since 0.49) ─────────────\n// anyplot's categorical palette (https://anyplot.ai/palette), by a colleague of\n// Marcel's. 8 hues + an amber semantic anchor, derived from Wong 2011 by a\n// Petroff-style max-min ΔE search in CAM02-UCS with Machado 2009 CVD simulation —\n// the same lineage as Paul Tol \"Muted\", one generation on, and tuned for exactly the\n// warm cream paper our light theme uses.\n//\n// 0.49 (Marcel, 2026-10-22): the DEFAULT, in anyplot's slot order, eight slots — amber\n// #DDCC77 left the pool, it is anyplot's semantic warning anchor, not a series colour.\n// The SAME hues in both themes: 0.48 lifted matte red and rose for the dark ground\n// (#BF413E, #A25083), which made the dark chart a different palette from the light one.\n// anyplot answers a hue under 3:1 with a 1px ink edge instead, and so does the kit now\n// (`chartEdge`, drawn by SeriesChart's bars and areas and by Treemap's tiles).\n//\n// Measured on our surfaces (CIEDE2000, Machado severity-100, 8 like-for-like slots on\n// the cream light surface #f6ecd9) — neither palette dominates:\n// Paul Tol Muted min-contrast 1.38 · worst-CVD ΔE00 11.8 · normal ΔE00 16.1\n// imprint min-contrast 1.95 · worst-CVD ΔE00 7.2 · normal ΔE00 22.2\n// i.e. imprint is punchier and has no near-invisible pale slot (Tol's #88ccee sits at\n// 1.5:1 on cream), while Tol separates better under colour-vision deficiency — which is\n// why `paletteFor` offers anyplot's CVD-optimal order (`{ order: \"cvd\" }`).\nconst IMPRINT_HUES = [\n \"#009e73\", // brand green — first series, always\n \"#c475fd\", // lavender — creative\n \"#4467a3\", // blue — cool / info\n \"#bd8233\", // ochre — earth / commodity\n \"#ae3030\", // matte red — bad / loss / error\n \"#2abccd\", // cyan — sky / tech-cool\n \"#954477\", // rose — wellness / health\n \"#99b314\", // lime — growth / nature\n];\n\n/** anyplot's scales (0.49): sequential green → blue, diverging blue (under) ↔ paper or\n * ink (0) ↔ matte red (over). `chart-palette.ts`'s `HEATMAP_HEX` holds the same. */\nconst ANYPLOT_HEAT: Record<\"light\" | \"dark\", HeatStops> = {\n light: {\n neutral: \"#faf8f1\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#1a1a17\",\n under: \"#4467a3\",\n over: \"#ae3030\",\n seqLow: \"#009e73\",\n seqHigh: \"#4467a3\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n};\n\n// The shipped default (feedback #328): SOLAR-DUSK warm light + INDIGO-NOIR deep\n// dark surfaces — the two curated presets the user picked per mode — but with a\n// SINGLE unified brand accent (indigo) across both modes rather than each\n// preset's own accent (the user asked for the brand accent to stay the same).\n// So the light block is Solar Dusk with its teal brand overridden to indigo, and\n// the dark block is Indigo Noir (already indigo). The money trio\n// (income/expense/net) drives the chart bars and stays palette-driven so those\n// semantic colours adapt to each mode's contrast (best practice — feedback #328).\n//\n// 0.49: its chart ramp and heat stops are anyplot's (\"Imprint\", above) — the money\n// trio stays the kit's own (Marcel, 2026-10-22: chart income/expense/net/neutral keep\n// the money tokens, never anyplot's green/red). The 0.48 default, the same surfaces\n// with Paul Tol's ramp, is `PAUL_TOL_PRESET`.\nexport const DEFAULT_PRESET: PalettePreset = {\n id: \"default\",\n name: \"Imprint\",\n blurb: \"anyplot.ai chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n // Solar Dusk (light) surfaces, with the brand unified to indigo (see above).\n bgPage: \"#ede3cf\",\n bgSurface: \"#f6ecd9\",\n // The well is the page in light (0.33, k26): #f3ead6 stood 1.02:1 from the card it\n // is inset in. Every light preset does the same; see SURFACE_L in palette-derive.ts.\n bgSurface2: \"#ede3cf\",\n border: \"#e2d6bd\",\n textPrimary: \"#463f33\",\n textSecondary: \"#50493c\",\n textMuted: \"#696153\",\n brand: \"#4f46e5\",\n brandHover: \"#4338ca\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#0b6650\",\n moneyExpense: \"#905902\",\n moneyNet: \"#7a4fa3\",\n moneyNeutral: \"#6b6453\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.light,\n },\n dark: {\n // Indigo Noir (dark)\n bgPage: \"#0e0f1c\",\n bgSurface: \"#181a2c\",\n bgSurface2: \"#21243a\",\n border: \"#2e3252\",\n textPrimary: \"#e6e8f4\",\n textSecondary: \"#adafbe\",\n textMuted: \"#8d90a0\",\n brand: \"#8b93f8\",\n brandHover: \"#aab1fb\",\n brandContrast: \"#16182b\",\n moneyIncome: \"#22c3b6\",\n // Amber-400 (#fbbf24) sat at OKLab L 0.84 while every other dark token lives\n // at 0.68–0.74, so outflows outshouted the inflows they are paired with. Same\n // hue and chroma, pulled to L 0.785 (dev#434). The heat stops below keep the\n // brighter amber on purpose: a gradient endpoint is meant to be the loudest\n // thing in its scale, and it is a fill, not a figure.\n moneyExpense: \"#e4b035\",\n moneyNet: \"#b39aef\",\n moneyNeutral: \"#9aa1bd\",\n chart: IMPRINT_HUES,\n heat: ANYPLOT_HEAT.dark,\n },\n};\n\n/**\n * The 0.48 default: the same surfaces, brand and money, with Paul Tol's \"Muted\" nine\n * as the chart ramp and the teal/amber heat stops that went with it. Selectable, no\n * longer the default (0.49). `paletteFor` cycles eight, so its ninth hue (#aa4499) is\n * reachable only as `var(--chart-9)`.\n */\nexport const PAUL_TOL_PRESET: PalettePreset = {\n id: \"paul-tol\",\n name: \"Paul Tol muted\",\n blurb: \"Paul Tol's CVD-safe chart hues · solar-dusk light · indigo-noir dark\",\n light: {\n ...DEFAULT_PRESET.light,\n chart: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n heat: {\n neutral: \"#f1e7d2\",\n under: \"#0b6650\",\n over: \"#9c6418\",\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n },\n dark: {\n ...DEFAULT_PRESET.dark,\n chart: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n heat: {\n neutral: \"#363a5a\",\n under: \"#22c3b6\",\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\",\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n },\n};\n\n// The full-theme switcher from #307 stayed retired (feedback #328) — the default\n// is still the single source of truth for surfaces, brand and money (its five\n// alternatives are gone, see the header). What IS selectable is\n// the categorical chart ramp (feedback #119), so Imprint (the default) and Paul Tol are\n// the live set; presetById falls back to the default for any stale persisted id.\n/**\n * The categorical chart ramp, shared by every derived preset below.\n *\n * The default's ramp (anyplot's eight since 0.49, Paul Tol's \"Muted\" before), and\n * deliberately NOT re-derived per preset. A\n * categorical ramp solves a different problem from a UI palette — separability among\n * nine unknown siblings, rather than contrast against one known surface — and a set\n * optimised against real dichromat confusion lines beats one spaced evenly by formula.\n * `auditChartRamp` in palette-derive.ts measures it: 0.059 minimum separation in the\n * light variant, which is the number MIN_SEPARATION was calibrated under.\n *\n * It pays for that in contrast: four of the nine light hues sit below the 3:1 WCAG\n * 1.4.11 asks of a filled shape. That is not fixable by darkening them — see\n * SINGLE_LIGHTNESS_MAX_SERIES — so a chart with more than four series must carry a\n * second channel (a direct label, a pattern, a legend adjacent to the mark).\n */\nconst KIT_RAMP = {\n light: DEFAULT_PRESET.light.chart,\n dark: DEFAULT_PRESET.dark.chart,\n} as const;\n\n/**\n * Presets DERIVED rather than picked, by `derivePalette` from one brand colour each.\n *\n * Every text role here was solved to a contrast target against the worst of the three\n * surfaces it can land on, every semantic colour against both the page and its own badge\n * tint, and the whole set is re-checked by `__tests__/palette-audit.test.ts` (\"derived\n * presets\") — which re-runs the deriver and fails if these committed values drift from\n * what it now produces.\n *\n * They are committed rather than derived at import because deriving eight token sets\n * costs ~34ms, and charging every consumer that at startup for palettes most of them\n * will never select is the wrong trade. The test is what keeps the two in agreement.\n *\n * Regenerated in 0.34 (round 0.34 BUG-8): the solve now measures the hex it ships rather\n * than the float before rounding, and the audit no longer lets a hex pair pass 0.05 under\n * its threshold. 52 values moved one step (66 counting the heat stops that repeat them);\n * Contrast's neutrals lost half their slate tint, its brand having half the chroma at\n * which the neutrals carry a hue in full.\n */\nexport const DERIVED_PRESETS: PalettePreset[] = [\n {\n id: \"ink\",\n name: \"Ink\",\n blurb: \"Cool blue-grey ground · solved for AAA body text\",\n light: {\n bgPage: \"#dce2ef\",\n bgSurface: \"#e6edfa\",\n bgSurface2: \"#dce2ef\",\n border: \"#b0bace\",\n textPrimary: \"#2e3139\",\n textSecondary: \"#454850\",\n textMuted: \"#5f636c\",\n brand: \"#2f5fd0\",\n brandHover: \"#1f4cbc\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#61646c\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#dce2ef\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#090d15\",\n bgSurface: \"#131720\",\n bgSurface2: \"#1d212b\",\n border: \"#363e4f\",\n textPrimary: \"#c8ccd6\",\n textSecondary: \"#a7abb5\",\n textMuted: \"#858a92\",\n brand: \"#3466d7\",\n brandHover: \"#2353c3\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a1a5ac\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1d212b\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"moss\",\n name: \"Moss\",\n blurb: \"Green ground · solved for AAA body text\",\n light: {\n bgPage: \"#d8e6dd\",\n bgSurface: \"#e3f1e8\",\n bgSurface2: \"#d8e6dd\",\n border: \"#abbfb2\",\n textPrimary: \"#2c332e\",\n textSecondary: \"#424b45\",\n textMuted: \"#5d6660\",\n brand: \"#2f7a52\",\n brandHover: \"#196842\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00725f\",\n moneyExpense: \"#8e5900\",\n moneyNet: \"#7356a2\",\n moneyNeutral: \"#5f6662\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#d8e6dd\",\n under: \"#00725f\",\n over: \"#8e5900\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8e5900\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#060f0a\",\n bgSurface: \"#101a14\",\n bgSurface2: \"#1a241e\",\n border: \"#314337\",\n textPrimary: \"#c6cfc9\",\n textSecondary: \"#a5aea8\",\n textMuted: \"#838c86\",\n brand: \"#317b53\",\n brandHover: \"#1c6943\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b87c2a\",\n moneyNet: \"#977ac9\",\n moneyNeutral: \"#9fa7a2\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#1a241e\",\n under: \"#009a80\",\n over: \"#b87c2a\",\n seqLow: \"#331f05\",\n seqHigh: \"#b87c2a\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"plum\",\n name: \"Plum\",\n blurb: \"Violet ground · solved for AAA body text\",\n light: {\n bgPage: \"#e7dfea\",\n bgSurface: \"#f2e9f5\",\n bgSurface2: \"#e7dfea\",\n border: \"#c2b5c6\",\n textPrimary: \"#352f36\",\n textSecondary: \"#4c464e\",\n textMuted: \"#676169\",\n brand: \"#7b3f8f\",\n brandHover: \"#692d7d\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7355a2\",\n moneyNeutral: \"#676269\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e7dfea\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#100b12\",\n bgSurface: \"#1b151d\",\n bgSurface2: \"#251f27\",\n border: \"#453b48\",\n textPrimary: \"#d0cad2\",\n textSecondary: \"#afa9b1\",\n textMuted: \"#8d8790\",\n brand: \"#9154a6\",\n brandHover: \"#7f4293\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009980\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a8a2aa\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#251f27\",\n under: \"#009980\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n {\n id: \"contrast\",\n name: \"High contrast\",\n blurb: \"Near-neutral ground · text solved to 13:1 / 8.5:1 / 5.5:1\",\n light: {\n bgPage: \"#e1e2e3\",\n bgSurface: \"#ecedee\",\n bgSurface2: \"#e1e2e3\",\n border: \"#b8babc\",\n textPrimary: \"#1b1d21\",\n textSecondary: \"#3a3c40\",\n textMuted: \"#55585c\",\n brand: \"#1f2937\",\n brandHover: \"#111b28\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#00715e\",\n moneyExpense: \"#8d5800\",\n moneyNet: \"#7255a1\",\n moneyNeutral: \"#616467\",\n chart: KIT_RAMP.light,\n heat: {\n neutral: \"#e1e2e3\",\n under: \"#00715e\",\n over: \"#8d5800\",\n seqLow: \"#fee3c5\",\n seqHigh: \"#8d5800\",\n empty: \"rgba(120,110,95,0.14)\",\n },\n },\n dark: {\n bgPage: \"#0c0d0e\",\n bgSurface: \"#171718\",\n bgSurface2: \"#212122\",\n border: \"#3d3e3f\",\n textPrimary: \"#e4e8ec\",\n textSecondary: \"#b9bdc1\",\n textMuted: \"#95979c\",\n brand: \"#556c8e\",\n brandHover: \"#455b7c\",\n brandContrast: \"#ffffff\",\n moneyIncome: \"#009a80\",\n moneyExpense: \"#b77b29\",\n moneyNet: \"#9679c9\",\n moneyNeutral: \"#a2a5a8\",\n chart: KIT_RAMP.dark,\n heat: {\n neutral: \"#212122\",\n under: \"#009a80\",\n over: \"#b77b29\",\n seqLow: \"#331f05\",\n seqHigh: \"#b77b29\",\n empty: \"rgba(160,170,190,0.10)\",\n },\n },\n },\n];\n\nexport const PALETTES: PalettePreset[] = [DEFAULT_PRESET, PAUL_TOL_PRESET, ...DERIVED_PRESETS];\n\nexport function presetById(id: string): PalettePreset {\n return PALETTES.find((p) => p.id === id) ?? DEFAULT_PRESET;\n}\n\n/**\n * Write a token set onto an element's inline style as CSS custom properties.\n *\n * This is the only writer, and there is no un-writer: `presetById` falls back to\n * `DEFAULT_PRESET` and this then writes the full set, so returning to the default\n * APPLIES it rather than clearing anything. A `clearTokenSet` and the hand-kept\n * 24-name `TOKEN_VARS` list it looped over used to live here, both exported, both\n * called by nobody in either consumer — a second copy of these property names with\n * nothing comparing the two, so adding a token here and forgetting it there would\n * have been silent and only a dead function would have noticed. The list is gone;\n * the test beside this asserts instead that every field of a `TokenSet` reaches the\n * element, which is what that list was standing in for.\n *\n * **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`\n * the text and border tokens are written one step stronger — in a light set the well and\n * the four money colours too (0.36, docs/colour-roles-harmonization.md §13) — and `--text-placeholder` —\n * otherwise derived in tokens.css — is written inline too; without it that inline\n * placeholder is removed again, so the stylesheet's formula applies. The step is made\n * HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.\n *\n * `contrast` defaults to the element's own `data-contrast=\"more\"`, which the contrast\n * layer (`applyContrast`, `useApplyContrast`) sets first. That is how the palette and the\n * contrast cooperate without knowing each other: a palette change re-applies through\n * here and keeps the contrast, and a contrast change re-applies the set remembered here.\n */\nexport function applyTokenSet(\n el: HTMLElement,\n t: TokenSet,\n contrast: boolean = el.getAttribute(\"data-contrast\") === \"more\",\n): void {\n rememberTokenSet(el, t);\n const step = contrast ? contrastStep(t) : null;\n el.style.setProperty(\"--bg-page\", t.bgPage);\n el.style.setProperty(\"--bg-surface\", t.bgSurface);\n // The light well and money step under More contrast (0.36, colour-roles §13); in dark\n // and at Standard `well` is null and the set's own are written.\n el.style.setProperty(\"--bg-surface-2\", step?.well?.bgSurface2 ?? t.bgSurface2);\n el.style.setProperty(\"--border\", step?.border ?? t.border);\n el.style.setProperty(\"--text-primary\", t.textPrimary);\n el.style.setProperty(\"--text-secondary\", step?.textSecondary ?? t.textSecondary);\n el.style.setProperty(\"--text-muted\", step?.textMuted ?? t.textMuted);\n if (step) el.style.setProperty(\"--text-placeholder\", step.textPlaceholder);\n else el.style.removeProperty(\"--text-placeholder\");\n el.style.setProperty(\"--brand\", t.brand);\n el.style.setProperty(\"--brand-hover\", t.brandHover);\n el.style.setProperty(\"--brand-contrast\", t.brandContrast);\n el.style.setProperty(\"--money-income\", step?.well?.moneyIncome ?? t.moneyIncome);\n el.style.setProperty(\"--money-expense\", step?.well?.moneyExpense ?? t.moneyExpense);\n el.style.setProperty(\"--money-net\", step?.well?.moneyNet ?? t.moneyNet);\n el.style.setProperty(\"--money-neutral\", step?.well?.moneyNeutral ?? t.moneyNeutral);\n // All nine `--chart-N`, a shorter ramp repeated: Imprint's eight write `--chart-9` as\n // slot 0 — what `paletteFor(8)` names — so a host addressing `var(--chart-9)` directly\n // follows the cycle, and switching away from Paul Tol's nine leaves no stale ninth hue.\n // An empty ramp (a `derivePalette` set) leaves whatever was there, deliberately.\n if (t.chart.length > 0) {\n for (let i = 0; i < 9; i++) el.style.setProperty(`--chart-${i + 1}`, t.chart[i % t.chart.length]);\n }\n}\n"],"mappings":"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","names":[]}