@bompus/house-rules 0.0.0-stage → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +378 -2
  5. package/THIRD_PARTY_NOTICES.md +188 -0
  6. package/compose.mjs +125 -0
  7. package/composition.mjs +361 -0
  8. package/config-view.mjs +209 -0
  9. package/config.mjs +381 -0
  10. package/docs/assets/banner-dark.png +0 -0
  11. package/docs/assets/banner-light.png +0 -0
  12. package/docs/assets/overview.png +0 -0
  13. package/docs/configuration.md +269 -0
  14. package/docs/skill-names.md +50 -0
  15. package/examples/person/house-rules.json +15 -0
  16. package/examples/person/rules/10-tooling.md +8 -0
  17. package/examples/person/rules/20-reporting.md +9 -0
  18. package/examples/project/AGENTS.md +18 -0
  19. package/package.json +42 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +340 -0
  22. package/rules/modifiers/backlog-outline.md +25 -0
  23. package/rules/modifiers/backlog-visual.md +16 -0
  24. package/rules/modifiers/coded-offers.md +70 -0
  25. package/rules/modifiers/cross-session-relay.md +23 -0
  26. package/rules/modifiers/effort-estimates.md +41 -0
  27. package/rules/modifiers/land-when-done.md +12 -0
  28. package/rules/modifiers/low-quota-handoff.md +20 -0
  29. package/rules/modifiers/model-default-questions.md +33 -0
  30. package/rules/modifiers/multi-agent.md +38 -0
  31. package/rules/modifiers/no-attribution.md +16 -0
  32. package/rules/modifiers/periodic-updates.md +25 -0
  33. package/rules/modifiers/plan-files.md +82 -0
  34. package/rules/modifiers/question-cards.md +48 -0
  35. package/rules/modifiers/release-batching.md +15 -0
  36. package/rules/modifiers/scratch-on-disk.md +15 -0
  37. package/rules/modifiers/shared-host-load.md +19 -0
  38. package/rules/modifiers/solo-operator.md +13 -0
  39. package/rules/modifiers/squash-landing.md +22 -0
  40. package/rules/modifiers/upstream-contributions.md +45 -0
  41. package/rules/modifiers/web-image-formats.md +35 -0
  42. package/rules/references/backlog-view.md +154 -0
  43. package/rules/references/github-actions.md +45 -0
  44. package/rules/references/landing.md +39 -0
  45. package/rules/references/multi-agent.md +51 -0
  46. package/rules/references/release-batching.md +66 -0
  47. package/rules/references/squash-landing.md +50 -0
  48. package/rules/references/thread-visuals.md +45 -0
  49. package/setup.mjs +445 -0
  50. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  51. package/skills/hr-agent-guidance-audit/SKILL.md +91 -0
  52. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  53. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  54. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  55. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  56. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  57. package/skills/hr-agent-guidance-refresh/SKILL.md +36 -0
  58. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  59. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  60. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  61. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  62. package/skills/hr-audit-choices/SKILL.md +67 -0
  63. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  64. package/skills/hr-benchmarking/NOTICE.md +36 -0
  65. package/skills/hr-benchmarking/SKILL.md +279 -0
  66. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  67. package/skills/hr-benchmarking/references/javascript.md +130 -0
  68. package/skills/hr-benchmarking/references/tools.md +165 -0
  69. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  70. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  71. package/skills/hr-better-accessibility/LICENSE +22 -0
  72. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  73. package/skills/hr-better-accessibility/SKILL.md +119 -0
  74. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  75. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  76. package/skills/hr-better-accessibility/forms.md +82 -0
  77. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  78. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  79. package/skills/hr-better-accessibility/screen-readers.md +102 -0
  80. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  81. package/skills/hr-better-colors/LICENSE +21 -0
  82. package/skills/hr-better-colors/NOTICE.md +37 -0
  83. package/skills/hr-better-colors/SKILL.md +114 -0
  84. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  85. package/skills/hr-better-colors/color-formats.md +90 -0
  86. package/skills/hr-better-colors/color-usage.md +140 -0
  87. package/skills/hr-better-colors/contrast.md +80 -0
  88. package/skills/hr-better-colors/palette-generation.md +104 -0
  89. package/skills/hr-better-colors/palette-structure.md +76 -0
  90. package/skills/hr-better-colors/token-naming.md +97 -0
  91. package/skills/hr-better-layout/LICENSE +22 -0
  92. package/skills/hr-better-layout/NOTICE.md +48 -0
  93. package/skills/hr-better-layout/SKILL.md +92 -0
  94. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  95. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  96. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  97. package/skills/hr-better-typography/LICENSE +22 -0
  98. package/skills/hr-better-typography/NOTICE.md +46 -0
  99. package/skills/hr-better-typography/SKILL.md +172 -0
  100. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  101. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  102. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  103. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  104. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  105. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  106. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  107. package/skills/hr-better-writing/LICENSE +22 -0
  108. package/skills/hr-better-writing/NOTICE.md +46 -0
  109. package/skills/hr-better-writing/SKILL.md +122 -0
  110. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  111. package/skills/hr-code-review/LICENSE +21 -0
  112. package/skills/hr-code-review/NOTICE.md +26 -0
  113. package/skills/hr-code-review/SKILL.md +163 -0
  114. package/skills/hr-code-review/agents/openai.yaml +3 -0
  115. package/skills/hr-code-review/references/acp-client.md +43 -0
  116. package/skills/hr-code-review/references/evidence.md +57 -0
  117. package/skills/hr-code-review/references/impact.md +103 -0
  118. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  119. package/skills/hr-code-review/references/panel-plan.md +61 -0
  120. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  121. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  122. package/skills/hr-code-review/references/panel-setup.md +54 -0
  123. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  124. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  125. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  126. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  127. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  128. package/skills/hr-design-exploration/SKILL.md +105 -0
  129. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  130. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  131. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  132. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  133. package/skills/hr-diagnosing-bugs/references/reporting.md +97 -0
  134. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  135. package/skills/hr-explain-code/NOTICE.md +37 -0
  136. package/skills/hr-explain-code/SKILL.md +110 -0
  137. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  138. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  139. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  140. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  141. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  142. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  143. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  144. package/skills/hr-handoff/LICENSE +21 -0
  145. package/skills/hr-handoff/NOTICE.md +29 -0
  146. package/skills/hr-handoff/SKILL.md +114 -0
  147. package/skills/hr-handoff/agents/openai.yaml +5 -0
  148. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  149. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  150. package/skills/hr-handoff/assets/source.example.json +20 -0
  151. package/skills/hr-handoff/references/reconciliation.md +124 -0
  152. package/skills/hr-handoff/references/successor-thread.md +76 -0
  153. package/skills/hr-handoff/scripts/check-reconciliation.mjs +324 -0
  154. package/skills/hr-house-rules-setup/SKILL.md +114 -0
  155. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  156. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  157. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  158. package/skills/hr-lean-plan/SKILL.md +146 -0
  159. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  160. package/skills/hr-maintainability-review/SKILL.md +186 -0
  161. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  162. package/skills/hr-manual-qa/NOTICE.md +36 -0
  163. package/skills/hr-manual-qa/SKILL.md +37 -0
  164. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  165. package/skills/hr-navigation-retro/LICENSE +21 -0
  166. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  167. package/skills/hr-navigation-retro/SKILL.md +86 -0
  168. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  169. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  170. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  171. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  172. package/skills/hr-ordering-tests/SKILL.md +51 -0
  173. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  174. package/skills/hr-plain-prose/SKILL.md +264 -0
  175. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  176. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  177. package/skills/hr-pr-followup/LICENSE +21 -0
  178. package/skills/hr-pr-followup/NOTICE.md +9 -0
  179. package/skills/hr-pr-followup/SKILL.md +74 -0
  180. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  181. package/skills/hr-pr-followup/references/review-helpers.md +111 -0
  182. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  183. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  184. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  185. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  186. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  187. package/skills/hr-progress-report/SKILL.md +49 -0
  188. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  189. package/skills/hr-read-reddit/SKILL.md +44 -0
  190. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  191. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  192. package/skills/hr-read-x-links/SKILL.md +42 -0
  193. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  194. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  195. package/skills/hr-research-shared-links/SKILL.md +103 -0
  196. package/skills/hr-split-to-prs/LICENSE +21 -0
  197. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  198. package/skills/hr-split-to-prs/SKILL.md +61 -0
  199. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  200. package/skills/hr-sqlite/SKILL.md +174 -0
  201. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  202. package/skills/hr-sqlite/references/evidence.md +94 -0
  203. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  204. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  205. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  206. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  207. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  208. package/skills/hr-tdd/LICENSE +21 -0
  209. package/skills/hr-tdd/NOTICE.md +10 -0
  210. package/skills/hr-tdd/SKILL.md +40 -0
  211. package/skills/hr-tdd/agents/openai.yaml +3 -0
  212. package/skills/hr-tdd/mocking.md +39 -0
  213. package/skills/hr-tdd/tests.md +77 -0
  214. package/skills/hr-test-audit/LICENSE +21 -0
  215. package/skills/hr-test-audit/NOTICE.md +67 -0
  216. package/skills/hr-test-audit/SKILL.md +146 -0
  217. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  218. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  219. package/skills/hr-usage-report/SKILL.md +90 -0
  220. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  221. package/skills/hr-usage-report/assets/example-report.json +125 -0
  222. package/skills/hr-usage-report/assets/report-template.html +377 -0
  223. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  224. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  225. package/skills/hr-what-next/SKILL.md +151 -0
  226. package/skills/hr-what-next/agents/openai.yaml +4 -0
  227. package/skills/hr-what-next/references/reporting.md +59 -0
  228. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  229. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  230. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  231. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  232. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  233. package/skills/hr-writing-pr/NOTICE.md +48 -0
  234. package/skills/hr-writing-pr/SKILL.md +119 -0
  235. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
  236. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
