@bompus/house-rules 0.0.0-stage → 0.11.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 (191) hide show
  1. package/CONTRIBUTING.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +347 -2
  4. package/THIRD_PARTY_NOTICES.md +188 -0
  5. package/compose.mjs +119 -0
  6. package/composition.mjs +353 -0
  7. package/config-view.mjs +194 -0
  8. package/config.mjs +320 -0
  9. package/docs/assets/banner-dark.png +0 -0
  10. package/docs/assets/banner-light.png +0 -0
  11. package/docs/assets/overview.png +0 -0
  12. package/docs/configuration.md +193 -0
  13. package/docs/npm-release.md +45 -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 +40 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +431 -0
  22. package/rules/modifiers/coded-offers.md +40 -0
  23. package/rules/modifiers/effort-estimates.md +41 -0
  24. package/rules/modifiers/land-when-done.md +12 -0
  25. package/rules/modifiers/low-quota-handoff.md +19 -0
  26. package/rules/modifiers/multi-agent.md +34 -0
  27. package/rules/modifiers/no-attribution.md +10 -0
  28. package/rules/modifiers/plan-files.md +50 -0
  29. package/rules/modifiers/question-cards.md +48 -0
  30. package/rules/modifiers/release-batching.md +13 -0
  31. package/rules/modifiers/scratch-on-disk.md +15 -0
  32. package/rules/modifiers/shared-host-load.md +48 -0
  33. package/rules/modifiers/solo-operator.md +13 -0
  34. package/rules/modifiers/squash-landing.md +22 -0
  35. package/rules/modifiers/swarmail.md +12 -0
  36. package/rules/references/github-actions.md +45 -0
  37. package/rules/references/landing.md +39 -0
  38. package/rules/references/multi-agent.md +47 -0
  39. package/rules/references/release-batching.md +43 -0
  40. package/rules/references/squash-landing.md +44 -0
  41. package/setup.mjs +375 -0
  42. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  43. package/skills/hr-agent-guidance-audit/SKILL.md +86 -0
  44. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  45. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  46. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  47. package/skills/hr-agent-guidance-refresh/SKILL.md +68 -0
  48. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  49. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  50. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  51. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  52. package/skills/hr-audit-choices/SKILL.md +67 -0
  53. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  54. package/skills/hr-benchmarking/NOTICE.md +36 -0
  55. package/skills/hr-benchmarking/SKILL.md +316 -0
  56. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  57. package/skills/hr-benchmarking/references/javascript.md +17 -0
  58. package/skills/hr-benchmarking/references/priority-findings-20261006.md +115 -0
  59. package/skills/hr-benchmarking/references/tools.md +160 -0
  60. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  61. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  62. package/skills/hr-better-accessibility/LICENSE +22 -0
  63. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  64. package/skills/hr-better-accessibility/SKILL.md +119 -0
  65. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  66. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  67. package/skills/hr-better-accessibility/forms.md +82 -0
  68. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  69. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  70. package/skills/hr-better-accessibility/screen-readers.md +101 -0
  71. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  72. package/skills/hr-better-colors/LICENSE +21 -0
  73. package/skills/hr-better-colors/NOTICE.md +37 -0
  74. package/skills/hr-better-colors/SKILL.md +114 -0
  75. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  76. package/skills/hr-better-colors/color-formats.md +90 -0
  77. package/skills/hr-better-colors/color-usage.md +140 -0
  78. package/skills/hr-better-colors/contrast.md +80 -0
  79. package/skills/hr-better-colors/palette-generation.md +104 -0
  80. package/skills/hr-better-colors/palette-structure.md +76 -0
  81. package/skills/hr-better-colors/token-naming.md +97 -0
  82. package/skills/hr-better-layout/LICENSE +22 -0
  83. package/skills/hr-better-layout/NOTICE.md +48 -0
  84. package/skills/hr-better-layout/SKILL.md +92 -0
  85. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  86. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  87. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  88. package/skills/hr-better-typography/LICENSE +22 -0
  89. package/skills/hr-better-typography/NOTICE.md +46 -0
  90. package/skills/hr-better-typography/SKILL.md +172 -0
  91. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  92. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  93. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  94. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  95. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  96. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  97. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  98. package/skills/hr-better-writing/LICENSE +22 -0
  99. package/skills/hr-better-writing/NOTICE.md +46 -0
  100. package/skills/hr-better-writing/SKILL.md +122 -0
  101. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  102. package/skills/hr-change-impact/SKILL.md +113 -0
  103. package/skills/hr-change-impact/agents/openai.yaml +5 -0
  104. package/skills/hr-code-review/LICENSE +21 -0
  105. package/skills/hr-code-review/NOTICE.md +26 -0
  106. package/skills/hr-code-review/SKILL.md +147 -0
  107. package/skills/hr-code-review/agents/openai.yaml +3 -0
  108. package/skills/hr-code-review/references/evidence.md +57 -0
  109. package/skills/hr-design-exploration/SKILL.md +82 -0
  110. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  111. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  112. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  113. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  114. package/skills/hr-diagnosing-bugs/references/reporting.md +72 -0
  115. package/skills/hr-explain-code/NOTICE.md +37 -0
  116. package/skills/hr-explain-code/SKILL.md +110 -0
  117. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  118. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  119. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  120. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  121. package/skills/hr-handoff/SKILL.md +91 -0
  122. package/skills/hr-handoff/agents/openai.yaml +5 -0
  123. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  124. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  125. package/skills/hr-handoff/assets/source.example.json +20 -0
  126. package/skills/hr-handoff/references/reconciliation.md +105 -0
  127. package/skills/hr-handoff/scripts/check-reconciliation.mjs +282 -0
  128. package/skills/hr-house-rules-setup/SKILL.md +100 -0
  129. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  130. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  131. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  132. package/skills/hr-lean-plan/SKILL.md +142 -0
  133. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  134. package/skills/hr-maintainability-review/SKILL.md +176 -0
  135. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  136. package/skills/hr-manual-qa/NOTICE.md +36 -0
  137. package/skills/hr-manual-qa/SKILL.md +37 -0
  138. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  139. package/skills/hr-navigation-retro/LICENSE +21 -0
  140. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  141. package/skills/hr-navigation-retro/SKILL.md +86 -0
  142. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  143. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  144. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  145. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  146. package/skills/hr-ordering-tests/SKILL.md +51 -0
  147. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  148. package/skills/hr-plain-prose/SKILL.md +261 -0
  149. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  150. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  151. package/skills/hr-pr-followup/LICENSE +21 -0
  152. package/skills/hr-pr-followup/NOTICE.md +9 -0
  153. package/skills/hr-pr-followup/SKILL.md +71 -0
  154. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  155. package/skills/hr-pr-followup/references/review-helpers.md +90 -0
  156. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  157. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  158. package/skills/hr-progress-report/SKILL.md +49 -0
  159. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  160. package/skills/hr-read-reddit/SKILL.md +44 -0
  161. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  162. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  163. package/skills/hr-read-x-links/SKILL.md +42 -0
  164. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  165. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  166. package/skills/hr-split-to-prs/LICENSE +21 -0
  167. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  168. package/skills/hr-split-to-prs/SKILL.md +61 -0
  169. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  170. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  171. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  172. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  173. package/skills/hr-tdd/LICENSE +21 -0
  174. package/skills/hr-tdd/NOTICE.md +10 -0
  175. package/skills/hr-tdd/SKILL.md +40 -0
  176. package/skills/hr-tdd/agents/openai.yaml +3 -0
  177. package/skills/hr-tdd/mocking.md +39 -0
  178. package/skills/hr-tdd/tests.md +77 -0
  179. package/skills/hr-test-audit/SKILL.md +146 -0
  180. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  181. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  182. package/skills/hr-what-next/SKILL.md +61 -0
  183. package/skills/hr-what-next/agents/openai.yaml +4 -0
  184. package/skills/hr-what-next/references/reporting.md +68 -0
  185. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  186. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  187. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  188. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  189. package/skills/hr-writing-pr/NOTICE.md +48 -0
  190. package/skills/hr-writing-pr/SKILL.md +104 -0
  191. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
