jig-ui 0.1.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.
@@ -0,0 +1,501 @@
1
+ # 00 · Anti-Patterns
2
+
3
+ **Status:** draft v0.1
4
+ **Scope:** universal. Every rule here is wrong in *all* modes (`editorial`, `product`, `operator`). Anything that depends on mode belongs in that mode's profile, not here.
5
+ **Floor:** WCAG 2.1 level AA. Not an aspiration — the minimum this system is built on.
6
+ **Numbering:** rule numbers are stable identifiers, not an ordering. A new rule takes the next free number and sits in its topic section. Numbers are never reused or renumbered, so a citation stays valid.
7
+ **Framework:** agnostic. Examples are stated in CSS properties and token names. Where a utility-class framework is in use, translate — the rule is about the resulting style, not the syntax.
8
+
9
+ ## How to use this file
10
+
11
+ You are generating or reviewing UI. Treat every rule below as a hard constraint unless the task explicitly overrides it.
12
+
13
+ - Rules are numbered (`A-14`). Cite the number when you follow or deliberately break one.
14
+ - Each rule has a **correction**, not just a prohibition. Apply the correction; do not substitute your own default.
15
+ - If a rule conflicts with an explicit instruction in the task, the task wins — but say so in one line rather than silently deviating.
16
+ - Before finishing, run the checklist at the end.
17
+
18
+ ---
19
+
20
+ ## A. Generic-AI aesthetic
21
+
22
+ These are the strongest defaults in a model's training data and the fastest way to make work look machine-made. They resurface every session; assume they will.
23
+
24
+ ### A-01 Purple and violet as the unspecified default
25
+ ❌ A violet or indigo fill, or a violet→pink gradient, chosen because no colour was specified
26
+ ✅ Use `--color-brand` from the brand file. The unbranded default resolves it to near-black, which ships a coherent monochrome UI and makes the missing decision visible. Then ask.
27
+
28
+ ### A-02 Gradient text on headings
29
+ ❌ `background-clip: text` with a gradient fill and transparent text colour
30
+ ✅ Solid `--color-text-strong`. Gradient text has unmeasurable contrast and reads as template output.
31
+
32
+ ### A-03 Decorative gradient blobs and orbs
33
+ ❌ Absolutely-positioned blurred radial shapes behind a hero
34
+ ✅ Flat background, or a single subtle surface change. Backgrounds do not need decoration to justify existing.
35
+
36
+ ### A-04 Trend styles that fight legibility
37
+ ❌ Glassmorphism (translucent fill + `backdrop-filter: blur()`), neumorphism (soft inset/outset shadows on a matching background), and their successors
38
+ ✅ Opaque `--color-surface` with a `--color-stroke-weak` edge. Use translucency only over media, and only when legibility is verified against the worst frame.
39
+ Both styles make sufficient contrast and clear hierarchy structurally difficult — neumorphism in particular defines every element with shadow alone, which fails at 3:1 almost by construction. Trend styles also age badly: the more of them a product carries, the more precisely it is dated. Minimal styling that foregrounds content lasts longer.
40
+ Experiment freely — but not where it costs legibility or excludes people.
41
+
42
+ ### A-58 Decorative styling that implies meaning
43
+ ❌ List items in assorted colours chosen for variety; a decorative icon beside a heading that looks pressable; a heading coloured and underlined though it is not a link
44
+ ✅ Style carries information or it goes. People assume differences mean something — arbitrary colour invites them to hunt for a pattern that does not exist, and an icon that looks interactive will be clicked.
45
+ Decoration is allowed. Decoration that mimics a functional signal is not.
46
+
47
+ ### A-59 Repeated information
48
+ ❌ Every item in a list restating the shared context: "UI Course – Chapter 1", "UI Course – Chapter 2"
49
+ ✅ Lift the shared part into a heading above the list and let each item carry only what differs. Repetition costs space, adds reading, and buries the part the user is actually scanning for.
50
+
51
+ ### A-60 Icons all competing at equal weight
52
+ ❌ A row of large, high-contrast icons beside secondary text
53
+ ✅ Icons supporting text are subordinate to it — smaller, lower contrast, or both. An icon at the same visual weight as the content it decorates competes with it for attention it does not deserve.
54
+
55
+ ### A-67 A container around every group
56
+ ❌ Borders, cards and background panels wrapping each cluster on the page
57
+ ✅ A container is the **strongest** grouping cue and the heaviest. Reach for it last.
58
+ Four tools group elements, in ascending strength: **continuity** (aligned in a line), **similarity** (same size, shape, colour), **proximity** (closer together than to anything else), **common region** (a shared container). Use the weakest one that works.
59
+ Where several already apply — a table's rows are aligned, alike, and close — the container adds clutter and no information. Remove it and check whether the grouping still reads. It usually does.
60
+
61
+ ### A-05 Emoji as interface iconography
62
+ ❌ `<h3>🚀 Fast deploys</h3>`, emoji bullets in feature lists
63
+ ✅ A real icon set, or no icon. Emoji render inconsistently across platforms, carry unpredictable screen-reader announcements, and are almost never in the brand.
64
+
65
+ ### A-06 The three-column feature grid reflex
66
+ ❌ Icon-in-rounded-square + heading + two lines, three across, for any set of three things
67
+ ✅ Let the content pick the layout. Three items of unequal weight are a list, not a grid.
68
+
69
+ ### A-07 Oversized radius everywhere
70
+ ❌ A single large radius (16px+) applied to cards, buttons, inputs and badges alike
71
+ ✅ `--radius-control` for controls, `--radius-surface` for containers. Both derive from `--radius-sm`, which is a brand decision. Small elements take small radii.
72
+
73
+ ### A-08 Shadow as the only depth cue
74
+ ❌ A drop shadow on every card, or several shadow sizes with no rule governing which means what
75
+ ✅ `--shadow-surface` is `none` in all three modes. Depth comes from `--color-stroke-weak` or a surface-colour step. `--shadow-raised` exists for overlays only — dialogs, popovers, dropdowns.
76
+
77
+ ### A-09 Marketing voice in an application
78
+ ❌ "Supercharge your workflow" on an internal dashboard
79
+ ✅ Copy matches the context. Operator tools name the object and the action.
80
+
81
+ ### A-10 Placeholder content shipped
82
+ ❌ Lorem ipsum, "Acme Inc", `https://example.com`, stock avatars left in
83
+ ✅ Real content, or clearly marked `TODO:` that fails a build check. Placeholder text that survives to review costs a reviewer more than it saved you.
84
+
85
+ ---
86
+
87
+ ## B. Typography
88
+
89
+ ### B-11 Unbounded line length
90
+ ❌ Paragraphs spanning the full width of a wide viewport
91
+ ✅ Cap at `--measure-prose` (68ch editorial, 60ch product, 72ch operator). Applies to any run of prose in any mode.
92
+
93
+ ### B-12 Centred or justified body text
94
+ ❌ A centred paragraph of four lines. Justified text of any length.
95
+ ✅ Start-align anything longer than a short heading or label.
96
+ **Centred** text moves the start of every line, so the eye hunts for it. Acceptable for a heading or a couple of lines; never for a paragraph.
97
+ **Justified** text is worse and has no acceptable case here. Stretching word spacing to force a straight right edge creates uneven gaps and vertical "rivers" of white space running down the block — actively harmful for dyslexic readers, and the reason books that justify are harder to read than they look.
98
+
99
+ ### B-13 One line-height for everything
100
+ ❌ One line-height value applied to both a 48px heading and 16px body
101
+ ✅ `--leading-body` for prose, `--leading-heading` for headings, `--leading-display` for the largest tier. Line height decreases as size increases.
102
+
103
+ ### B-14 Thin weights for body text
104
+ ❌ Weight 300 or lighter for paragraphs
105
+ ✅ `--font-weight-body` (400) minimum. Thin weights fail on low-density screens and in bright light — both of which describe most of your users' actual conditions.
106
+
107
+ ### B-15 Ad-hoc type sizes
108
+ ❌ A one-off `font-size` because something looked slightly wrong
109
+ ✅ Use a `--text-*` step. If the scale genuinely lacks one, add it to the mode file rather than bypassing the scale at the call site.
110
+
111
+ ### B-16 All-caps for anything long
112
+ ❌ All-caps sentences, all-caps buttons with three or more words
113
+ ✅ Reserve caps for short labels, and add letter-spacing when you do. Caps destroy word-shape recognition.
114
+
115
+ ### B-17 Skipping heading levels for visual reasons
116
+ ❌ `<h4>` because `<h2>` looked too big
117
+ ✅ Correct level, styled to the size you want. Heading order is document structure, not typography.
118
+
119
+ ### B-75 Long-form text below 18px
120
+ ❌ 14px or 16px body text for an article, description or any sustained reading
121
+ ✅ `--text-prose` (18px) with `--leading-prose` (1.6) for long-form text. `--text-body` (16px) is for **UI text** — labels, controls, table cells, short strings — which is read in glances rather than sustained passages.
122
+ People read at roughly arm's length on every device. Small type is a designer's preference, not a reader's. When the text is a paragraph someone must actually get through, size up.
123
+
124
+ ### B-76 More than two typefaces
125
+ ❌ A serif for headings, a script for the description, a sans for the UI
126
+ ✅ One sans serif for everything is the safe default: most legible at small sizes, neutral enough to suit any brand, and it keeps the content rather than the lettering as the focus.
127
+ A **second** typeface is permitted for headings only, where the brand needs a mood the sans cannot carry. Never a third. Never a script or display face below heading size — they are drawn for large sizes and become unreadable small.
128
+
129
+ ### B-77 More than two font weights
130
+ ❌ Thin, light, regular, medium, semibold and bold across one interface
131
+ ✅ Two: `--font-weight-regular` (400) and `--font-weight-bold` (600). Bold for headings and emphasis, regular for everything else.
132
+ Extra weights read as noise, are impossible to apply consistently, and add a decision to every text element. Reserve very thin or very heavy weights for large display text, where they are legible.
133
+
134
+ ### B-78 Text placed directly on a photo
135
+ ❌ White text over an image, legible on the mockup's photo and not on the next one
136
+ ✅ Contrast still has to be met — 4.5:1 for text 18px and under, 3:1 for larger. A photo cannot guarantee it, because the photo changes.
137
+ Four workable treatments:
138
+ 1. **Linear gradient overlay** — dark, ~90% opacity at the text edge fading to 0% partway across. Preserves most of the image.
139
+ 2. **Semi-transparent overlay** — a flat dark layer at ~50% over the whole image.
140
+ 3. **Blurred semi-transparent overlay** — as above with a blur behind the text only.
141
+ 4. **Solid background behind the text** — the caption approach; most reliable, least subtle.
142
+ A text shadow may reinforce any of these but never substitutes for one. Verify against the worst image the slot will ever hold, not the one in the mockup.
143
+
144
+ ---
145
+
146
+ ## C. Colour and contrast
147
+
148
+ ### C-66 Depth built from shadow in dark mode
149
+ ❌ The same shadow tokens carried into dark mode to lift cards off the page
150
+ ✅ In dark mode, elevation comes from the **background colour** — `bg-base` → `bg-raised` → `bg-overlay`, each lighter than the last. Shadows are close to invisible on a dark surface. `--shadow-raised` resolves to `none` in dark for exactly this reason.
151
+ Also: shadow colour derives from the text colour, never pure black, so it sits inside the palette rather than on top of it.
152
+
153
+ ### C-18 Pure black on pure white
154
+ ❌ `#000` text on `#fff`
155
+ ✅ Near-black on off-white. Full-contrast black/white causes halation and reads as unconsidered. This is a house preference and it is deliberate.
156
+
157
+ ### C-19 Grey text below contrast floor
158
+ ❌ A mid-grey (ramp step `-500` or lighter) used for secondary text, placeholders or timestamps
159
+ ✅ `--color-text-weak` for secondary text, `--color-text-weak` for large text only. See the contrast contract in `02-tokens.md`: `-500` and lighter are never text on a light background. Check placeholders and disabled states specifically; they are the usual failures.
160
+
161
+ ### C-20 Colour as the sole signal
162
+ ❌ Red border alone to indicate an invalid field
163
+ ✅ Colour plus text plus (where useful) an icon. Applies to status badges, chart series, diff views, and validation.
164
+
165
+ ### C-21 Dark mode by inversion
166
+ ❌ Flipping to `#000` background and keeping the same shadows and borders
167
+ ✅ Dark mode is its own ramp: elevated surfaces get *lighter*, shadows lose most of their meaning, and saturated colours need desaturating. If dark mode is out of scope, say so rather than shipping a broken one.
168
+
169
+ ### C-22 Semantic colours invented inline
170
+ ❌ Two different reds in two places, both meaning "error"
171
+ ✅ One token per meaning — `--color-danger`, `--color-danger-subtle`, `--color-danger-stroke` — referenced everywhere.
172
+
173
+ ### C-49 Link treatment
174
+ The default for a link **inside running text** is colour **and** underline. Colour-blind users cannot separate a coloured link from surrounding prose; the underline is what makes it a link for them.
175
+
176
+ Two legitimate departures, and they are not the same:
177
+
178
+ | Case | Treatment | Why |
179
+ | --- | --- | --- |
180
+ | Body text link | Colour + underline | Default. Never drop both. |
181
+ | Already-obviously-interactive component — nav item, tab, card, button-styled link | Neither required | Position, container and grouping already signal it. Adding link styling clutters without informing. |
182
+ | Secondary link that colour + underline would over-weight | **Underline, no colour** | Keeps the affordance while restoring hierarchy — a supporting link should not compete with the primary action beside it. |
183
+
184
+ ❌ Dropping the underline but keeping the colour, in body text. That is the one combination that fails colour-blind users while looking fine to everyone else.
185
+ ✅ Keep the underline. Colour is the part you may drop — an underlined link without colour still reads as a link to everyone; a coloured link without an underline reads as a link only to people who can see the colour.
186
+
187
+ ### C-50 Coloured heading text
188
+ ❌ A heading tinted with the accent colour for emphasis
189
+ ✅ Headings use `--color-text-strong`. Coloured text reads as interactive, so a coloured heading invites a click that does nothing. Emphasise with size, weight and space — the tools that already carry hierarchy.
190
+
191
+ ### C-68 Non-interactive elements styled like interactive ones
192
+ ❌ A "Verified" badge with the brand fill and the shape of the primary button; decorative icons carrying the same border and colour as a secondary button
193
+ ✅ Things that look alike are expected to behave alike. If an element does nothing when clicked, it must not carry the visual signature of something that does — brand fill, button shape, or control border.
194
+ Differentiate deliberately: change the shape (a badge is more rounded than a button), the tone (`success` for a verified state rather than `brand`), and the emphasis (a `fill` background rather than a solid one, so the real primary action stays the most prominent thing on screen).
195
+ The converse also holds: two elements that do the same job should look the same.
196
+
197
+ ---
198
+
199
+ ## D. Spacing and layout
200
+
201
+ ### D-23 Spacing off the scale
202
+ ❌ An arbitrary margin or gap (13px, 7px) written at the call site
203
+ ✅ Every spacing value comes from a `--spacing-*` token, all multiples of `--spacing-unit` (4px). An arbitrary value signals a missing token, not an exception.
204
+
205
+ ### D-24 Symmetric spacing around headings
206
+ ❌ Equal margin above and below a section heading
207
+ ✅ A heading belongs to the content beneath it. Space above must exceed space below, usually by 2–3×. This one rule fixes more "it looks off and I can't say why" than any other in this file.
208
+
209
+ ### D-69 Spacing that does not grow outward
210
+ ❌ The same gap between a card's heading and its body, between cards, and between page sections
211
+ ✅ An interface is rectangles inside rectangles. Spacing starts small at the innermost level and **increases as you move outward**:
212
+
213
+ | Level | Option |
214
+ | --- | --- |
215
+ | Text inside a component | XS (8) |
216
+ | Component padding, content within a section | M (24) |
217
+ | Between components, grid gutters | L (32) |
218
+ | Between page sections | XXL (80) |
219
+
220
+ `--spacing-group` → `--spacing-card` → `--grid-gutter` → `--spacing-section` already encode this per mode. When in doubt between two steps, take the larger one — tight spacing hides grouping and hierarchy, and generous spacing is the cheapest improvement available to any interface.
221
+
222
+ ### D-96 Icon and text out of balance
223
+ ❌ A large, heavy icon beside small, light label text — the icon shouts and the pair reads as two things
224
+ ✅ Match the icon's weight and size to the text it accompanies. Where the icon set will not match, lower its **contrast** instead: `--color-stroke-strong` for the icon against `--color-text-weak` for the label. The pair should read as one unit.
225
+
226
+ ### D-70 Broken left edge
227
+ ❌ An icon sitting to the left of a heading while the body text below starts further left, so the text block has two left edges
228
+ ✅ Keep one straight left edge down a block of related text. The eye returns to that edge on every line and every jump; breaking it costs a small amount of work on each one. Where an icon sits beside text, align the *text* edges and let the icon hang outside, or put the icon above.
229
+
230
+ ### D-71 Multiple alignments in one component
231
+ ❌ A centred name, a left-aligned quote, a right-aligned photo and centred stars in a single testimonial
232
+ ✅ One alignment per component, start-aligned by default. Every additional alignment makes the eye zig-zag. Centring is acceptable for a short isolated block — a few lines of text and one action — but then centre *everything* in it, and make the action full-width so both hands can reach it.
233
+
234
+ ### D-72 Mixed-size text centred vertically
235
+ ❌ `$10` and `/month` on one line, vertically centred, so the small text floats
236
+ ✅ Align text of different sizes to the **baseline**, not the vertical centre. The shared baseline connects them into one reading unit; vertical centring leaves the smaller item drifting in its own space.
237
+
238
+ ### D-25 No proximity hierarchy
239
+ ❌ Identical gaps between items within a group and between groups
240
+ ✅ Related things sit closer together than unrelated things. If everything is evenly spaced, nothing is grouped.
241
+
242
+ ### D-26 One padding value for all containers
243
+ ❌ The same padding value on a badge, a card and a page section
244
+ ✅ `--spacing-inline` for inline elements, `--spacing-card` for containers, `--spacing-section` for page sections. Padding scales with the container.
245
+
246
+ ### D-27 Optical alignment ignored
247
+ ❌ Icon and text label boxes aligned mathematically but reading as misaligned
248
+ ✅ Align to what the eye sees. Icons frequently need a 1–2px nudge; circular shapes need slightly more size than square ones to look equal.
249
+
250
+ ---
251
+
252
+ ## E. States and interaction
253
+
254
+ Agents render the happy path. This section exists because that is the single most common gap in generated UI.
255
+
256
+ ### E-28 Missing states
257
+ ❌ A component with only a default appearance
258
+ ✅ Every interactive element defines: `hover`, `focus-visible`, `active`, `disabled`. Every data view defines: loading, empty, error, and partial/truncated.
259
+
260
+ ### E-29 Focus removed without replacement
261
+ ❌ `outline: none` with nothing in its place
262
+ ✅ A `:focus-visible` rule with a visible indicator built on `--color-focus`. Never remove the outline without replacing it. This locks out every keyboard user, and it is the most common accessibility failure in generated code.
263
+
264
+ ### E-30 Empty states omitted
265
+ ❌ A table that renders an empty `<tbody>` when there is no data
266
+ ✅ Empty state says what would be here, why it isn't, and the one action that changes it. Distinguish "no data yet" from "no results for this filter" — they need different copy and different actions.
267
+
268
+ ### E-31 Hover-only affordances
269
+ ❌ Actions that appear only on `:hover`
270
+ ✅ Visible on touch, or reachable via a persistent control. Half your traffic has no hover.
271
+
272
+ ### E-32 Disabled buttons
273
+ ❌ A greyed-out submit button with no indication of what's missing
274
+ ✅ **Prefer not to disable at all.** Three better options, in order: enable it and validate on submit; remove the action and say why it is unavailable; or keep it at full contrast with a lock icon and explain how to unlock it.
275
+ A disabled button gives no feedback on press, usually fails contrast, and is skipped by keyboard focus — so the user cannot reach the element to discover why it is dead.
276
+ Where disabling is genuinely right, put a message beside it or a tooltip on it explaining what is needed, and **keep it keyboard-focusable** so assistive technology can reach that explanation. See `P-02`.
277
+
278
+ ### E-33 `div` with a click handler
279
+ ❌ A `div` or `span` carrying a click handler
280
+ ✅ `<button type="button">` for actions, `<a href>` for navigation. If it performs an action it is a button; if it navigates it is an anchor. Keyboard operability and screen-reader semantics both come free with the correct element.
281
+
282
+ ### E-34 Icon-only controls without names
283
+ ❌ `<button><TrashIcon /></button>`
284
+ ✅ `aria-label`, or visually-hidden text — and see `E-51`, which is a different problem. Verify the target is at least `--size-touch-target` (48px, unchanged in every mode) including padding.
285
+
286
+ ### E-35 Dynamic results with no announcement
287
+ ❌ A filtered count that updates silently
288
+ ✅ `aria-live="polite"` on result counts and validation summaries.
289
+
290
+ ### E-51 Icon without a visible label
291
+ ❌ A toolbar of icon-only buttons whose meaning depends on recognising the glyph
292
+ ✅ A visible text label beside the icon, or the icon plus label on the primary path.
293
+ This is **not** the same problem as `E-34`. An `aria-label` serves a screen reader and does nothing for a sighted user with low computer literacy, or anyone meeting an unfamiliar glyph. Both are required.
294
+ Icon-only is acceptable for a small set of near-universal glyphs (close, search, menu) and in `operator`, where repetition builds recognition — and even there, on first-run surfaces the label stays.
295
+
296
+ ### E-52 Unconventional controls
297
+ ❌ A bespoke form field, checkbox or select that looks and behaves unlike every other one the user has met
298
+ ✅ Conventional shapes: inputs are rectangles with the label above, checkboxes are squares with a tick, radios are circles, links are underlined.
299
+ People arrive with a mental model built from every other product they use (Jakob's law). Matching it is free comprehension; departing from it charges the user to learn something that gains them nothing. Innovate on the product's actual purpose, not on its form fields.
300
+
301
+ ### E-61 Important navigation hidden when it fits
302
+ ❌ A hamburger menu on a viewport with room for three visible links
303
+ ✅ Show what fits. People do not use what they cannot see, and every tap behind a menu is a tap some users will not make. Collapse only under genuine space pressure.
304
+
305
+ ### E-62 Off-screen content with no affordance
306
+ ❌ A horizontally scrolling row that ends flush at the viewport edge
307
+ ✅ Expose the edge of the next item, or show an explicit control. If the user cannot tell there is more, there is not more.
308
+
309
+ ### E-63 Minimal but unreadable
310
+ ❌ Unlabelled icon navigation, a selected state signalled by a barely-different tint, primary actions hidden in an overflow menu, low-contrast icons
311
+ ✅ Minimal is not the same as simple. A sparse interface that omits labels, states and actions is harder to use than a slightly busier one that names them.
312
+ The test is not how little is on screen. It is whether someone can tell what things are, which one is selected, and what they can do next. Remove decoration freely; never remove the answers to those three questions.
313
+
314
+ ### E-64 Brand colour colliding with a system meaning
315
+ ❌ A red brand colour used for links and primary buttons, on an interface that also uses red for errors and destructive actions
316
+ ✅ Where the brand colour is red, amber or green, do **not** use it for interactive elements. Use `--color-text-strong` for links and buttons and keep the brand colour decorative.
317
+ One colour cannot mean both "act on this" and "something is wrong" without teaching the user that neither is reliable. The system colours have prior claim, because their meanings arrive with the user.
318
+
319
+ ### E-65 More than one interactive colour
320
+ ❌ A palette with three brand colours, all appearing on buttons and links
321
+ ✅ **One** colour marks interactive elements — the highest-contrast one. Additional brand colours are decorative: backgrounds, borders, icons, illustration. A second interactive colour raises a question the user has to answer ("does this one do something different?") and there is no good answer.
322
+
323
+ ### E-73 Interface built only for short content
324
+ ❌ A layout tested with "Vite" and a two-word title, which breaks on a forty-character name or a four-digit count
325
+ ✅ Design for the long case as well as the short one. Let content reflow, allow the component to grow, or reduce the type size — but do not clip data out of sight. Hidden overflow hides information the user may need.
326
+
327
+ ### E-74 Truncating where items share a prefix
328
+ ❌ "User Interface Design Fundamentals C…" repeated down a list, every row identical
329
+ ✅ Where truncation is unavoidable and items share a leading string, **crop in the middle**: "User Interface De…Chapter 2 – Typography". Truncation must preserve the part that tells items apart, which is rarely the beginning.
330
+ Better still, remove the shared prefix entirely and put it in a heading (`A-59`).
331
+
332
+ ### E-91 Button hierarchy carried by colour alone
333
+ ❌ A blue primary beside a green secondary, identical in every other respect
334
+ ✅ Weights differ in **structure** — solid fill, outline, underlined text — not just hue. Two buttons differing only in colour are the same button to a colour-blind user, and if their contrast against each other is under 3:1 they are the same button to a low-vision user too.
335
+ This also means a button's fill or border is not decorative: it is the thing identifying the element as a button, so it carries the 3:1 non-text floor.
336
+
337
+ ### E-92 Light grey secondary button
338
+ ❌ A pale grey filled or outlined button beside the primary
339
+ ✅ Grey reads as **disabled**, so users skip it. Its text and border rarely clear 4.5:1 and 3:1 either. Use an outlined button in `--color-brand`.
340
+
341
+ ### E-93 Inconsistent button shapes
342
+ ❌ A pill-shaped primary next to a rounded-rectangle secondary
343
+ ✅ One shape across all weights. Different shapes imply different behaviour; if the behaviour is the same, the difference is noise the user has to resolve.
344
+
345
+ ### E-94 Destructive actions coloured red at rest
346
+ ❌ A red "Delete" sitting in a list of rows
347
+ ✅ At rest a destructive action is **tertiary** — less prominent, further from the primary, or disclosed. Red makes it *more* prominent, which is backwards: the goal is friction, not attention.
348
+ `--color-danger` styling belongs on the **confirming** button inside the confirmation step, where the user has already chosen and needs to feel the weight of it.
349
+
350
+ ### E-95 Primary action parked at the right
351
+ ❌ A right-aligned "Next" with "Back" beside it at the bottom of a multi-step form
352
+ ✅ Start-align the primary, ordered most to least important. Right-aligned actions get missed on wide screens and by screen-magnifier users, and sit further from the fields they submit.
353
+ On multi-step forms put **"Back" as a tertiary button at the top left** — away from the primary, where it cannot be hit by mistake and lose everything just entered.
354
+
355
+ ---
356
+
357
+ ## F. Forms
358
+
359
+ ### F-36 Placeholder as label
360
+ ❌ `<input placeholder="Email address">`
361
+ ✅ Persistent `<label>`. Placeholders vanish on focus, fail low-vision users, and break autofill heuristics.
362
+
363
+ ### F-37 Unhelpful error text
364
+ ❌ "Invalid input", "Error", "This field is required"
365
+ ✅ Say what is wrong and what to do: "Enter a date after today." Errors are instructions, not verdicts.
366
+
367
+ ### F-38 Validation timing wrong at both ends
368
+ ❌ Validating on every keystroke, or only on submit with no inline recovery
369
+ ✅ Validate on blur, revalidate on change once a field has errored, summarise on submit.
370
+
371
+ ### F-39 Input cleared on error
372
+ ❌ Re-rendering the form empty after a failed submit
373
+ ✅ Echo every submitted value back into the field. Losing a user's typing is the most expensive small bug in forms.
374
+
375
+ ### F-40 Missing input affordances
376
+ ❌ `<input type="text">` for email, phone, numbers, one-time codes
377
+ ✅ Correct `type`, `inputmode`, `autocomplete`, and `enterkeyhint`. On mobile this is the difference between a usable form and an abandoned one.
378
+
379
+ ### F-41 Critical function dependent on JavaScript
380
+ ❌ A form that only submits via `fetch`, a nav that only opens with JS
381
+ ✅ Works without JS, enhanced with it. On an unreliable mobile connection, JS-dependency is not a hypothetical — it is a silent failure that looks to the user like nothing happened.
382
+
383
+ ### F-97 Required fields left unmarked
384
+ ❌ "All fields are required unless marked optional" at the top, with only optional fields marked
385
+ ✅ Mark **both**. Required takes `*` (with the convention stated at the top) or the word "required"; optional takes the word "optional".
386
+ Instructions at the top of a form get scanned past, leaving people guessing field by field. Marking both is also an accessibility requirement for screen reader users, so sighted users may as well get the same clarity. **Never colour the asterisk red** — red means error.
387
+ Unmarked required fields are acceptable only where: the product has no optional fields at all; the form is short and familiar (login, newsletter); one question is asked per screen with its reason given; or testing has shown the markers unnecessary.
388
+
389
+ ### F-98 Labels beside inputs
390
+ ❌ Labels to the left of their fields, left- or right-aligned
391
+ ✅ Stack the label directly above its input, `--spacing-label` (4px) away.
392
+ Left-placed labels make the eye zig-zag between column and field. Right-aligning them to shorten that distance creates a jagged left edge that is harder to scan. Long labels wrap awkwardly in the narrow column. Stacked above and close, label and input are read in one focus.
393
+
394
+ ### F-99 Uniform field widths
395
+ ❌ Every field full-width, including a four-digit postcode and a three-digit CVC
396
+ ✅ Width should match the expected input. It is the strongest signal people have about how much is wanted, and a wide box for a short answer creates hesitation. Where length varies, size for the common case.
397
+
398
+ ### F-100 A dropdown for a small set of options
399
+ ❌ A select holding three, five or eight choices
400
+ ✅ Radio buttons, stacked vertically, for roughly ten options or fewer. A dropdown costs open, scroll and choose — several precise interactions — hides its options from scanning and comparison, and looks filled when empty, so it gets skipped. Radios cost one press and stay visible.
401
+
402
+ ### F-101 A long dropdown where autocomplete belongs
403
+ ❌ A 200-entry country select
404
+ ✅ An autocomplete field when people already know the answer. Keep suggestions to about ten and **bold the differing part** so they can be told apart quickly.
405
+ Where people must *browse* to decide, split the list into two dependent fields — industry then occupation — rather than one enormous one.
406
+
407
+ ### F-102 Toggle and checkbox used interchangeably
408
+ ❌ A toggle inside a form that only applies on submit
409
+ ✅ The distinction is **when the change takes effect**. A checkbox waits for a submit button; a toggle applies immediately. Label both with what happens when they are **on**.
410
+
411
+ ### F-103 Negatively phrased checkbox labels
412
+ ❌ "Don't allow automatic updates"
413
+ ✅ Describe what happens when it is **checked**. Test by prefixing "Yes,": "Yes, allow automatic updates" reads cleanly; "Yes, don't allow automatic updates" does not.
414
+
415
+ ### F-104 Instructional verbs in labels
416
+ ❌ "Enter your email", "Type your email here"
417
+ ✅ "Email". The input field already tells people to type in it.
418
+
419
+ ---
420
+
421
+ ## G. Motion
422
+
423
+ ### G-42 Entrance animation on everything
424
+ ❌ Every section fading and rising on scroll
425
+ ✅ Motion earns its place by explaining a change of state or spatial relationship. Decoration on a page the user will visit twice a day becomes friction.
426
+
427
+ ### G-43 `prefers-reduced-motion` ignored
428
+ ❌ Animation with no reduced-motion path
429
+ ✅ Always provide the reduced path. Non-negotiable — this is a vestibular safety issue, not a preference.
430
+
431
+ ### G-44 Durations too long
432
+ ❌ 500ms+ on UI feedback
433
+ ✅ 100–200ms for state change, up to 300ms for larger transitions. If it can be perceived as waiting, it is too slow.
434
+
435
+ ---
436
+
437
+ ## H. Code-level
438
+
439
+ ### H-45 New component instead of the existing one
440
+ ❌ Writing a fresh button component because the repo's was not read
441
+ ✅ Search the codebase first. Extend what exists. Duplication here is where design systems die.
442
+
443
+ ### H-46 Local convention overridden
444
+ ❌ Introducing a second styling approach, naming scheme or file layout
445
+ ✅ Match the surrounding file. Consistency with the codebase outranks personal preference and outranks this document.
446
+
447
+ ### H-47 Values hard-coded past the token layer
448
+ ❌ A raw hex colour or pixel size written in component code
449
+ ✅ Reference the token. Consume the semantic role (`--color-text-strong`), not the primitive (`--color-neutral-900`). A value that cannot be expressed as a token indicates a missing token.
450
+
451
+ ### H-48 JavaScript for something CSS does
452
+ ❌ Scroll listeners for sticky positioning; scripted accordions and dialogs that have native equivalents
453
+ ✅ Platform first: `position: sticky`, `<details>`, `<dialog>`, `:has()`, container queries, `scroll-behavior`, `popover`. Reach for a framework when the platform genuinely lacks the capability.
454
+
455
+ ---
456
+
457
+ ## I. Copy
458
+
459
+ **Moved to `05-copy.md`.** Interface text outgrew a section here. Rules `I-53` through `I-57` kept their numbers and live in that file, alongside the rest of the copy rules.
460
+
461
+ Load `05-copy.md` whenever writing or reviewing a user-facing string.
462
+
463
+ ---
464
+
465
+ ## Self-check before finishing
466
+
467
+ Run this against what you produced. Any "no" is a defect to fix, not a note to mention.
468
+
469
+ 1. Would this look different from a generic template if the accent colour were removed? (A-01 → A-10)
470
+ 2. Is every run of prose measure-capped and left-aligned? (B-11, B-12)
471
+ 3. Does any text or placeholder fall below 4.5:1? (C-19)
472
+ 4. Is every spacing value a `--spacing-*` token, and does `--spacing-heading-before` exceed `--spacing-heading-after`? (D-23, D-24)
473
+ 5. Does every interactive element have hover, focus-visible, active and disabled? (E-28)
474
+ 5b. Are links underlined, headings uncoloured, and icon-only controls labelled? (C-49, C-50, E-51)
475
+ 5c. Do control borders use `--color-stroke-strong` and clear 3:1? (`02-tokens.md`)
476
+ 5d. Has the copy been checked against `05-copy.md`? (sentence case, front-loaded, plain, descriptive links, actionable errors)
477
+ 5e. Can a user tell what each control is, which item is selected, and what to do next? (E-63)
478
+ 5f. Does spacing grow from the innermost rectangle outward, and does the component use one alignment? (D-69, D-71)
479
+ 5g. Does the layout survive the longest realistic content, and does nothing non-interactive look interactive? (E-73, C-68)
480
+ 5i. Do button weights differ in structure rather than colour, is there one primary, is the tertiary underlined, and are destructive actions low-prominence? (E-91, P-02, E-94)
481
+ 5h. Is long-form text 18px+ at 1.5+, start-aligned, 40–80 characters per line, in one typeface and two weights? (B-75, B-12, B-76, B-77)
482
+ 5j. Single column, labels above and close, both required and optional marked, hints above the input, widths matched, borders at 3:1? (P-04, F-98, F-97, P-03, F-99)
483
+ 6. Does `outline-none` appear anywhere without a replacement indicator? (E-29)
484
+ 7. Does every data view have a loading, empty and error state? (E-28, E-30)
485
+ 8. Does every form field have a persistent label, correct `type`/`inputmode`/`autocomplete`, and value echo on error? (F-36, F-39, F-40)
486
+ 9. Does the primary action still work with JavaScript disabled? (F-41)
487
+ 10. Is there a `prefers-reduced-motion` path? (G-43)
488
+ 11. Did you reuse existing components and tokens rather than adding new ones? (H-45, H-47)
489
+
490
+ ---
491
+
492
+ ## Notes for the author (not for the agent)
493
+
494
+ Rules carrying a deliberate house position rather than a general best practice — these are where your taste is recorded, and they should be defended or changed consciously:
495
+
496
+ - **C-18** — off-white over pure white. Already evidenced in your site's `#fafaf7`, now the anchor of the default neutral ramp.
497
+ - **A-07, A-08** — bordered, low-radius, low-shadow surfaces. This is a stance, not a consensus.
498
+ - **F-41, G-42** — resilience and restraint weighted above visual richness. Connects directly to your writing on JS-dependent form fields.
499
+ - **D-24** — heading space asymmetry. Universal advice, but stating a ratio makes it enforceable.
500
+
501
+ Candidates deferred to mode profiles because they are not universal: information density, table row height, use of colour fills for status, page-section rhythm, hero presence, illustration and imagery policy, animation budget.