@@ -0,0 +1,81 @@
1
+ # Motion and zoom
2
+
3
+ `prefers-reduced-motion`, zoom and reflow, and unit choices that respect user settings.
4
+
5
+ ## prefers-reduced-motion
6
+
7
+ Make motion opt-in. Wrap animations in `@media (prefers-reduced-motion: no-preference)` so users who asked for reduced motion get the static version by default, rather than you chasing every animation with an override.
8
+
9
+ ```css
10
+ /* Good: motion is opt-in */
11
+ .card {
12
+ /* static styles */
13
+ }
14
+ @media (prefers-reduced-motion: no-preference) {
15
+ .card {
16
+ transition: transform 200ms ease-out;
17
+ }
18
+ }
19
+ ```
20
+
21
+ ```tsx
22
+ // Tailwind: motion-safe / motion-reduce variants
23
+ <div className="motion-safe:transition-transform motion-safe:hover:-translate-y-1" />
24
+ ```
25
+
26
+ For an existing codebase where opt-in isn't feasible, a global reset is the fallback. Exclude loading spinners and progress indicators, including animated descendants and pseudo-elements. This example uses `.progress-indicator`; replace it with the project's existing indicator classes:
27
+
28
+ ```css
29
+ @media (prefers-reduced-motion: reduce) {
30
+ *:not(.progress-indicator, .progress-indicator *),
31
+ *:not(.progress-indicator, .progress-indicator *)::before,
32
+ *:not(.progress-indicator, .progress-indicator *)::after {
33
+ animation-duration: 0.01ms !important;
34
+ animation-iteration-count: 1 !important;
35
+ transition-duration: 0.01ms !important;
36
+ scroll-behavior: auto !important;
37
+ }
38
+ }
39
+ ```
40
+
41
+ `0.01ms` rather than `none`, so `animationend` and `transitionend` still fire and JS waiting on them doesn't hang.
42
+
43
+ ### What to disable vs reduce
44
+
45
+ Reduced motion means reduced, not eliminated. It targets vestibular triggers, not feedback.
46
+
47
+ | Disable entirely | Replace | Keep |
48
+ | ------------------------------------------------ | ------------------------------------------------ | ----------------------------------------------- |
49
+ | Parallax scrolling | Slide/scale/zoom transitions → opacity crossfade | Loading spinners and progress |
50
+ | Autoplaying video, GIFs, looping decoration | Smooth scrolling → instant jump | Instant state changes (hover color, focus ring) |
51
+ | Spinning, large-scale movement across the screen | Auto-rotating carousels → start paused | Brief functional feedback (button press) |
52
+
53
+ Animations are interruptible and driven by user input. Nothing autoplays or refuses to stop, and under reduced motion carousels start paused.
54
+
55
+ ## Autoplay and timed UI
56
+
57
+ Motion the user didn't ask for and UI acting on its own schedule:
58
+
59
+ - **No autoplaying media without visible controls** (WCAG 2.2.2). Anything moving, blinking, or updating on its own for more than 5 seconds needs a visible pause or stop control, muted looping hero videos included.
60
+ - **Prefer explicit dismissal over timers.** Auto-dismissal suits low-stakes confirmations and nothing else. A toast carrying an action, an error, or information the user may need stays until dismissed. Where one must time out, 5 seconds is the floor, and hovering or focusing it pauses the timer.
61
+ - **Never put critical information only in a timed element.** A vanished toast with the only link to an undo action is data loss on a schedule.
62
+
63
+ ## Zoom and reflow
64
+
65
+ - **200% zoom** (WCAG 1.4.4). All content and functionality survives text scaled to 200%, and the viewport leaves the reader able to zoom.
66
+ - **Reflow at 320px** (WCAG 1.4.10). At 400% zoom on a 1280px viewport, equivalent to a 320px one, the page must work with vertical scrolling alone. Genuinely 2D content is the exception: tables, maps and code blocks scroll inside their own container.
67
+
68
+ Fixed heights are what break under zoom. Use `min-height` on anything containing text and let containers grow.
69
+
70
+ ### rem vs px
71
+
72
+ Respect how the codebase is set up. Where the project sizes in `px`, or on an established Tailwind scale, stay consistent and never introduce mixed units into someone else's system. Where you do have the choice, in new code or a codebase already on `rem`, `rem` respects the user's base font size and `px` ignores it:
73
+
74
+ | Use `rem` | Use `px` |
75
+ | ----------------------------------------------------- | ------------------------------ |
76
+ | `font-size` | Borders and hairlines |
77
+ | `max-width` of text containers | Focus outline width and offset |
78
+ | Media-query breakpoints (`@media (min-width: 48rem)`) | `box-shadow` details |
79
+ | Spacing that should scale with text | Fixed-size decorations |
80
+
81
+ Breakpoints are where the choice matters most. At a larger base font size an `em` or `rem` query switches to the mobile layout when the text needs it, and a `px` query does not.
@@ -0,0 +1,102 @@
1
+ # Screen readers
2
+
3
+ Visually hidden content, live regions, toasts, alt text and SVG.
4
+
5
+ ## Visually hidden content
6
+
7
+ The canonical `.sr-only` pattern hides content visually while keeping it in the accessibility tree:
8
+
9
+ ```css
10
+ .sr-only {
11
+ position: absolute;
12
+ width: 1px;
13
+ height: 1px;
14
+ padding: 0;
15
+ margin: -1px;
16
+ overflow: hidden;
17
+ clip: rect(0 0 0 0);
18
+ clip-path: inset(50%);
19
+ white-space: nowrap;
20
+ border: 0;
21
+ }
22
+ ```
23
+
24
+ Use `1px` boxes, not `0`, because some screen readers skip zero-sized elements. `white-space: nowrap` stops words being read as one run-together string. Never `display: none` or `visibility: hidden`, which remove the content from assistive tech entirely.
25
+
26
+ Tailwind ships this as `sr-only`. Skip links add a focus variant that un-hides it, `focus:not-sr-only` or an override on `:focus`.
27
+
28
+ Use it for context sighted users get visually: `<span class="sr-only">Opens in new tab</span>`, table captions, or an icon-only control's label where `aria-label` isn't an option.
29
+
30
+ ## Choosing how to announce a change
31
+
32
+ Work down this list and stop at the first match:
33
+
34
+ 1. **Focus moves to named content**, such as a modal with an accessible name and description. Focus exposes that associated content; no duplicate live announcement is needed. For custom field validation, use the next rule even when focus moves to the invalid field.
35
+ 2. **Tied to a specific control**, such as a field error or character count: `aria-describedby` on the control, announced with the field.
36
+ 3. **Non-urgent, not tied to a control**, such as a toast, "Saved", a result count, or a loading state: a polite live region, `role="status"`.
37
+ 4. **Urgent and not tied to a control**, such as a form-level failure or session expiry: `role="alert"`.
38
+
39
+ ## Live regions
40
+
41
+ Live regions announce content that changes without a page load: toasts, validation, search-result counts, loading states.
42
+
43
+ | Mechanism | Politeness | Use for |
44
+ | ----------------------------------------------------------------- | ---------------------- | ----------------------------------------------- |
45
+ | `role="status"` (= `aria-live="polite"` + `aria-atomic="true"`) | Waits for a pause | Toasts, "Saved", result counts, loading updates |
46
+ | `role="alert"` (= `aria-live="assertive"` + `aria-atomic="true"`) | Interrupts immediately | Errors and urgent problems only |
47
+
48
+ Rules for reliable announcements:
49
+
50
+ - For repeated polite updates, keep a stable empty region in the DOM before changing its text. Inserting a new polite region with its content is announced inconsistently.
51
+ - Dynamically inserted `role="alert"` content is usually announced, but behavior varies. Use it only for urgent errors not tied to a control, and test the target browser and screen-reader combinations.
52
+ - Default to polite. Overusing `assertive` is the most common live-region mistake, because it interrupts whatever the user was reading.
53
+ - Keep messages short and self-contained. `aria-atomic="true"` re-reads the whole region on change.
54
+ - Never move focus to a toast. Announce it and leave focus where the user is working. Give toasts a generous timeout or a dismiss button, and never put the only path to an action inside an auto-dismissing one.
55
+
56
+ ```tsx
57
+ // Region rendered from the start, message injected later
58
+ <div role="status" className="sr-only">
59
+ {statusMessage}
60
+ </div>
61
+ ```
62
+
63
+ For loading states: set `aria-busy="true"` on the updating region, announce "Loading…" politely, then announce the outcome ("Loaded, 12 results").
64
+
65
+ ## aria-hidden
66
+
67
+ `aria-hidden="true"` removes an element and its whole subtree from assistive tech. Use it for decorative icons and content duplicated for visual effect. Never put it on or above a focusable element, which creates stops you can Tab to that do not exist for a screen reader. Hiding something interactive means removing it from the tab order too.
68
+
69
+ ## Alt text
70
+
71
+ Choose by purpose, not by what the image looks like:
72
+
73
+ | Purpose | Alt | Example |
74
+ | ------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------- |
75
+ | Decorative, or redundant with adjacent text | `alt=""` (empty, but present) | Logo next to the company name in text |
76
+ | Informative | Describe the meaning it adds | `alt="Ticket QR code"` |
77
+ | Functional (image is the link/button) | Describe the action or destination | Search icon → `alt="Search"`, not `alt="magnifying glass"` |
78
+ | Image of text | The exact text (better: use real text) | `alt="50% off everything"` |
79
+ | Complex (chart, diagram) | Short summary in `alt`, full data as a table or text nearby | `alt="Revenue by quarter, described below"` |
80
+
81
+ A missing `alt` is worse than an empty one, because screen readers fall back to reading the file name.
82
+
83
+ ## SVG
84
+
85
+ - Decorative SVG: `aria-hidden="true"` and `focusable="false"`, the latter for legacy Edge and IE tabbing. No title needed.
86
+ - Meaningful inline SVG: `role="img"` plus `aria-label="…"`, or a `<title>` as the first child referenced by `aria-labelledby`.
87
+ - Give each `<title>` and `<desc>` an `id` that is unique on the page, such as `chart-revenue-title`. Two inline SVGs that share a bare `id="title"` both resolve to the first one. Keep `<title>` to a short name and make `<desc>` one sentence about what the figure shows, not how it is drawn.
88
+ - Simple cases: `<img src="icon.svg" alt="…">` is the most reliable delivery.
89
+
90
+ ```tsx
91
+ // Decorative icon inside a labeled button
92
+ <button aria-label="Close">
93
+ <svg aria-hidden="true" focusable="false">…</svg>
94
+ </button>
95
+
96
+ // Standalone meaningful icon
97
+ <svg role="img" aria-label="Verified account">…</svg>
98
+ ```
99
+
100
+ ## Video and audio
101
+
102
+ Prerecorded video needs captions; provide transcripts for audio. Never autoplay with sound, and always render controls.
@@ -0,0 +1,84 @@
1
+ # Semantics and ARIA
2
+
3
+ Native elements first, landmarks, accessible names and the ARIA rules that keep custom widgets honest.
4
+
5
+ ## The rules of ARIA
6
+
7
+ 1. If a native HTML element with the semantics and behavior you need exists, use it instead of repurposing another element with ARIA.
8
+ 2. Don't change native semantics unless you really have to.
9
+ 3. Every interactive ARIA control must be keyboard-operable; a role is a promise of the full keyboard model, states and behavior.
10
+ 4. Never put `role="presentation"` or `aria-hidden="true"` on a focusable element.
11
+ 5. All interactive elements must have an accessible name.
12
+
13
+ No ARIA is better than bad ARIA. A screen reader trusts your roles, so a wrong one is worse than none.
14
+
15
+ ## Button vs link vs div
16
+
17
+ | Element | Use for | Why |
18
+ | --------------- | ----------------------------------------------------------- | --------------------------------------------------------------------- |
19
+ | `<a href>` | Navigation: anything that goes somewhere or changes the URL | Free Cmd/Ctrl/middle-click, right-click → copy link, Enter activation |
20
+ | `<button>` | Actions: submit, toggle, open, delete | Free focus, Enter _and_ Space activation, form semantics |
21
+ | `<div onClick>` | Nothing | No role, no focus, no keyboard; screen readers see plain text |
22
+
23
+ ```tsx
24
+ // Bad: invisible to keyboard and screen readers
25
+ <div onClick={openSettings}>Settings</div>
26
+
27
+ // Good: focus, Enter/Space activation and semantics for free
28
+ <button onClick={openSettings}>Settings</button>
29
+ ```
30
+
31
+ If it looks clickable it must be clickable, and if it's clickable it must be a real interactive element. Rebuilding a link as a button, or the reverse, breaks user expectations. A "button" that navigates is a styled `<a>`.
32
+
33
+ Where a native element is truly impossible, the full polyfill is `role="button"` plus `tabindex="0"` plus Enter and Space handlers, which is why the native element is always less code.
34
+
35
+ ## Landmarks and headings
36
+
37
+ - Expose one visible primary `<main>` landmark. `<header>`, `<nav>`, `<aside>`, `<footer>` map to landmarks screen-reader users jump between.
38
+ - Multiple landmarks of the same type need distinguishing labels: `<nav aria-label="Primary">`, `<nav aria-label="Breadcrumbs">`.
39
+ - Do not report the one-`<h1>` or the no-skipped-levels convention as a standalone WCAG failure without a concrete navigation or comprehension impact. Headings are structure, not styling; style a heading level with CSS instead of picking the tag by size.
40
+ - `<title>` matches the current context, most specific first: `Billing · Settings · Acme`.
41
+
42
+ ## Accessible names
43
+
44
+ Name precedence: `aria-labelledby` > `aria-label` > native label (`<label>`, text content, `alt`) > `title` attribute.
45
+
46
+ - Prefer visible text or `aria-labelledby` over `aria-label`, which is invisible, drifts out of sync with the UI and is handled inconsistently by translation tools.
47
+ - Icon-only buttons always need a name: `<button aria-label="Close">` with the icon `aria-hidden="true"`.
48
+ - WCAG 2.5.3 Label in Name: a button showing "Send" with `aria-label="Submit message"` breaks voice control users who say "click Send".
49
+ - Accessible names must exist even when the design omits visible labels.
50
+
51
+ ```tsx
52
+ // Good: name from visible text, icon hidden
53
+ <button>
54
+ <TrashIcon aria-hidden="true" /> Delete
55
+ </button>
56
+
57
+ // Good: icon-only, explicit name
58
+ <button aria-label="Delete">
59
+ <TrashIcon aria-hidden="true" />
60
+ </button>
61
+ ```
62
+
63
+ Add `translate="no"` to brand names, code tokens and identifiers so auto-translation doesn't garble them.
64
+
65
+ ## Common ARIA mistakes
66
+
67
+ | Mistake | Why it fails |
68
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
69
+ | `aria-label` on a plain `<div>` or `<span>` | Most screen readers ignore names on non-interactive, role-less elements |
70
+ | `<button role="button">` | Redundant role; adds noise, no benefit |
71
+ | `aria-hidden="true"` on or above a focusable element | Creates elements you can Tab to but that don't exist for screen readers |
72
+ | `aria-labelledby`/`aria-describedby` pointing at a missing ID | Silently produces no name or description |
73
+ | `role="menu"` on a nav list | `menu` promises app-style arrow-key behavior; site navigation is `<nav>` with a list |
74
+
75
+ ## Disabled states
76
+
77
+ Native `disabled` supplies the platform's complete disabled behavior. It removes the control from the tab order, suppresses activation, applies `:disabled` and excludes form controls from submission. Use it when a native control is genuinely unavailable. `aria-disabled="true"` only announces the state, changing neither focusability, nor behavior, nor styling.
78
+
79
+ - Never disable submit buttons. Keep them enabled, validate on submit and focus the first error ([forms.md](forms.md)).
80
+ - A natively `disabled` control suppresses pointer events and leaves the tab order, so a tooltip on it never opens for keyboard or touch users and is unreliable for mouse users. Put the reason in visible text beside the control, or switch to `aria-disabled="true"`, which keeps it focusable and hoverable and can carry a tooltip.
81
+ - Use `aria-disabled="true"` where keeping a control discoverable in the tab order is intentional, or where a custom control cannot use native `disabled`.
82
+ - With `aria-disabled="true"`, block pointer and keyboard activation in the handler, prevent form submission where applicable, add explicit styling including forced-colors support and explain nearby why the action is unavailable.
83
+ - Never set both `disabled` and `aria-disabled` on the same element.
84
+ - Disabled controls are exempt from contrast minimums. Keep them legible anyway.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakub Krehel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ # Source notices
2
+
3
+ This notice accompanies the hr-better-colors bundle when copied on its own.
4
+
5
+ ## jakubkrehel/skills
6
+
7
+ Source: https://github.com/jakubkrehel/skills (compared at revision `267330e1adfc66a718fb65fa6918c1f06d0a689e`)
8
+
9
+ - `skills/hr-better-accessibility/`, `skills/hr-better-colors/`, `skills/hr-better-layout/`,
10
+ `skills/hr-better-typography/` and `skills/hr-better-writing/`: adapted skill bundles,
11
+ including their supporting references and discovery metadata.
12
+
13
+ Copyright (c) 2026 Jakub Krehel
14
+
15
+ ## house-rules and MIT permission notice
16
+
17
+ MIT License
18
+
19
+ Copyright (c) 2026 Aaron Queen
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ of this software and associated documentation files (the "Software"), to deal
23
+ in the Software without restriction, including without limitation the rights
24
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ copies of the Software, and to permit persons to whom the Software is
26
+ furnished to do so, subject to the following conditions:
27
+
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: hr-better-colors
3
+ description: "Color rules for web UI: palette ramps, semantic color tokens, color notation and gamut, and measured contrast of rendered pairs. Use when choosing, reviewing or fixing colors, color tokens, themes or text contrast in web UI."
4
+ license: MIT
5
+ metadata:
6
+ upstream: https://github.com/jakubkrehel/skills
7
+ upstream_ref: 267330e1adfc66a718fb65fa6918c1f06d0a689e
8
+ upstream_path: skills/better-colors/
9
+ local_patches: "Use-when description; 14pt bold = 18.67px; WCAG 2 text-color crossover; Tailwind v4 utility names; P3 fallback severity matches Reporting; better-ui handoff dropped (not vendored); enforce tokens with a lint or guard"
10
+ house_rules_patches: "Portable provenance and review verdict"
11
+ ---
12
+
13
+ # Colors
14
+
15
+ A color system is a small set of ramps, named by role and verified against the backgrounds they actually render on. Most color bugs are system bugs. A value picked in isolation, a token borrowed because it looked right, a pair nobody measured.
16
+
17
+ Never report a contrast value you did not measure, and never estimate a color you could compute. Colors are one of the few interface concerns with an exact answer, so produce the exact answer.
18
+
19
+ Contrast requirements belong to `hr-better-accessibility`. Surfaces, shadows and icon color are outside this skill.
20
+
21
+ ## Match the project's color system
22
+
23
+ Reuse the project's tokens and notation. A second representation added to fix one value makes the palette harder to reason about. A consistent hex system beats hex with `oklch()` scattered through it.
24
+
25
+ Once the tokens exist, enforce them with a lint rule or guard test that fails on a color literal outside the token files, with a short allowlist for places that need raw values by design. A written "use tokens" rule with no check behind it drifts.
26
+
27
+ For a new system, `oklch()` is the best default, because its numbers behave the way the ramp rules below describe. Everywhere else, a color library produces the same ramp in the project's own notation ([color-formats.md](color-formats.md)).
28
+
29
+ ## A system is ramps, not colors
30
+
31
+ One neutral ramp, one accent ramp and only the status ramps the product actually renders. A `warning` ramp nothing imports is maintenance for zero pixels. A second accent hue earns its place only when two things must be distinguishable at a glance.
32
+
33
+ ## Every step has a job
34
+
35
+ A ramp is not a gradient to pick from by eye. Each step exists because a role needs it: page background, component hover, border, solid fill, body text. Do not generate a step no role consumes. Both the Tailwind `50`–`950` and Radix `1`–`12` conventions map to those roles ([palette-structure.md](palette-structure.md)).
36
+
37
+ ## Name primitives by hue, semantics by role
38
+
39
+ Primitives name a value (`--blue-500`) and are never applied in a component. Semantic tokens name a job (`--color-text-secondary`), point at a primitive and are the only tier components reference.
40
+
41
+ That seam is what makes theming possible. Without it, dark mode means auditing every usage to work out which meant "the accent" and which just wanted blue ([token-naming.md](token-naming.md)).
42
+
43
+ ## Use a token only in its role
44
+
45
+ Never borrow a token because its value is right today. A separator used as a text color works until borders get lighter, and then the text goes with them. If a role has no token, add the token.
46
+
47
+ ## Hold the hue across the ramp
48
+
49
+ Four properties define a well-formed ramp:
50
+
51
+ - Steps follow a deliberate _perceived_ lightness progression ([palette-generation.md](palette-generation.md)).
52
+ - Hue stays constant end to end.
53
+ - Vividness peaks mid-ramp and falls off at both ends.
54
+ - Steps sit denser at the light end than at the dark end.
55
+
56
+ Both ends stop short of pure black and white, which cannot carry hue at all. Use a color library rather than eyeballing it ([palette-generation.md](palette-generation.md)).
57
+
58
+ ## One color, one meaning
59
+
60
+ Use a color for one purpose across the whole interface, treating anything within `15°` of hue as the same color. If the accent means interactive, that hue on static text tells users to click something that is not clickable, and an interactive element rendered neutral misleads just as badly. Color is never the only carrier of meaning, which `hr-better-accessibility` owns.
61
+
62
+ ## Fill exactly one action per view
63
+
64
+ When filled color encodes primary emphasis, one primary action gets it and peers stay neutral. Put the color on the background, not the label. A filled button reads as primary across the room; accent-colored text on a neutral button reads as a link.
65
+
66
+ Several colored backgrounds are fine when they encode distinct states or categories rather than competing as peers.
67
+
68
+ ## Measure the rendered pair, then report
69
+
70
+ Measure a foreground against the background it actually renders on, not the page background. When a pair fails, report the pair, its measured value and the threshold it misses, then leave the colors alone. They are a design decision. Change them only when asked, and remeasure after ([contrast.md](contrast.md)).
71
+
72
+ ## Pick a gradient's interpolation space
73
+
74
+ The space is a look, not a correctness setting.
75
+
76
+ - **`in oklab`** is the best default: even brightness, no hue surprises.
77
+ - **`in oklch`** travels around the hue wheel rather than through the middle, staying vivid and sweeping every hue between the stops. Reach for it when a two-hue gradient goes gray in the middle.
78
+ - **The sRGB default** darkens and mutes the midpoint. It is what most interfaces already have, because it is what you get without asking.
79
+
80
+ See [color-usage.md](color-usage.md).
81
+
82
+ ## Before you finish
83
+
84
+ | Mistake | Fix |
85
+ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
86
+ | A raw value where the project has a token | Reuse or add the role token, in the project's notation |
87
+ | An isolated `oklch()` value dropped into a hex codebase | Keep the established notation unless a migration is in scope |
88
+ | A primitive like `--blue-500` used directly in a component | Point a semantic token at it |
89
+ | Token named for its appearance (`--color-blue-button`) or first use (`--color-sidebar-gray`) | Name it for its role: `--color-accent-solid`, `--color-bg-surface` |
90
+ | `--color-primary` meaning the brand and `--color-text-primary` meaning body text | Reserve `accent` for the brand; let `primary` mean "most prominent of its group" |
91
+ | Semantic token used outside its role (separator as text) | Add a token for the missing role; never borrow by value |
92
+ | Ramp built by varying HSL lightness | Rebuild against perceived lightness with a constant hue |
93
+ | Ramp spaced evenly across the full range | Tighten the light end until `50` and `100` read as two surfaces |
94
+ | Same saturation number reused across hues | Match the proportion of each hue's own maximum, not the raw value |
95
+ | Status hue that collides with the accent hue | Move it until destructive and primary read apart side by side |
96
+ | Dark mode made by mechanically reversing the light palette | Reverse as a starting point, then reduce vividness, widen the dark end and recheck every pair |
97
+ | `prefers-color-scheme` setting some tokens and a `.dark` class setting others | Pick one switching mechanism and use it throughout |
98
+ | Contrast fixed by changing hue | Change lightness, the channel contrast responds to |
99
+ | P3 color with no sRGB fallback | Declare the sRGB value first, then override inside `@media (color-gamut: p3)` |
100
+
101
+ ## Reporting
102
+
103
+ **Severity.** `HIGH` makes content unreadable or assigns a misleading semantic color. `MEDIUM` is a noticeable theme, token, or gamut failure. `LOW` is isolated polish.
104
+
105
+ **Verification.** Without a browser: token values, the gamut of every declared color, both theme blocks present and contrast computed from the declared token pair. With one: the background actually rendered behind the text, including opacity and any image beneath it, measured in both light and dark. A failing pair is reported, not repainted. Report every check you could not run as `Not verified`.
106
+
107
+ **Format.** Group findings under the principle each violates, ordered by severity, one row per root cause listing every location it appears in:
108
+
109
+ | Severity | Location | Before | After | Why |
110
+ | -------- | -------- | ------ | ----- | --- |
111
+
112
+ `Location` is `path/to/file:line`. `Why` names the principle and the user impact.
113
+
114
+ Report `Block` when any `HIGH` remains, `Approve` otherwise, leaving the rest in the table as work to do. Never `Approve` coverage you did not inspect. With nothing to report, state "No actionable color findings" and report verification.
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "HR: Better Colors"
3
+ short_description: "Building, naming and applying color systems"
@@ -0,0 +1,90 @@
1
+ # Color formats
2
+
3
+ Which notation to write colors in, how to convert between them and what happens at the edges of a display's gamut. Every other rule in this skill is stated perceptually and holds whatever notation you write.
4
+
5
+ ## Choosing a notation
6
+
7
+ | Notation | Good for | Weakness |
8
+ | --------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
9
+ | Hex | Universal support, compact, what design tools hand you | Opaque. No channel is readable or editable by hand |
10
+ | `rgb()` | Same reach as hex, readable alpha | Channels do not correspond to anything a designer thinks about |
11
+ | `hsl()` | Channels look like design controls | Its lightness is not perceptual and its hue drifts; a ramp built by varying lightness bunches at one end and shifts hue |
12
+ | `oklch()` | Perceptually uniform lightness, stable hue, predictable ramps | Baseline 2023, so very old browser matrices need a fallback |
13
+
14
+ **Match whatever the project already uses.** Notation is not a defect: a project on hex is not doing it wrong.
15
+
16
+ **For a genuinely new color system, `oklch()` is the best default.** Even lightness steps stay even, and a fixed hue stays fixed. See [palette-generation.md](palette-generation.md).
17
+
18
+ ```css
19
+ oklch(L C H) /* lightness 0–1, chroma 0–~0.4, hue 0–360 */
20
+ oklch(L C H / alpha) /* alpha uses a slash, never a comma */
21
+ ```
22
+
23
+ ## Converting
24
+
25
+ Convert when the user asks, when an agreed migration is in scope, or when the project is standardizing on a notation and this value is the straggler. Never convert an isolated value in a project that deliberately uses something else and never because this skill happened to load.
26
+
27
+ When conversion is in scope, change the values and nothing else:
28
+
29
+ - Leave CSS keywords alone: `currentColor`, `inherit`, `transparent`, `initial`, `unset`.
30
+ - Leave gradient functions alone. Convert the color stops inside them; do not touch the interpolation method.
31
+ - Leave colors in third-party configs that expect a specific format.
32
+ - Preserve comments and formatting.
33
+
34
+ ```css
35
+ /* Before */
36
+ color: #3b82f6;
37
+ border: 1px solid rgba(0, 0, 0, 0.1);
38
+
39
+ /* After */
40
+ color: oklch(0.623 0.188 259.815);
41
+ border: 1px solid oklch(0 0 0 / 0.1);
42
+ ```
43
+
44
+ Bulk conversion is a migration, not cleanup. It shifts every rendered color by a rounding margin and touches files nobody asked about, so it has to be the task rather than a side effect.
45
+
46
+ ## Gamut
47
+
48
+ Every sRGB color exists in Display P3, but not the reverse. P3 covers roughly 50% more colors, which matters only for the most saturated values. A color at 60% of maximum vividness looks the same on both.
49
+
50
+ Out-of-gamut colors are mapped into the display gamut. CSS gamut mapping reduces chroma while aiming to preserve lightness and hue; browser implementations can differ. Neighboring ramp steps may lose distinctions after mapping, so inspect the rendered palette on each target gamut. Maximum vividness varies by hue and lightness, so one chroma value can exceed the gamut at some steps and fit at others.
51
+
52
+ The fix is to reduce vividness while holding hue and lightness. Generate ramps against sRGB unless the product is display-restricted, and add P3 as an enhancement:
53
+
54
+ ```css
55
+ .accent {
56
+ background: #3b82f6;
57
+ }
58
+
59
+ @media (color-gamut: p3) {
60
+ .accent {
61
+ background: oklch(0.62 0.24 259);
62
+ }
63
+ }
64
+ ```
65
+
66
+ Order matters. The sRGB value comes first so every display gets something, and the P3 rule overrides only where it will render. A P3 color with no fallback is a `MEDIUM` gamut finding: browsers that cannot parse it drop the declaration.
67
+
68
+ For browser matrices predating `oklch()` support, the same layering works with `@supports`:
69
+
70
+ ```css
71
+ .accent {
72
+ background: #3b82f6;
73
+ }
74
+
75
+ @supports (color: oklch(0 0 0)) {
76
+ .accent {
77
+ background: oklch(0.62 0.19 259);
78
+ }
79
+ }
80
+ ```
81
+
82
+ Check the project's actual browser matrix before adding this. On a modern baseline it is dead weight.
83
+
84
+ ## Modern CSS worth knowing
85
+
86
+ - **`color-mix()`** derives one color from another, as in `color-mix(in oklab, var(--color-accent-solid) 15%, white)` for a tinted background. Useful for states, but keep generated values out of the token layer, since a mixed color cannot be inspected in a design tool.
87
+ - **Relative color syntax** adjusts one channel of an existing color. `oklch(from var(--color-accent-solid) calc(l - 0.1) c h)` darkens the accent by hand. Powerful and easy to overuse, since a token defined by three chained derivations is unreadable.
88
+ - **`light-dark()`** puts both appearances in one declaration. See [palette-generation.md](palette-generation.md).
89
+
90
+ All three compute at render time, so their output cannot be contrast-checked statically. Measure the rendered result.