@@ -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.
@@ -0,0 +1,140 @@
1
+ # Color usage
2
+
3
+ Deploying color once the system exists: meaning, emphasis, gradients and appearance variants. For picking values see [palette-generation.md](palette-generation.md), for naming them [token-naming.md](token-naming.md), for checking pairs [contrast.md](contrast.md).
4
+
5
+ ## One color, one meaning
6
+
7
+ Users read a near-miss in hue as a slightly different shade, not as a different color.
8
+
9
+ ```css
10
+ /* Bad: the accent means both "link" and "decorative heading" */
11
+ a {
12
+ color: #3b82f6;
13
+ }
14
+ .section-title {
15
+ color: #4f8ef7;
16
+ }
17
+
18
+ /* Good: interactive elements own the accent; headings stay neutral */
19
+ a {
20
+ color: var(--color-accent-text);
21
+ }
22
+ .section-title {
23
+ color: var(--color-text-primary);
24
+ }
25
+ ```
26
+
27
+ The rule runs both ways. A color must not be _absent_ where its meaning occurs. If the accent means interactive, an interactive element rendered neutral is just as misleading.
28
+
29
+ Color is never the only carrier of meaning. Pair it with an icon, a label, or a shape. `hr-better-accessibility` owns that requirement.
30
+
31
+ ## Use tokens in their role
32
+
33
+ Apply a semantic token only for the role it names. `--color-text-secondary` is muted foreground text. Use it as a background and every future theme change that assumes the role breaks, because a value that happened to work as both stops working as both.
34
+
35
+ ```css
36
+ /* Bad: separator token repurposed as a text color because it looked right */
37
+ .caption {
38
+ color: var(--color-border);
39
+ }
40
+
41
+ /* Bad: text token repurposed as a background */
42
+ .tag {
43
+ background: var(--color-text-secondary);
44
+ }
45
+ ```
46
+
47
+ The role inventory in [token-naming.md](token-naming.md) is the list of roles a system needs.
48
+
49
+ ## One colored action per view
50
+
51
+ Preserve an established component hierarchy that communicates emphasis another way. Do not recolor controls merely to impose this recipe.
52
+
53
+ ```html
54
+ <!-- Good: one filled primary action, neutral secondaries -->
55
+ <button class="bg-accent-solid text-white">Save</button>
56
+ <button class="text-neutral-700">Cancel</button>
57
+
58
+ <!-- Bad: every action colored, so nothing is primary -->
59
+ <button class="bg-accent-solid text-white">Save</button>
60
+ <button class="bg-accent-solid text-white">Duplicate</button>
61
+ <button class="bg-accent-solid text-white">Export</button>
62
+ ```
63
+
64
+ Selected states may use the accent on the glyph and label. An active tab or a checked segment is state, not emphasis.
65
+
66
+ ## Gradients
67
+
68
+ **The interpolation space is a look, not a correctness setting.** Three are worth knowing, and the difference between them is most visible in the middle of the gradient:
69
+
70
+ ```css
71
+ /* sRGB: the default and the classic. Midpoint darkens and mutes. */
72
+ background: linear-gradient(#3b82f6, #ec4899);
73
+
74
+ /* oklab: even brightness across the transition. The best default. */
75
+ background: linear-gradient(in oklab, #3b82f6, #ec4899);
76
+
77
+ /* oklch: travels around the hue wheel, staying vivid throughout. */
78
+ background: linear-gradient(in oklch, #3b82f6, #ec4899);
79
+ ```
80
+
81
+ `oklab` and sRGB are **rectangular**, interpolating in a straight line through the color space. `oklch` is **polar**, interpolating the hue angle, so it arcs around the wheel through every hue between the stops. That is why it stays saturated and why it can produce hues nobody asked for. A blue-to-pink gradient routes through purple, which is either the look or a surprise.
82
+
83
+ **The gray dead zone is a rectangular-space problem.** Two hues on opposite sides of the wheel sit either side of the neutral axis. A straight line between them passes near gray, and the middle goes lifeless. Either switch to a polar space, which routes around the axis, or add a third stop between the two and keep the space you have.
84
+
85
+ With a polar space you also control which way it goes around:
86
+
87
+ ```css
88
+ /* The short way round, usually what you want */
89
+ background: linear-gradient(in oklch shorter hue, #3b82f6, #ec4899);
90
+
91
+ /* The long way, sweeps most of the spectrum */
92
+ background: linear-gradient(in oklch longer hue, #3b82f6, #ec4899);
93
+ ```
94
+
95
+ **Banding shows up on large areas.** A gradient spanning a hero with little contrast between its stops steps visibly on 8-bit displays. Widen the contrast, shrink the area, or overlay a subtle noise texture.
96
+
97
+ **Keep text off gradients where you can.** Contrast varies continuously across one, so a single measurement does not describe it. Where text must sit on a gradient, measure the worst region rather than the average, or put a scrim behind it.
98
+
99
+ ## Color across cultures
100
+
101
+ Color meaning is not universal. Where a color is load-bearing in finance, status, or alerts, verify the meaning holds in every locale you ship to.
102
+
103
+ | Color | Common Western reading | Elsewhere |
104
+ | ----- | ---------------------- | ---------------------------------------------------- |
105
+ | Red | Danger, loss, errors | Luck, prosperity; **gains** in Chinese financial UIs |
106
+ | Green | Success, gains, go | Losses in Chinese financial UIs |
107
+ | White | Purity, cleanliness | Mourning in parts of East Asia |
108
+ | Gold | Premium, luxury | Religious significance in some regions |
109
+
110
+ The classic case is stock tickers, which show gains in green for English locales and red for Chinese ones. Where the product ships to such markets, make gain and loss per-locale tokens rather than hardcoded values.
111
+
112
+ ## Light, dark and increased contrast
113
+
114
+ Every custom color needs a light and a dark variant, derived per [palette-generation.md](palette-generation.md). Beyond that, users who enable increased contrast expect visibly stronger differentiation:
115
+
116
+ ```css
117
+ :root {
118
+ --color-accent-solid: #3b82f6;
119
+ }
120
+
121
+ @media (prefers-color-scheme: dark) {
122
+ :root {
123
+ --color-accent-solid: #60a5fa;
124
+ }
125
+ }
126
+
127
+ @media (prefers-contrast: more) {
128
+ :root {
129
+ --color-accent-solid: #1d4ed8;
130
+ }
131
+ }
132
+
133
+ @media (prefers-color-scheme: dark) and (prefers-contrast: more) {
134
+ :root {
135
+ --color-accent-solid: #bfdbfe;
136
+ }
137
+ }
138
+ ```
139
+
140
+ Check light, dark and each increased-contrast combination against its actual surfaces. The example values illustrate the cascade, not guaranteed contrast for an unspecified background. The increased-contrast variant widens the foreground/background gap by at least 15 points of perceived lightness over the default. Re-verify against APCA's preferred thresholds, Lc 90 body and Lc 75 non-body. Widening the gap without remeasuring is not fixing it.
@@ -0,0 +1,80 @@
1
+ # Contrast
2
+
3
+ Contrast is measured between a **foreground color**, meaning text, an icon, or a UI element, and the **background color** it actually renders against, usually the nearest ancestor that paints one. Identify that background first. Measuring against the page background when the element sits on a card gives the wrong answer.
4
+
5
+ **Report, don't repaint.** When a check fails, report the pair, its measured value and the threshold it misses, and leave the colors unchanged. They are a design decision. Apply the fix below only when asked.
6
+
7
+ `hr-better-accessibility` decides when contrast is required and whether a given pair must pass. This file covers measuring the pair and, on request, changing it.
8
+
9
+ ## APCA thresholds (recommended)
10
+
11
+ APCA (Accessible Perceptual Contrast Algorithm) models perceived contrast more accurately than WCAG 2 and is the better default for design decisions. Lc (Lightness Contrast) measures perceived contrast between foreground and background. These levels simplify APCA's full font-size and weight lookup table:
12
+
13
+ | Content type | Minimum | Preferred |
14
+ | ---------------------------------------------------------------------- | ------- | --------- |
15
+ | Body text (columns or blocks of text) | Lc 75 | Lc 90 |
16
+ | Non-body text (labels, headlines) | Lc 60 | Lc 75 |
17
+ | Large text meeting APCA size/weight guidance (e.g. 36px at weight 400) | Lc 45 | Lc 60 |
18
+ | Large solid semantic shapes | Lc 30 | n/a |
19
+ | Fine-detail pictograms and small outline icons | Lc 45 | n/a |
20
+
21
+ Use [APCA's size and weight guidance](https://github.com/Myndex/SAPC-APCA/blob/master/documentation/APCA_in_a_Nutshell.md) for the actual typeface; 36px text at weight 300 needs Lc 60. Lc 30 is also APCA's minimum for disabled and placeholder text. Lc 15 applies only to sufficiently large, solid non-semantic elements.
22
+
23
+ Lc is signed: positive means dark text on a light background, negative means light text on a dark background. Compare the absolute value against the threshold.
24
+
25
+ ## WCAG 2 thresholds (for legal compliance)
26
+
27
+ WCAG 2 is still required for formal WCAG 2.x conformance claims. Its luminance ratio is both too strict and too lenient depending on the pair, but it has the legal standing.
28
+
29
+ | Content type | AA | AAA |
30
+ | ----------------------------------- | ----- | ----- |
31
+ | Normal text (<24px / <18.67px bold) | 4.5:1 | 7:1 |
32
+ | Large text (≥24px / ≥18.67px bold) | 3:1 | 4.5:1 |
33
+ | UI components and graphical objects | 3:1 | n/a |
34
+
35
+ WCAG defines large text in points: 18pt ≈ `24px`, 14pt bold ≈ `18.67px`.
36
+
37
+ When a project must claim WCAG conformance, WCAG is the gate and APCA is the tiebreaker for anything above it.
38
+
39
+ ## Fixing a failing pair (on request)
40
+
41
+ **Change lightness first.** It is the channel contrast responds to. Hue and saturation move the measured value far less, so fixing contrast by changing hue is wasted effort.
42
+
43
+ Move the foreground away from the background in perceived lightness, holding hue and saturation, then remeasure. Keeping hue fixed is what stops a contrast fix becoming a palette change.
44
+
45
+ ```css
46
+ /* Failing: text too close to its background in lightness (Lc ≈ 50) */
47
+ color: #7d93b0;
48
+ background: #eef2f7;
49
+
50
+ /* Fixed: darker text, same hue (Lc ≈ 90) */
51
+ color: #2b3a4f;
52
+ background: #eef2f7;
53
+ ```
54
+
55
+ Two constraints on the fix:
56
+
57
+ - **Mid-lightness backgrounds cap what is achievable.** On a background near 75% perceived lightness, even pure black text reaches only about Lc 60. Body text needs a background near one extreme, so a mid-range background is the thing that has to change.
58
+ - **Pushing lightness can push the color out of gamut.** Reduce saturation as needed to keep it renderable. See [color-formats.md](color-formats.md).
59
+
60
+ Always remeasure after changing a value. Do not assume a fix landed.
61
+
62
+ ## Quick approximations
63
+
64
+ Useful for a first pass and approximations. Verify by measuring before reporting a result.
65
+
66
+ For body text targeting |Lc| ≥ 75:
67
+
68
+ - **Light background (above ~90% perceived lightness):** foreground below ~35%.
69
+ - **Dark background (below ~25% perceived lightness):** foreground above ~90%.
70
+
71
+ The gap is asymmetric because APCA is polarity-aware. Mirrored pairs do not score identically, which is why a pair passing in light mode can fail in dark.
72
+
73
+ **Light or dark background?** Under APCA the crossover is around 73% perceived lightness: above it dark text scores higher, at or below it light text does. Under WCAG 2 the crossover for neutrals sits near 56% OKLCH lightness, so between roughly 56% and 73% white text can fail AA where black passes. Measure both candidates against the standard the project gates on.
74
+
75
+ ## What to check
76
+
77
+ - **Every pair, in every appearance.** A pair passing in light mode can fail in dark. The palettes are not mirror images.
78
+ - **Translucent surfaces.** A color on a `backdrop-filter` header or an overlay shifts with whatever scrolls behind it. Test against the lightest and darkest content it can sit over, or make the surface opaque enough that the shift cannot break the pair.
79
+ - **Computed colors.** `color-mix()`, relative color syntax and opacity modifiers resolve at render time; measure the rendered result, not the declaration.
80
+ - **Text over images.** There is no single background color. Measure the worst region, or guarantee one with a scrim.
@@ -0,0 +1,104 @@
1
+ # Palette generation
2
+
3
+ Producing the values once the structure is decided. For which ramps to build and what each step is for, see [palette-structure.md](palette-structure.md).
4
+
5
+ ## Start from the brand color
6
+
7
+ A brand color arrives as one value, usually a hex. Two decisions come before any ramp exists:
8
+
9
+ **Which step does it occupy?** A brand color meant for buttons and links belongs on the solid-fill step: `500` in Tailwind, `9` in Radix. Then `bg-brand-500` renders the actual brand color, not an approximation.
10
+
11
+ **Is it pinned or snapped?** Pin a contractually fixed brand color. It stays exact, the ramp builds outward from it and that one step spaces slightly unevenly. Otherwise snap it onto the ramp so it follows the chosen lightness spacing. That looks better almost always, and nobody notices without a swatch held to the screen.
12
+
13
+ A brand color that fails contrast behind white text is still the brand color, just not the solid-fill step. Put it where it lands and use a darker step for interactive fills. Never quietly darken the brand.
14
+
15
+ ## What a correct ramp looks like
16
+
17
+ Properties of the finished ramp, checkable against any output in any notation:
18
+
19
+ - **Steps follow a deliberate perceived-lightness progression.** Use a perceptual color space rather than evenly spaced HSL values. The denser light-end spacing below takes priority over equal intervals across the whole ramp.
20
+ - **Hue is constant end to end.** Every step is recognisably the same color. A wandering hue reads as two colors blended and will not sit correctly against a neutral built on a different hue.
21
+ - **Vividness peaks in the middle and falls off at both ends.** The lightest and darkest steps are nearly neutral; the middle carries the color. Holding full vividness into the extremes gives a `50` that glows and a `950` like ink spilled on the brand.
22
+ - **Steps are denser at the light end.** Light backgrounds need finer distinctions than dark ones. Keep `50` to `200` close together and `800` to `950` further apart. Even spacing across the whole range makes the pale end unusable, because `50` and `100` stop reading as two surfaces.
23
+ - **No two adjacent steps are indistinguishable.** If `200` and `300` look identical on a calibrated screen, the ramp has more steps than decisions. Drop one.
24
+ - **Both ends stop short of pure black and white.** A ramp that reaches them loses its identity exactly where the page background lives.
25
+
26
+ ## Use a color library
27
+
28
+ Never compute these by hand or by eye. `culori`, `colorjs.io` and `chroma.js` all convert between notations, measure perceived lightness and interpolate perceptually. Read the brand color in whatever format it arrives, do the math in a perceptual space and emit the project's notation:
29
+
30
+ ```js
31
+ import { formatHex, interpolate, samples } from "culori";
32
+
33
+ // Interpolation preview only: uniform samples are not a finished role-based ramp.
34
+ const ramp = interpolate(["#eff6ff", "#3b82f6", "#172554"], "lab");
35
+ const steps = samples(11).map((t) => formatHex(ramp(t)));
36
+ ```
37
+
38
+ The example previews interpolation with uniform samples; it does not generate a finished role-based ramp. The output format is the project's choice. For a ramp the interpolation space is not, because lightness spacing must be deliberate and sRGB interpolation produces muddy mid-steps. Choose sample positions to achieve the denser light-end spacing, then inspect the rendered steps. Decorative gradients are the opposite case, where the space is a deliberate look ([color-usage.md](color-usage.md)).
39
+
40
+ ```css
41
+ :root {
42
+ --brand-50: #eff6ff;
43
+ --brand-100: #dbeafe;
44
+ --brand-200: #bfdbfe;
45
+ --brand-300: #93c5fd;
46
+ --brand-400: #60a5fa;
47
+ --brand-500: #3b82f6;
48
+ --brand-600: #2563eb;
49
+ --brand-700: #1d4ed8;
50
+ --brand-800: #1e40af;
51
+ --brand-900: #1e3a8a;
52
+ --brand-950: #172554;
53
+ }
54
+ ```
55
+
56
+ ## Several hues at once
57
+
58
+ With an accent plus status ramps, the ramps must agree step for step. `danger-500` and `brand-500` should read as equally bright and vivid, or a red button looks heavier than a blue one at the same step.
59
+
60
+ - **Match perceived lightness exactly.** Same step, same brightness, across every hue.
61
+ - **Match vividness relatively, not absolutely.** Hues do not share a maximum vividness. A saturated yellow and a saturated blue are not equally far from gray, and no format makes them so. Set each ramp to the same _proportion_ of what its own hue reaches. Copying a saturation number across hues leaves one washed out.
62
+
63
+ Yellows and cyans are the usual casualties, peaking much lower than reds and blues. Copy the numbers across and the warning color looks weak beside the danger one.
64
+
65
+ ## Dark mode
66
+
67
+ A dark palette is not the light one reversed. Reversal is the starting point, not the output.
68
+
69
+ Swap the semantic roles first, then tune the values:
70
+
71
+ ```css
72
+ :root {
73
+ --color-bg: var(--brand-50);
74
+ --color-text: var(--brand-950);
75
+ }
76
+
77
+ .dark {
78
+ --color-bg: var(--brand-950);
79
+ --color-text: var(--brand-50);
80
+ }
81
+ ```
82
+
83
+ Three things almost always need hand-tuning after the swap:
84
+
85
+ - **Vividness comes down.** A color that reads as confident on white reads as neon on near-black. Dark appearances need the accent a step or two less vivid.
86
+ - **The dark end needs more separation.** Steps distinguishable as pale backgrounds collapse into each other as dark surfaces.
87
+ - **Contrast does not survive the mirror.** A pair passing in light mode can fail reversed, because contrast is not symmetric. Recheck every foreground against its real background in both appearances ([contrast.md](contrast.md)).
88
+
89
+ ### Choosing the switching mechanism
90
+
91
+ Pick one and use it throughout:
92
+
93
+ - **`prefers-color-scheme` alone** is correct with no theme toggle. Nothing to persist, nothing to hydrate.
94
+ - **A `.dark` class** is required as soon as users can override the system setting. The media query then sets only the initial value.
95
+ - **`light-dark()`** collapses both values into one declaration, the least code when the project also sets `color-scheme`. It reads that property rather than a class, so a class-based toggle must set `color-scheme` too.
96
+
97
+ ```css
98
+ :root {
99
+ color-scheme: light dark;
100
+ --color-bg: light-dark(#ffffff, #172554);
101
+ }
102
+ ```
103
+
104
+ Mixing mechanisms is the common failure. A media query setting some tokens and a class setting others gives a half-themed interface the moment a user overrides their system preference.
@@ -0,0 +1,76 @@
1
+ # Palette structure
2
+
3
+ What a color system is made of, before any values exist. For computing the values see [palette-generation.md](palette-generation.md); for what to call them see [token-naming.md](token-naming.md).
4
+
5
+ ## What a system needs
6
+
7
+ Most products need three kinds of ramp and nothing else:
8
+
9
+ | Ramp | How many | Notes |
10
+ | ------- | -------- | -------------------------------------------------------------------------------------- |
11
+ | Neutral | 1 | Carries 80–90% of the interface: backgrounds, borders, body text |
12
+ | Accent | 1 | The brand hue. Interactive and selected states |
13
+ | Status | 0–4 | `danger`, `warning`, `success`, `info`. Add one only when the product shows that state |
14
+
15
+ Add a second accent only when two things must be distinguishable, as required by `SKILL.md`. Check the rendered colors side by side in each appearance; if they are indistinguishable, use the existing accent ramp's steps or choose a distinct second hue.
16
+
17
+ ## Every step has a job
18
+
19
+ Each step maps to a role. Generate the steps the roles below call for and skip the rest.
20
+
21
+ | Role | Tailwind | Radix |
22
+ | --------------------------- | -------- | ----- |
23
+ | Page background | `50` | `1` |
24
+ | Subtle background | `50` | `2` |
25
+ | Component background | `100` | `3` |
26
+ | Component hover | `200` | `4` |
27
+ | Component active / selected | `200` | `5` |
28
+ | Subtle border | `200` | `6` |
29
+ | Border, separator | `300` | `7` |
30
+ | Strong border, focus ring | `400` | `8` |
31
+ | Solid fill | `500` | `9` |
32
+ | Solid fill hover | `600` | `10` |
33
+ | Low-contrast text | `700` | `11` |
34
+ | High-contrast text | `900` | `12` |
35
+
36
+ The two conventions differ in kind, not only in numbering:
37
+
38
+ - **Radix defines its 12 steps by role.** Step 9 is "the solid fill" in every ramp and appearance. The dark scale is a separate ramp reusing the same numbers, so `--accent-9` is the fill in both and component CSS never changes.
39
+ - **Tailwind defines its 11 steps by lightness.** `50` is light, `950` is dark. The mapping above therefore holds in light mode and inverts in dark, with the page background at `950` and high-contrast text at `50`. Components either swap step numbers per appearance or read a semantic token that swaps once.
40
+
41
+ Match whichever the project uses. For a new system prefer Radix's model, because a role-defined step survives a theme change that a lightness-defined step does not. On Tailwind, keep `50`–`950` and put the role mapping in the semantic tier.
42
+
43
+ Tailwind's 11 steps cover 12 roles, so some do double duty. Where the table repeats a step, the two roles are adjacent in practice and the collision is real. A design needing a subtle border and a component hover to be distinguishable needs a 12-step ramp.
44
+
45
+ ## Neutrals
46
+
47
+ A pure gray ramp is a perfectly good default. It sits under any accent hue without competing and never needs revisiting when the brand color changes.
48
+
49
+ Tinting the neutral toward the accent hue is a stylistic option, not a correction. A trace of the accent, a few percent of its vividness, puts the greys in the same family as the accent rather than merely coexisting with it. Enough to measure, not enough to name. Neither choice reads as a mistake.
50
+
51
+ Warm neutrals, hue toward orange, read approachable and editorial; cool ones, toward blue, read technical and precise. Whichever you pick, including none, hold it across the whole ramp. A warm gray border on a cool gray background is visible even when neither color is nameable alone.
52
+
53
+ Neutrals carry the most roles, so they need the most steps. Never generate fewer neutral steps than accent steps.
54
+
55
+ ## Status colors
56
+
57
+ Convention constrains status hues before taste does. Red reads as danger, amber as warning, green as success. See the cultural exceptions in [color-usage.md](color-usage.md).
58
+
59
+ Two rules govern them:
60
+
61
+ - **Keep every status hue distinct from the accent.** If the brand is red, the danger ramp cannot also be red. Move danger toward a deeper crimson and check the two side by side, or the destructive and primary actions are the same button.
62
+ - **Status ramps need fewer steps than the accent.** Most render four roles: a background, a border, a solid fill and text. Generate the full ramp only where the product styles status components across the whole range.
63
+
64
+ Status color is never the only signal of a state change; pair it with an icon or text. `hr-better-accessibility` owns that requirement.
65
+
66
+ ## Auditing an existing palette
67
+
68
+ Before restructuring a system, inventory it. Most codebases hold several times more colors than the design has decisions.
69
+
70
+ 1. **Collect every literal.** Grep for hex, `rgb(`, `hsl(`, `oklch(` and the project's utility-class prefixes. Include SVG `fill`/`stroke`, chart configs and email templates. Colors hide outside stylesheets.
71
+ 2. **Sort by perceived lightness within each hue family.** Duplicates surface immediately as near-identical neighbors.
72
+ 3. **Collapse near-duplicates.** Two colors closer than about one ramp step are one color that drifted. Keep the one used most and retire the others. Never average them.
73
+ 4. **Assign each survivor a role** from the table above. A color matching no role is a missing token or a mistake. Decide which, and say so in the finding.
74
+ 5. **Count what is left.** More than one ramp per role above means the palette outgrew its structure, not that the product needs more color.
75
+
76
+ Report the inventory before changing anything. Consolidating a palette changes rendered output on screens nobody asked you to touch, so it stays a proposal until the user accepts it.