@gessobuild/anti-slop 0.4.2

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,1650 @@
1
+ # The guard catalog
2
+
3
+ Every rule the detector actually runs, with its exact detection condition,
4
+ why the pattern reads as generated-UI slop, a before/after pair, and what
5
+ the auto-fix rewrites. Severity is per hit; a rule's total contribution to a
6
+ file's score is capped at 4 so one runaway pattern cannot drown the rest.
7
+
8
+ Tiers: **FIX** rules are auto-fixed by `npx -y @gessobuild/anti-slop fix <file>
9
+ --write`, deterministically and idempotently. **GATE** rules are
10
+ detect-only: the right fix needs a decision the tool refuses to fake, so
11
+ they are reported for you to resolve. **BASE** rules are additive polish:
12
+ their absence is not a defect (they never count toward pass/severity), but
13
+ `fix` injects the default once, idempotently, into full documents.
14
+
15
+ Opt-outs (for deliberate design decisions, never for making the check
16
+ pass): element rules honor a `data-slop-allow="rule-id"` attribute on the
17
+ element; CSS rules honor a `--slop-allow: rule-id` custom property inside
18
+ the same declaration block. Both accept a space/comma list of ids or
19
+ `"all"`.
20
+
21
+ ---
22
+
23
+ ## Color
24
+
25
+ ### gradient-text (severity 1, FIX)
26
+
27
+ **Detects:** a CSS declaration group (a `<style>` rule body or an inline
28
+ `style=""`) that clips a background into the letterforms: `background-clip:
29
+ text` (with or without the `-webkit-` prefix) together with a transparent
30
+ fill (`color`, `-webkit-text-fill-color`, or `fill` set to `transparent`).
31
+
32
+ **Why it reads as slop:** a gradient poured into a headline or metric is
33
+ decoration carrying no meaning, and it is the single most recognizable
34
+ generated-heading treatment of the current era. Strong type earns emphasis
35
+ from size, weight, and color, not from a rainbow fill.
36
+
37
+ ```html
38
+ <!-- bad -->
39
+ <h1 style="background: linear-gradient(90deg, #ff00cc, #3333ff);
40
+ -webkit-background-clip: text; background-clip: text;
41
+ color: transparent">Grow faster</h1>
42
+ <!-- good -->
43
+ <h1 style="color: #16181d">Grow faster</h1>
44
+ ```
45
+
46
+ **Auto-fix:** removes the clip and gradient declarations and sets the fill
47
+ to the gradient's first color stop (or `currentColor` when no stop is
48
+ parseable), so the headline keeps a related solid color.
49
+
50
+ **Sanctioned when:** replication mode (`{ replicate: true }` in the library
51
+ API), for faithfully reproducing a reference whose hero legitimately uses a
52
+ gradient headline. CSS opt-out: `--slop-allow: gradient-text`.
53
+
54
+ ### indigo-accent (severity 1, FIX)
55
+
56
+ **Detects:** any of the ten Tailwind indigo/violet accent hexes (`#6366f1`,
57
+ `#818cf8`, `#4f46e5`, `#4338ca`, `#3730a3`, `#8b5cf6`, `#7c3aed`,
58
+ `#6d28d9`, `#a78bfa`, `#5b21b6`) used in a declaration VALUE. Custom
59
+ property definitions (`--accent: #6366f1`), `content` literals, and
60
+ `url(...)` values (SVG fragment ids) are skipped.
61
+
62
+ **Why it reads as slop:** indigo-500 is what a model reaches for when
63
+ nobody chose a brand color. It is the fingerprint hue of generated UI; a
64
+ real product has an accent someone picked on purpose.
65
+
66
+ ```html
67
+ <!-- bad -->
68
+ <button style="background: #6366f1; color: #fff">Upgrade</button>
69
+ <!-- good -->
70
+ <button style="background: var(--accent, currentColor); color: #fff">Upgrade</button>
71
+ ```
72
+
73
+ **Auto-fix:** replaces each indigo hex with `var(--accent, currentColor)`,
74
+ deferring to the host page's accent token. If your design system genuinely
75
+ uses indigo, define it as your `--accent` (or opt out with `--slop-allow:
76
+ indigo-accent` in that block) so the choice is explicit.
77
+
78
+ ### gradient-fill (severity 1, FIX)
79
+
80
+ **Detects:** a declaration group that is a rounded ENTITY (it declares a
81
+ `border-radius`) whose `background` / `background-image` /
82
+ `background-color` carries a `linear-`, `radial-`, or `conic-gradient()`
83
+ whose readable, saturated color stops all belong to ONE hue family (stops
84
+ within 28 degrees of hue, saturation at least 0.15). Blocks with any
85
+ `url()` in a background declaration are skipped (that is a photo plus a
86
+ scrim, not a tile fill), as are `background-clip: text` groups
87
+ (gradient-text owns those) and gradients with no saturated stop (pure
88
+ black/white scrims and tints are not a color tell).
89
+
90
+ **Why it reads as slop:** the gradient-filled icon tile and button is a
91
+ top generated-UI fingerprint. A designer fills a tile with one confident
92
+ color; the gradient version is what a model produces when it wants a
93
+ surface to feel "designed" without making a choice.
94
+
95
+ ```html
96
+ <!-- bad -->
97
+ <div style="border-radius: 12px;
98
+ background: linear-gradient(135deg, #22c55e, #16a34a)">...</div>
99
+ <!-- good -->
100
+ <div style="border-radius: 12px; background: #22c55e">...</div>
101
+ ```
102
+
103
+ **Auto-fix:** collapses every background declaration in the block to one
104
+ solid `background: <color>`, keeping the gradient's first saturated stop
105
+ so the tile stays in its own hue. CSS opt-out: `--slop-allow:
106
+ gradient-fill` in the same block.
107
+
108
+ ### multicolor-fill (severity 1, FIX)
109
+
110
+ **Detects:** the same rounded-entity gradient shape as gradient-fill, but
111
+ with TWO OR MORE divergent saturated hues among the stops (any pair more
112
+ than 28 degrees apart): the pink-to-purple or orange-to-pink tile. The
113
+ same carve-outs apply (no radius, `url()` backgrounds, clip-to-text, no
114
+ saturated stop).
115
+
116
+ **Why it reads as slop:** two competing hues blended across one small
117
+ surface is chromatic noise with no meaning, and it is the LOUD version of
118
+ the gradient-tile tell. No hue can fairly be kept, so the fix mutes
119
+ rather than picks a winner.
120
+
121
+ ```html
122
+ <!-- bad -->
123
+ <div style="border-radius: 12px;
124
+ background: linear-gradient(135deg, #f97316, #ec4899)">...</div>
125
+ <!-- good -->
126
+ <div style="border-radius: 12px;
127
+ background: var(--surface, rgba(128,128,128,0.12))">...</div>
128
+ ```
129
+
130
+ **Auto-fix:** collapses the background declarations to one muted neutral
131
+ surface tone (`var(--surface, rgba(128,128,128,0.12))`), deferring to the
132
+ host page's surface token when defined. CSS opt-out: `--slop-allow:
133
+ multicolor-fill`.
134
+
135
+ ---
136
+
137
+ ### multicolor-heading (severity 1, FIX)
138
+
139
+ **Detects:** an `h1`/`h2`/`h3` that is a prose headline (three or more
140
+ words, lowercase letters present) containing at least one inline
141
+ descendant (`span`, `em`, `strong`, `b`, `i`, `mark`, `small`, `u`, or an
142
+ `<a>` with no href) whose resolved `color` (class rules + inline style,
143
+ inline winning) is set and not `inherit`/`currentColor`, while base text
144
+ outside those spans remains. The prose gate spares app patterns that
145
+ borrow a heading tag: a stat value with a colored unit, an all-caps brand
146
+ statement, a two-word screen label. Wholly recolored headlines never fire;
147
+ the rule needs a MIX of inks.
148
+
149
+ **Why it reads as slop:** dipping half the sentence in the accent is the
150
+ default emphasis move of generated landings. It splits one thought into
151
+ two visual voices and spends the accent color on decoration instead of
152
+ action.
153
+
154
+ ```html
155
+ <!-- bad -->
156
+ <h1>Composed <span style="color:#e2725b">in sequence.</span></h1>
157
+ <!-- good -->
158
+ <h1>Composed in sequence.</h1>
159
+ ```
160
+
161
+ **Auto-fix:** forces each colored fragment to `color:inherit` (its other
162
+ styling survives), so the headline reads as one ink. Element opt-out:
163
+ `data-slop-allow="multicolor-heading"` on the heading. Sanctioned in
164
+ replication mode (`{ replicate: true }`), where the reference's exact
165
+ treatment is the contract.
166
+
167
+ ---
168
+
169
+ ### purple-violet-wash (severity 1, FIX)
170
+
171
+ **Detects:** any literal color (hex / rgb() / hsl()) in a declaration value
172
+ whose hue lands in 252-296 degrees at saturation >= 0.30 and lightness
173
+ 0.25-0.88: the saturated violet band. The ten exact indigo hexes are
174
+ excluded (indigo-accent owns them, so nothing double-counts), as are
175
+ custom-property definitions, `content:` literals, `url(...)` values, and
176
+ all shadow/filter colors (a violet glow belongs to dark-glow, which drops
177
+ the layer instead of recoloring it).
178
+
179
+ **Why it reads as slop:** indigo has a hex list; purple has a whole band.
180
+ Models that dodge the exact fingerprint hexes still land in saturated
181
+ violet, the most recognized "nobody chose this" color story in generated
182
+ UI.
183
+
184
+ **Auto-fix:** swaps each violet token for `var(--accent, currentColor)`,
185
+ deferring to the page's real accent. A genuinely purple brand defines
186
+ `--accent` (or opts out per block with `--slop-allow: purple-violet-wash`)
187
+ so the choice is explicit. Sanctioned in replication mode.
188
+
189
+ ---
190
+
191
+ ### safe-green-default (severity 1, FLAG)
192
+
193
+ **Detects:** Tailwind's emerald/green accent hexes (`#10b981`, `#34d399`,
194
+ `#059669`, `#047857`, `#065f46`, `#22c55e`, `#16a34a`, `#4ade80`,
195
+ `#15803d`) in declaration values, with the same custom-property /
196
+ `content` / `url()` exclusions as the violet rule.
197
+
198
+ **Why it reads as slop:** it is the second-order tell: deny a model
199
+ purple and it retreats to emerald, the next "distinctive" non-choice.
200
+ Green is a fine accent when the brand owns it, which is why this is
201
+ advisory.
202
+
203
+ **Advisory (FLAG):** reported, never counted; green fintech and
204
+ sustainability brands are real. Opt out per block with
205
+ `--slop-allow: safe-green-default`.
206
+
207
+ ---
208
+
209
+ ### cream-default-wash (severity 1, FLAG)
210
+
211
+ **Detects:** the page ground (a `body`/`html` background hex) sitting in
212
+ the warm cream band (hue 25-60, saturation 0.10-0.50, lightness >= 0.82)
213
+ COMBINED with a serif display voice (a `font-family` whose leading family
214
+ is serif, not a sans stack's trailing generic). One hit per page.
215
+
216
+ **Why it reads as slop:** cream + serif is the "tasteful startup"
217
+ costume: an editorial voice applied by default to products that are not
218
+ editorial, cited across every 2026 tell list as the polite twin of the
219
+ purple gradient.
220
+
221
+ **Advisory (FLAG):** genuinely editorial briefs earn this pairing, so it
222
+ reports without gating. Opt out with `data-slop-allow` on the body or
223
+ `--slop-allow: cream-default-wash` in the body rule.
224
+
225
+ ---
226
+
227
+ ## Type
228
+
229
+ ### hollow-text (severity 2, FIX)
230
+
231
+ **Detects:** a declaration group carrying a glyph only by its outline:
232
+ any `-webkit-text-stroke` (or `text-stroke`) declaration combined with a
233
+ transparent fill, in the same rule body or inline style. Groups already
234
+ flagged as gradient-text are excluded.
235
+
236
+ **Why it reads as slop:** outlined "hollow" display type is a poster
237
+ gimmick that generated UI overuses, and it is a rendering hazard:
238
+ `-webkit-text-stroke` is non-standard, and wherever the stroke is not
239
+ painted the text is literally invisible. Severity 2 because it can fail
240
+ WCAG outright.
241
+
242
+ ```html
243
+ <!-- bad -->
244
+ <h2 style="color: transparent; -webkit-text-stroke: 1.5px #16181d">SS26</h2>
245
+ <!-- good -->
246
+ <h2 style="color: #16181d; opacity: 0.85">SS26</h2>
247
+ ```
248
+
249
+ **Auto-fix:** strips the stroke declarations and restores a solid
250
+ `currentColor` fill. For a deliberately quiet read, reduce opacity instead
251
+ of hollowing the glyph.
252
+
253
+ ### underlined-text (severity 1, FIX)
254
+
255
+ **Detects:** `text-decoration: underline` (including `text-decoration-line`
256
+ and multi-value forms) anywhere in markup or styles, and any `<u>` tag.
257
+
258
+ **Why it reads as slop:** underlines in product UI read as either a raw
259
+ hyperlink default nobody styled or a typewriter document. Polished
260
+ interfaces set links apart with weight or color and reserve decoration for
261
+ prose contexts.
262
+
263
+ ```html
264
+ <!-- bad -->
265
+ <a style="text-decoration: underline">View report</a>
266
+ <!-- good -->
267
+ <a style="text-decoration: none; color: var(--accent, currentColor); font-weight: 600">View report</a>
268
+ ```
269
+
270
+ **Auto-fix:** rewrites the declaration to `text-decoration:none` and strips
271
+ `<u>` tags (keeping their content).
272
+
273
+ ### all-caps-body (severity 1, FIX)
274
+
275
+ **Detects:** a `<p>` element with `text-transform: uppercase` in its inline
276
+ style whose visible text is longer than 60 characters.
277
+
278
+ **Why it reads as slop:** we recognize words by their ascender/descender
279
+ silhouette; long uppercase passages flatten that shape and force
280
+ letter-by-letter reading. All-caps is a label treatment (a few words with
281
+ letterspacing), never a paragraph treatment.
282
+
283
+ ```html
284
+ <!-- bad -->
285
+ <p style="text-transform: uppercase">Our platform helps teams move faster by
286
+ automating the busywork that slows every launch down.</p>
287
+ <!-- good -->
288
+ <p>Our platform helps teams move faster by automating the busywork that
289
+ slows every launch down.</p>
290
+ ```
291
+
292
+ **Auto-fix:** removes the `text-transform: uppercase` declaration from the
293
+ long paragraph; short labels are untouched. Element opt-out:
294
+ `data-slop-allow="all-caps-body"`.
295
+
296
+ ### emoji-icon (severity 1, FIX)
297
+
298
+ **Detects:** an emoji (with optional variation selectors / ZWJ sequences)
299
+ sitting at the very start of an `<a>`, `<button>`, heading, `<span>`,
300
+ `<li>`, `<dt>`, `<dd>`, `<figcaption>`, `<label>`, `<strong>`, `<b>`, or
301
+ `<small>`, immediately followed by text.
302
+
303
+ **Why it reads as slop:** an emoji standing in for an icon is the tell that
304
+ no icon system exists. Emoji render differently on every platform, ignore
305
+ your palette, and read as chat, not chrome.
306
+
307
+ ```html
308
+ <!-- bad -->
309
+ <li>🚀 Instant deploys</li>
310
+ <!-- good -->
311
+ <li><svg class="icon" aria-hidden="true">...</svg> Instant deploys</li>
312
+ <!-- also good -->
313
+ <li>Instant deploys</li>
314
+ ```
315
+
316
+ **Auto-fix:** strips the leading emoji and keeps the label. Emoji inside
317
+ running prose are left alone; only the icon position (leading a label) is
318
+ flagged. Element opt-out: `data-slop-allow="emoji-icon"`.
319
+
320
+ ### text-wrap-orphans (severity 1, BASE)
321
+
322
+ **Detects:** a full document (it has `<html>`, `<body>`, or `</head>`)
323
+ that contains headings or body-copy elements (`h1`-`h3`, `p`, `li`,
324
+ `figcaption`, `blockquote`) but no `<style id="gesso-text-wrap">` polish
325
+ block yet. Bare fragments are never touched.
326
+
327
+ **Why it matters:** headings that rag unevenly and paragraphs that strand
328
+ a single word on the last line read as unpolished, and the platform fixes
329
+ both for free: `text-wrap: balance` evens heading lines, `text-wrap:
330
+ pretty` prevents orphans in copy. Absence is not a defect (BASE tier), so
331
+ this never affects the verdict; it is a default worth having.
332
+
333
+ ```html
334
+ <!-- injected once, idempotent on the id -->
335
+ <style id="gesso-text-wrap">h1,h2,h3{text-wrap:balance}p,li,figcaption,blockquote{text-wrap:pretty}</style>
336
+ ```
337
+
338
+ **Auto-fix:** injects that one marked block before `</head>` (falling
339
+ back to just after `<body>`). Opt out by shipping your own (even empty)
340
+ `<style id="gesso-text-wrap">` block.
341
+
342
+ ### font-smoothing (severity 1, BASE)
343
+
344
+ **Detects:** a full document with no `<style id="gesso-font-smoothing">`
345
+ polish block.
346
+
347
+ **Why it matters:** default macOS text rendering is heavier than most
348
+ type was designed for; `-webkit-font-smoothing: antialiased` plus
349
+ `-moz-osx-font-smoothing: grayscale`, set ONCE at the root, lightens it
350
+ to the intended weight. Non-macOS platforms ignore both, so the
351
+ declaration is safe everywhere. BASE tier: never affects the verdict.
352
+
353
+ ```html
354
+ <!-- injected once, idempotent on the id -->
355
+ <style id="gesso-font-smoothing">html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}</style>
356
+ ```
357
+
358
+ **Auto-fix:** injects that one marked block. Opt out by shipping your own
359
+ `<style id="gesso-font-smoothing">` block.
360
+
361
+ ---
362
+
363
+ ### mixed-style-headline (severity 1, FIX)
364
+
365
+ **Detects:** a prose `h1`/`h2`/`h3` whose own resolved `font-style` is
366
+ upright but which contains a descendant resolving to italic or oblique,
367
+ via an `em`/`i`/`cite`/`var`/`dfn` tag default or a
368
+ `font-style: italic` from class or inline styles, while upright base text
369
+ remains. A wholly italic headline is one consistent voice and never
370
+ fires; the rule requires the mid-sentence switch.
371
+
372
+ **Why it reads as slop:** the upright-then-italic swerve is manufactured
373
+ sophistication, italicizing a phrase to fake an editorial cadence the
374
+ copy does not have. One headline, one style.
375
+
376
+ ```html
377
+ <!-- bad -->
378
+ <h1>Thrown on a kick wheel. <em>Fired three times.</em></h1>
379
+ <!-- good -->
380
+ <h1>Thrown on a kick wheel. Fired three times.</h1>
381
+ ```
382
+
383
+ **Auto-fix:** pins each italic fragment to `font-style:normal`, keeping
384
+ the words and any other styling. Element opt-out:
385
+ `data-slop-allow="mixed-style-headline"` on the heading. Sanctioned in
386
+ replication mode.
387
+
388
+ ---
389
+
390
+ ### overused-font-stack (severity 1, FLAG)
391
+
392
+ **Detects:** Inter, Space Grotesk, Geist, or Instrument Serif named in
393
+ any `font-family` declaration or Google Fonts stylesheet URL. One hit per
394
+ distinct family found.
395
+
396
+ **Why it reads as slop:** these four faces headline every list of AI
397
+ design tells; they are good typefaces exhausted by being the default
398
+ reach. A face that argues for the subject is the difference between a
399
+ designed page and a generated one.
400
+
401
+ **Advisory (FLAG):** a deliberate Inter body under a distinctive display
402
+ face is defensible, so this reports without gating.
403
+
404
+ ---
405
+
406
+ ### single-font-page (severity 1, FLAG)
407
+
408
+ **Detects:** a full document with two or more `font-family` declarations
409
+ whose leading (non-generic) family names all resolve to ONE family.
410
+ Generic keywords (serif, sans-serif, monospace, system-ui, ui-*) are
411
+ ignored; pages with fewer than two declarations stay quiet.
412
+
413
+ **Why it reads as slop:** one family at one register carrying display,
414
+ body, and UI reads as unstyled output, not typographic restraint. Real
415
+ single-family systems vary optical size, width, or weight with intent.
416
+
417
+ **Advisory (FLAG):** deliberate one-face systems exist; treat the hit as
418
+ a prompt to check whether the page has a display voice at all.
419
+
420
+ ---
421
+
422
+ ### crushed-tracking (severity 1, FIX)
423
+
424
+ **Detects:** `letter-spacing` at -0.05em or tighter (px normalized at
425
+ 16px/em) in any declaration group.
426
+
427
+ **Why it reads as slop:** negative tracking is the one knob generated CSS
428
+ turns to make display type look "designed", and past about -0.04em the
429
+ glyphs weld together. Compression is not sophistication.
430
+
431
+ **Auto-fix:** clamps the declaration to -0.02em, the safe end of tight
432
+ display tracking. Opt out per block with `--slop-allow: crushed-tracking`.
433
+
434
+ ---
435
+
436
+ ### wide-body-tracking (severity 1, FIX)
437
+
438
+ **Detects:** `letter-spacing` at 0.08em or wider in a group that does NOT
439
+ also set `text-transform: uppercase` (tracked caps are a real pattern)
440
+ and is not a micro-label (font-size <= 13px in the same group is spared).
441
+
442
+ **Why it reads as slop:** wide tracking on mixed-case text destroys the
443
+ word shapes readers scan by; it looks airy in a screenshot and reads like
444
+ wading. The legitimate home of wide tracking is the short uppercase
445
+ label.
446
+
447
+ **Auto-fix:** clamps the declaration to 0.01em. Opt out per block with
448
+ `--slop-allow: wide-body-tracking`.
449
+
450
+ ---
451
+
452
+ ### tight-line-height (severity 1, FIX)
453
+
454
+ **Detects:** a declaration group that sets BOTH a body-range font-size
455
+ (13-20px) and a line-height under 1.25 (unitless, or px against that
456
+ size). Groups without their own font-size stay quiet (inheritance is
457
+ unknowable statically), and display/micro sizes set their own rules.
458
+
459
+ **Why it reads as slop:** shingled body lines are the density cosplay of
460
+ generated dashboards: 15px text at 1.1 leading photographs as "compact"
461
+ and reads as a wall.
462
+
463
+ **Auto-fix:** raises the group's line-height to 1.4. Opt out per block
464
+ with `--slop-allow: tight-line-height`.
465
+
466
+ ---
467
+
468
+ ### tiny-body-text (severity 1, FIX)
469
+
470
+ **Detects:** `font-size` under 11px (px, or rem/em normalized at 16px) in
471
+ a group without `text-transform: uppercase` (tracked uppercase
472
+ micro-labels at 10px are a deliberate pattern and are spared).
473
+
474
+ **Why it reads as slop:** sub-11px mixed-case text is below the
475
+ legibility floor on high-DPI screens; it exists to make a mock look
476
+ information-dense, not to be read.
477
+
478
+ **Auto-fix:** raises the declaration to 12px. Opt out per block with
479
+ `--slop-allow: tiny-body-text`.
480
+
481
+ ---
482
+
483
+ ### monospace-body (severity 1, FLAG)
484
+
485
+ **Detects:** a style rule whose selector targets `body` or `p` (bare
486
+ element selectors, not `.body-x` classes) with a `font-family` naming a
487
+ monospace face (the `monospace` generic, Mono-family names, Courier,
488
+ Consolas, Menlo).
489
+
490
+ **Why it reads as slop:** prose in a code font is terminal cosplay: a
491
+ whole page pretending to be a CLI because the product is technical. Code
492
+ belongs in `<code>`; paragraphs belong in a text face.
493
+
494
+ **Advisory (FLAG):** developer-tool landings do this deliberately often
495
+ enough that it reports without gating.
496
+
497
+ ---
498
+
499
+ ## Visual
500
+
501
+ ### heavy-box-shadow (severity 2, FIX)
502
+
503
+ **Detects:** a `box-shadow` value that is any of: three or more blurred
504
+ non-inset layers; two or more blurred non-inset layers where any layer's
505
+ alpha exceeds 0.12; or a single blurred non-inset layer with alpha above
506
+ 0.30. Inset layers, hard-edged offsets (blur 0), and values already using a
507
+ shadow token (`var(--*shadow*)`) never count.
508
+
509
+ **Why it reads as slop:** the stacked ambient-glow shadow under a routine
510
+ card is the "puffy floating card" signature of generated UI. Real elevation
511
+ systems use one subtle layer, or skip shadows entirely and separate
512
+ surfaces by tone.
513
+
514
+ ```html
515
+ <!-- bad -->
516
+ <div style="box-shadow: 0 4px 6px rgba(0,0,0,0.1), 0 10px 25px rgba(0,0,0,0.15),
517
+ 0 20px 48px rgba(0,0,0,0.18)">...</div>
518
+ <!-- good -->
519
+ <div style="box-shadow: 0 1px 2px rgba(0,0,0,0.06)">...</div>
520
+ ```
521
+
522
+ **Auto-fix:** flattens the offending value to the single subtle layer
523
+ `0 1px 2px rgba(0,0,0,0.06)`. CSS opt-out: `--slop-allow:
524
+ heavy-box-shadow` in the same block.
525
+
526
+ ### gradient-border (severity 1, FIX)
527
+
528
+ **Detects:** `border-image` or `border-image-source` whose value contains a
529
+ `linear-`, `radial-`, or `conic-gradient`.
530
+
531
+ **Why it reads as slop:** the multi-stop gradient ring around an avatar,
532
+ thumbnail, or card is decorative chrome borrowed from story UIs; on
533
+ anything else it is loud, dated, and screams template.
534
+
535
+ ```html
536
+ <!-- bad -->
537
+ <img class="avatar" style="border-image: linear-gradient(45deg, #f0f, #0ff) 1"
538
+ src="team/ana.jpg" alt="Ana">
539
+ <!-- good -->
540
+ <img class="avatar" style="border: 1px solid rgba(0,0,0,0.08)"
541
+ src="team/ana.jpg" alt="Ana">
542
+ ```
543
+
544
+ **Auto-fix:** strips the gradient border-image declarations; the element's
545
+ own rounded corners are the finish.
546
+
547
+ ### bare-hr (severity 1, FIX)
548
+
549
+ **Detects:** an `<hr>` with no border styling in its attributes.
550
+
551
+ **Why it reads as slop:** the user-agent default `<hr>` renders as a
552
+ full-opacity inset 3D groove, a 1996 artifact. Sections separate with
553
+ space, typography, or at most a hairline.
554
+
555
+ ```html
556
+ <!-- bad -->
557
+ <hr>
558
+ <!-- good -->
559
+ <hr style="border:none;border-top:1px solid rgba(0,0,0,0.08);">
560
+ ```
561
+
562
+ **Auto-fix:** rewrites the bare `<hr>` to that hairline (alpha 0.08).
563
+ Element opt-out: `data-slop-allow="bare-hr"`.
564
+
565
+ ### decorative-divider (severity 1, FIX)
566
+
567
+ **Detects:** runs of two or more box-drawing characters (U+2500 to U+2570,
568
+ U+2574 to U+257F), or runs of two or more em/en dashes, used in visible
569
+ text.
570
+
571
+ **Why it reads as slop:** terminal-art rules between labels are chrome that
572
+ conveys nothing and break the first time the font or width changes. They
573
+ are how a language model draws a line when it cannot draw a line.
574
+
575
+ ```html
576
+ <!-- bad -->
577
+ <div class="label">OVERVIEW ───────────── Q3</div>
578
+ <!-- good -->
579
+ <div class="label" style="display:flex; align-items:center; gap:12px">
580
+ OVERVIEW <span style="flex:1; border-top:1px solid rgba(0,0,0,0.08)"></span> Q3
581
+ </div>
582
+ ```
583
+
584
+ **Auto-fix:** removes the character runs. If you want a visual rule, draw a
585
+ real hairline as above.
586
+
587
+ ### repeating-gradient-stripe (severity 1, FIX)
588
+
589
+ **Detects:** any `repeating-linear-gradient()`, `repeating-radial-gradient()`,
590
+ or `repeating-conic-gradient()` in markup or styles.
591
+
592
+ **Why it reads as slop:** repeating-gradient stripes are texture without
593
+ intent, the CSS equivalent of construction tape. When generated UI wants a
594
+ surface to feel less empty it reaches for stripes; a designed surface uses
595
+ a flat tone or a pattern that means something.
596
+
597
+ ```html
598
+ <!-- bad -->
599
+ <div style="background: repeating-linear-gradient(45deg, #111 0 2px, transparent 2px 6px)"></div>
600
+ <!-- good -->
601
+ <div style="background: #111"></div>
602
+ ```
603
+
604
+ **Auto-fix:** replaces the whole repeating-gradient function (balanced
605
+ parens, so nested `var()`/`rgba()` stops are handled) with its first color
606
+ stop, falling back to `transparent`.
607
+
608
+ ### fake-dot-viz (severity 2, FIX)
609
+
610
+ **Detects:** a `<span>`/`<div>` wrapper whose class marks it as a
611
+ mini-chart (`pulse`, `momentum`, `sparkdots`, `trend-dots`, `dot-row`,
612
+ `dot-grid`, `dot-cluster`, `node-row`, `nodes`) and whose ENTIRE content
613
+ is three or more empty child elements classed `dot`/`node`/`spark`. The
614
+ wrapper-class anchor keeps carousel/pagination indicators and avatar
615
+ stacks safe.
616
+
617
+ **Why it reads as slop:** a row of identical dots pretending to be a
618
+ "momentum" or "pulse" chart encodes zero data. It exists to make a list
619
+ row look quantitative, and it collides with the real value column. Data
620
+ visualization that shows nothing is the purest form of slop, hence
621
+ severity 2.
622
+
623
+ ```html
624
+ <!-- bad -->
625
+ <span class="dot-row"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>
626
+ <!-- good: a real sparkline, or just the number -->
627
+ <span class="delta">+4.2%</span>
628
+ ```
629
+
630
+ **Auto-fix:** removes the whole cluster. Replace it with a real sparkline
631
+ or a plain number + delta.
632
+
633
+ ### viz-stray-ticks (severity 2, FIX)
634
+
635
+ **Detects:** inside an arc/gauge `<svg>` (one whose `<path d>` uses an
636
+ elliptic-arc `A`/`a` command with `fill="none"`; filled arcs like pie
637
+ slices and logos never qualify), THREE or more short `<line>`s (length at
638
+ most 16 viewBox units) or thin tick-sized `<rect>`s that do not live under
639
+ a labelled tick group (an ancestor classed `tick`/`axis`/`grid`/`scale`/
640
+ `marks`/`ruler`).
641
+
642
+ **Why it reads as slop:** unlabeled ticks radiating around a gauge rim are
643
+ the strongest generated-gauge tell. A real scale has labels; seven
644
+ anonymous 8px dashes are chrome pretending to be measurement.
645
+
646
+ ```html
647
+ <!-- bad: three anonymous ticks sprayed on the rim -->
648
+ <svg viewBox="0 0 100 60"><path d="M10 50 A40 40 0 0 1 90 50" fill="none"/>
649
+ <line x1="10" y1="10" x2="16" y2="12"/> ...
650
+ </svg>
651
+ <!-- good: the arc, the value, nothing else -->
652
+ <svg viewBox="0 0 100 60"><path d="M10 50 A40 40 0 0 1 90 50" fill="none"/></svg>
653
+ ```
654
+
655
+ **Auto-fix:** removes the stray ticks (and any wrapper `<g>` left truly
656
+ empty). The arc, values, and labelled tick groups survive. Element
657
+ opt-out: `data-slop-allow="viz-stray-ticks"` on the `<svg>`.
658
+
659
+ ### glyph-on-metric (severity 2, FIX)
660
+
661
+ **Detects:** two branches, one removal pass. (A) DATAVIS: a decorative
662
+ emoji-only element or catalog icon (`svg`/`[data-icon]`/`.ic`) inside a
663
+ viz container (an ancestor carrying `data-viz`, holding a real arc/gauge
664
+ svg, or ring/gauge-classed) that ALSO shows a numeric value in a different
665
+ branch of the same container; a catalog icon must additionally be floated
666
+ (aria-hidden or absolutely positioned) and not the intentional
667
+ centered-in-ring glyph (`translate(-50%,-50%)` is spared). (B)
668
+ PERCENTAGE: every catalog icon and emoji/symbol glyph inside the bounded
669
+ stat unit (at most 64 visible characters, one value) around an `NN%`
670
+ value. Page chrome (nav, status/tab bars) is never entered, and `<img>`
671
+ is never treated as an icon (a flag beside a stat can be the data).
672
+
673
+ **Why it reads as slop:** the number IS the visual. An emoji centered
674
+ behind a progress ring's "74%" collides with the digits; an arrow or icon
675
+ chip beside a stat pollutes the figure it decorates. Removal widens to
676
+ the smallest wholly-decorative wrapper (aria-hidden or
677
+ decoration-classed, holding no data) so orbiting blobs leave with the
678
+ glyph.
679
+
680
+ ```html
681
+ <!-- bad -->
682
+ <div class="progress-ring"><div class="figure">🏋️</div><span>74%</span></div>
683
+ <!-- good -->
684
+ <div class="progress-ring"><span>74%</span></div>
685
+ ```
686
+
687
+ **Auto-fix:** removes the glyph (or its wholly-decorative wrapper); the
688
+ value and the arc always survive. Element opt-out:
689
+ `data-slop-allow="glyph-on-metric"` on the glyph or any ancestor inside
690
+ the stat.
691
+
692
+ ### stat-label-icon (severity 1, FIX)
693
+
694
+ **Detects:** in a small stat tile (at most 24 visible characters within
695
+ three ancestor hops), a category label (pure word text of 2 to 20
696
+ letters, no digits or `%`) whose FIRST element child is an icon
697
+ (`svg`/`img`/`[data-icon]`/`.ic`), where a numeric value leaf (`42`,
698
+ `1.2k`, `98%`) sits in the same tile BEFORE the label in document order.
699
+ The number-before-label guard means a nav row with a leading icon and a
700
+ trailing count badge ("Settings ... 3") never matches: there the icon is
701
+ the affordance.
702
+
703
+ **Why it reads as slop:** the label word already names the category, so a
704
+ leading icon is duplicate information, the icon-plus-its-own-caption
705
+ double signal. In a stat tile the number is the primary read; everything
706
+ else should get out of its way.
707
+
708
+ ```html
709
+ <!-- bad -->
710
+ <div class="tile"><div class="num">42</div><div class="cat"><svg class="ic">...</svg> ALL</div></div>
711
+ <!-- good -->
712
+ <div class="tile"><div class="num">42</div><div class="cat">ALL</div></div>
713
+ ```
714
+
715
+ **Auto-fix:** removes the leading icon, keeping the number and the word.
716
+ Element opt-out: `data-slop-allow="stat-label-icon"` on the label
717
+ element.
718
+
719
+ ---
720
+
721
+ ### edge-stripe (severity 1, FIX)
722
+
723
+ **Detects:** any style-rule body or inline style declaring
724
+ `border-left` or `border-right` (or their `-width` longhands) at 3px or
725
+ wider in a visible color. Selector-aware: rules scoped to a selection
726
+ state (`[aria-selected]`, `[aria-current]`, `:checked`,
727
+ `.active`/`.selected`/`.current`/`.is-*`) are skipped, as are inline
728
+ styles on elements carrying those state markers, so a single selected
729
+ row keeping its accent edge is legal.
730
+
731
+ **Why it reads as slop:** the colored rail is how generated UI fakes a
732
+ category system: every row gets a 4px accent edge and the list looks
733
+ "designed" without any real encoding. Hairlines stay under the 3px
734
+ threshold; only the decorative rail is stripped.
735
+
736
+ ```html
737
+ <!-- bad -->
738
+ <div class="card" style="border-left:4px solid #e2725b">Standups</div>
739
+ <!-- good -->
740
+ <div class="card">Standups <span class="chip">Ritual</span></div>
741
+ ```
742
+
743
+ **Auto-fix:** removes the `border-left`/`border-right` declarations from
744
+ each offending group; every other declaration survives. Opt out per
745
+ group with `--slop-allow: edge-stripe` or per element with
746
+ `data-slop-allow="edge-stripe"`.
747
+
748
+ ---
749
+
750
+ ### redundant-border (severity 1, FIX)
751
+
752
+ **Detects:** a declaration group carrying BOTH a real background fill
753
+ (not `none`/`transparent`/`inherit`) AND a visible box border: 1px+ in a
754
+ color that is not transparent, under 10% alpha, or a
755
+ divider/hairline/stroke token. Skipped wholesale on interactive and
756
+ structural surfaces where a border is the affordance: buttons, inputs,
757
+ selects, table cells, `pre`/`code`/`kbd`, alert/callout/chip/tab/field
758
+ classes, `[type=]`/`[role=]` attributes, and any `:hover`/`:focus`/state
759
+ selector.
760
+
761
+ **Why it reads as slop:** boxing an already-filled card is double
762
+ separation, the visual equivalent of saying it twice. Real systems pick
763
+ one edge strategy per surface; generated output stacks both by default.
764
+
765
+ ```html
766
+ <!-- bad -->
767
+ <div style="background:#f6f1ea;border:1px solid #d8cfc2;border-radius:12px">...</div>
768
+ <!-- good -->
769
+ <div style="background:#f6f1ea;border-radius:12px">...</div>
770
+ ```
771
+
772
+ **Auto-fix:** strips the border declarations (radius stays, fill stays).
773
+ Low-alpha hairlines are never touched, so a deliberate 5% outline
774
+ survives. Opt out with `--slop-allow: redundant-border` in the block or
775
+ `data-slop-allow` on the element.
776
+
777
+ ---
778
+
779
+ ### dark-glow (severity 2, FIX)
780
+
781
+ **Detects:** a non-inset `box-shadow`/`text-shadow` layer, or a
782
+ `drop-shadow()` filter, whose color is saturated (S >= 0.4) with blur >=
783
+ 12px and alpha >= 0.15: the neon halo. Neutral elevation shadows have no
784
+ chroma and never match; heavy-box-shadow separately owns over-heavy
785
+ NEUTRAL stacks.
786
+
787
+ **Why it reads as slop:** light does not leak out from under cards. The
788
+ saturated glow behind buttons and bento tiles is the signature move of
789
+ the generated "premium dark SaaS" look, decoration posing as depth.
790
+
791
+ ```css
792
+ /* bad */ .cta { box-shadow: 0 0 40px rgba(6,182,212,0.4) }
793
+ /* good */ .cta { box-shadow: 0 1px 2px rgba(0,0,0,0.24) }
794
+ ```
795
+
796
+ **Auto-fix:** removes the chromatic glow layers, keeping any neutral
797
+ layers in the same declaration (an empty declaration is dropped whole).
798
+ Runs before purple-violet-wash so a violet glow dies as a glow. Opt out
799
+ per block with `--slop-allow: dark-glow`.
800
+
801
+ ---
802
+
803
+ ### over-rounded-card (severity 1, FIX)
804
+
805
+ **Detects:** a filled surface (real background) whose `border-radius` is
806
+ a single pixel value from 40 to 120. Pills and full circles pass (their
807
+ 9999px / 50% conventions fall outside the band), as do unfilled wrappers.
808
+
809
+ **Why it reads as slop:** 40px+ corners turn content cards into blobs:
810
+ the "friendly" dial turned past its stop, with text left floating in
811
+ amorphous shapes. Confident systems hold 8-24px.
812
+
813
+ **Auto-fix:** clamps the radius to 24px, keeping fill and everything
814
+ else. Opt out per block with `--slop-allow: over-rounded-card`.
815
+
816
+ ---
817
+
818
+ ### ghost-card (severity 1, FIX)
819
+
820
+ **Detects:** a declaration group carrying BOTH a hairline border (0.5px
821
+ or 1px) AND a wide soft shadow (blur >= 24px at alpha <= 0.18): two
822
+ separation strategies hedged onto one surface.
823
+
824
+ **Why it reads as slop:** the border says "I sit in the plane", the halo
825
+ says "I float above it". Together they read as a template that could not
826
+ decide, the ghost hovering under every generated pricing card.
827
+
828
+ **Auto-fix:** keeps the hairline (the more structural of the two) and
829
+ removes the box-shadow declaration. Opt out per block with
830
+ `--slop-allow: ghost-card`.
831
+
832
+ ---
833
+
834
+ ## Layout
835
+
836
+ ### floating-hero-card (severity 1, FIX)
837
+
838
+ **Detects:** a small card floated over a hero: an element that is (a)
839
+ `position: absolute` or `fixed`, (b) a card surface (`border-radius` plus
840
+ a background fill, backdrop blur, or shadow), (c) corner-pinned (a
841
+ vertical AND a horizontal offset, or a non-zero `inset`), (d) short
842
+ decorative content (at most 90 characters, and NO heading, link, button,
843
+ form control, nav, list, or image), and (e) overlaying a `section`/
844
+ `header`/hero-classed ancestor that also holds a real `h1`/`h2` outside
845
+ the card. Outermost matches only.
846
+
847
+ **Why it reads as slop:** the corner-pinned, backdrop-blurred spec chip
848
+ ("Atelier Lab / Edition No 08") is a generated-landing signature. It
849
+ overlaps the artwork, restates nothing a reader needs, and exists to make
850
+ a hero look "layered". Condition (e) is what makes the removal safe: the
851
+ hero's own content column can never match.
852
+
853
+ ```html
854
+ <!-- bad -->
855
+ <section class="hero"><h1>Skin is a living archive</h1>
856
+ <div style="position:absolute; top:16px; right:16px; border-radius:12px;
857
+ background:rgba(255,255,255,0.2)">Atelier Lab · No 08</div>
858
+ </section>
859
+ <!-- good: the detail moves inline, or disappears -->
860
+ <section class="hero"><h1>Skin is a living archive</h1></section>
861
+ ```
862
+
863
+ **Auto-fix:** removes the floating card(s). If the detail matters, place
864
+ it inline in the hero's content column. Element opt-out:
865
+ `data-slop-allow="floating-hero-card"` on the card.
866
+
867
+ ### grid-spacer-void (severity 2, FIX)
868
+
869
+ **Detects:** a grid whose effective declarations set a fixed
870
+ `grid-auto-rows` of 24px or more, and which contains a descendant that is
871
+ a thin full-span separator (`grid-column: 1 / -1` with a height of 12px
872
+ or less). The flagged class is the one actually carrying the fixed
873
+ `grid-auto-rows`.
874
+
875
+ **Why it reads as slop:** the hairline divider lands in its OWN implicit
876
+ row, so a 1px line is stretched into a 168px track: the screen renders as
877
+ content rows separated by giant empty bands. This is a genuine layout
878
+ bug, not a taste call; a fixed-row gallery with no separator child is
879
+ never touched.
880
+
881
+ ```css
882
+ /* bad */
883
+ .grid { display: grid; grid-auto-rows: 168px; }
884
+ .sep { grid-column: 1 / -1; height: 1px; }
885
+ /* good */
886
+ .grid { display: grid; grid-auto-rows: auto; }
887
+ ```
888
+
889
+ **Auto-fix:** rewrites that grid's `grid-auto-rows` to `auto`, so the
890
+ divider's row collapses to its 1px content while real cells size to their
891
+ content. The divider lines survive; only the void is removed. CSS
892
+ opt-out: `--slop-allow: grid-spacer-void` in the grid's rule.
893
+
894
+ ### wrap-padding-collision (severity 2, FIX)
895
+
896
+ **Detects:** an element that carries BOTH the page's inset container
897
+ class (detected by signature: horizontally centered via
898
+ `margin-inline: auto` or equivalent, plus a non-zero inline padding,
899
+ never by name) AND another class whose rule zeroes horizontal padding (a
900
+ `padding: V 0` shorthand, `padding-inline: 0`, or `padding-left/right:
901
+ 0`).
902
+
903
+ **Why it reads as slop:** at equal specificity the later rule wins, so
904
+ the section's `padding: 64px 0` silently clobbers the container's
905
+ `padding-inline` and the band runs flush to the screen edge. Another
906
+ genuine bug: the author wanted vertical rhythm and accidentally deleted
907
+ the page gutter.
908
+
909
+ ```css
910
+ /* bad */
911
+ .wrap { max-width: 1100px; margin-inline: auto; padding-inline: 24px; }
912
+ .band { padding: 64px 0; } /* zeroes .wrap's gutter on the same element */
913
+ /* good */
914
+ .band { padding-block: 64px; }
915
+ ```
916
+
917
+ **Auto-fix:** strips the horizontal zeros from the offending class's rule
918
+ (`padding: V 0` becomes `padding-block: V`), so the container's inset
919
+ survives and the vertical rhythm is preserved. If a band is intentionally
920
+ full-bleed, omit the container class instead. CSS opt-out: `--slop-allow:
921
+ wrap-padding-collision` in the zeroing rule.
922
+
923
+ ### body-display-contents (severity 2, FIX)
924
+
925
+ **Detects:** `display: contents` applied to `<body>`, either inline
926
+ (`<body style="display:contents">`, duplicates included) or via a
927
+ `body{}` CSS rule (selector lists like `html, body {}` count; descendant
928
+ selectors like `body .child {}` do not).
929
+
930
+ **Why it reads as slop:** a `display: contents` body generates no box, so
931
+ its padding, width, and flex gap are ALL discarded: the screen renders
932
+ with no side padding, content under the status bar, and zero section
933
+ rhythm. `display: contents` is legitimate on a nested wrapper `<div>`,
934
+ never on body.
935
+
936
+ ```html
937
+ <!-- bad -->
938
+ <body style="display: contents">
939
+ <!-- good -->
940
+ <body>
941
+ ```
942
+
943
+ **Auto-fix:** strips the `display: contents` declaration from body inline
944
+ styles and `body{}` rules; every other declaration is left intact. CSS
945
+ opt-out: `--slop-allow: body-display-contents` in the same declaration
946
+ block.
947
+
948
+ ### hscroll-snap-gutter (severity 1, BASE)
949
+
950
+ **Detects:** a horizontal scroll-snap container (`overflow-x: auto|scroll`
951
+ plus `scroll-snap-type` on the x/inline/both axis) whose side gutter
952
+ comes from its OWN non-zero inline padding, with no `scroll-padding*`
953
+ declared.
954
+
955
+ **Why it matters:** the snapport is the scrollport minus `scroll-padding`
956
+ (default 0), and the container's padding sits outside it, so
957
+ `scroll-snap-align: start` under mandatory snap rests the first card
958
+ flush to the edge: the leading gutter vanishes and the last card collides
959
+ with the right edge. BASE tier: a carousel without the fix is not "slop",
960
+ so it never affects the verdict, but the fix is free and always correct.
961
+
962
+ ```css
963
+ /* bad */
964
+ .carousel { overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 16px; }
965
+ /* good */
966
+ .carousel { overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 16px;
967
+ scroll-padding-inline: 16px; }
968
+ ```
969
+
970
+ **Auto-fix:** appends `scroll-padding-inline` mirroring the resolved
971
+ inline padding (handles the `padding` shorthand, logical properties, and
972
+ source order). Abstains when there is no non-zero inline padding to
973
+ mirror. CSS opt-out: `--slop-allow: hscroll-snap-gutter`.
974
+
975
+ ---
976
+
977
+ ### hero-kicker-eyebrow (severity 1, FIX)
978
+
979
+ **Detects:** two shapes above the page's primary prose `<h1>`: (a) the
980
+ element immediately preceding the H1, and (b) the standalone block
981
+ immediately preceding the hero's top-level section. Either fires only
982
+ when it is short (60 chars / 9 words max, contains letters), reads as an
983
+ eyebrow (uppercase text, `text-transform: uppercase`, or letter-spacing
984
+ at 0.08em/1px+ anywhere in its subtree), is not itself a heading, and
985
+ contains no links, lists, nav, buttons, forms, or images, so a real top
986
+ nav, promo bar, or toolbar is never touched. Eyebrows above section-level
987
+ `h2`s are deliberately left alone.
988
+
989
+ **Why it reads as slop:** the tiny tracked badge above the headline is
990
+ the most-cited generated-landing tell there is. It restates or merely
991
+ locates the title, spending the page's first pixels on chrome.
992
+
993
+ ```html
994
+ <!-- bad -->
995
+ <p class="eyebrow">MAISON NOIR · ATELIER DE SOIN · MMXXV</p>
996
+ <h1>Skin is a living archive of light.</h1>
997
+ <!-- good -->
998
+ <h1>Skin is a living archive of light.</h1>
999
+ ```
1000
+
1001
+ **Auto-fix:** removes the eyebrow element(s). Element opt-out:
1002
+ `data-slop-allow="hero-kicker-eyebrow"`.
1003
+
1004
+ ---
1005
+
1006
+ ### reveal-specificity-trap (severity 3, FIX)
1007
+
1008
+ **Detects:** per `<style>` block: every class hidden behind a JS gate
1009
+ (`.js .reveal` or `html.js .reveal` with `opacity: 0`) whose revealed
1010
+ state is then written WITHOUT the gate, as a compound selector starting
1011
+ with that class (`.reveal.in`, `.step.in.late`). Descendant forms and
1012
+ already-gated selectors are left alone.
1013
+
1014
+ **Why it reads as slop:** it is not taste, it is a bug that blanks the
1015
+ page. `html.js .reveal` (specificity 0-2-1) beats `.reveal.in` (0-2-0)
1016
+ forever, so the IntersectionObserver adds the class and nothing appears:
1017
+ hero renders, everything below stays at opacity 0. Severity 3 because
1018
+ the failure is total.
1019
+
1020
+ ```css
1021
+ /* bad */
1022
+ html.js .reveal { opacity: 0 }
1023
+ .reveal.in { opacity: 1 }
1024
+ /* good */
1025
+ html.js .reveal { opacity: 0 }
1026
+ html.js .reveal.in { opacity: 1 }
1027
+ ```
1028
+
1029
+ **Auto-fix:** prefixes each ungated revealed selector with `html.js `,
1030
+ tying the specificity so source order wins. Idempotent by construction:
1031
+ rewritten selectors no longer match the trap pattern.
1032
+
1033
+ ---
1034
+
1035
+ ### row-kicker-eyebrow (severity 2, FLAG)
1036
+
1037
+ **Detects:** within any group of 2+ repeating sibling rows (same tag +
1038
+ leading class, each with real text), rows whose text runs contain a short
1039
+ ALL-CAPS multi-token kicker ('LOCAL FAVORITE · 96 RAVING') BEFORE a
1040
+ lowercase title-like run. Fires only when 2+ rows in the group carry the
1041
+ pattern, so one deliberate label never trips it.
1042
+
1043
+ **Why it reads as slop:** a list where every item wears a status eyebrow
1044
+ buries the actual titles under repeated shouting. The eyebrow slot
1045
+ belongs to the section header, once.
1046
+
1047
+ **Advisory (FLAG):** no auto-fix and no verdict impact. On an app feed
1048
+ this is a real defect; on a marketing page a kicker-led card grid can be
1049
+ the genre, and a static detector cannot tell which page it is on.
1050
+ Element opt-out: `data-slop-allow="row-kicker-eyebrow"` on the row.
1051
+
1052
+ ---
1053
+
1054
+ ### multiline-row-meta (severity 2, FLAG)
1055
+
1056
+ **Detects:** in repeating row groups, a row holding either a leaf element
1057
+ whose content opens with a quotation mark (a review/pull-quote) or a
1058
+ `<br>` outside a heading (a hand-wrapped meta line). Both are proxies for
1059
+ "this cell will wrap to 2+ lines beside single-line neighbors" that a
1060
+ static pass can check without layout.
1061
+
1062
+ **Why it reads as slop:** one wrapping quote snaps the vertical rhythm of
1063
+ the whole list; rows stop scanning as rows. Quotes live on detail
1064
+ screens; list meta is one truncated line.
1065
+
1066
+ **Advisory (FLAG):** reported, never counted. A testimonial list is the
1067
+ one genre where per-row quotes are the point. Opt out per row with
1068
+ `data-slop-allow="multiline-row-meta"`.
1069
+
1070
+ ---
1071
+
1072
+ ### overstuffed-row (severity 2, FLAG)
1073
+
1074
+ **Detects:** in repeating row groups, rows whose info-slot count (visible
1075
+ text runs + media, where media means `<img>` or a non-icon `<svg>`:
1076
+ icon-marked, icon-classed lucide/feather/tabler/heroicons/etc., and
1077
+ canvases at 32 or under are exempt) exceeds 4. Fires only when 2+ rows in
1078
+ the group are over budget.
1079
+
1080
+ **Why it reads as slop:** thumbnail + kicker + title + location + quote +
1081
+ mini-viz in every row is density without hierarchy: nothing can be
1082
+ scanned when everything is present. Three slots (subject, title, one
1083
+ decision metric) is what a row can actually carry.
1084
+
1085
+ **Advisory (FLAG):** no verdict impact; rich marketing tiles legitimately
1086
+ run denser. Opt out per row with `data-slop-allow="overstuffed-row"`.
1087
+
1088
+ ---
1089
+
1090
+ ### row-as-card (severity 1, FLAG)
1091
+
1092
+ **Detects:** groups of 3+ repeating rows where each row has a thumbnail
1093
+ or media, 2+ text runs, AND a card surface (border-radius plus a fill,
1094
+ shadow, or border) resolved from its classes and inline styles.
1095
+
1096
+ **Why it reads as slop:** carding every uniform row floats a plain list
1097
+ on a sea of gaps and borders. Divider-separated rows read faster and
1098
+ quieter; per-item cards are for genuinely rich, differentiated tiles.
1099
+
1100
+ **Advisory (FLAG):** genre-dependent by nature (a product-card grid is
1101
+ carded on purpose), so it reports without affecting the verdict. Opt out
1102
+ per row with `data-slop-allow="row-as-card"`.
1103
+
1104
+ ---
1105
+
1106
+ ### nested-cards (severity 1, GATE)
1107
+
1108
+ **Detects:** a card-surfaced CONTAINER (radius plus fill/border resolved
1109
+ from classes + inline styles, holding a heading/paragraph or 2+ element
1110
+ children) with an ancestor that is also card-surfaced. Chips, badges,
1111
+ buttons, and links never qualify as the inner card.
1112
+
1113
+ **Why it reads as slop:** when every level of a region carries its own
1114
+ radius, fill, and edge, surface depth stops encoding anything: the
1115
+ telltale generated dashboard where panels hold panels holding panels.
1116
+
1117
+ **Detect-only (GATE):** un-nesting means choosing which surface to keep,
1118
+ a real layout decision. Group inner content with spacing and hairlines,
1119
+ or lift the inner card out. Element opt-out:
1120
+ `data-slop-allow="nested-cards"` on the inner container.
1121
+
1122
+ ---
1123
+
1124
+ ### numbered-section-markers (severity 1, FLAG)
1125
+
1126
+ **Detects:** two or more leaf elements whose entire text is a
1127
+ leading-zero index (`01`, `02.`, `03/`). A single marker, or plain
1128
+ unpadded digits, never fires.
1129
+
1130
+ **Why it reads as slop:** 01/02/03 scaffolding stamped on sections whose
1131
+ order encodes nothing is template structure left visible: numbering as
1132
+ decoration rather than sequence.
1133
+
1134
+ **Advisory (FLAG):** real steps, timelines, and ranked lists number
1135
+ their sections legitimately; the detector cannot know whether order is
1136
+ content, so it reports without gating.
1137
+
1138
+ ---
1139
+
1140
+ ### icon-topped-feature-card (severity 1, FLAG)
1141
+
1142
+ **Detects:** three or more repeating sibling cards that each open with a
1143
+ lone icon (an svg/img first child, bare or in an empty wrapper) followed
1144
+ by a heading (h2-h5) and a paragraph.
1145
+
1146
+ **Why it reads as slop:** icon-heading-blurb times three is the most
1147
+ recycled section structure in generated landings, the layout equivalent
1148
+ of lorem ipsum: it fills the "features" slot without deciding anything.
1149
+
1150
+ **Advisory (FLAG):** it is also a working convention real sites use, so
1151
+ it reports for judgment. Vary the geometry, lead with evidence, or opt
1152
+ out per card with `data-slop-allow="icon-topped-feature-card"`.
1153
+
1154
+ ---
1155
+
1156
+ ## Motion
1157
+
1158
+ ### transition-all (severity 1, GATE)
1159
+
1160
+ **Detects:** a `transition` or `transition-property` declaration whose
1161
+ value contains the keyword `all` (vendor prefixes included), in a `<style>`
1162
+ rule body or inline style.
1163
+
1164
+ **Why it reads as slop:** `transition: all` animates every property that
1165
+ happens to change, including layout, color, and shadow you never meant to
1166
+ move. It is the lazy default behind janky hovers, and it forces the browser
1167
+ to watch everything. Motion should name its subject.
1168
+
1169
+ ```css
1170
+ /* bad */
1171
+ .card { transition: all 0.3s ease; }
1172
+ /* good */
1173
+ .card { transition: transform 200ms ease-out, opacity 200ms ease-out; }
1174
+ ```
1175
+
1176
+ **Detect-only because:** the tool cannot know which properties you meant to
1177
+ animate; narrowing the list is a design decision. Make it explicitly, then
1178
+ re-run the check. CSS opt-out: `--slop-allow: transition-all`.
1179
+
1180
+ ---
1181
+
1182
+ ### will-change-misuse (severity 1, FIX)
1183
+
1184
+ **Detects:** any `will-change` declaration (style blocks + inline styles)
1185
+ naming a property outside the compositable set: `transform`, `opacity`,
1186
+ `filter`, `clip-path` (plus the spec values `scroll-position`,
1187
+ `contents`, and CSS-wide keywords). `will-change: all`, `top`, `width`,
1188
+ `background`, `box-shadow`, and friends all fire.
1189
+
1190
+ **Why it reads as slop:** `will-change` only helps properties the
1191
+ compositor can own. Pointed at layout or paint properties it allocates a
1192
+ GPU layer that cannot accelerate anything: memory spent, zero motion
1193
+ gained, the copy-paste residue of a performance cargo cult.
1194
+
1195
+ ```css
1196
+ /* bad */ .card { will-change: top, box-shadow }
1197
+ /* good */ .card { will-change: transform }
1198
+ ```
1199
+
1200
+ **Auto-fix:** keeps any compositable properties from the list and drops
1201
+ the rest; a declaration left empty is removed entirely (it is a pure
1202
+ hint, so appearance never changes). Opt out per block with
1203
+ `--slop-allow: will-change-misuse`.
1204
+
1205
+ ---
1206
+
1207
+ ### bounce-easing (severity 1, FIX)
1208
+
1209
+ **Detects:** any `cubic-bezier()` whose y-coordinates leave the 0-1 range
1210
+ (overshoot/elastic curves) in transitions or animations.
1211
+
1212
+ **Why it reads as slop:** interfaces settle; toys bounce. The spring-in
1213
+ dialog with visible overshoot is a demo-reel flourish that makes
1214
+ production UI feel wobbly and slow.
1215
+
1216
+ **Auto-fix:** replaces the overshoot curve with `ease-out`, preserving
1217
+ the rest of the shorthand. Opt out per block with
1218
+ `--slop-allow: bounce-easing`.
1219
+
1220
+ ---
1221
+
1222
+ ### layout-prop-animation (severity 1, GATE)
1223
+
1224
+ **Detects:** `transition` / `transition-property` values naming layout
1225
+ properties: width, height, max-width/height, top/left/right/bottom,
1226
+ inset, margin, padding. Transform/opacity transitions never match;
1227
+ `transition: all` is transition-all's finding.
1228
+
1229
+ **Why it reads as slop:** every animated frame of a layout property
1230
+ reflows the page, so the motion stutters exactly where it tries to
1231
+ impress. The compositor can only own transforms, opacity, and filters.
1232
+
1233
+ **Detect-only (GATE):** the right rewrite depends on intent (translate
1234
+ instead of top, grid-template-rows or a measured transform instead of a
1235
+ height accordion), so removing the transition blindly would break real
1236
+ behavior. Opt out per block with `--slop-allow: layout-prop-animation`.
1237
+
1238
+ ---
1239
+
1240
+ ### hover-scale-image (severity 1, FLAG)
1241
+
1242
+ **Detects:** a `:hover` rule on an image-ish selector (img, image, thumb,
1243
+ photo, media, cover, card) whose transform scales above 1.
1244
+
1245
+ **Why it reads as slop:** the hover zoom is the one effect everyone has
1246
+ seen, applied by reflex to every card and thumbnail. Ubiquity is the
1247
+ tell: it signals "default interaction" rather than a designed response.
1248
+
1249
+ **Advisory (FLAG):** a restrained, deliberate zoom is a legitimate
1250
+ choice; this reports so the choice gets made. Opt out per block with
1251
+ `--slop-allow: hover-scale-image`.
1252
+
1253
+ ---
1254
+
1255
+ ## Copy
1256
+
1257
+ ### cents-suffix (severity 1, FIX)
1258
+
1259
+ **Detects:** a price (a currency-prefixed number or a comma-grouped figure)
1260
+ immediately followed by a `<span>` containing one or two digits (with an
1261
+ optional leading dot): the classic superscripted decimal.
1262
+
1263
+ **Why it reads as slop:** `$28,461` with a floating little `.20` is
1264
+ screen-recording verisimilitude misapplied to a mockup. Design comps use
1265
+ whole, intentional numbers; fake precision reads as generated dashboard
1266
+ filler.
1267
+
1268
+ ```html
1269
+ <!-- bad -->
1270
+ <div class="metric">$28,461<span class="cents">.20</span></div>
1271
+ <!-- good -->
1272
+ <div class="metric">$28,461</div>
1273
+ ```
1274
+
1275
+ **Auto-fix:** collapses the suffix span, keeping the base figure.
1276
+
1277
+ ### oversized-number (severity 1, FIX)
1278
+
1279
+ **Detects:** a numeric value of 10,000 or more rendered raw in visible text
1280
+ (comma-grouped or 5+ digits), with or without a currency symbol. Numbers
1281
+ already suffixed with `%`, `K`, `M`, or `B` are spared.
1282
+
1283
+ **Why it reads as slop:** `$1,842,000` typeset raw overflows metric cards
1284
+ and reads as a database dump. Designed dashboards abbreviate magnitude so
1285
+ the number stays scannable.
1286
+
1287
+ ```html
1288
+ <!-- bad -->
1289
+ <div class="stat">$1,842,000</div>
1290
+ <!-- good -->
1291
+ <div class="stat">$1.8M</div>
1292
+ ```
1293
+
1294
+ **Auto-fix:** abbreviates to K/M/B with at most one decimal
1295
+ (`1,842,000` becomes `1.8M`). Values 9,999 and under stay raw digits.
1296
+
1297
+ ### em-dash-copy (severity 1, FIX)
1298
+
1299
+ **Detects:** a single em dash (U+2014, raw or as `&mdash;` / `&#8212;` /
1300
+ `&#x2014;`) in visible copy, or an en dash used with spaces around it as an
1301
+ em-dash substitute. Runs of two or more dashes belong to
1302
+ `decorative-divider`; unspaced en-dash ranges (`Mon-Fri`, `9-5`) are
1303
+ legitimate and spared.
1304
+
1305
+ **Why it reads as slop:** the mid-sentence em dash is the most recognizable
1306
+ generated-TEXT tell there is. Interface copy is short; it wants commas,
1307
+ colons, and periods, not essayistic asides.
1308
+
1309
+ ```html
1310
+ <!-- bad -->
1311
+ <p>Ship faster &mdash; without the busywork.</p>
1312
+ <!-- good -->
1313
+ <p>Ship faster, without the busywork.</p>
1314
+ ```
1315
+
1316
+ **Auto-fix:** replaces the dash (and its surrounding spaces) with a comma
1317
+ and a space; a dash that opened a text node is dropped without leaving a
1318
+ stray comma.
1319
+
1320
+ ### lorem-ipsum (severity 2, GATE)
1321
+
1322
+ **Detects:** the phrases `lorem ipsum` or `dolor sit amet` (case
1323
+ insensitive) in visible text.
1324
+
1325
+ **Why it reads as slop:** filler copy in a finished screen means the design
1326
+ was never finished. Realistic domain copy is half of what makes a mockup
1327
+ feel designed; lorem ipsum is an abandoned template wearing your layout.
1328
+ Severity 2 because shipping it is always wrong.
1329
+
1330
+ ```html
1331
+ <!-- bad -->
1332
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
1333
+ <!-- good -->
1334
+ <p>Track every shipment from pickup to doorstep, in one timeline.</p>
1335
+ ```
1336
+
1337
+ **Detect-only because:** no tool can write your product's copy. Replace it
1338
+ with short, specific text in the product's own domain, then re-run.
1339
+
1340
+ ### viz-redundant-scale (severity 1, FIX)
1341
+
1342
+ **Detects:** inside an arc/gauge `<svg>` whose surrounding container
1343
+ already shows the scale on the value (a `7/10` ratio, or a percent, which
1344
+ implies 100), a PAIR of `<text>` endpoint labels reading exactly `0` and
1345
+ `N` that horizontally SPAN the arc (one within the leftmost 20% of the
1346
+ viewBox width, one within the rightmost 20%). Neither may be the hero
1347
+ value itself (resolved font-size over 20px, or positioned in the center
1348
+ region). A one-sided y-axis never spans, so it is spared; an svg with no
1349
+ readable viewBox is skipped entirely.
1350
+
1351
+ **Why it reads as slop:** the gauge states its scale twice. Once the
1352
+ value carries the denominator (`7/10`), rim labels `0` and `10` are
1353
+ redundant chrome that a generated gauge adds because real gauges
1354
+ "usually have numbers there".
1355
+
1356
+ ```html
1357
+ <!-- bad: value says 7/10 AND the rim says 0 ... 10 -->
1358
+ <div><svg viewBox="0 0 100 60"><path d="M10 50 A40 40 0 0 1 90 50" fill="none"/>
1359
+ <text x="5" y="58">0</text><text x="95" y="58">10</text></svg><strong>7/10</strong></div>
1360
+ <!-- good -->
1361
+ <div><svg viewBox="0 0 100 60"><path d="M10 50 A40 40 0 0 1 90 50" fill="none"/></svg><strong>7/10</strong></div>
1362
+ ```
1363
+
1364
+ **Auto-fix:** removes the redundant endpoint pair. Descriptive endpoint
1365
+ labels (TODAY / GOAL) never match and always survive. Element opt-out:
1366
+ `data-slop-allow="viz-redundant-scale"` on the `<svg>`.
1367
+
1368
+ ### live-clock-eyebrow (severity 1, FIX)
1369
+
1370
+ **Detects:** a small element (span/div/p/small/em/strong/b/i/time/label/
1371
+ h6/figcaption) whose ENTIRE visible text is a `LIVE` or `NOW` token,
1372
+ optionally preceded by a status dot/bullet and optionally followed by a
1373
+ separator plus a clock time (`HH:MM`, with or without am/pm). The
1374
+ whole-text equality is the safety: a "LIVE STREAM setup" heading, a
1375
+ "Departs 09:41" row, or any time not fronted by LIVE/NOW never matches.
1376
+ Outermost match only, so a flagged badge is removed once, cleanly.
1377
+
1378
+ **Why it reads as slop:** the device status bar already shows the time,
1379
+ and a "LIVE" dot badge on a mockup is decorative urgency. Together
1380
+ ("LIVE 09:41") they are a generated-dashboard eyebrow that asserts
1381
+ liveness no data supports.
1382
+
1383
+ ```html
1384
+ <!-- bad -->
1385
+ <span class="eyebrow">● LIVE · 09:41</span>
1386
+ <!-- good: nothing; the status bar shows the time -->
1387
+ ```
1388
+
1389
+ **Auto-fix:** removes the whole eyebrow element. Element opt-out:
1390
+ `data-slop-allow="live-clock-eyebrow"` (a genuinely live broadcast badge
1391
+ is a deliberate design decision).
1392
+
1393
+ ---
1394
+
1395
+ ### publication-masthead-block (severity 2, FIX)
1396
+
1397
+ **Detects:** a terse container (`div`/`header`/`aside`/`dl`/`section`/
1398
+ `table`, text at most 140 chars) whose text carries at least one
1399
+ NUMBERED periodical label (VOLUME/VOL., ISSUE, EDITION, CATALOGUE,
1400
+ FOLIO, or a `№ N`) plus a second distinct tell: another label family or
1401
+ a fabricated serial code (`HV-IDX-029`, 2+ dash-joined uppercase
1402
+ groups). Containers holding real content (headings, links, controls,
1403
+ media), anything inside `<footer>`/`<nav>`, bare years/dates, a single
1404
+ repeated family ("Volume 1 / Volume 2"), and serial-only spec tables
1405
+ ("SKU AB-CD-123") are all spared.
1406
+
1407
+ **Why it reads as slop:** invented print metadata is pure costume: the
1408
+ screen has no volume, no catalogue, no edition. It is the fastest way a
1409
+ generated landing signals "editorial" without having anything editorial
1410
+ about it.
1411
+
1412
+ ```html
1413
+ <!-- bad -->
1414
+ <div class="hero-meta">VOL. 04 / 2024 · Catalogue HV-IDX-029 · Updated 14 MAR 2025</div>
1415
+ <!-- good: nothing; the product is not a periodical -->
1416
+ ```
1417
+
1418
+ **Auto-fix:** removes the whole cluster container (outermost match), so
1419
+ no orphaned label fragments remain. Runs BEFORE masthead-eyebrow so the
1420
+ eyebrow rule cannot strip an inner span first and orphan the rest.
1421
+ Element opt-out: `data-slop-allow="publication-masthead-block"`.
1422
+
1423
+ ---
1424
+
1425
+ ### masthead-eyebrow (severity 1, FIX)
1426
+
1427
+ **Detects:** a small element (span/small/em/strong/b/i/time/label/
1428
+ figcaption/p/div) whose ENTIRE text is a numbered issue reference:
1429
+ `ISSUE`/`EDITION`/`SERIAL` + number, or the dotted abbreviations
1430
+ `VOL.`/`NO.` + number, with optional `№`/separators. Whole-text equality
1431
+ is the safety: prose that merely contains "issue", a bare "№ 1" rank
1432
+ badge, an undotted "NO 1" answer, and real headings (h1-h6 are not
1433
+ eligible tags) never match. Outermost match only.
1434
+
1435
+ **Why it reads as slop:** software ships versions, not issues. The
1436
+ VOL./№ eyebrow is print-magazine chrome pasted onto a product screen to
1437
+ borrow editorial gravity it has not earned.
1438
+
1439
+ ```html
1440
+ <!-- bad -->
1441
+ <span class="eyebrow">VOL. 04 · № 27</span>
1442
+ <!-- good: nothing, or a real version/date the product actually has -->
1443
+ ```
1444
+
1445
+ **Auto-fix:** removes the eyebrow element. Element opt-out:
1446
+ `data-slop-allow="masthead-eyebrow"` (a genuine digital magazine keeps
1447
+ its issue label deliberately).
1448
+
1449
+ ---
1450
+
1451
+ ### benefit-speak (severity 1, GATE)
1452
+
1453
+ **Detects:** the marketing filler lexicon in visible text: elevate,
1454
+ supercharge, streamline, empower, effortless(ly), seamless(ly),
1455
+ revolutionize, game-changer/-changing, world-class, next-level, unleash,
1456
+ turbocharge, and "unlock the/your/a/new". Functional uses survive:
1457
+ "Unlock with Face ID" never matches. Styles, scripts, comments, and
1458
+ attributes are never read.
1459
+
1460
+ **Why it reads as slop:** these verbs sell nothing specific; they are
1461
+ placeholders where a claim should be. "Search your meeting notes" beats
1462
+ "Unlock your knowledge" because it can be true.
1463
+
1464
+ **Detect-only (GATE):** only you know what the product actually does, so
1465
+ the fix is writing the concrete claim. Quote each flagged phrase and
1466
+ replace it with a specific verb + object.
1467
+
1468
+ ---
1469
+
1470
+ ### not-x-but-y-cadence (severity 1, FLAG)
1471
+
1472
+ **Detects:** the manufactured-rebuttal rhythm in a visible text run:
1473
+ "it's/this is/we're not (just) X, it's Y" and its close variants.
1474
+
1475
+ **Why it reads as slop:** it is the single most recognized generated-copy
1476
+ cadence: contrast as a tic. If the difference matters, specifics carry
1477
+ it; the construction is what remains when they are missing.
1478
+
1479
+ **Advisory (FLAG):** occasionally a writer earns it, so it reports for
1480
+ judgment rather than gating.
1481
+
1482
+ ---
1483
+
1484
+ ### fabricated-precision (severity 1, FLAG)
1485
+
1486
+ **Detects:** the invented-evidence stat shapes in visible text: 99.9% /
1487
+ 99.99%, 10x / 100x, "#1", and "trusted by" followed by a round crowd
1488
+ (thousands, millions, 10,000+).
1489
+
1490
+ **Why it reads as slop:** precision without provenance is decoration.
1491
+ Nobody measured the 10x; the number exists to look like evidence, and
1492
+ readers have learned the pattern.
1493
+
1494
+ **Advisory (FLAG):** a real 99.9% SLA with a source is legitimate, so
1495
+ this reports the shapes and leaves the verdict to you.
1496
+
1497
+ ---
1498
+
1499
+ ### apologetic-error-copy (severity 1, GATE)
1500
+
1501
+ **Detects:** "Oops", "Whoops", "Uh oh", and "something went wrong" in
1502
+ visible text.
1503
+
1504
+ **Why it reads as slop:** the apologetic error is the template default
1505
+ that helps no one: it names no failure and offers no next step. Errors
1506
+ are guidance surfaces, not mood management.
1507
+
1508
+ **Detect-only (GATE):** the fix is stating what failed and what to do
1509
+ ("Couldn't save. Check your connection and retry."), which needs product
1510
+ knowledge. Quote each occurrence and write the real message.
1511
+
1512
+ ---
1513
+
1514
+ ## Imagery
1515
+
1516
+ ### broken-image (severity 1, FIX)
1517
+
1518
+ **Detects:** an `<img>` whose `src` is missing, empty, or a known
1519
+ placeholder literal: `#`, `about:blank`, `undefined`, `null`, `todo`,
1520
+ `{{template}}` mustaches, `placeholder...`, `your-image-here`, `path/to/`,
1521
+ or `example.com/placeholder|img|image`. Resolver slots
1522
+ (`data-photo-query`, `data-photo-placeholder`, `data-illustration`,
1523
+ `data-attachment-ref`) are sanctioned: a host pipeline fills those after
1524
+ generation.
1525
+
1526
+ **Why it reads as slop:** a broken-image glyph in a corner of a card is the
1527
+ fastest way for a screen to read as generated and unreviewed.
1528
+
1529
+ ```html
1530
+ <!-- bad -->
1531
+ <img src="path/to/hero.jpg">
1532
+ <!-- good -->
1533
+ <img src="https://images.unsplash.com/photo-15060..." alt="Alpine ridge at dawn">
1534
+ <!-- also fine: a resolver slot a pipeline will fill -->
1535
+ <img data-photo-query="alpine ridge dawn" alt="">
1536
+ ```
1537
+
1538
+ **Auto-fix:** removes the broken `<img>` element entirely; a missing image
1539
+ beats a broken one. Element opt-out: `data-slop-allow="broken-image"`.
1540
+
1541
+ ### missing-alt (severity 1, FIX)
1542
+
1543
+ **Detects:** an `<img>` with no `alt` attribute at all. Resolver slots are
1544
+ skipped (the pipeline that fills the `src` owns the `alt` too), and images
1545
+ already flagged as broken are left to `broken-image`.
1546
+
1547
+ **Why it reads as slop:** without `alt`, a screen reader announces the raw
1548
+ filename. Even `alt=""` (explicitly decorative) is a decision; silence is
1549
+ the absence of one.
1550
+
1551
+ ```html
1552
+ <!-- bad -->
1553
+ <img src="team/ana.jpg">
1554
+ <!-- good -->
1555
+ <img src="team/ana.jpg" alt="Ana Ruiz, head of design">
1556
+ <!-- good, decorative -->
1557
+ <img src="texture.png" alt="">
1558
+ ```
1559
+
1560
+ **Auto-fix:** adds `alt=""` (the safe decorative default). Upgrade
1561
+ meaningful images to a real description yourself; the tool cannot know what
1562
+ the image shows.
1563
+
1564
+ ### placeholder-image (severity 1, GATE)
1565
+
1566
+ **Detects:** an `<img>` whose `src` points at a stock placeholder service:
1567
+ `i.pravatar.cc`, `randomuser.me`, `ui-avatars.com`, `api.dicebear.com`,
1568
+ `placekitten.com`, `placehold.co`, `via.placeholder.com`, `placeimg.com`,
1569
+ `dummyimage.com`, `fakeimg.pl`, `lorempixel.com`, `loremflickr.com`,
1570
+ `picsum.photos`, or `source.unsplash.com` (a shut-down endpoint). Real
1571
+ photo CDNs (`images.unsplash.com`, `images.pexels.com`) are not flagged.
1572
+
1573
+ **Why it reads as slop:** placeholder-service imagery is lorem ipsum for
1574
+ pictures: the screen was never finished with real assets, and some of these
1575
+ services rotate or die, so the design changes under you.
1576
+
1577
+ ```html
1578
+ <!-- bad -->
1579
+ <img src="https://i.pravatar.cc/150?img=3" alt="avatar">
1580
+ <!-- good -->
1581
+ <img src="team/ana.jpg" alt="Ana Ruiz">
1582
+ ```
1583
+
1584
+ **Detect-only because:** choosing the real asset is your call. Element
1585
+ opt-out: `data-slop-allow="placeholder-image"` (e.g. a deliberate
1586
+ avatar-generator integration).
1587
+
1588
+ ---
1589
+
1590
+ ### image-outline (severity 1, BASE)
1591
+
1592
+ **Detects:** a full document containing `<img>` elements but no
1593
+ `<style id="gesso-image-outline">` polish block.
1594
+
1595
+ **Why it matters:** a photo sitting flush on the surface has no edge:
1596
+ light image regions dissolve into light grounds and the layout loses its
1597
+ shape. A 1px inset hairline in PURE black or white at 5% alpha defines
1598
+ the edge invisibly; a tinted near-black or accent-colored outline reads
1599
+ as dirt.
1600
+
1601
+ ```css
1602
+ img { outline: 1px solid rgba(0,0,0,0.05); outline-offset: -1px }
1603
+ ```
1604
+
1605
+ **Auto-fix:** injects one marked block targeting content images
1606
+ (`data-illustration`/`data-icon`/`aria-hidden` images are excluded).
1607
+ The hairline color is picked from the page's own ground: a body/html
1608
+ background hex with luminance under 0.5 gets the white hairline,
1609
+ anything else black. `outline` + `outline-offset:-1px`, so layout never
1610
+ shifts. Idempotent on the block id; opt a document out by shipping your
1611
+ own (even empty) `<style id="gesso-image-outline">`.
1612
+
1613
+ ---
1614
+
1615
+ ## Quality
1616
+
1617
+ ### justified-text (severity 1, FIX)
1618
+
1619
+ **Detects:** `text-align: justify` anywhere in styles.
1620
+
1621
+ **Why it reads as slop:** browsers justify without proper hyphenation, so
1622
+ justified UI copy grows uneven "rivers of white" between words. Books earn
1623
+ justification with hyphenation engines; interfaces do not.
1624
+
1625
+ ```html
1626
+ <!-- bad -->
1627
+ <p style="text-align: justify">...</p>
1628
+ <!-- good -->
1629
+ <p style="text-align: left">...</p>
1630
+ ```
1631
+
1632
+ **Auto-fix:** rewrites to `text-align: left`.
1633
+
1634
+ ### missing-lang (severity 1, FIX)
1635
+
1636
+ **Detects:** an `<html>` element with no `lang` attribute.
1637
+
1638
+ **Why it reads as slop:** screen readers use `lang` to pick a
1639
+ pronunciation engine; without it, assistive tech guesses. It is the
1640
+ one-attribute difference between a page and a document.
1641
+
1642
+ ```html
1643
+ <!-- bad -->
1644
+ <html>
1645
+ <!-- good -->
1646
+ <html lang="en">
1647
+ ```
1648
+
1649
+ **Auto-fix:** adds `lang="en"`. Change it if the page is not English; the
1650
+ fixer cannot detect the language of your copy.