@ericrisco/rsc 1.0.24 → 1.0.25

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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.24",
2
+ "version": "1.0.25",
3
3
  "counts": {
4
4
  "skills": 264
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -234,6 +234,36 @@ img { outline: 1px solid oklch(0 0 0 / 0.1); outline-offset: -1px; }
234
234
 
235
235
  Depth recipes, glass, noise, concentric math → `references/visual-system.md`.
236
236
 
237
+ ## The three locks, and who checks them
238
+
239
+ A long page generated in passes drifts against itself. Three things must be **one** thing for
240
+ the whole page, and they are not preferences — a page that breaks one of them is defective:
241
+
242
+ 1. **Theme lock.** One theme for the page (light, dark, or auto). No section flipping to
243
+ inverted halfway down because it looked good alone.
244
+ 2. **Accent lock.** One accent, used identically in every section. **This one is judgement**:
245
+ deciding which token is the accent needs to read the system's intent, so no script claims
246
+ to check it — you do, before you claim done.
247
+ 3. **Radius lock.** One radius system. Four different `rounded-*` scales in one tree means
248
+ there is no system, and `scripts/verify.sh` counts them.
249
+
250
+ ## The tells the script owns, and the ones you own
251
+
252
+ `scripts/verify.sh` carries a **registry of mechanical tells** and reports each with file and
253
+ line: dashes in rendered copy, scroll cues, numbered eyebrows, build stamps on marketing
254
+ pages, unstable viewport heights, middot chains, hype words, plus counted ratios (eyebrows
255
+ against sections, double-bordered rows, radius scales). Run it; do not re-derive that list
256
+ from memory, and do not treat its silence as a design review.
257
+
258
+ What it cannot see is the other half: fabricated substance (a product UI built from divs,
259
+ invented photo credits, the Jane Doe cast), decoration posing as information, labels that say
260
+ nothing, and composition habits like three identical feature cards. That half is
261
+ `references/ai-tells.md`, and it is read with your eyes on the page.
262
+
263
+ When the brief corresponds to an official, maintained design system rather than an aesthetic —
264
+ UK or US public sector, Shopify admin, Carbon, Atlassian, Primer, Fluent — install the
265
+ official package instead of approximating it: `references/design-systems.md`.
266
+
237
267
  ## Anti-patterns
238
268
 
239
269
  | Rationalization | Reality / Fix |
@@ -314,6 +344,6 @@ Score = Σ(dimension × weight), max 115 (normalize to /100 by ×100/115 if you
314
344
  Two records, both indexed in `02-DOCS/wiki/index.md` (the Knowledge map; root `CLAUDE.md` keeps only a short pointer to it), both read first on every use so outputs stay consistent with them:
315
345
 
316
346
  - The **brand study** at `02-DOCS/wiki/brand/` — the hard gate above: missing or incomplete → ask until complete before designing.
317
- - The **design-system decisions** at `02-DOCS/wiki/stack/design.md` — the chosen tokens (color/OKLCH, type scale, spacing, radius, shadow, motion), the 2026 direction picked, and the reference sites. Recorded, not gated; create/update it as decisions are made.
347
+ - The **design-system decisions** at `02-DOCS/wiki/stack/design.md` — the chosen tokens (color/OKLCH, type scale, spacing, radius, shadow, motion), the 2026 direction picked, and the reference sites. Recorded, not gated; create/update it as decisions are made. **Read it before fixing type and palette on a new surface**, and either reuse what is there or state why you are departing — otherwise every surface of the project drifts into a different design. (Across *different* projects this record cannot help: it lives inside each one. Distrusting your own second default is the only guard there — see the repetition note in `references/ai-tells.md`.)
318
348
 
319
349
  The wiki article protocol both follow is `../harness/SKILL.md`'s.
@@ -49,7 +49,7 @@ capability:
49
49
  - scenario: "User asks: 'Here's our current pricing page — do a design review and tell me how good it actually is.'"
50
50
  must_include:
51
51
  - "Applies the design-review QA checklist (value prop in 5s, contrast >= 4.5:1, visible focus, one <h1>, semantic landmarks, LCP image priority, next/font, no transition: all, tokens not magic numbers, ban-list-clean copy, 360px fit, empty/loading/error states)."
52
- - "Scores the 10-dimension visual-audit rubric 0-10 with weights and reports a weighted total out of 100, mapped to a band (<60 generic / 60-79 competent / 80-94 premium / 95+ award-tier)."
52
+ - "Scores the 11-dimension visual-audit rubric 0-10 with weights and reports the weighted total on its /115 scale, mapped to a band (<69 generic / 69-91 competent / 92-108 premium / 109+ award-tier)."
53
53
  - "For each dimension scoring below 8, gives a concrete actionable fix (e.g. exact spacing/token change), not a vague 'improve spacing'."
54
54
  - "Checks copy against the ban-list and flags hype/vague value props that fail the 5s test."
55
55
  - "Flags any transition: all, magic hex/px, cards-in-cards, or purple->blue everything gradient as defects, not preferences."
@@ -0,0 +1,75 @@
1
+ # AI tells — the ones only a reader can catch
2
+
3
+ The mechanical tells are **not here**. `scripts/verify.sh` carries a registry of them and
4
+ flags them with file and line, so re-teaching them as prose would be paying twice for the
5
+ same rule. This file holds only what a `grep` cannot decide: presences you have to *look* at
6
+ the page to see.
7
+
8
+ Part of this corpus is adapted from [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill)
9
+ (MIT), whose lists came out of real production tests. The split into checkable-vs-judged, the
10
+ thresholds, and the wording are ours.
11
+
12
+ Every item below is a **default**, overridable by a brief that explicitly asks for it. What
13
+ makes them tells is that they show up *unasked*, as the model's idea of "looking designed".
14
+
15
+ ## Fabricated substance
16
+
17
+ - **A product UI built out of divs.** A fake task list, fake terminal or fake dashboard
18
+ assembled from styled rectangles is the single most reliable signature of generated work.
19
+ Use a real screenshot, a real component, a generated image, or show nothing.
20
+ - **Hand-rolled decorative SVG.** Abstract squiggles and blobs authored inline to fill space.
21
+ Icons come from a maintained set; decoration comes from a real asset or not at all.
22
+ - **The Jane Doe cast.** "John Doe", "Sarah Chan", egg avatars, `99.99%`, `50%`, round
23
+ thousands, and startup-slop brand names ("Acme", "Nexus", "SmartFlow"). Real work has
24
+ specific, slightly awkward names and messy numbers (`47.2%`, not `50%`).
25
+ - **Testimonials nobody said.** A quote with no attributable person, or five quotes with the
26
+ same cadence, reads as filler. One real quote beats three invented ones.
27
+
28
+ ## Decoration wearing the costume of information
29
+
30
+ - **Atmospheric locale strips.** A city, a timezone, a temperature in the nav or footer. Only
31
+ earns its place for a genuinely distributed studio, a travel brand, or a physical venue.
32
+ - **Status dots with no status.** A coloured dot before nav items, list rows or badges. Keep
33
+ it only where it reports real state, and then only once per section.
34
+ - **Invented photo credits.** `Field study no. 12 · Ines Caetano` under a stock image.
35
+ Credit a real photographer or write a functional caption.
36
+ - **Pills laid over images.** Tags floated on top of a photo. Either the image speaks alone,
37
+ or the caption sits below it, outside the frame.
38
+ - **Decoration strips under the hero.** `BRAND. MOTION. SPATIAL.` in small mono caps. Only
39
+ legitimate when those words are real navigation or real status.
40
+
41
+ ## Labels that say nothing
42
+
43
+ - **Poetic section labels.** "Field notes", "From the field", "On our desks", "Currently on
44
+ the bench". Use the functional name ("Testimonials", "Latest writing") or no label.
45
+ - **Generic step labels.** "Stage 1 / Stage 2", "Phase 01 / 02". The step content *is* the
46
+ label: "Install", "Configure", "Ship".
47
+ - **Mock-humble asides** and micro-meta sentences under a heading ("The list will stay short
48
+ on purpose"). Eyebrow, headline, body. Nothing else.
49
+ - **"Quietly trusted by".** Say "Used at", "Customers include", or let the logos speak.
50
+
51
+ ## Composition habits
52
+
53
+ - **Three identical feature cards.** The default feature row. Reach for a two-column zig-zag,
54
+ an asymmetric grid, or a horizontal rail instead — and do not run the same split three
55
+ sections in a row.
56
+ - **The corner floater.** A giant left-aligned headline with a small explainer paragraph
57
+ floating in the section's top-right, aligned to nothing. Stack it, or build a real
58
+ two-column header.
59
+ - **Rotated vertical text** and crosshair grid lines drawn only to look designed. Both are
60
+ agency-portfolio clichés unless they organise real content.
61
+ - **A bento with empty cells.** N items means N cells, and at least two cells need real
62
+ visual variation — otherwise it is a white card grid with extra steps.
63
+
64
+ ## The second default (repetition)
65
+
66
+ Obeying every rule produces correct pages that all look the same. Before fixing type and
67
+ palette, read the project's recorded design decisions and either reuse them deliberately or
68
+ say why you are departing. Two specific defaults to distrust: reaching for the same display
69
+ serif again, and answering every premium-consumer brief with the beige/brass/oxblood family.
70
+
71
+ ## Surface treatments to distrust by default
72
+
73
+ Outer glows and neon; pure `#000000` (use an off-black); oversaturated accents that refuse to
74
+ sit with the neutrals; gradient fills on large headings; custom cursors (outdated, hostile to
75
+ accessibility and to performance); glass on every surface rather than on floating ones.
@@ -0,0 +1,41 @@
1
+ # When the brief is a design system, not an aesthetic
2
+
3
+ This skill's default is Tailwind v4 + Next.js, and for most briefs that is right. But some
4
+ briefs correspond to a **maintained, official design system**, and there the correct answer is
5
+ to install it. Recreating its CSS by hand produces something that looks like it, fails its
6
+ accessibility work, and drifts on its next release.
7
+
8
+ The list is deliberately short: only the cases where using the official package is
9
+ **obligatory or strongly expected**, and where the default instinct would be to hand-roll it.
10
+ Free choices (Bootstrap, Material for a non-Google product, Radix Themes, shadcn/ui) are not
11
+ here — they are ordinary picks, and shadcn/ui in particular is already this skill's territory,
12
+ where the rule is simply never to ship it in its default state.
13
+
14
+ | The brief reads as | Install | Why it is not a choice |
15
+ | --- | --- | --- |
16
+ | A UK public-sector service | `govuk-frontend` | Expected by the Service Standard; deviating is a finding at assessment |
17
+ | A US federal or trust-first public service | `uswds` | Same, under the US Web Design System |
18
+ | A Shopify admin surface or embedded app | `@shopify/polaris` (or Polaris web components) | Required for admin UI to be accepted |
19
+ | IBM-flavoured enterprise or dense analytics | `@carbon/react` + `@carbon/styles` | Mature data-density patterns you will otherwise reinvent badly |
20
+ | An Atlassian / Jira-adjacent product surface | `@atlaskit/*` + `@atlaskit/tokens` | Official DS; token-driven theming |
21
+ | A GitHub-style devtool or community page | `@primer/css`, or `@primer/react-brand` for marketing | Official Primer; the Brand variant is the marketing half |
22
+ | A Microsoft / Fluent-flavoured product | `@fluentui/react-components` | Official Fluent, Microsoft tokens, accessibility already done |
23
+
24
+ ## The three rules that come with them
25
+
26
+ 1. **Do not recreate it.** If the brief names one of these, install the package. Importing its
27
+ tokens and then overriding ninety percent of them is the same mistake with extra steps.
28
+ 2. **One system per project.** No Fluent components inside a Carbon tree, no shadcn/ui
29
+ dropped into a Material app. Mixing two systems produces a third, worse one.
30
+ 3. **Name what is an approximation.** When the brief is an *aesthetic* rather than a system —
31
+ glassmorphism, bento tiles, brutalism, editorial, aurora gradients, kinetic type — there is
32
+ no official package, and saying so is part of the work. Apple's Liquid Glass is documented
33
+ for Apple platforms only: there is no official web implementation, so a web version is a
34
+ `backdrop-filter` approximation and must be labelled as one, in the code and to the user.
35
+
36
+ ## What still applies
37
+
38
+ Everything in `../SKILL.md` that is not styling: the brand gate, the accessibility floor,
39
+ the Core Web Vitals budget, the 5-second value-prop test, and the tell registry in
40
+ `../scripts/verify.sh`. An official design system settles which components you use. It does
41
+ not settle whether the page says anything.
@@ -174,6 +174,41 @@ export function Toast({ open, message }: { open: boolean; message: string }) {
174
174
 
175
175
  Once you are in `motion/react`, defer the mechanics (spring config, variants, `layoutId` shared transitions) to the external *motion-foundations* and *motion-ui* references.
176
176
 
177
+ ## Escalating past the native path
178
+
179
+ Native `animation-timeline: view()` covers reveals, progress rails and parallax. Two effects it
180
+ cannot express are **pinning** (a section holds still while its content advances) and a
181
+ **horizontal pan** driven by vertical scroll. Those need a scroll library, and the improvised
182
+ version is always wrong in the same three ways. Escalate deliberately:
183
+
184
+ **Never listen to scroll directly.** `window.addEventListener('scroll')` fires off the main
185
+ thread's budget, blows INP, and never gets a passive-listener or cleanup story right. The
186
+ allowed inputs are `IntersectionObserver`, a scroll-driven CSS timeline, `useScroll()` from
187
+ `motion/react`, or GSAP's ScrollTrigger. Nothing else.
188
+
189
+ **Sticky stack** (pin a section, advance its layers):
190
+
191
+ ```ts
192
+ // 'use client' leaf only. One trigger per pinned section, killed on unmount.
193
+ const st = ScrollTrigger.create({
194
+ trigger: sectionRef.current,
195
+ start: "top top", // pin the moment the section's top reaches the viewport top
196
+ end: () => `+=${panels.length * window.innerHeight}`,
197
+ pin: true,
198
+ pinSpacing: true, // without this the next section slides under the pin
199
+ scrub: 1, // 1 = follow scroll with a 1s catch-up, not a hard 1:1 jump
200
+ invalidateOnRefresh: true, // recompute on resize, or the end point is stale on rotate
201
+ });
202
+ // cleanup is not optional: return () => st.kill();
203
+ ```
204
+
205
+ **Horizontal pan** is the same trigger with `end: () => "+=" + track.scrollWidth` and the
206
+ track moved via `x`, not `left` — `transform` is compositor-only, `left` triggers layout.
207
+
208
+ Both are opt-in above a motion budget, both stay inside
209
+ `@media (prefers-reduced-motion: no-preference)`, and both collapse to a plain vertical stack
210
+ below `768px` rather than pinning on a phone.
211
+
177
212
  ## See Also
178
213
 
179
214
  - `visual-system.md` — the easing and duration tokens these animations consume.
@@ -7,9 +7,17 @@
7
7
  # performance + accessibility audit and checks Core Web Vitals against the
8
8
  # skill's hard thresholds (LCP < 2.5s, CLS < 0.1, INP proxy via TBT < 200ms,
9
9
  # a11y score >= 0.9).
10
- # 2. Always runs static, network-free design-review grep checks (one <h1>,
11
- # no `transition: all`, hardcoded hex vs tokens, missing image alt, missing
12
- # prefers-reduced-motion, marketing ban-list words).
10
+ # 2. Always runs static, network-free design-review checks, in three groups:
11
+ # - four judgement-shaped ones (more than one <h1>, image without alt,
12
+ # hardcoded hex despite a token system, animation with no reduced-motion
13
+ # guard) — see static_checks();
14
+ # - the tell registry: one row per checkable AI tell, iterated generically
15
+ # (dashes in rendered copy, scroll cues, numbered eyebrows, build stamps,
16
+ # unstable viewport heights, middot chains, transition-all, hype words)
17
+ # — see tell_table(); every row has a fixture in the repo's test suite;
18
+ # - three counters that need a ratio rather than a match (eyebrows against
19
+ # sections, double-bordered rows, radius scales) — see counter_checks().
20
+ # Everything here WARNS. Nothing here fails, unless --strict is passed.
13
21
  # 3. If Lighthouse did not run, prints the manual QA checklist. For a graded
14
22
  # 0-10 critique, use the visual-audit rubric in SKILL.md instead.
15
23
  #
@@ -78,7 +86,10 @@ have() { command -v "$1" >/dev/null 2>&1; }
78
86
  SELF_NAME="$(basename "$0")"
79
87
  search() {
80
88
  [ "$#" -gt 0 ] || return 1
81
- if have rg; then
89
+ # VERIFY_FORCE_GREP=1 pins the grep branch. It exists so the test suite can prove BOTH
90
+ # engines agree on the multibyte patterns (em-dash, middot) on any machine, instead of
91
+ # only exercising whichever one happens to be installed.
92
+ if have rg && [ -z "${VERIFY_FORCE_GREP:-}" ]; then
82
93
  # rg already ignores .gitignore'd paths (node_modules, .next, dist); also skip self.
83
94
  rg -n --no-heading --glob "!$SELF_NAME" "$@"
84
95
  else
@@ -165,6 +176,102 @@ run_lighthouse() {
165
176
  fi
166
177
  }
167
178
 
179
+ # --- the tell registry ------------------------------------------------------
180
+ # One row per checkable AI tell. A row is DATA, not code, so the test suite can enumerate
181
+ # every row and demand a fixture for each: a check nobody has ever seen fire cannot ship.
182
+ # Add a tell by adding a row plus tests/fixtures/design-tells/tells/<id>.tsx — nothing else.
183
+ #
184
+ # Columns, delimited by %% (the pattern column needs `|` for ERE alternation):
185
+ # id %% sev %% extensions %% POSIX-ERE pattern %% what to do instead / the legit exception
186
+ #
187
+ # Patterns are POSIX ERE: no lookahead, no \b, no \s — they must behave identically under rg
188
+ # and under grep -E. Part of the corpus is adapted from Leonxlnx/taste-skill (MIT).
189
+ tell_table() {
190
+ cat <<'TELLS'
191
+ em-dash%%warn%%tsx,jsx,html,vue,svelte,mdx%%—|–%%em-dash or en-dash in rendered copy is the loudest AI tell. Use a period, a comma, a colon or parentheses instead
192
+ scroll-cue%%warn%%tsx,jsx,html,vue,svelte%%[Ss]croll to (explore|walk|discover|see|begin)|↓ *[Ss]croll%%a scroll cue explains scrolling to someone already looking at the hero. Delete it, or use a real anchor link instead
193
+ numbered-eyebrow%%warn%%tsx,jsx,html,vue,svelte%%(^|[^0-9A-Za-z])[0-9]{2,3} +(·|/) +[A-Za-z]%%numbered section eyebrow (001 · Capabilities). Name the topic in plain language instead
194
+ version-stamp%%warn%%tsx,jsx,html,vue,svelte%%v[0-9]+\.[0-9]+\.[0-9]+|Build [0-9]{3,4}%%a build or version stamp is devtool furniture on a marketing page. Delete it. Legitimate exception: a real devtool footer, a changelog or a release page
195
+ viewport-unit%%warn%%tsx,jsx,html,vue,svelte,css%%h-screen%%h-screen jumps when mobile browser chrome slides away. Use min-h-[100dvh] instead
196
+ middot-chain%%warn%%tsx,jsx,html,vue,svelte%%·[^·]*·%%more than one middot on a line is decoration pretending to be metadata. Use line breaks, columns or a hairline instead
197
+ transition-all%%warn%%tsx,jsx,css,html,vue,svelte%%transition:[[:space:]]*all|transition-all%%transition-all animates layout properties and janks. List the exact properties instead
198
+ ban-list%%warn%%tsx,jsx,html,md,mdx%%revolutionary|game-?changer|cutting-edge|supercharge|seamless|unlock%%hype words carry no value prop. Use the concrete benefit and a real number instead
199
+ TELLS
200
+ }
201
+
202
+ run_tell_table() {
203
+ local line id sev globs pat msg hits exts rest
204
+ # NOT `IFS='%%'`: IFS is a set of characters, so it would split on a single `%` and put an
205
+ # empty field between every pair. Parameter expansion splits on the two-character
206
+ # delimiter exactly, which is what the pattern column needs to keep its ERE `|`.
207
+ while IFS= read -r line; do
208
+ case "$line" in ''|'#'*) continue ;; esac
209
+ id="${line%%"%%"*}"; rest="${line#*"%%"}"
210
+ sev="${rest%%"%%"*}"; rest="${rest#*"%%"}"
211
+ globs="${rest%%"%%"*}"; rest="${rest#*"%%"}"
212
+ pat="${rest%%"%%"*}"; msg="${rest#*"%%"}"
213
+ exts="${globs//,/|}"
214
+ # Two post-filters, both learned from a real run over this repo:
215
+ # 1. keep only the extensions the row declares (anchored before the first colon, so it
216
+ # behaves the same under rg and grep and never matches content that names a file);
217
+ # 2. drop lines whose match sits in a code comment. A dash in `/* tokens — lifted */`
218
+ # is not rendered copy, and flagging it trains the reader to ignore the tool.
219
+ hits="$(search "$pat" 2>/dev/null \
220
+ | grep -E "^[^:]*\.($exts):" \
221
+ | grep -vE "^[^:]*:[0-9]+:[[:space:]]*(//|/\*|\*[^a-zA-Z]|#|<!--|\{/\*)" || true)"
222
+ if [ -n "$hits" ]; then
223
+ # The sev column is real, not decoration: anything other than `warn` breaks the build.
224
+ case "$sev" in
225
+ warn) warn "$id: $msg" ;;
226
+ *) fail "$id: $msg" ;;
227
+ esac
228
+ printf '%s\n' "$hits" | head -n 5
229
+ fi
230
+ done <<EOF
231
+ $(tell_table)
232
+ EOF
233
+ }
234
+
235
+ # --- counters: the tells that are a ratio or a repetition, not a pattern -----
236
+ # These cannot be table rows because one match proves nothing — it takes a count. Each one
237
+ # carries its own named test instead of riding the generic row mechanism.
238
+ counter_checks() {
239
+ local eyebrows sections ceiling hits variants v
240
+
241
+ # Eyebrow ceiling. Threshold ceil(sections/3) is BORROWED from taste-skill and has never
242
+ # been calibrated against a page of ours — the warning says so rather than implying rigour.
243
+ eyebrows="$(search 'uppercase[^"]*tracking|tracking[^"]*uppercase' 2>/dev/null | grep -cE '^[^:]*\.(tsx|jsx|html|vue|svelte):' || true)"
244
+ sections="$(search '<section' 2>/dev/null | grep -cE '^[^:]*\.(tsx|jsx|html|vue|svelte):' || true)"
245
+ [ -z "$eyebrows" ] && eyebrows=0
246
+ [ -z "$sections" ] && sections=0
247
+ if [ "$sections" -eq 0 ]; then
248
+ if [ "$eyebrows" -gt 0 ]; then
249
+ skip "eyebrow ceiling: no <section> found, so there is no denominator — not guessing one"
250
+ fi
251
+ else
252
+ ceiling=$(( (sections + 2) / 3 ))
253
+ if [ "$eyebrows" -gt "$ceiling" ]; then
254
+ warn "eyebrow count $eyebrows exceeds the ceiling $ceiling for $sections sections (uncalibrated threshold, borrowed): drop the uppercase micro-labels, or let the headline carry it instead"
255
+ fi
256
+ fi
257
+
258
+ # A hairline above AND below every row of a long list is the laziest possible layout.
259
+ hits="$(search 'border-t[^"]*border-b|border-b[^"]*border-t' 2>/dev/null | grep -cE '^[^:]*\.(tsx|jsx|html|vue|svelte):' || true)"
260
+ [ -z "$hits" ] && hits=0
261
+ if [ "$hits" -gt 3 ]; then
262
+ warn "double border on $hits list rows: pick one edge, or use spacing instead of hairlines"
263
+ fi
264
+
265
+ # More than two radius scales in one tree means no radius system at all.
266
+ variants=0
267
+ for v in none sm md lg xl 2xl 3xl full; do
268
+ if search "rounded-$v" >/dev/null 2>&1; then variants=$((variants + 1)); fi
269
+ done
270
+ if [ "$variants" -gt 2 ]; then
271
+ warn "$variants different radius scales in use: one radius system per project — use a --radius-* token instead"
272
+ fi
273
+ }
274
+
168
275
  # --- static design-review checks (always run, no network) -------------------
169
276
  static_checks() {
170
277
  local hits
@@ -182,9 +289,8 @@ static_checks() {
182
289
  done < <(grep -rl --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=.next '<h1' . 2>/dev/null || true)
183
290
  fi
184
291
 
185
- # 2. transition: all / transition-all ([[:space:]] is POSIX; \s is not portable in ERE)
186
- hits="$(search 'transition:[[:space:]]*all|transition-all' 2>/dev/null || true)"
187
- if [ -n "$hits" ]; then warn "transition: all / transition-all found:"; printf '%s\n' "$hits" | head -n 5; fi
292
+ # 2. (moved) `transition: all` is now a row in tell_table() — it is a plain pattern, so it
293
+ # gets the generic mechanism and its fixture for free.
188
294
 
189
295
  # 3. hardcoded hex when a token system exists (\b is not portable in ERE; a
190
296
  # 3-8 char hex run after # is a sufficient heuristic without the boundary).
@@ -209,9 +315,7 @@ static_checks() {
209
315
  fi
210
316
  fi
211
317
 
212
- # 6. ban-list marketing words in copy
213
- hits="$(search 'revolutionary|game-?changer|cutting-edge|supercharge|seamless|unlock' 2>/dev/null | grep -iE '\.(tsx|jsx|html|md|mdx)' || true)"
214
- if [ -n "$hits" ]; then warn "ban-list marketing words found in copy:"; printf '%s\n' "$hits" | head -n 5; fi
318
+ # 6. (moved) the marketing ban-list is now a row in tell_table(), same reason as #2.
215
319
  }
216
320
 
217
321
  # --- fallback manual checklist ----------------------------------------------
@@ -239,6 +343,8 @@ EOF
239
343
  # --- run --------------------------------------------------------------------
240
344
  run_lighthouse
241
345
  static_checks
346
+ run_tell_table
347
+ counter_checks
242
348
  [ "$LH_RAN" -eq 0 ] && print_checklist
243
349
 
244
350
  printf '\nok=%d skip=%d warn=%d fail=%d\n' "$ok_count" "$skip_count" "$warn_count" "$fail_count"