@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.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/commands/critique.md +67 -0
- package/dist/cli/install.d.ts +10 -0
- package/dist/cli/install.js +65 -0
- package/dist/cli/templates.d.ts +13 -0
- package/dist/cli/templates.js +46 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +130 -0
- package/dist/engine.d.ts +22 -0
- package/dist/engine.js +138 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -0
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +4046 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +11 -0
- package/package.json +59 -0
- package/skills/anti-slop/SKILL.md +322 -0
- package/skills/anti-slop/references/rules.md +1650 -0
|
@@ -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 `—` / `—` /
|
|
1300
|
+
`—`) 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 — 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.
|