@elitedcs/ghl-mcp 3.81.1 → 3.83.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,1098 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "ruleset": "anti-default",
4
+ "title": "Anti-sameness rules — the visual twin of the Copy Gate",
5
+ "spec": "shared/design-studio-plan.md §3.11b",
6
+ "authoredBy": "command-center lane",
7
+ "authoredAt": "2026-09-07",
8
+ "status": "SPEC DATA — not yet implemented. Implementation lands in the ghl-command-mcp lane at Design Studio phase 0 as src/command-os/design/rules/anti-default.json.",
9
+ "howToRead": {
10
+ "check": {
11
+ "measured": "The verifier reads a number or boolean straight off the rendered DOM or computed style. Reported with its value.",
12
+ "proxy": "A heuristic that approximates the real rule. Reported with its value AND labelled a proxy in verification.json.",
13
+ "judged": "Only a model can decide. Listed in the report, never hidden, never scored as a pass.",
14
+ "undecidable": "A fourth state the verifier must be able to return for ANY check: the subject was not readable in this run, so the rule was neither passed nor failed. Styles in an external sheet, a utility framework, or injected by JS all produce it. Required because a checker without this state reports an unreadable page as a clean one — proved 2026-09-08 on fixtures/external-css.html, which carries two real failures and scored zero findings before the state existed."
15
+ },
16
+ "independence": "Per design-studio-plan §3.6 (F7): the verifier derives every subject itself and only cross-checks the model's data-* hooks. Headline = largest rendered text. Primary CTA = filled control whose background is within ΔE of brand accent. Section = top-level landmark or a direct child of main. A hook/DOM mismatch is a conformance warning, never a design finding.",
17
+ "threshold": "The value the rule fails at or beyond. Where a rule is a plain ban the threshold is the string 'any'.",
18
+ "rewrite": "Fed back verbatim to the generator by the retry protocol (design-studio-plan §3.9) as the next instruction.",
19
+ "reporting": "Every report, including anything a member or an operator sees, states findings AND a decided count. A finding count on its own is invalid output: it cannot distinguish a clean page from an unreadable one. An undecidable check is information, not an absence, and is named in the report with its rule id. A bar of the form 'must trip 0' is only met when nothing was undecided. Requirement raised by the ghl-command-mcp lane 2026-09-08 and adopted into the contract."
20
+ },
21
+ "boundary": {
22
+ "ownedHere": "Everything visible on the rendered page: layout, colour, type, decoration, motion, interaction states, and the literal strings as they render.",
23
+ "ownedByCopyGate": "Tone, claims, persuasion structure and voice. Those never appear here.",
24
+ "deliberateOverlap": "The renderedText group duplicates a handful of Copy Gate string bans on purpose. The Copy Gate grades copy before it becomes HTML and never sees the final page, so alt text, aria-labels, button labels and image captions reach the browser ungraded. This group is the last filter on the artifact itself.",
25
+ "duplicatesOf": "Rules marked duplicateOf restate a web/slide rule that already exists in rules/web.json. They stay here so an anti-default-only run is self-contained; the verifier must report each finding once, keyed on the web.json id."
26
+ },
27
+ "provenance": {
28
+ "note": "Command OS is a paid licensed product. These attribution lines must ship with the package that carries this file.",
29
+ "sources": [
30
+ {
31
+ "id": "plan",
32
+ "name": "shared/design-studio-plan.md §3.11b",
33
+ "licence": "internal",
34
+ "obligation": "none"
35
+ },
36
+ {
37
+ "id": "impeccable",
38
+ "name": "pbakaus/impeccable, reached via h3nryprod01/design-taste reference/anti-slop.md Part 1 and reference/interaction-states.md",
39
+ "licence": "Apache-2.0",
40
+ "derivedFrom": "Anthropic frontend-design skill (Apache-2.0)",
41
+ "obligation": "Ship LICENSES/Apache-2.0.txt and a NOTICE naming Paul Bakaus and Anthropic, Inc. Same obligation Agency OS already discharges in skills/frontend-design/LICENSE-APACHE-2.0."
42
+ },
43
+ {
44
+ "id": "taste-skill",
45
+ "name": "leonxlnx/taste-skill, reached via design-taste reference/anti-slop.md Part 2 and reference/pre-flight.md",
46
+ "licence": "MIT",
47
+ "obligation": "Copyright and permission notice."
48
+ },
49
+ {
50
+ "id": "emil",
51
+ "name": "emilkowalski/skills review-animations/STANDARDS.md",
52
+ "licence": "MIT",
53
+ "obligation": "Copyright and permission notice."
54
+ },
55
+ {
56
+ "id": "house",
57
+ "name": "Standing house rule set by the owner",
58
+ "licence": "internal",
59
+ "obligation": "none"
60
+ }
61
+ ],
62
+ "restated": "Rules below carry facts and thresholds, not prose. No block of upstream text is reproduced verbatim in this file."
63
+ },
64
+ "fixtures": {
65
+ "mustTrip": {
66
+ "file": "fixtures/claude-default.html",
67
+ "bar": "≥5 tells light up",
68
+ "why": "design-studio-plan §3.11b names this fixture as the ruleset's own proof."
69
+ },
70
+ "mustStayClean": {
71
+ "file": "fixtures/house-clean.html",
72
+ "bar": "0 tells light up",
73
+ "why": "A tell catalogue with no false-positive fixture grades every honest page as slop. This is the other half of the proof."
74
+ },
75
+ "mustNotOverclaim": {
76
+ "file": "fixtures/external-css.html",
77
+ "bar": "0 findings and >= 20 undecidable",
78
+ "why": "A page carrying real failures behind an external stylesheet. Proves the checker refuses to decide what it cannot see, rather than scoring it clean."
79
+ }
80
+ },
81
+ "rules": [
82
+ {
83
+ "id": "AD01",
84
+ "group": "type",
85
+ "medium": "both",
86
+ "name": "Default display face",
87
+ "threshold": "any",
88
+ "unit": "font-family",
89
+ "check": "measured",
90
+ "howMeasured": "computed font-family of the largest rendered text run on the page resolves to Inter, Space Grotesk, Roboto, Arial or Helvetica.",
91
+ "rewrite": "Pick a display face that belongs to this brand. If the brand has no licensed face, use the brand-dna googleFallback, never the generator default.",
92
+ "source": "plan"
93
+ },
94
+ {
95
+ "id": "AD02",
96
+ "group": "type",
97
+ "medium": "web",
98
+ "name": "Body copy size floor",
99
+ "threshold": 18,
100
+ "unit": "px",
101
+ "check": "measured",
102
+ "howMeasured": "computed font-size of the dominant body run (the longest text run outside nav, footer and caption roles) at 1440x900. Below 18px fails.",
103
+ "rewrite": "Raise body copy to 18px or larger. 16px is a house failure, not a preference.",
104
+ "source": "house"
105
+ },
106
+ {
107
+ "id": "AD03",
108
+ "group": "type",
109
+ "medium": "web",
110
+ "name": "Body copy is dark, not muted grey",
111
+ "threshold": 7.0,
112
+ "unit": "contrast ratio",
113
+ "check": "measured",
114
+ "howMeasured": "WCAG contrast of the dominant body run against its own resolved background. Below 7.0:1 fails. Verified separation: #4a5163 on #ffffff is 7.93:1 and on #f7f7f5 is 7.39:1; #666666 on #ffffff is 5.74:1; Tailwind gray-500 #6b7280 and zinc-500 #71717a are both 4.83:1.",
115
+ "rewrite": "Darken body copy until it clears 7:1 against its own background. Stated as a ratio, not a hex, so it holds in dark mode and on brand-tinted neutrals.",
116
+ "source": "house"
117
+ },
118
+ {
119
+ "id": "AD04",
120
+ "group": "type",
121
+ "medium": "both",
122
+ "name": "Flat type scale",
123
+ "threshold": 1.25,
124
+ "unit": "ratio between adjacent steps",
125
+ "check": "measured",
126
+ "howMeasured": "sorted distinct computed font-sizes of heading-role elements; any adjacent pair whose ratio is below 1.25 fails.",
127
+ "rewrite": "Open the scale. Hierarchy comes from scale plus weight contrast, not from four sizes within 2px of each other.",
128
+ "source": "taste-skill"
129
+ },
130
+ {
131
+ "id": "AD05",
132
+ "group": "type",
133
+ "medium": "web",
134
+ "name": "Line length",
135
+ "threshold": 75,
136
+ "unit": "ch",
137
+ "check": "measured",
138
+ "howMeasured": "rendered width of the dominant body run divided by the width of a '0' glyph at that computed style, at 1440x900.",
139
+ "rewrite": "Cap the measure at 65-75ch with max-width on the text column.",
140
+ "source": "taste-skill"
141
+ },
142
+ {
143
+ "id": "AD06",
144
+ "group": "type",
145
+ "medium": "web",
146
+ "name": "Body leading",
147
+ "threshold": "outside 1.45-1.65",
148
+ "unit": "unitless line-height",
149
+ "check": "measured",
150
+ "howMeasured": "computed line-height / font-size on the dominant body run.",
151
+ "rewrite": "Set body line-height between 1.5 and 1.6. Headings go tight, 1.1 to 1.2.",
152
+ "source": "taste-skill"
153
+ },
154
+ {
155
+ "id": "AD07",
156
+ "group": "type",
157
+ "medium": "both",
158
+ "name": "Font family count",
159
+ "threshold": 3,
160
+ "unit": "distinct families",
161
+ "check": "measured",
162
+ "howMeasured": "distinct first-choice families across every computed font-family on rendered text, generic fallbacks excluded. More than 3 fails.",
163
+ "rewrite": "Display plus body plus optional mono. Drop the rest.",
164
+ "source": "taste-skill"
165
+ },
166
+ {
167
+ "id": "AD08",
168
+ "group": "type",
169
+ "medium": "web",
170
+ "name": "Runaway display size",
171
+ "threshold": 96,
172
+ "unit": "px",
173
+ "check": "measured",
174
+ "howMeasured": "computed font-size of the h1 / largest heading at 1440x900. Above 96px fails.",
175
+ "rewrite": "Cap the hero clamp max at 6rem. Control hierarchy with weight and colour, not raw scale.",
176
+ "source": "taste-skill"
177
+ },
178
+ {
179
+ "id": "AD09",
180
+ "group": "type",
181
+ "medium": "both",
182
+ "name": "Letters touching",
183
+ "threshold": -0.04,
184
+ "unit": "em letter-spacing",
185
+ "check": "measured",
186
+ "howMeasured": "computed letter-spacing on display-role text, normalised to em. Tighter than -0.04em fails.",
187
+ "rewrite": "Loosen display tracking to -0.04em or wider.",
188
+ "source": "taste-skill"
189
+ },
190
+ {
191
+ "id": "AD10",
192
+ "group": "type",
193
+ "medium": "both",
194
+ "name": "All-caps body copy",
195
+ "threshold": "any",
196
+ "unit": "run",
197
+ "check": "measured",
198
+ "howMeasured": "any text run longer than 120 characters with computed text-transform uppercase, or whose rendered string is all uppercase.",
199
+ "rewrite": "Sentence case. Reserve caps for a short label.",
200
+ "source": "taste-skill"
201
+ },
202
+ {
203
+ "id": "AD11",
204
+ "group": "type",
205
+ "medium": "both",
206
+ "name": "Reflex serif",
207
+ "threshold": "any",
208
+ "unit": "font-family",
209
+ "check": "measured",
210
+ "howMeasured": "display face resolves to Fraunces or Instrument Serif.",
211
+ "rewrite": "These two are the current AI-default serifs. Choose a different serif, or drop the serif; 'feels premium' is not a reason.",
212
+ "source": "taste-skill"
213
+ },
214
+ {
215
+ "id": "AD12",
216
+ "group": "colour",
217
+ "medium": "both",
218
+ "name": "Pure black on pure white",
219
+ "threshold": "any",
220
+ "unit": "pair",
221
+ "check": "measured",
222
+ "howMeasured": "body or heading colour resolves to #000000 on a #ffffff ground, or the inverse.",
223
+ "rewrite": "Off-black and off-white. Pure values kill depth.",
224
+ "source": "plan"
225
+ },
226
+ {
227
+ "id": "AD13",
228
+ "group": "colour",
229
+ "medium": "both",
230
+ "name": "More than one accent",
231
+ "threshold": 1,
232
+ "unit": "accent hue",
233
+ "check": "proxy",
234
+ "howMeasured": "distinct hues among background-colors of filled interactive controls with OKLCH chroma above 0.08, clustered at 15 degrees. More than one cluster fails.",
235
+ "rewrite": "Lock one accent across the whole page. Secondary emphasis comes from weight, size and neutrals.",
236
+ "source": "taste-skill"
237
+ },
238
+ {
239
+ "id": "AD14",
240
+ "group": "colour",
241
+ "medium": "both",
242
+ "name": "Oversaturated accent",
243
+ "threshold": 0.8,
244
+ "unit": "normalised saturation",
245
+ "check": "measured",
246
+ "howMeasured": "HSL saturation of the accent cluster centroid. Above 0.80 fails unless brand-dna.colors.accent itself is above it.",
247
+ "rewrite": "Desaturate the accent so it sits with the neutrals instead of vibrating against them.",
248
+ "source": "taste-skill"
249
+ },
250
+ {
251
+ "id": "AD15",
252
+ "group": "colour",
253
+ "medium": "both",
254
+ "name": "Gradient hero",
255
+ "threshold": "any",
256
+ "unit": "declaration",
257
+ "check": "measured",
258
+ "howMeasured": "computed background-image of the first full-width section contains linear-gradient or radial-gradient.",
259
+ "rewrite": "Solid ground, or a real photographic or textural asset. The gradient hero is the single most recognisable generated-page signature.",
260
+ "source": "plan"
261
+ },
262
+ {
263
+ "id": "AD16",
264
+ "group": "colour",
265
+ "medium": "both",
266
+ "name": "Gradient text",
267
+ "threshold": "any",
268
+ "unit": "declaration",
269
+ "check": "measured",
270
+ "howMeasured": "any element with background-clip or -webkit-background-clip set to text and a gradient background-image.",
271
+ "rewrite": "One solid colour. Emphasis via weight or size.",
272
+ "source": "impeccable"
273
+ },
274
+ {
275
+ "id": "AD17",
276
+ "group": "colour",
277
+ "medium": "both",
278
+ "name": "AI purple-blue glow",
279
+ "threshold": "any",
280
+ "unit": "hue window",
281
+ "check": "proxy",
282
+ "howMeasured": "accent hue falls in 250-285 degrees AND the page carries a box-shadow or filter using that hue at above 0.5 alpha.",
283
+ "rewrite": "Different hue family, or drop the glow. Inner borders and tinted shadows do the same job without the tell.",
284
+ "source": "taste-skill"
285
+ },
286
+ {
287
+ "id": "AD18",
288
+ "group": "colour",
289
+ "medium": "both",
290
+ "name": "Premium-consumer default palette",
291
+ "threshold": 12,
292
+ "unit": "ΔE2000 to cluster centroid",
293
+ "check": "proxy",
294
+ "howMeasured": "the token triple (ground, display colour, accent) sits within ΔE 12 of the cream / brass / oxblood / espresso centroid AND the display face is a serif.",
295
+ "rewrite": "This is the reflex palette for cookware, wellness, artisan and luxury briefs. Pick a different family and say which.",
296
+ "source": "plan"
297
+ },
298
+ {
299
+ "id": "AD19",
300
+ "group": "colour",
301
+ "medium": "both",
302
+ "name": "Placeholder contrast",
303
+ "threshold": 4.5,
304
+ "unit": "contrast ratio",
305
+ "check": "measured",
306
+ "howMeasured": "computed ::placeholder colour against the input's resolved background.",
307
+ "rewrite": "Placeholder text is text. Bring it to 4.5:1.",
308
+ "source": "taste-skill",
309
+ "duplicateOf": "W11"
310
+ },
311
+ {
312
+ "id": "AD20",
313
+ "group": "colour",
314
+ "medium": "both",
315
+ "name": "CTA label contrast",
316
+ "threshold": 4.5,
317
+ "unit": "contrast ratio",
318
+ "check": "measured",
319
+ "howMeasured": "label colour of every filled control against that control's own background.",
320
+ "rewrite": "The button no one can read is the button no one clicks.",
321
+ "source": "taste-skill",
322
+ "duplicateOf": "W11"
323
+ },
324
+ {
325
+ "id": "AD21",
326
+ "group": "layout",
327
+ "medium": "web",
328
+ "name": "Three identical cards in a row",
329
+ "threshold": "any",
330
+ "unit": "row",
331
+ "check": "measured",
332
+ "howMeasured": "a grid or flex row of exactly 3 boxed siblings whose widths agree within 4px and whose child tag sequences are identical.",
333
+ "rewrite": "Two-column zigzag, an asymmetric grid, a horizontal scroller, or a list. Not everything in the row is equally important.",
334
+ "source": "plan"
335
+ },
336
+ {
337
+ "id": "AD22",
338
+ "group": "layout",
339
+ "medium": "web",
340
+ "name": "Identical card grid at any width",
341
+ "threshold": 4,
342
+ "unit": "matching siblings",
343
+ "check": "measured",
344
+ "howMeasured": "4 or more boxed siblings with identical child tag sequences, equal widths within 4px, and the same computed border-radius, padding and background.",
345
+ "rewrite": "Vary size, emphasis or content shape across the set, or group with dividers and space instead of boxes.",
346
+ "source": "impeccable"
347
+ },
348
+ {
349
+ "id": "AD23",
350
+ "group": "layout",
351
+ "medium": "web",
352
+ "name": "Nested cards",
353
+ "threshold": "any",
354
+ "unit": "nesting",
355
+ "check": "measured",
356
+ "howMeasured": "an element carrying both a border or box-shadow and a non-transparent background, contained by another element with the same combination.",
357
+ "rewrite": "Nested cards are always wrong. Flatten the inner one to spacing.",
358
+ "source": "taste-skill"
359
+ },
360
+ {
361
+ "id": "AD24",
362
+ "group": "layout",
363
+ "medium": "web",
364
+ "name": "Eyebrow on every section",
365
+ "threshold": "ceil(sectionCount / 3)",
366
+ "unit": "eyebrows",
367
+ "check": "measured",
368
+ "howMeasured": "count text runs under 40 characters whose computed text-transform is uppercase and letter-spacing is above 0.05em, positioned directly above a heading-role element. Hero counts as 1. Exceeding ceil(sectionCount / 3) fails.",
369
+ "rewrite": "One deliberate kicker is voice. An eyebrow above every section is AI grammar. Delete the rest.",
370
+ "source": "impeccable"
371
+ },
372
+ {
373
+ "id": "AD25",
374
+ "group": "layout",
375
+ "medium": "both",
376
+ "name": "Numbered section markers",
377
+ "threshold": "any",
378
+ "unit": "marker",
379
+ "check": "measured",
380
+ "howMeasured": "an eyebrow-shaped run matching ^\\s*(0?\\d{1,3})\\s*[·./|-]?\\s*\\S, or containing 'INDEX', where the page has no ordered-sequence semantics.",
381
+ "rewrite": "Numbers earn their place only when the section really is a sequence the reader must follow in order. Name the topic in plain language instead.",
382
+ "source": "taste-skill"
383
+ },
384
+ {
385
+ "id": "AD26",
386
+ "group": "layout",
387
+ "medium": "web",
388
+ "name": "One radius on everything",
389
+ "threshold": 0.9,
390
+ "unit": "share of boxed elements",
391
+ "check": "proxy",
392
+ "howMeasured": "share of elements with a non-zero border-radius that carry the single most common radius value. At or above 0.90 fails.",
393
+ "rewrite": "One radius system does not mean one radius value. Differentiate cards, inputs, tags and buttons.",
394
+ "source": "plan"
395
+ },
396
+ {
397
+ "id": "AD27",
398
+ "group": "layout",
399
+ "medium": "web",
400
+ "name": "Over-rounded cards",
401
+ "threshold": 24,
402
+ "unit": "px",
403
+ "check": "measured",
404
+ "howMeasured": "computed border-radius on a card-role element (bordered or shadowed box containing a heading and body text) at or above 24px. Pills on tags and buttons are exempt.",
405
+ "rewrite": "Cards top out at 12-16px. 24, 28, 32 and 40px on a card is a generator tell.",
406
+ "source": "impeccable"
407
+ },
408
+ {
409
+ "id": "AD28",
410
+ "group": "layout",
411
+ "medium": "web",
412
+ "name": "Ghost card",
413
+ "threshold": 16,
414
+ "unit": "px shadow blur alongside a 1px border",
415
+ "check": "measured",
416
+ "howMeasured": "an element with border-width 1px AND a box-shadow whose blur radius is 16px or greater.",
417
+ "rewrite": "Pick one. A single solid border at the brand colour, or a defined shadow at 8px blur or less. Never both as decoration.",
418
+ "source": "impeccable"
419
+ },
420
+ {
421
+ "id": "AD29",
422
+ "group": "layout",
423
+ "medium": "web",
424
+ "name": "Panic z-index",
425
+ "threshold": 999,
426
+ "unit": "z-index",
427
+ "check": "measured",
428
+ "howMeasured": "any computed z-index at or above 999.",
429
+ "rewrite": "Semantic scale: dropdown, sticky, modal, toast, tooltip. 9999 means the stacking context was never designed.",
430
+ "source": "taste-skill"
431
+ },
432
+ {
433
+ "id": "AD30",
434
+ "group": "layout",
435
+ "medium": "both",
436
+ "name": "Everything centred",
437
+ "threshold": 0.6,
438
+ "unit": "share of text blocks",
439
+ "check": "proxy",
440
+ "howMeasured": "share of text-bearing blocks with computed text-align center. Above 0.60 fails.",
441
+ "rewrite": "Centre the hero if it earns it. Left-align the rest; centred body copy is hard to read and reads as a template.",
442
+ "source": "plan"
443
+ },
444
+ {
445
+ "id": "AD31",
446
+ "group": "layout",
447
+ "medium": "web",
448
+ "name": "Nav height and wrap",
449
+ "threshold": 80,
450
+ "unit": "px",
451
+ "check": "measured",
452
+ "howMeasured": "bounding height of the nav landmark at 1440x900, and whether its direct children occupy more than one line box.",
453
+ "rewrite": "One line, 80px or less at desktop.",
454
+ "source": "taste-skill"
455
+ },
456
+ {
457
+ "id": "AD32",
458
+ "group": "layout",
459
+ "medium": "web",
460
+ "name": "Hero does not fit the fold",
461
+ "threshold": "any",
462
+ "unit": "overflow",
463
+ "check": "measured",
464
+ "howMeasured": "at 1440x900 and 390x844: h1 renders in more than 2 line boxes, OR the subline exceeds 20 words, OR no primary CTA intersects the first viewport rectangle.",
465
+ "rewrite": "Headline 2 lines, subtext 20 words, CTA above the fold on both sizes.",
466
+ "source": "taste-skill",
467
+ "duplicateOf": "W6"
468
+ },
469
+ {
470
+ "id": "AD33",
471
+ "group": "layout",
472
+ "medium": "web",
473
+ "name": "Hero floating down the page",
474
+ "threshold": 96,
475
+ "unit": "px top padding",
476
+ "check": "measured",
477
+ "howMeasured": "computed padding-top of the hero section at 1440x900. Above 96px fails.",
478
+ "rewrite": "The hero starts near the top. Pushing content halfway down the viewport is padding by reflex.",
479
+ "source": "taste-skill"
480
+ },
481
+ {
482
+ "id": "AD34",
483
+ "group": "layout",
484
+ "medium": "web",
485
+ "name": "Hero stack overload",
486
+ "threshold": 4,
487
+ "unit": "text elements",
488
+ "check": "measured",
489
+ "howMeasured": "count of distinct text-bearing blocks that are descendants of the hero section, CTA labels excluded. More than 4 fails.",
490
+ "rewrite": "Eyebrow or brand strip, headline, subtext, CTAs. No tagline under the buttons, no trust micro-strip in the hero.",
491
+ "source": "taste-skill"
492
+ },
493
+ {
494
+ "id": "AD35",
495
+ "group": "layout",
496
+ "medium": "web",
497
+ "name": "Split header floater",
498
+ "threshold": "any",
499
+ "unit": "section header",
500
+ "check": "proxy",
501
+ "howMeasured": "a section header containing a heading in the left 60 percent and a text block under 200 characters in the right 40 percent, whose top edges differ by more than 8px and which shares no grid line with the heading.",
502
+ "rewrite": "Put the sub-text under the headline, or build an aligned two-column header. The tiny corner paragraph is the tell.",
503
+ "source": "taste-skill"
504
+ },
505
+ {
506
+ "id": "AD36",
507
+ "group": "layout",
508
+ "medium": "web",
509
+ "name": "Zigzag on repeat",
510
+ "threshold": 3,
511
+ "unit": "consecutive sections",
512
+ "check": "proxy",
513
+ "howMeasured": "3 or more consecutive sections classified into the same layout family, where family is derived from child count, column count and media position.",
514
+ "rewrite": "Break the alternation. Three image-and-text splits in a row is one idea repeated, not a page.",
515
+ "source": "taste-skill"
516
+ },
517
+ {
518
+ "id": "AD37",
519
+ "group": "layout",
520
+ "medium": "web",
521
+ "name": "Layout family variety",
522
+ "threshold": 4,
523
+ "unit": "distinct families across 8+ sections",
524
+ "check": "proxy",
525
+ "howMeasured": "on a page with 8 or more sections, fewer than 4 distinct layout families fails.",
526
+ "rewrite": "A page needs at least four ways of arranging things before it stops reading as a template.",
527
+ "source": "taste-skill"
528
+ },
529
+ {
530
+ "id": "AD38",
531
+ "group": "layout",
532
+ "medium": "web",
533
+ "name": "Duplicate CTA intent",
534
+ "threshold": "any",
535
+ "unit": "pair",
536
+ "check": "judged",
537
+ "howMeasured": "two CTA labels that mean the same thing in different words. Normalised string equality catches only the trivial case, so this is listed for the model, never scored.",
538
+ "rewrite": "One intent, one label, repeated verbatim if it repeats.",
539
+ "source": "taste-skill"
540
+ },
541
+ {
542
+ "id": "AD39",
543
+ "group": "layout",
544
+ "medium": "both",
545
+ "name": "Text overflows its container",
546
+ "threshold": "any",
547
+ "unit": "px overflow",
548
+ "check": "measured",
549
+ "howMeasured": "at 390, 768 and 1440 wide: any text-bearing element whose scrollWidth exceeds its clientWidth, or whose bounding box escapes its parent's padding box.",
550
+ "rewrite": "Test the real copy at every breakpoint. Reduce the clamp max or rewrite the headline. The viewport is part of the design.",
551
+ "source": "impeccable"
552
+ },
553
+ {
554
+ "id": "AD40",
555
+ "group": "layout",
556
+ "medium": "web",
557
+ "name": "Viewport instability",
558
+ "threshold": "any",
559
+ "unit": "declaration",
560
+ "check": "measured",
561
+ "howMeasured": "a full-height container sized with 100vh rather than 100dvh, or documentElement.scrollWidth exceeding the viewport width at 390.",
562
+ "rewrite": "min-height: 100dvh, never height: 100vh. No horizontal scroll on any breakpoint.",
563
+ "source": "taste-skill"
564
+ },
565
+ {
566
+ "id": "AD41",
567
+ "group": "layout",
568
+ "medium": "web",
569
+ "name": "Hairline under every row",
570
+ "threshold": 5,
571
+ "unit": "rows",
572
+ "check": "measured",
573
+ "howMeasured": "a list or table with more than 5 sibling rows where every row carries both a top and a bottom border.",
574
+ "rewrite": "Pick one edge and use it sparsely, or move to a component that suits a long list.",
575
+ "source": "taste-skill"
576
+ },
577
+ {
578
+ "id": "AD42",
579
+ "group": "decoration",
580
+ "medium": "both",
581
+ "name": "Side-stripe accent border",
582
+ "threshold": 1,
583
+ "unit": "px",
584
+ "check": "measured",
585
+ "howMeasured": "border-left-width or border-right-width above 1px with a colour distinct from the other three edges.",
586
+ "rewrite": "Full border, background tint, a leading number or icon, or nothing.",
587
+ "source": "impeccable",
588
+ "alsoIn": "plan"
589
+ },
590
+ {
591
+ "id": "AD43",
592
+ "group": "decoration",
593
+ "medium": "both",
594
+ "name": "Decorative glassmorphism",
595
+ "threshold": "any",
596
+ "unit": "declaration",
597
+ "check": "measured",
598
+ "howMeasured": "computed backdrop-filter containing blur on an element that is not a modal, sheet or sticky nav.",
599
+ "rewrite": "Rare and purposeful, or nothing.",
600
+ "source": "impeccable"
601
+ },
602
+ {
603
+ "id": "AD44",
604
+ "group": "decoration",
605
+ "medium": "both",
606
+ "name": "Stripe background",
607
+ "threshold": "any",
608
+ "unit": "declaration",
609
+ "check": "measured",
610
+ "howMeasured": "computed background-image contains repeating-linear-gradient or repeating-radial-gradient.",
611
+ "rewrite": "Remove it. Diagonal stripes behind a section are pure generator decoration.",
612
+ "source": "impeccable"
613
+ },
614
+ {
615
+ "id": "AD45",
616
+ "group": "decoration",
617
+ "medium": "both",
618
+ "name": "Sketchy SVG illustration",
619
+ "threshold": "any",
620
+ "unit": "asset",
621
+ "check": "measured",
622
+ "howMeasured": "inline SVG containing feTurbulence or feDisplacementMap, or a class or id matching sketch, doodle, wavy, squiggle, hand-drawn.",
623
+ "rewrite": "If the scene cannot be rendered with a real asset, ship no illustration. Sketchy SVG reads as amateurish, not whimsical.",
624
+ "source": "impeccable"
625
+ },
626
+ {
627
+ "id": "AD46",
628
+ "group": "decoration",
629
+ "medium": "both",
630
+ "name": "Emoji as section marker",
631
+ "threshold": "any",
632
+ "unit": "codepoint",
633
+ "check": "measured",
634
+ "howMeasured": "an emoji codepoint inside a heading-role element or an eyebrow run.",
635
+ "rewrite": "Use a real icon from the chosen set, or nothing.",
636
+ "source": "plan"
637
+ },
638
+ {
639
+ "id": "AD47",
640
+ "group": "decoration",
641
+ "medium": "web",
642
+ "name": "Decorative status dots",
643
+ "threshold": 1,
644
+ "unit": "dot per section",
645
+ "check": "proxy",
646
+ "howMeasured": "elements under 16px square with border-radius at or above 50 percent and a saturated background, positioned before a text run. More than one per section fails; the default expectation is zero.",
647
+ "rewrite": "A dot is allowed only when it carries real semantic state. A dot before every nav link or list row is decoration.",
648
+ "source": "taste-skill"
649
+ },
650
+ {
651
+ "id": "AD48",
652
+ "group": "decoration",
653
+ "medium": "both",
654
+ "name": "Middle-dot separator run",
655
+ "threshold": 1,
656
+ "unit": "· per line",
657
+ "check": "measured",
658
+ "howMeasured": "more than one U+00B7 in a single rendered line box.",
659
+ "rewrite": "One middle dot in a metadata strip is fine. A chain of them is a separator family that should have been line breaks, hairlines or columns.",
660
+ "source": "taste-skill"
661
+ },
662
+ {
663
+ "id": "AD49",
664
+ "group": "decoration",
665
+ "medium": "web",
666
+ "name": "Div-based fake screenshot",
667
+ "threshold": "any",
668
+ "unit": "block",
669
+ "check": "proxy",
670
+ "howMeasured": "a hero-region subtree with 8 or more descendant elements carrying background, border-radius and border, containing no img, picture, video, canvas or svg with real content.",
671
+ "rewrite": "Real screenshot, a generated image, a live component, or no preview. Fake product UI built out of divs is the most recognisable generated-page tell there is.",
672
+ "source": "taste-skill"
673
+ },
674
+ {
675
+ "id": "AD50",
676
+ "group": "decoration",
677
+ "medium": "web",
678
+ "name": "Scroll cue",
679
+ "threshold": "any",
680
+ "unit": "string",
681
+ "check": "measured",
682
+ "howMeasured": "a rendered run in the first viewport matching /^(scroll|↓\\s*scroll|scroll to (explore|discover|begin|walk))/i, or an element with a scroll-hint class and a looping animation.",
683
+ "rewrite": "The reader is looking at the hero. They know what scrolling is.",
684
+ "source": "taste-skill"
685
+ },
686
+ {
687
+ "id": "AD51",
688
+ "group": "decoration",
689
+ "medium": "web",
690
+ "name": "Hero-bottom decoration strip",
691
+ "threshold": "any",
692
+ "unit": "strip",
693
+ "check": "proxy",
694
+ "howMeasured": "a run in the lower quarter of the hero, computed text-transform uppercase, under 60 characters, containing 2 or more separators, whose tokens are not links.",
695
+ "rewrite": "Allowed only when the strip carries real navigable links or real status. Otherwise delete it.",
696
+ "source": "taste-skill"
697
+ },
698
+ {
699
+ "id": "AD52",
700
+ "group": "decoration",
701
+ "medium": "web",
702
+ "name": "Locale, time or weather strip",
703
+ "threshold": "any",
704
+ "unit": "string",
705
+ "check": "measured",
706
+ "howMeasured": "a header or footer run matching a city name adjacent to a time pattern \\d{1,2}:\\d{2} or a temperature pattern -?\\d{1,3}\\s?°[CF].",
707
+ "rewrite": "Allowed only for a genuinely distributed studio, a travel brand or a real physical venue. A contact address in the footer is fine; an atmospheric locale strip is not.",
708
+ "source": "taste-skill"
709
+ },
710
+ {
711
+ "id": "AD53",
712
+ "group": "decoration",
713
+ "medium": "web",
714
+ "name": "Version or status label in the hero",
715
+ "threshold": "any",
716
+ "unit": "string",
717
+ "check": "measured",
718
+ "howMeasured": "a run inside the hero matching /\\bv?\\d+\\.\\d+(\\.\\d+)?\\b|\\b(beta|alpha|early access|invite[- ]only|preview|waitlist)\\b/i.",
719
+ "rewrite": "Only when the brief is explicitly about launch status.",
720
+ "source": "taste-skill"
721
+ },
722
+ {
723
+ "id": "AD54",
724
+ "group": "decoration",
725
+ "medium": "web",
726
+ "name": "Version footer on a marketing page",
727
+ "threshold": "any",
728
+ "unit": "string",
729
+ "check": "measured",
730
+ "howMeasured": "a footer run matching /\\bv\\d+\\.\\d+\\.\\d+|build \\d+|last sync|\\bmain\\b\\s*$/i on a page whose medium is marketing or landing.",
731
+ "rewrite": "CLI and devtool fixtures, not landing-page content.",
732
+ "source": "taste-skill"
733
+ },
734
+ {
735
+ "id": "AD55",
736
+ "group": "decoration",
737
+ "medium": "web",
738
+ "name": "Label overlaid on an image",
739
+ "threshold": "any",
740
+ "unit": "overlay",
741
+ "check": "measured",
742
+ "howMeasured": "an absolutely or fixed positioned text element whose bounding box intersects an img or picture by more than 50 percent of its own area.",
743
+ "rewrite": "Let the image speak, or caption it directly below and outside the frame.",
744
+ "source": "taste-skill"
745
+ },
746
+ {
747
+ "id": "AD56",
748
+ "group": "decoration",
749
+ "medium": "both",
750
+ "name": "Decorative photo credit",
751
+ "threshold": "any",
752
+ "unit": "string",
753
+ "check": "proxy",
754
+ "howMeasured": "a caption adjacent to an image matching /(plate|frame|field study|study|archive)\\s*(no\\.?\\s*)?[\\divxIVX]+/i where the image src resolves to a placeholder host.",
755
+ "rewrite": "Credit a real photographer for a real photo, or write one functional caption, or none.",
756
+ "source": "taste-skill"
757
+ },
758
+ {
759
+ "id": "AD57",
760
+ "group": "decoration",
761
+ "medium": "web",
762
+ "name": "Filled-track score bar",
763
+ "threshold": "any",
764
+ "unit": "bar",
765
+ "check": "proxy",
766
+ "howMeasured": "an element with a non-transparent neutral background containing a narrower child with a saturated background, in a non-form, non-dashboard context.",
767
+ "rewrite": "A number with a small icon, or a thin inline bar with no background track. Big filled tracks are dashboard clutter on a marketing page.",
768
+ "source": "taste-skill"
769
+ },
770
+ {
771
+ "id": "AD58",
772
+ "group": "decoration",
773
+ "medium": "web",
774
+ "name": "More than one marquee",
775
+ "threshold": 1,
776
+ "unit": "marquee",
777
+ "check": "measured",
778
+ "howMeasured": "count of horizontally translating infinite animations with a duration above 5s.",
779
+ "rewrite": "One marquee at most, and only if it carries content worth reading twice.",
780
+ "source": "taste-skill"
781
+ },
782
+ {
783
+ "id": "AD59",
784
+ "group": "decoration",
785
+ "medium": "web",
786
+ "name": "Custom cursor",
787
+ "threshold": "any",
788
+ "unit": "declaration",
789
+ "check": "measured",
790
+ "howMeasured": "computed cursor resolves to a url() on any element, or a fixed-position element that tracks pointermove.",
791
+ "rewrite": "Outdated, accessibility-hostile and slow. Use the system cursor.",
792
+ "source": "taste-skill"
793
+ },
794
+ {
795
+ "id": "AD60",
796
+ "group": "motion",
797
+ "medium": "web",
798
+ "name": "transition: all",
799
+ "threshold": "any",
800
+ "unit": "declaration",
801
+ "check": "measured",
802
+ "howMeasured": "computed transition-property contains 'all' on any element.",
803
+ "rewrite": "Name the properties: transition: transform 200ms var(--ease-out).",
804
+ "source": "emil"
805
+ },
806
+ {
807
+ "id": "AD61",
808
+ "group": "motion",
809
+ "medium": "web",
810
+ "name": "Entry from scale(0)",
811
+ "threshold": "any",
812
+ "unit": "keyframe",
813
+ "check": "measured",
814
+ "howMeasured": "a keyframe or starting-style declaring a transform scale at or below 0.5.",
815
+ "rewrite": "Start at scale(0.95) with opacity 0. Nothing in the real world appears from nothing.",
816
+ "source": "emil"
817
+ },
818
+ {
819
+ "id": "AD62",
820
+ "group": "motion",
821
+ "medium": "web",
822
+ "name": "ease-in on UI",
823
+ "threshold": "any",
824
+ "unit": "timing function",
825
+ "check": "measured",
826
+ "howMeasured": "computed transition-timing-function or animation-timing-function resolves to ease-in, or to a cubic-bezier whose first control point x is above its y, on an interactive element.",
827
+ "rewrite": "ease-out, or a strong custom curve: cubic-bezier(0.23, 1, 0.32, 1). ease-in delays the exact moment the user is watching.",
828
+ "source": "emil"
829
+ },
830
+ {
831
+ "id": "AD63",
832
+ "group": "motion",
833
+ "medium": "web",
834
+ "name": "UI animation over 300ms",
835
+ "threshold": 300,
836
+ "unit": "ms",
837
+ "check": "measured",
838
+ "howMeasured": "computed transition-duration or animation-duration on an interactive element, modals and drawers excluded (their budget is 500ms).",
839
+ "rewrite": "Button press 100-160ms. Tooltip 125-200ms. Dropdown 150-250ms. A 180ms dropdown feels faster than a 400ms one.",
840
+ "source": "emil"
841
+ },
842
+ {
843
+ "id": "AD64",
844
+ "group": "motion",
845
+ "medium": "web",
846
+ "name": "Animating layout properties",
847
+ "threshold": "any",
848
+ "unit": "property",
849
+ "check": "measured",
850
+ "howMeasured": "transition-property or an animated keyframe touches width, height, top, left, right, bottom, margin or padding.",
851
+ "rewrite": "transform and opacity only. Everything else forces layout and paint on every frame.",
852
+ "source": "emil"
853
+ },
854
+ {
855
+ "id": "AD65",
856
+ "group": "motion",
857
+ "medium": "web",
858
+ "name": "No reduced-motion fallback",
859
+ "threshold": "any",
860
+ "unit": "rule",
861
+ "check": "measured",
862
+ "howMeasured": "the stylesheet declares one or more animations or transitions with movement but contains no prefers-reduced-motion media block.",
863
+ "rewrite": "Add the block. Reduced motion means gentler, not zero: keep opacity and colour, drop the movement.",
864
+ "source": "emil"
865
+ },
866
+ {
867
+ "id": "AD66",
868
+ "group": "motion",
869
+ "medium": "web",
870
+ "name": "Ungated hover motion",
871
+ "threshold": "any",
872
+ "unit": "rule",
873
+ "check": "measured",
874
+ "howMeasured": "a :hover rule animating transform that is not nested inside @media (hover: hover) and (pointer: fine).",
875
+ "rewrite": "Gate it. Touch devices fire a false hover on tap and the state sticks.",
876
+ "source": "emil"
877
+ },
878
+ {
879
+ "id": "AD67",
880
+ "group": "motion",
881
+ "medium": "web",
882
+ "name": "Centre-origin popover",
883
+ "threshold": "any",
884
+ "unit": "declaration",
885
+ "check": "proxy",
886
+ "howMeasured": "an element with popover, role=menu, role=tooltip or role=listbox whose computed transform-origin is the box centre. Modals and dialogs are exempt and stay centred.",
887
+ "rewrite": "Scale from the trigger. transform-origin: var(--radix-popover-content-transform-origin) or the Base UI equivalent.",
888
+ "source": "emil"
889
+ },
890
+ {
891
+ "id": "AD68",
892
+ "group": "motion",
893
+ "medium": "both",
894
+ "name": "Everything enters at once",
895
+ "threshold": "30-80",
896
+ "unit": "ms stagger",
897
+ "check": "proxy",
898
+ "howMeasured": "4 or more siblings share an identical entrance animation with zero delay spread. Where a stagger exists, a per-item delay outside 30-80ms also fails.",
899
+ "rewrite": "Stagger by 30-80ms. Longer feels slow, and stagger must never block interaction while it plays.",
900
+ "source": "emil"
901
+ },
902
+ {
903
+ "id": "AD69",
904
+ "group": "states",
905
+ "medium": "web",
906
+ "name": "Focus removed without replacement",
907
+ "threshold": "any",
908
+ "unit": "rule",
909
+ "check": "measured",
910
+ "howMeasured": "a rule setting outline to none or 0 on a focusable element with no :focus-visible rule restoring a visible indicator on the same selector.",
911
+ "rewrite": "Never remove focus without replacing it. Keyboard users never see hover.",
912
+ "source": "impeccable",
913
+ "duplicateOf": "W13"
914
+ },
915
+ {
916
+ "id": "AD70",
917
+ "group": "states",
918
+ "medium": "web",
919
+ "name": "Placeholder used as label",
920
+ "threshold": "any",
921
+ "unit": "field",
922
+ "check": "measured",
923
+ "howMeasured": "an input or textarea with a placeholder attribute and no associated label element, aria-label or aria-labelledby.",
924
+ "rewrite": "Label above the input. The placeholder disappears the moment the user starts typing.",
925
+ "source": "impeccable"
926
+ },
927
+ {
928
+ "id": "AD71",
929
+ "group": "states",
930
+ "medium": "web",
931
+ "name": "Touch target below 44px",
932
+ "threshold": 44,
933
+ "unit": "px",
934
+ "check": "measured",
935
+ "howMeasured": "bounding box of any interactive element at 390x844, including padding. Either dimension below 44px fails.",
936
+ "rewrite": "44px minimum on both axes at mobile.",
937
+ "source": "impeccable"
938
+ },
939
+ {
940
+ "id": "AD72",
941
+ "group": "states",
942
+ "medium": "web",
943
+ "name": "Missing interactive states",
944
+ "threshold": 8,
945
+ "unit": "states",
946
+ "check": "proxy",
947
+ "howMeasured": "for each interactive component, presence of rules or attributes covering default, hover, focus-visible, active, disabled, loading, error and success. Absence of a state that the component can actually reach fails.",
948
+ "rewrite": "Design all eight. A control with only default and hover is a mockup, not a component.",
949
+ "source": "impeccable"
950
+ },
951
+ {
952
+ "id": "AD73",
953
+ "group": "states",
954
+ "medium": "web",
955
+ "name": "No empty, loading or error state",
956
+ "threshold": "any",
957
+ "unit": "state",
958
+ "check": "judged",
959
+ "howMeasured": "a data-bearing surface with no markup for the empty, loading or error case. Only a model can tell which surfaces are data-bearing, so this is listed, never scored.",
960
+ "rewrite": "Every surface that can be empty, slow or broken needs its own designed state.",
961
+ "source": "taste-skill"
962
+ },
963
+ {
964
+ "id": "AD74",
965
+ "group": "renderedText",
966
+ "medium": "both",
967
+ "name": "Em-dash or en-dash",
968
+ "threshold": 0,
969
+ "unit": "characters",
970
+ "check": "measured",
971
+ "howMeasured": "count of U+2014 and U+2013 across all rendered text, alt attributes, aria-label attributes, title attributes and button labels. Any occurrence fails.",
972
+ "rewrite": "Zero. Not 'sparingly'. Two sentences with a period, or a comma, or parentheses, or a colon. Ranges take a hyphen.",
973
+ "source": "house"
974
+ },
975
+ {
976
+ "id": "AD75",
977
+ "group": "renderedText",
978
+ "medium": "both",
979
+ "name": "Placeholder person name",
980
+ "threshold": "any",
981
+ "unit": "string",
982
+ "check": "measured",
983
+ "howMeasured": "rendered text matches /\\b(john|jane|joe|jack)\\s+(doe|smith|su|chan)\\b|\\blorem ipsum\\b/i.",
984
+ "rewrite": "Real-sounding, locale-appropriate names, or real people with permission.",
985
+ "source": "taste-skill"
986
+ },
987
+ {
988
+ "id": "AD76",
989
+ "group": "renderedText",
990
+ "medium": "both",
991
+ "name": "Placeholder brand name",
992
+ "threshold": "any",
993
+ "unit": "string",
994
+ "check": "measured",
995
+ "howMeasured": "rendered text matches /\\b(acme|nexus|smartflow|cloudly|innovatech|globex|initech|vertex solutions)\\b/i.",
996
+ "rewrite": "Invent a name that sounds like it belongs to a real company in this category, or use the client's own.",
997
+ "source": "taste-skill"
998
+ },
999
+ {
1000
+ "id": "AD77",
1001
+ "group": "renderedText",
1002
+ "medium": "both",
1003
+ "name": "Fake-perfect number",
1004
+ "threshold": "any",
1005
+ "unit": "string",
1006
+ "check": "measured",
1007
+ "howMeasured": "rendered text matches /\\b(99\\.9{1,2}%|100%|10x|50%|1,?234,?567)\\b/ in a statistic or social-proof position.",
1008
+ "rewrite": "Real numbers are messy. If the real number is unknown, remove the claim rather than rounding it to a marketing shape.",
1009
+ "source": "taste-skill"
1010
+ },
1011
+ {
1012
+ "id": "AD78",
1013
+ "group": "renderedText",
1014
+ "medium": "both",
1015
+ "name": "Filler verb",
1016
+ "threshold": "any",
1017
+ "unit": "string",
1018
+ "check": "measured",
1019
+ "howMeasured": "rendered text matches /\\b(elevate|seamless(ly)?|unleash|supercharge|streamline|empower|world[- ]class|next[- ]gen|revolutioni[sz]e|cutting[- ]edge|game[- ]chang)/i.",
1020
+ "rewrite": "Concrete verbs. Say what it does, not how it feels to say it.",
1021
+ "source": "taste-skill"
1022
+ },
1023
+ {
1024
+ "id": "AD79",
1025
+ "group": "renderedText",
1026
+ "medium": "both",
1027
+ "name": "Performative-craftsman label",
1028
+ "threshold": "any",
1029
+ "unit": "string",
1030
+ "check": "measured",
1031
+ "howMeasured": "rendered text matches /\\b(quietly (in use|trusted)|from the field|field notes|on our desks|currently on the bench|loose plates)\\b/i.",
1032
+ "rewrite": "Plain functional labels: Testimonials, Latest writing, Customers include. Or no label at all.",
1033
+ "source": "taste-skill"
1034
+ },
1035
+ {
1036
+ "id": "AD80",
1037
+ "group": "renderedText",
1038
+ "medium": "both",
1039
+ "name": "Generic step label",
1040
+ "threshold": "any",
1041
+ "unit": "string",
1042
+ "check": "measured",
1043
+ "howMeasured": "rendered text matches /\\b(stage|step|phase|pass)\\s*(one|two|three|four|0?\\d)\\b/i.",
1044
+ "rewrite": "The step content is the label. Install, Configure, Ship. Not 'Stage 1: Install'.",
1045
+ "source": "taste-skill"
1046
+ },
1047
+ {
1048
+ "id": "AD81",
1049
+ "group": "renderedText",
1050
+ "medium": "both",
1051
+ "name": "Meta-criticism copy shape",
1052
+ "threshold": "any",
1053
+ "unit": "string",
1054
+ "check": "measured",
1055
+ "howMeasured": "rendered text matches /\\b\\w+ theat(er|re)\\b|\\bnot just (a |an )?\\w+,? it'?s\\b|\\bactually \\w+, not\\b/i.",
1056
+ "rewrite": "Name a specific noun. Meta-criticism phrasing is instant slop.",
1057
+ "source": "impeccable"
1058
+ },
1059
+ {
1060
+ "id": "AD82",
1061
+ "group": "category",
1062
+ "medium": "both",
1063
+ "name": "Category reflex, first order",
1064
+ "threshold": "any",
1065
+ "unit": "judgement",
1066
+ "check": "judged",
1067
+ "howMeasured": "could a reader guess this palette and type family from the client's category alone? If yes, the design is the first training-data reflex. Listed for the model on every run; never scored.",
1068
+ "rewrite": "Rework the scene sentence and the colour strategy until the answer is not obvious from the category.",
1069
+ "source": "impeccable"
1070
+ },
1071
+ {
1072
+ "id": "AD83",
1073
+ "group": "category",
1074
+ "medium": "both",
1075
+ "name": "Category reflex, second order",
1076
+ "threshold": "any",
1077
+ "unit": "judgement",
1078
+ "check": "judged",
1079
+ "howMeasured": "could a reader guess the aesthetic family from category-plus-anti-reference, for example 'clinic that is not clinical-blue therefore warm-editorial'? If yes, the first reflex was avoided and the second was not. Listed on every run; never scored.",
1080
+ "rewrite": "Rework until neither question has an obvious answer.",
1081
+ "source": "impeccable"
1082
+ }
1083
+ ],
1084
+ "originalNine": {
1085
+ "why": "design-studio-plan §3.11b listed 9 tells. Every one is preserved; this maps each to its rule id so nothing was dropped in the expansion.",
1086
+ "map": {
1087
+ "Default faces: Inter, Space Grotesk, Roboto/Arial as display": "AD01",
1088
+ "Everything centred": "AD30",
1089
+ "Three identical cards in a row": "AD21",
1090
+ "Gradient hero": "AD15",
1091
+ "One corner radius on every block": "AD26",
1092
+ "Emoji as section markers": "AD46",
1093
+ "Cream ground + serif display + terracotta accent": "AD18",
1094
+ "Accent bar/rail on rounded cards; rounded-lg everywhere": "AD42 + AD26",
1095
+ "Pure #000 on #FFF, or #FFF on #000": "AD12"
1096
+ }
1097
+ }
1098
+ }