command-code 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,20 @@
1
1
  # command-code
2
2
 
3
+ ## 1.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ feat(design): add accessibility discipline and shared finding severity to the bundled /design skill
8
+
9
+ ### Patch Changes
10
+
11
+ - feat: default ZDR mode vision to MiMo V2.5 instead of Kimi K2.5
12
+
3
13
  ## 1.25.0
4
14
 
5
15
  ### Patch Changes
6
16
 
7
- - feat: add GLM-5.3
17
+ - feat: add GLM-5.3
8
18
 
9
19
  ## 1.24.0
10
20
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: design
3
- description: "Design partner for frontend interfaces. One command covers every visual discipline: color, typography, layout, motion, interaction, responsive behavior, voice, surface, review, and refinement."
3
+ description: "Design partner for frontend interfaces. One command covers every visual discipline: color, typography, layout, motion, interaction, accessibility, responsive behavior, voice, surface, review, and refinement."
4
4
  ---
5
5
 
6
6
  # Design
@@ -29,6 +29,8 @@ These modes report findings only. Fixes happen when the user runs `/design redes
29
29
 
30
30
  No design changes. No fixing. No calling other modes. Just reports.
31
31
 
32
+ All three use the shared severity scale, evidence bar, findings table, and verdict in [references/severity.md](references/severity.md). A finding without a severity, a location, and a concrete **After** is an observation, not a finding — the fix modes cannot consume it.
33
+
32
34
  ## Bare `/design` routing
33
35
 
34
36
  When the user runs `/design` with no tool and no freeform prompt, I do not show the table. I route immediately and act.
@@ -43,7 +45,7 @@ If interface code exists, I check `.commandcode/design/` for any of:
43
45
  - `review-report.md`
44
46
  - `smell-report.md`
45
47
 
46
- **If a report exists**, the interface has already been audited. I read it, identify the highest-severity findings, and choose `redesign`, `relayout`, or `refine` — whichever addresses what the report flags most critically. I apply those changes to real files now.
48
+ **If a report exists**, the interface has already been audited. I read it, identify the highest-severity findings, and choose `redesign`, `relayout`, `a11y`, or `refine` — whichever addresses what the report flags most critically. I apply those changes to real files now. When the report contains an escalation trigger from [references/severity.md](references/severity.md), that is what I fix first, ahead of any visual finding.
47
49
 
48
50
  **If no report exists**, the interface has not been diagnosed yet. I run an audit first, then act on the findings in the same pass. I do not stop after writing the report.
49
51
 
@@ -53,7 +55,7 @@ I pick the audit tool that fits:
53
55
  - `checkup` — for a fast vitals scan with traffic-light scores
54
56
  - `review` — for a thorough critique with scoring and a section-by-section walkthrough
55
57
 
56
- After the audit I write the report to `.commandcode/design/`, then immediately apply the most critical fixes via `redesign`, `relayout`, or `refine`. The report is the diagnostic. The design change is the treatment. I do not deliver one without the other.
58
+ After the audit I write the report to `.commandcode/design/`, then immediately apply the most critical fixes via `redesign`, `relayout`, `a11y`, or `refine`. The report is the diagnostic. The design change is the treatment. I do not deliver one without the other.
57
59
 
58
60
  ## Composition comes from work
59
61
 
@@ -133,7 +135,7 @@ I prioritize confirmed blockers, high-severity issues, repeated smell patterns,
133
135
 
134
136
  I do not merely mention the report. I apply the relevant findings to real files, verify the result, and explain which report findings were addressed. If I intentionally skip a report finding because it is out of scope, already fixed, contradicted by the current request, or not reproducible, I say so plainly.
135
137
 
136
- Report-producing modes (`review`, `checkup`, and `smell`) create the required markdown and HTML report artifacts. Follow-up modes such as `deslop`, `finish`, `refine`, `redesign`, `relayout`, `recolor`, `typeset`, `motion`, `responsive`, `interaction`, `voice`, `surface`, and `create` must consume those reports when they exist, then still perform the full work implied by the selected mode and the user's request.
138
+ Report-producing modes (`review`, `checkup`, and `smell`) create the required markdown and HTML report artifacts, using the severity scale and findings format in [references/severity.md](references/severity.md). Follow-up modes such as `deslop`, `finish`, `refine`, `redesign`, `relayout`, `recolor`, `typeset`, `motion`, `responsive`, `interaction`, `a11y`, `voice`, `surface`, and `create` must consume those reports when they exist, then still perform the full work implied by the selected mode and the user's request.
137
139
 
138
140
  ## Blank project behavior
139
141
 
@@ -192,6 +194,7 @@ The HTML file becomes the working canvas. All subsequent design work builds on t
192
194
  | `recolor [target]` | Systems | Build a color system: palette, roles, contrast, state color | [references/color.md](references/color.md) |
193
195
  | `motion [target]` | Systems | Add a page-wide motion system, then tune existing motion | [references/motion.md](references/motion.md) |
194
196
  | `interaction [target]` | Systems | Add missing behavior, states, affordances, feedback, targets | [references/interaction.md](references/interaction.md) |
197
+ | `a11y [target]` | Systems | Repair the access system: focus, keyboard path, semantics, names, forms, announcements, hit areas, zoom | [references/accessibility.md](references/accessibility.md) |
195
198
  | `relayout [target]` | Compose | Change the structural composition, not just spacing | [references/relayout.md](references/relayout.md) |
196
199
  | `responsive [target]` | Compose | Recompose across screens, devices, input modes, contexts | [references/responsive.md](references/responsive.md) |
197
200
  | `redesign [target]` | Build | Complete visual transformation of existing interface | [references/redesign.md](references/redesign.md) |
@@ -205,6 +208,8 @@ The HTML file becomes the working canvas. All subsequent design work builds on t
205
208
  Discipline references, available to any tool:
206
209
 
207
210
  - [references/color.md](references/color.md), [references/layout.md](references/layout.md), [references/border.md](references/border.md), [references/shadow.md](references/shadow.md), [references/motion.md](references/motion.md), [references/interaction.md](references/interaction.md), [references/responsive.md](references/responsive.md), [references/writing.md](references/writing.md)
211
+ - [references/accessibility.md](references/accessibility.md) for focus, keyboard, ARIA, forms, live regions, hit areas, zoom (`a11y`) — required reading for any mode that touches a control, a form, an overlay, or motion
212
+ - [references/severity.md](references/severity.md) for the shared severity scale, escalation triggers, findings table, and verdict used by every report mode
208
213
  - [references/smell.md](references/smell.md) for the AI-tells catalog
209
214
  - [references/voice.md](references/voice.md) for marketing, landing, portfolio surfaces (`voice`)
210
215
  - [references/surface.md](references/surface.md) for app UI, dashboards, tools (`surface`)
@@ -340,6 +345,21 @@ Words are interface. Bad copy breaks experiences faster than bad color.
340
345
  - **Sentence case everywhere.** "Save changes" not "Save Changes".
341
346
  - **Strip filler.** Restated headings, marketing preamble, and transition sentences add noise. Cut anything that exists to fill space.
342
347
 
348
+ ### Access is the Floor
349
+
350
+ This is not a checklist I run once the design is done. It is the ground every other discipline stands on, and most of it costs nothing as long as I stop fighting the platform: the browser already gives buttons a keyboard, already reads a real label aloud, already draws a focus ring until someone writes a rule to erase it.
351
+
352
+ - **Native first, ARIA last.** `<button>` for actions, `<a href>` for navigation, never `<div onClick>`. No ARIA is better than bad ARIA — a screen reader trusts my roles, so a wrong role is worse than none. A role is a promise of a full keyboard model.
353
+ - **Walk it twice before judging.** Once with the keyboard only: every flow must complete without a mouse. Once as a screen-reader user: does each control announce a name, a role, and its state?
354
+ - **`:focus-visible`, never bare `:focus`,** and never `outline: none` without a verified replacement.
355
+ - **Two legal `tabindex` values:** `0` and `-1`. A positive value hijacks the tab order for the whole page; the DOM order is what needs fixing.
356
+ - **Modals trap and restore.** `inert` on the background, focus moved in on open, focus returned to the trigger on close.
357
+ - **Placeholders are never labels,** and submit is never disabled until the form is valid — a disabled action hides the thing that must be fixed.
358
+ - **Never color alone.** Every state carried by color also carries an icon, text, or shape.
359
+ - **It has to survive 200% zoom and reflow at 320px.** Fixed heights on text containers are what break first.
360
+
361
+ These eight failures are `HIGH` on sight in any report, never averaged down because the surface is minor. The full list and the mechanics behind each rule: [references/accessibility.md](references/accessibility.md) and [references/severity.md](references/severity.md).
362
+
343
363
  ### The Smell Test
344
364
 
345
365
  If a stranger can look at the design for two seconds and say "AI made that" without hesitation, it has failed. The fix is rarely a single edit — it is almost always a category reflex. If the palette, layout, and type choice are all predictable from the industry alone — SaaS in cream and purple, developer tool in dark terminal mono, fintech in navy serif, health app in white and teal — rework the scene sentence and color strategy until none of those answers fit.
@@ -0,0 +1,404 @@
1
+ # Accessibility: `/design a11y`
2
+
3
+ This is not a checklist I run once the design is done. It is the ground the design stands on, and most of it costs nothing as long as I stop fighting the platform: the browser already gives buttons a keyboard, already reads a real label aloud, already draws a focus ring until someone writes a rule to erase it.
4
+
5
+ A design that only works for a sighted mouse user is a sketch, not a design.
6
+
7
+ ---
8
+
9
+ ## Pre-execution checklist
10
+
11
+ Before proceeding, check for existing reports in the `.commandcode/design/` directory. Look for these files:
12
+
13
+ - `checkup-report.md`
14
+ - `review-report.md`
15
+ - `smell-report.md`
16
+
17
+ If any of these files exist, read the report content and use it as context for your analysis. Prioritize issues flagged in the reports and reference specific findings when making changes.
18
+
19
+ If no reports are found, proceed with the task normally.
20
+
21
+ ---
22
+
23
+ ## Access Follows Composition
24
+
25
+ The work pattern decides which access failures hurt most.
26
+
27
+ Monitor screens fail when alerts are color-only, live updates never announce, or an operator cannot acknowledge without a mouse.
28
+
29
+ Operate screens fail when direct manipulation has no keyboard equivalent, focus is lost after a command, or a drag handle is the only path.
30
+
31
+ Compare screens fail when table structure is faked with divs, sort controls have no state, or numbers are unreadable at zoom.
32
+
33
+ Configure screens fail when labels are placeholders, errors are a red border, or the submit button is disabled until the user guesses what is wrong.
34
+
35
+ Learn screens fail when headings are picked for their size, reading order jumps, or media autoplays.
36
+
37
+ Decide screens fail when the one action is an unlabeled icon, or the whole pitch is an image with no alt text.
38
+
39
+ Explore screens fail when filters are unreachable, results never announce their count, or hover is the only way to see anything.
40
+
41
+ ---
42
+
43
+ ## Access Bar
44
+
45
+ `/design a11y` repairs the access system. It is not an `aria-label` sprinkle.
46
+
47
+ At minimum, I inspect and repair: focus visibility, the complete keyboard path, semantic elements, accessible names, landmark and heading structure, form labels and error announcement, live regions, hit areas, alt text, reduced motion, and behavior at 200% zoom and 320px width.
48
+
49
+ I make two passes before I judge anything. Pass one, the mouse is off the table — if a task cannot be finished on the keyboard, it is not finished. Pass two, I listen instead of look: every control has to say what it is called, what kind of thing it is, and what state it is in.
50
+
51
+ Adding one `aria-label` is not an accessibility pass unless the user asked for that one fix.
52
+
53
+ ---
54
+
55
+ ## Native First, ARIA Last
56
+
57
+ The first rule of ARIA is: don't use ARIA. If a native element with the semantics and behavior I need exists, I use it.
58
+
59
+ | Element | Use for | What it gives me free |
60
+ |---|---|---|
61
+ | `<a href>` | Navigation, anything that changes the URL | Cmd/Ctrl/middle-click, right-click to copy, Enter |
62
+ | `<button>` | Actions: submit, toggle, open, delete | Focus, Enter *and* Space, form semantics |
63
+ | `<div onClick>` | Nothing at all | Nothing — it is styled text that happens to run code |
64
+
65
+ No ARIA is better than bad ARIA. A screen reader trusts my roles, so a wrong role is worse than none. A role is a promise: `role="tab"` promises the full tab keyboard model, and I have to deliver it.
66
+
67
+ The five rules I do not break:
68
+
69
+ 1. Use the native element when one exists.
70
+ 2. Don't change native semantics unless I truly have to.
71
+ 3. Every interactive ARIA control is keyboard-operable.
72
+ 4. Nothing that can hold focus may be erased with `role="presentation"` or `aria-hidden="true"`.
73
+ 5. Nothing interactive ships without a name.
74
+
75
+ The rule runs both directions. Anything that reads as pressable has to be pressable — and anything that is not has to stop dressing like it. Give a status badge the same shape as the buttons around it and users will keep clicking it, forever, getting nothing back.
76
+
77
+ ---
78
+
79
+ ## Focus Is Architecture
80
+
81
+ I style `:focus-visible`, never bare `:focus`. The browser shows `:focus-visible` for keyboard and assistive tech but suppresses it for mouse clicks, where focus is already obvious.
82
+
83
+ I prefer the browser's own ring, because it adapts to platform and forced-color settings without me predicting every background:
84
+
85
+ ```css
86
+ /* Best: keep the browser ring, give it breathing room */
87
+ :focus-visible {
88
+ outline-offset: 2px;
89
+ }
90
+
91
+ /* Custom ring when the design requires one: use the project's verified token */
92
+ :focus-visible {
93
+ outline: 2px solid var(--focus-ring);
94
+ outline-offset: 2px;
95
+ }
96
+ ```
97
+
98
+ A bare `outline: 2px solid` renders `currentColor`, which is not automatically safe — the ring may cross colors unrelated to the text's own background. Before I ship a custom ring I inspect the whole perimeter against every adjacent color it crosses: component fills, page surfaces, images, gradients, hover and selected states. At least a `2px` solid perimeter, or an equivalent visible area.
99
+
100
+ `outline: none` and `focus:outline-none` without a verified replacement are hostile. In `forced-colors: active` I keep the default color adjustment or use a system color such as `Highlight`; I never freeze the authored color with `forced-color-adjust: none` unless the control stays perceivable.
101
+
102
+ `:focus-within` groups the ring on a wrapper when an inner input takes focus — a search box with an icon inside the border.
103
+
104
+ ---
105
+
106
+ ## The Keyboard Path
107
+
108
+ Every pointer interaction needs a keyboard path. `tabindex` has exactly two legal values:
109
+
110
+ - `tabindex="0"` — puts a custom control into the tab order it should already have been in. Only for things the platform does not focus on its own.
111
+ - `tabindex="-1"` — reachable from script, skipped by Tab. This is for the heading I throw focus at after a route change, the modal shell, and the inactive members of a roving group.
112
+ - Anything positive — no. One positive value reorders the entire page around itself, and the actual problem is always the DOM order, so that is what I go fix.
113
+
114
+ Composite widgets occupy one Tab stop and use roving tabindex: the active item is `0`, every other is `-1`, and arrow keys move both focus and the `0`.
115
+
116
+ | Widget | Keys |
117
+ |---|---|
118
+ | Dialog | Tab/Shift+Tab cycle inside and wrap; Escape closes |
119
+ | Tabs | Arrows move between tabs and wrap; Tab exits to the panel; Home/End jump to the ends |
120
+ | Menu button | Enter/Space/ArrowDown opens on the first item; ArrowUp opens on the last; Escape closes and refocuses the button |
121
+ | Disclosure / accordion | The header itself is a `<button aria-expanded>`, toggled by Enter or Space |
122
+ | Combobox | ArrowDown opens and moves in; Enter accepts; Escape closes and returns to the input |
123
+ | Listbox / radio group | Arrows move selection; one Tab stop for the whole group |
124
+
125
+ Three rules hold everywhere. Escape unwinds one layer at a time, newest first — the tooltip goes before the menu it sits in, and the menu goes before the dialog holding both. Arrows travel *inside* a widget while Tab travels *between* them. And Enter submits from a focused input, except in a `<textarea>`, where it owes the user a newline and Cmd/Ctrl+Enter does the submitting.
126
+
127
+ A tab set has to decide what an arrow key means. If the panels are cheap to render, arrowing onto a tab can open it outright. If switching costs a fetch or a heavy re-render, arrowing only moves focus and the user confirms with Enter or Space — otherwise skimming the row fires off every panel on the way past.
128
+
129
+ ---
130
+
131
+ ## Trap and Restore
132
+
133
+ A modal has to hold focus inside itself. The cheapest way to get that is `inert` on whatever sits behind the dialog: one attribute, and the background stops being tabbable and stops existing for assistive tech at the same time.
134
+
135
+ ```js
136
+ // On open
137
+ appContent.inert = true;
138
+ (dialog.querySelector("[autofocus]") ??
139
+ dialog.querySelector("button, [href], input, select, textarea"))?.focus();
140
+
141
+ // On close
142
+ appContent.inert = false;
143
+ trigger?.focus(); // always return focus to what opened it
144
+ ```
145
+
146
+ Native `<dialog>` with `showModal()` gives me the trap, the inert background, and Escape handling for free, so I prefer it. A custom overlay that cannot use `<dialog>` needs `role="dialog"`, `aria-modal="true"`, and an accessible name via `aria-labelledby` pointing at its heading.
147
+
148
+ Either way: on open, focus the first focusable element — but for a destructive confirmation, focus the *least* destructive action. On close, return focus to the trigger; if the trigger is gone, move focus to the nearest logical container. Add `overscroll-behavior: contain` so scrolling inside never scrolls the page behind it.
149
+
150
+ **Client-side navigation** changes the screen and tells nobody. The page never reloads, so focus stays wherever it was and a screen reader announces nothing at all. I do that work by hand: retitle the document for wherever the user has landed, then throw focus at the new view's `<h1>` — `tabindex="-1"` makes it a legal target — or at `<main>` if there is no heading. Going back or forward restores the old scroll position; going somewhere new starts at the top.
151
+
152
+ ---
153
+
154
+ ## Accessible Names
155
+
156
+ When several naming sources compete, the winner is decided in this order: `aria-labelledby`, then `aria-label`, then whatever the platform derives (`<label>`, text content, `alt`), and `title` only if nothing else exists.
157
+
158
+ I reach for visible text or `aria-labelledby` first. `aria-label` is a string nobody can see, which means it quietly falls out of step with the interface it describes, and translation tooling treats it unevenly.
159
+
160
+ ```tsx
161
+ // Name from visible text, icon hidden
162
+ <button>
163
+ <TrashIcon aria-hidden="true" /> Delete
164
+ </button>
165
+
166
+ // Icon-only, explicit name
167
+ <button aria-label="Delete">
168
+ <TrashIcon aria-hidden="true" />
169
+ </button>
170
+ ```
171
+
172
+ Whatever the user can read on the control has to be contained in the name the control reports. Label a button "Send" on screen and `"Submit message"` underneath, and someone driving the interface by voice says "click Send" and nothing happens.
173
+
174
+ Brand names, code tokens, and identifiers get `translate="no"`, so machine translation leaves them alone instead of mangling them.
175
+
176
+ | Mistake | Why it fails |
177
+ |---|---|
178
+ | `aria-label` on a plain `<div>` or `<span>` | Names on role-less, non-interactive elements are ignored |
179
+ | `<button role="button">` | Redundant role: noise, no benefit |
180
+ | `aria-hidden="true"` on or above a focusable element | A Tab stop that does not exist for screen readers |
181
+ | `aria-labelledby`/`aria-describedby` aimed at an ID that isn't there | Fails without a warning: the control ends up nameless |
182
+ | `role="menu"` on site navigation | `menu` promises app-style arrow keys; site nav is `<nav>` with a list |
183
+
184
+ ---
185
+
186
+ ## Structure Is Navigation
187
+
188
+ There is exactly one primary `<main>` on the page. The other structural elements — `<header>`, `<nav>`, `<aside>`, `<footer>` — become the waypoints a screen-reader user jumps between, so when two of the same kind exist I name them apart: `<nav aria-label="Primary">` next to `<nav aria-label="Breadcrumbs">`.
189
+
190
+ Headings describe their sections and form a coherent outline. One page-level `<h1>` with properly nested levels is the recommended default. Headings are structure, not styling — I pick the level semantically and set the visual size in CSS, never the reverse.
191
+
192
+ When repeated navigation or chrome precedes the content, the first focusable element is a skip link:
193
+
194
+ ```css
195
+ .skip-link { position: absolute; inset-inline-start: -999px; }
196
+ .skip-link:focus { inset-inline-start: 16px; top: 16px; }
197
+ ```
198
+
199
+ In-page anchor targets get `scroll-margin-top` so a sticky header does not swallow them. `<title>` matches the current context, most specific first: `Billing · Settings · Acme`.
200
+
201
+ ---
202
+
203
+ ## Forms
204
+
205
+ The label has to be wired to the field, not merely near it: `<label for>` aimed at the input's `id`, or a `<label>` wrapped around it. Placeholder text does not qualify. It vanishes at the first keystroke — exactly when the user most needs to remember what they are filling in — and it usually fails contrast on the way out.
206
+
207
+ Label and field are one target, not two. If the words "Send me updates" do not toggle the checkbox, there is a dead strip between them, and I close it.
208
+
209
+ The complete error pattern:
210
+
211
+ ```html
212
+ <label for="email">Email</label>
213
+ <input id="email" type="email" autocomplete="email"
214
+ aria-invalid="true" aria-describedby="email-error" />
215
+ <p id="email-error">Enter a valid email address.</p>
216
+ ```
217
+
218
+ - `aria-invalid="true"` goes on while the field is wrong and comes back off the moment it is right.
219
+ - `aria-describedby` ties the field to the message beneath it, so the two are read as one thing rather than as an orphaned sentence.
220
+ - Errors render inline next to their field with text or an icon. A red border alone is color-only and fails.
221
+ - Submitting a broken form throws focus at the first thing that failed. I do not need to announce it separately; landing there says it.
222
+ - I do not disable submit until the form is valid. A disabled action hides what must be fixed. I keep it enabled until the request starts, then disable it with a spinner **while keeping the original label** — the label is what tells assistive tech which button is busy.
223
+ - I accept free text and validate after; I never filter characters as the user types. I trim before validating, because autocomplete and text expansion add trailing spaces.
224
+
225
+ `autocomplete` with a meaningful `name` fills a form in one tap and is a WCAG requirement for fields about the user:
226
+
227
+ | Field | `autocomplete` |
228
+ |---|---|
229
+ | Name | `name`, or `given-name` / `family-name` |
230
+ | Email / phone | `email` / `tel` |
231
+ | Address | `street-address`, `address-line1`, `postal-code`, `country` |
232
+ | Card | `cc-number`, `cc-exp`, `cc-csc`, `cc-name` |
233
+ | Login | `username`, `current-password` |
234
+ | Signup / reset | `new-password` |
235
+ | 2FA code | `one-time-code` |
236
+
237
+ Correct `type` and `inputmode` pick the right mobile keyboard: `type="email"` / `url` / `tel`; `type="text" inputmode="numeric"` for OTP, PIN, and card numbers (keeps text semantics, no spinner); `type="text" inputmode="decimal"` for money; `type="number"` only for a true numeric quantity. `spellcheck="false"` on emails, codes, and usernames.
238
+
239
+ I never block paste — users paste passwords and one-time codes — and I stay compatible with password managers: a real `<form>`, correct `autocomplete`, no fake inputs.
240
+
241
+ **Disabled states.** Native `disabled` supplies the platform's complete behavior: out of the tab order, no activation, `:disabled` styling, excluded from submission. `aria-disabled="true"` only *announces* the state — it changes nothing else. I use it only when keeping the control discoverable is intentional, and then I block pointer and keyboard activation in code, style the state explicitly, and say nearby why the action is unavailable. Never both attributes on one element.
242
+
243
+ ---
244
+
245
+ ## Announcing Change
246
+
247
+ I take the first of these that fits and stop there:
248
+
249
+ 1. **Focus already goes there** — an opening modal, the first broken field. The movement carries the message; adding anything else means saying it twice.
250
+ 2. **It belongs to one control** — a field's error, a character count. `aria-describedby` on that control.
251
+ 3. **It belongs to no control and can wait** — a toast, "Saved", a result count, a loading update. A polite region, `role="status"`.
252
+ 4. **It belongs to no control and cannot wait** — the form failed as a whole, the session is about to expire. `role="alert"`.
253
+
254
+ For repeated polite updates I keep a stable empty region in the DOM and change its text; inserting a new polite region together with its content is announced inconsistently.
255
+
256
+ ```tsx
257
+ <div role="status" className="sr-only">{statusMessage}</div>
258
+ ```
259
+
260
+ Polite is the default and `assertive` is the exception, because reaching for `assertive` by habit is how a live region goes from helpful to hostile: it cuts off whatever sentence the user was in the middle of. I keep each message short and able to stand alone, since `aria-atomic` reads the entire region again every time any of it changes. And a toast never steals focus — it speaks from where it is, and the user keeps their hands where they were.
261
+
262
+ The canonical visually-hidden pattern (Tailwind ships it as `sr-only`):
263
+
264
+ ```css
265
+ .sr-only {
266
+ position: absolute;
267
+ width: 1px; height: 1px;
268
+ padding: 0; margin: -1px;
269
+ overflow: hidden;
270
+ clip-path: inset(50%);
271
+ white-space: nowrap;
272
+ border: 0;
273
+ }
274
+ ```
275
+
276
+ The box is `1px`, not `0`, because a few screen readers walk straight past anything with no size. And this is the one job `display: none` and `visibility: hidden` cannot do: they do not hide the text visually, they delete it from the accessibility tree along with everything else.
277
+
278
+ ---
279
+
280
+ ## Alt Text by Purpose
281
+
282
+ I choose alt by what the image *does*, not by what it looks like.
283
+
284
+ | Purpose | Alt | Example |
285
+ |---|---|---|
286
+ | Decorative or redundant with adjacent text | `alt=""`, empty but present | Logo beside the company name in text |
287
+ | Informative | The meaning it adds | `alt="Ticket QR code"` |
288
+ | Functional (the image is the control) | The action or destination | Search icon → `alt="Search"`, not `alt="magnifying glass"` |
289
+ | Image of text | The exact text (better: use real text) | `alt="50% off everything"` |
290
+ | Complex (chart, diagram) | Short summary, full data nearby | `alt="Revenue by quarter, described below"` |
291
+
292
+ Leaving `alt` off entirely is the worst of the options — with nothing to read, screen readers fall back to announcing the file name.
293
+
294
+ **SVG:** decorative gets `aria-hidden="true"` and `focusable="false"`. Meaningful inline SVG gets `role="img"` plus `aria-label`. Prerecorded video needs captions, audio needs transcripts, nothing autoplays with sound, controls always render.
295
+
296
+ ---
297
+
298
+ ## Hit Areas
299
+
300
+ What the user sees is allowed to be tiny. What the user has to hit is not.
301
+
302
+ | Standard | Minimum |
303
+ |---|---|
304
+ | WCAG 2.5.8 (AA) | 24×24px — the hard floor |
305
+ | WCAG 2.5.5 (AAA) | 44×44px |
306
+ | Apple HIG | 44×44pt |
307
+ | Material | 48×48dp |
308
+
309
+ I treat 44px as the target for touch and 40px as a useful desktop size when density permits. Smaller controls are not automatic failures — I check the spacing, equivalent-control, inline, user-agent, and essential exceptions first. Under the spacing exception, an undersized target passes if a 24px circle centered on it does not intersect another target's circle; in the simple case, 20px targets need a 4px gap.
310
+
311
+ ```css
312
+ /* Expand without changing the visual size — on the label or button, never the input */
313
+ .checkbox-label { position: relative; width: 20px; height: 20px; }
314
+ .checkbox-label::after {
315
+ content: "";
316
+ position: absolute;
317
+ top: 50%; left: 50%;
318
+ transform: translate(-50%, -50%);
319
+ width: 44px; height: 44px;
320
+ }
321
+ ```
322
+
323
+ When the element can afford real box size I skip the pseudo-element and let the box be the target (`min-width: 44px; min-height: 44px; display: inline-grid; place-items: center`) — the browser then gets the real geometry for scrolling and gestures.
324
+
325
+ **Collision rule:** two interactive elements never have overlapping hit areas. If an extended area would collide, I shrink it to the largest size that does not.
326
+
327
+ `touch-action: manipulation` removes the double-tap-to-zoom delay. `-webkit-tap-highlight-color` matches the design instead of flashing gray.
328
+
329
+ ---
330
+
331
+ ## Motion, Zoom, and Reflow
332
+
333
+ Motion is opt-in, not chased with overrides:
334
+
335
+ ```css
336
+ .card { /* static */ }
337
+ @media (prefers-reduced-motion: no-preference) {
338
+ .card { transition: transform 200ms ease-out; }
339
+ }
340
+ ```
341
+
342
+ Inheriting a codebase too far gone to invert, I fall back to a blanket override — but I set durations to `0.01ms` instead of `none`. Zero means the browser never fires `transitionend` or `animationend`, and any script waiting on one of those events sits there forever.
343
+
344
+ The preference asks for less motion, not none. What it is protecting against is vestibular upset, not the interface answering the user:
345
+
346
+ | Cut it | Swap it | Leave it alone |
347
+ |---|---|---|
348
+ | Parallax | Anything that slides, scales, or zooms → a plain opacity crossfade | Spinners and progress indicators |
349
+ | Video, GIFs, and looping decoration that start themselves | Smooth scrolling → jump straight there | State that changes instantly: hover color, the focus ring |
350
+ | Spinning, and anything travelling a long way across the screen | Carousels that rotate on their own → hand them over paused | The short confirmation that a press registered |
351
+
352
+ Independent of the preference: anything that moves, blinks, or updates automatically for more than 5 seconds needs a visible pause control — muted looping hero video included. Auto-dismissing toasts are for low-stakes confirmations only; anything carrying an action or an error stays until dismissed. Never put the only path to an action inside a timed element — that is data loss on a schedule.
353
+
354
+ **Zoom:** all content and functionality survives 200% zoom, and the page reflows at 320px width with vertical scrolling only. Genuinely two-dimensional content (tables, maps, code blocks) scrolls inside its own container. Fixed heights are what break under zoom — `min-height` on anything containing text, and let containers grow.
355
+
356
+ **rem vs px:** I respect how the codebase is set up and never introduce mixed units into someone else's system. Where I do have the choice: `rem` for `font-size`, text container `max-width`, media-query breakpoints, and spacing that should scale with text; `px` for borders, focus outline width and offset, shadow details, and fixed decorations. Breakpoints are where it matters most — at a larger base font size, a `rem` query switches to the mobile layout when the text needs it and a `px` query does not.
357
+
358
+ ---
359
+
360
+ ## Never Color Alone
361
+
362
+ Status needs a redundant cue: an icon, text, or an underline alongside the color. Red-and-green as the only difference between two states is a failure for roughly one in twelve men.
363
+
364
+ Contrast is measured between a foreground and the background it actually renders against. I identify which requirement applies, measure the rendered pair, and report the pair, its measured value, and the threshold it misses. Thresholds and the OKLCH remediation method live in [color.md](color.md#contrast-mechanics).
365
+
366
+ ---
367
+
368
+ ## What I Refuse
369
+
370
+ - `outline: none` with no verified replacement
371
+ - Calling one `aria-label` an accessibility pass
372
+ - `<div onClick>` where a `<button>` or `<a href>` belongs
373
+ - Placeholder used as the only label
374
+ - Positive `tabindex` to fix a focus order the DOM should fix
375
+ - Submit disabled until the form is valid
376
+ - `assertive` live regions for routine toasts
377
+ - `aria-hidden="true"` on a focusable element
378
+ - Functional icon alt that describes the picture instead of the action
379
+ - A modal that traps nothing and returns focus nowhere
380
+ - Motion or autoplay that ignores `prefers-reduced-motion`
381
+ - Fixed heights on text containers that clip at 200% zoom
382
+ - Hover as the only way to reach a feature
383
+ - Reporting a color contrast failure by repainting the brand without being asked
384
+
385
+ ---
386
+
387
+ ## How I Know Access Works
388
+
389
+ - The primary task completes with the keyboard alone, start to finish
390
+ - Focus is visible on every interactive element and never disappears
391
+ - Every control announces a name, a role, and its state
392
+ - Overlays trap focus, close on Escape, and return focus to the trigger
393
+ - Every input has a real label, the right `type`, and useful `autocomplete`
394
+ - Errors announce, sit beside their field, and say how to recover
395
+ - No state is carried by color alone
396
+ - Every hit area clears the floor and none of them overlap
397
+ - Motion respects `prefers-reduced-motion` and nothing autoplays without a pause
398
+ - The page works at 200% zoom and reflows at 320px without horizontal scrolling
399
+ - I verified these by walking the interface, not by reading the CSS
400
+
401
+ STRICT RULE — NEVER BREAK THIS
402
+ Do not create report.md, any kind of report, summary, analysis file,
403
+ or extra documentation. This applies every time this file is used.
404
+ Generate no reports unless explicitly asked.
@@ -74,6 +74,10 @@ Product UI usually wants tighter corners. Brand surfaces can be softer. Data-hea
74
74
 
75
75
  I keep inner elements visually nested. Inner radius should feel smaller than outer radius. If nested corners fight, the surface looks careless.
76
76
 
77
+ The math is concentric: **outer radius = inner radius + the padding between them.** A card with `8px` padding around a `12px`-radius child wants a `20px` radius, not another `12px`. Mismatched radii on closely nested surfaces is the single most common reason an interface feels subtly off.
78
+
79
+ The rule applies while the layers are visually nested. Past roughly `24px` of padding they read as separate surfaces, and each radius is chosen on its own rather than forced through the formula. An established component token beats the formula when the layers are independent or the padding is deliberately asymmetric.
80
+
77
81
  ---
78
82
 
79
83
  ## Focus Rings
@@ -84,6 +88,8 @@ It must be visible, offset enough to avoid clipping, shaped to match the element
84
88
 
85
89
  If a component cannot use a normal outline, I still provide a clear ring or halo that survives nearby colors.
86
90
 
91
+ I style `:focus-visible`, not bare `:focus`, so keyboard users get the ring and mouse users usually do not. Where the design allows it, the browser's own indicator with `outline-offset: 2px` is the strongest choice, because it adapts to platform and forced-color settings without me predicting every background. A custom ring uses the project's focus token and gets checked against every color its perimeter crosses. Full mechanics, including forced-colors behavior: [accessibility.md](accessibility.md#focus-is-architecture).
92
+
87
93
  ---
88
94
 
89
95
  ## Dividers
@@ -114,6 +114,8 @@ For every critical issue, I write the prescription in my working answer:
114
114
 
115
115
  I do not bury the next move in a report. The user should know the practical fix immediately.
116
116
 
117
+ Prescriptions carry a severity, a `path/to/file:line` location, and a concrete replacement, per [severity.md](severity.md). Any escalation trigger in that file makes the Accessibility vital `Critical` on sight — no keyboard path, no visible focus, meaning carried by color alone, motion ignoring `prefers-reduced-motion`, or content unreachable at 320px or 200% zoom. A checkup does not average those away because the other five vitals look healthy.
118
+
117
119
  ---
118
120
 
119
121
  ## Report Boundary
@@ -121,6 +121,73 @@ I never let color carry meaning alone. State needs shape, label, icon, position,
121
121
 
122
122
  ---
123
123
 
124
+ ## Contrast Mechanics
125
+
126
+ Contrast is always measured between a **foreground** and the **background it actually renders against** — usually the nearest parent's background, not the page's.
127
+
128
+ **Report, don't repaint.** When a pair fails, I report the pair, its measured value, and the threshold it misses. A project's colors are a design decision; I change them when the user asks, or when the mode I am running is a fix mode.
129
+
130
+ APCA is the default because it is perceptually accurate and pairs naturally with OKLCH. Lc is signed — positive is dark-on-light, negative is light-on-dark — so I compare absolute values.
131
+
132
+ | Content | APCA minimum | Preferred |
133
+ |---|---|---|
134
+ | Body text (blocks and columns) | Lc 75 | Lc 90 |
135
+ | Non-body text (labels, headlines) | Lc 60 | Lc 75 |
136
+ | Large text (≥36px) | Lc 45 | Lc 60 |
137
+ | UI components, placeholder, disabled | Lc 30 | — |
138
+
139
+ WCAG 2 still governs a formal conformance claim: 4.5:1 AA and 7:1 AAA for normal text, 3:1 AA for large text (≥24px, or ≥18.5px bold) and for UI components and graphical objects.
140
+
141
+ **Fixing in OKLCH.** Lightness is the clearest first lever. I move L and preserve C and H where I can, then remeasure the rendered pair:
142
+
143
+ ```css
144
+ /* Failing: too little lightness distance (Lc ≈ 50) */
145
+ color: oklch(0.65 0.08 250);
146
+ background: oklch(0.95 0.02 250);
147
+
148
+ /* Fixed: darken the foreground, C and H unchanged (Lc ≈ 90) */
149
+ color: oklch(0.3 0.08 250);
150
+ background: oklch(0.95 0.02 250);
151
+ ```
152
+
153
+ Mid-lightness backgrounds cap what is achievable: on a background at L 0.75, even pure black text reaches only about Lc 60. Body text needs a background near one of the extremes.
154
+
155
+ Quick gaps for body text: on a light background (L > 0.9) the foreground wants L below 0.35; on a dark background (L < 0.25) the foreground wants L above 0.9. The gap is asymmetric because APCA is polarity-aware.
156
+
157
+ The light/dark crossover sits at **L 0.73** — higher than intuition suggests. Between 0.6 and 0.73 a background already looks light, but white text still scores meaningfully better than black.
158
+
159
+ ---
160
+
161
+ ## OKLCH Mechanics
162
+
163
+ `oklch(L C H)` or `oklch(L C H / alpha)`. L is 0–1 and perceptually uniform. C is 0 to about 0.4 and its maximum depends on both L and H. H is 0–360. Alpha uses slash syntax, never commas. Three decimals for L and C, and `-0` is written `0`.
164
+
165
+ **The gamut is irregular.** At L 0.5 in sRGB, purple (H ≈ 285) reaches C ≈ 0.29, red-orange C ≈ 0.20, and cyan (H ≈ 195) only C ≈ 0.09. The peak hue moves with lightness. If chroma exceeds the maximum for its L and H, the color clips — I reduce C and hold L and H.
166
+
167
+ ```css
168
+ .accent { color: oklch(0.7 0.2 150); } /* sRGB-safe */
169
+
170
+ @media (color-gamut: p3) {
171
+ .accent { color: oklch(0.7 0.3 150); } /* wider gamut enhancement */
172
+ }
173
+ ```
174
+
175
+ **Palette scales** run 50 (lightest) to 950 (darkest); 11 steps matches Tailwind, 9 is a leaner default. I clamp lightness to [0.05, 0.95] — pure black and pure white carry zero chroma — distribute L evenly, then clamp chroma *per step* to a percentage of that step's maximum. High-chroma base colors come out less chromatic at the ends. That is correct.
176
+
177
+ **Multi-hue palettes share the same L and the same chroma *percentage*, not the same absolute C.** Same L guarantees equal perceived brightness; same percentage guarantees equal vividness relative to each hue's own ceiling:
178
+
179
+ ```css
180
+ --blue-500: oklch(0.623 0.141 250); /* 80% of max 0.176 */
181
+ --green-500: oklch(0.623 0.157 145); /* 80% of max 0.196 */
182
+ --red-500: oklch(0.623 0.202 25); /* 80% of max 0.253 */
183
+ ```
184
+
185
+ **Hue drift** is how I diagnose an inherited HSL ramp: convert each step to OKLCH and compare H. More than 10° of spread across the scale is visible drift — `hsl(240, 80%, 20%)` and `hsl(240, 80%, 90%)` land ~16° apart, which is why the light end of a blue ramp goes purple.
186
+
187
+ **Respect the existing notation.** I do not convert a hex or RGB token system to OKLCH because this file was loaded. A consistent hex system beats a second color representation introduced for one isolated fix. I convert when the task *is* a color-system pass. In Tailwind v4 the `@theme` block takes OKLCH values directly, and the `/50` opacity modifier compiles to slash-alpha.
188
+
189
+ ---
190
+
124
191
  ## The Grey Test
125
192
 
126
193
  I mentally strip every hue to gray. The hierarchy must survive.
@@ -135,6 +202,8 @@ Dark mode is a second theme. It is not light mode with the lights off.
135
202
 
136
203
  Depth comes from surface lightness, not heavy shadow. Accents lose a little chroma so they do not glow. Borders become subtle light, often with a trace of brand hue. Light text needs careful weight because it reads heavier and brighter than dark text.
137
204
 
205
+ I start by swapping the semantic roles, then tune the dark values independently. I do not mechanically reverse every palette step: equal OKLCH steps do not guarantee that every foreground/background pair keeps its contrast when the polarity flips. Every pair gets rechecked in both appearances.
206
+
138
207
  ---
139
208
 
140
209
  ## Domain Default Trap