quiver-cli 0.8.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +127 -45
  2. package/bin/quiver-cli.mjs +3 -1
  3. package/dist/cli.js +666 -436
  4. package/package.json +2 -2
  5. package/template/.agents/AGENTS.md +4 -3
  6. package/template/.agents/config.json +7 -0
  7. package/template/.agents/plugins/opencode/rtk.ts +34 -0
  8. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  9. package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
  10. package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
  11. package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
  12. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
  13. package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
  14. package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
  15. package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
  16. package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
  17. package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
  18. package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
  19. package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
  21. package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
  22. package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
  23. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
  24. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
  25. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
  26. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  27. package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
  28. package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
  29. package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
  30. package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
  31. package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
  32. package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
  33. package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
  34. package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
  35. package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
  36. package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
  37. package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
  38. package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
  39. package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
  40. package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
  41. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
  42. package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
  43. package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
  44. package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
  45. package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
  46. package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
  47. package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
  48. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
  49. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
  50. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
  51. package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
  52. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
  53. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  54. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
  55. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
  56. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
  57. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  58. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
  59. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
  60. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
  61. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
  62. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
  63. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
  64. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
  65. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
  66. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  67. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
  68. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
  69. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
  70. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
  71. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
  72. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
  73. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
  74. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
  76. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
  77. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
  78. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
  79. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  85. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
  86. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
  88. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
  89. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
  90. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
  91. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
  92. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  93. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
  95. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
  96. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  97. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
  98. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
  99. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  100. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  101. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  102. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  103. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  104. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
  105. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
  106. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
  107. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
  108. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
  109. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
  110. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
  111. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
  112. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
  113. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
  114. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  115. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
  116. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
  117. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
  118. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  119. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
  120. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
  121. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
  122. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  123. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
  124. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
  125. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
  126. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
  127. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
  128. package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
  129. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
  130. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
  131. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
  132. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
  133. package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
  134. package/template/.agents/skills/design/shadcn/cli.md +49 -16
  135. package/template/.agents/skills/design/shadcn/customization.md +14 -7
  136. package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
  137. package/template/.agents/skills/design/shadcn/mcp.md +27 -16
  138. package/template/.agents/skills/design/shadcn/registry.md +277 -0
  139. package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
  140. package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
  141. package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
  142. package/template/.agents/skills/find-skills/SKILL.md +2 -3
  143. package/template/.agents/skills/hono/SKILL.md +579 -0
  144. package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
  145. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
  146. package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
  147. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
  148. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
  149. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
  150. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
  151. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
  152. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
  153. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
  154. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
  155. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
  156. package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
  157. package/template/.agents/skills/supabase/SKILL.md +145 -0
  158. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
  159. package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
  160. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
  161. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  162. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  163. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  164. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  165. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  166. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  167. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  168. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  169. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  170. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  171. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  172. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  173. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  174. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  175. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  176. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  177. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  178. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  179. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  180. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  181. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  182. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  183. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  184. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  185. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  186. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  187. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  188. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  189. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  190. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  191. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  192. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  193. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  194. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  195. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  196. package/template/.agents/upstreams.json +34 -12
  197. package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
  198. package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
  199. package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
  200. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
@@ -1,20 +1,23 @@
1
- import { GENERIC_FONTS } from '../../shared/constants.mjs';
1
+ import { GENERIC_FONTS, OVERUSED_FONTS, EM_DASH_FLOOR, EM_DASH_CHARS_PER_DASH } from '../../shared/constants.mjs';
2
2
  import { isNeutralColor } from '../../shared/color.mjs';
3
+ import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
3
4
  import { checkSourceDesignSystem } from '../../design-system.mjs';
5
+ import { scanCssTextForGlow, scanCssTextForGridBackground, scanCssTextForMarquee, scanCssTextForPseudoStripe, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
4
6
  import { isFullPage } from '../../shared/page.mjs';
5
7
  import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
6
8
  import { finding } from '../../findings.mjs';
7
- import { filterByProviders } from '../../registry/antipatterns.mjs';
8
9
  import { profileFindings, profileStep } from '../../profile/profiler.mjs';
9
10
 
10
11
  // ---------------------------------------------------------------------------
11
12
  // Regex fallback (non-HTML files: CSS, JSX, TSX, etc.)
12
13
  // ---------------------------------------------------------------------------
13
14
 
14
- const hasRounded = (line) => /\brounded(?:-\w+)?\b/.test(line);
15
+ const hasRounded = (line) =>
16
+ /\brounded(?:-\w+)?\b/.test(line.replace(/\brounded-none\b/g, ''));
15
17
  const hasBorderRadius = (line) => /border-radius/i.test(line);
16
18
  const isSafeElement = (line) => /<(?:blockquote|nav[\s>]|pre[\s>]|code[\s>]|a\s|input[\s>]|span[\s>])/i.test(line);
17
19
 
20
+
18
21
  /** Strip HTML to plain text — drops script/style/comments/tags so
19
22
  * content-text analyzers don't false-positive on code or CSS. */
20
23
  function stripHtmlToText(html) {
@@ -38,25 +41,100 @@ function shouldRunPageAnalyzers(content, filePath) {
38
41
  return !ext || PAGE_ANALYZER_EXTS.has(ext);
39
42
  }
40
43
 
41
- function isNeutralBorderColor(str) {
42
- const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
43
- if (!m) return false;
44
- const c = m[1].toLowerCase();
45
- if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true;
46
- if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
47
- const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
48
- if (hex) {
49
- const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)];
50
- return (Math.max(r, g, b) - Math.min(r, g, b)) < 30;
44
+ function firstOverusedGoogleFont(text) {
45
+ return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
46
+ }
47
+
48
+ // CSS named colors whose channels are equal (achromatic). Anything outside
49
+ // this set falls through to the format parsers, and an unrecognized spelling
50
+ // stays non-neutral so a real accent is never skipped.
51
+ const NEUTRAL_COLOR_KEYWORDS = new Set([
52
+ 'transparent', 'currentcolor',
53
+ 'black', 'white', 'gray', 'grey', 'silver',
54
+ 'dimgray', 'dimgrey', 'darkgray', 'darkgrey', 'lightgray', 'lightgrey',
55
+ 'gainsboro', 'whitesmoke',
56
+ ]);
57
+
58
+ function hexChannels(color) {
59
+ const long = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})(?:[0-9a-f]{2})?$/i);
60
+ if (long) return [parseInt(long[1], 16), parseInt(long[2], 16), parseInt(long[3], 16)];
61
+ const short = color.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])(?:[0-9a-f])?$/i);
62
+ if (short) return [1, 2, 3].map((i) => parseInt(short[i] + short[i], 16));
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Split one box-shadow layer into top-level tokens.
68
+ *
69
+ * Whitespace inside parens does not separate tokens: `rgb(0 0 0)` and
70
+ * `var(--x, 4px)` are each a single value, and splitting them on spaces would
71
+ * read their innards as separate lengths.
72
+ */
73
+ function tokenizeShadowLayer(layer) {
74
+ const tokens = [];
75
+ let depth = 0;
76
+ let current = '';
77
+ for (const char of String(layer || '')) {
78
+ if (char === '(') depth++;
79
+ else if (char === ')') depth--;
80
+ else if (depth === 0 && /\s/.test(char)) {
81
+ if (current) tokens.push(current);
82
+ current = '';
83
+ continue;
84
+ }
85
+ current += char;
51
86
  }
52
- const shex = c.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
53
- if (shex) {
54
- const [r, g, b] = [parseInt(shex[1] + shex[1], 16), parseInt(shex[2] + shex[2], 16), parseInt(shex[3] + shex[3], 16)];
55
- return (Math.max(r, g, b) - Math.min(r, g, b)) < 30;
87
+ if (current) tokens.push(current);
88
+ return tokens;
89
+ }
90
+
91
+ function lastMatch(text, re) {
92
+ const all = [...String(text || '').matchAll(re)];
93
+ return all.length ? all[all.length - 1] : null;
94
+ }
95
+
96
+ function isShadowLength(token) {
97
+ return /^-?\d*\.?\d+(?:px)?$/i.test(String(token || ''));
98
+ }
99
+
100
+ /**
101
+ * Neutrality test for colors as written in source CSS.
102
+ *
103
+ * shared/color.mjs's isNeutralColor only parses the computed function forms a
104
+ * browser or jsdom emits (rgb/oklch/lab/...) and deliberately reports every
105
+ * other spelling as chromatic so an unknown format is never silently skipped.
106
+ * That default is wrong for authored CSS, where `#000` and `black` are the
107
+ * normal spellings: calling it directly reports a plain black hairline as a
108
+ * colored stripe. Handle hex and named neutrals here, then defer.
109
+ */
110
+ function isNeutralAuthoredColor(rawColor) {
111
+ const c = String(rawColor || '').trim().toLowerCase();
112
+ if (!c) return false;
113
+ if (NEUTRAL_COLOR_KEYWORDS.has(c)) return true;
114
+ // Modern rgb() takes space-separated channels (`rgb(0 0 0)`). shared/color.mjs
115
+ // parses only the comma form a browser's getComputedStyle emits, so authored
116
+ // space-separated neutrals fell through it and reported as chromatic — the
117
+ // exemption this function exists for, missed. Normalize before delegating.
118
+ if (/^rgba?\(/i.test(c)) {
119
+ const channels = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
120
+ if (channels) {
121
+ const values = [1, 2, 3].map((i) => Number(channels[i]));
122
+ return (Math.max(...values) - Math.min(...values)) < 30;
123
+ }
124
+ return isNeutralColor(c);
56
125
  }
126
+ if (/^(?:hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
127
+ const channels = hexChannels(c);
128
+ if (channels) return (Math.max(...channels) - Math.min(...channels)) < 30;
57
129
  return false;
58
130
  }
59
131
 
132
+ function isNeutralBorderColor(str) {
133
+ const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
134
+ if (!m) return false;
135
+ return isNeutralAuthoredColor(m[1]);
136
+ }
137
+
60
138
  const REGEX_MATCHERS = [
61
139
  // --- Side-tab ---
62
140
  { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g,
@@ -88,9 +166,12 @@ const REGEX_MATCHERS = [
88
166
  { id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans|Instrument Serif)\b/gi,
89
167
  test: () => true,
90
168
  fmt: (m) => m[0] },
91
- { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Instrument\+Serif|Mona\+Sans|Geist)\b/gi,
92
- test: () => true,
93
- fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` },
169
+ { id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi,
170
+ test: (m) => {
171
+ m.overusedGoogleFont = firstOverusedGoogleFont(m[0]);
172
+ return Boolean(m.overusedGoogleFont);
173
+ },
174
+ fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` },
94
175
  // --- Gradient text ---
95
176
  { id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi,
96
177
  test: (m, line) => /gradient/i.test(line),
@@ -160,27 +241,6 @@ const REGEX_MATCHERS = [
160
241
  ];
161
242
 
162
243
  const REGEX_ANALYZERS = [
163
- // Single font
164
- (content, filePath) => {
165
- const fontFamilyRe = /font-family\s*:\s*([^;}]+)/gi;
166
- const fonts = new Set();
167
- let m;
168
- while ((m = fontFamilyRe.exec(content)) !== null) {
169
- for (const f of m[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) {
170
- if (f && !GENERIC_FONTS.has(f)) fonts.add(f);
171
- }
172
- }
173
- const gfRe = /fonts\.googleapis\.com\/css2?\?family=([^&"'\s]+)/gi;
174
- while ((m = gfRe.exec(content)) !== null) {
175
- for (const f of m[1].split('|').map(f => f.split(':')[0].replace(/\+/g, ' ').toLowerCase())) fonts.add(f);
176
- }
177
- if (fonts.size !== 1 || content.split('\n').length < 20) return [];
178
- const name = [...fonts][0];
179
- const lines = content.split('\n');
180
- let line = 1;
181
- for (let i = 0; i < lines.length; i++) { if (lines[i].toLowerCase().includes(name)) { line = i + 1; break; } }
182
- return [finding('single-font', filePath, `only font used is ${name}`, line)];
183
- },
184
244
  // Flat type hierarchy
185
245
  (content, filePath) => {
186
246
  const sizes = new Set();
@@ -230,15 +290,34 @@ const REGEX_ANALYZERS = [
230
290
  const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
231
291
  return [finding('monotonous-spacing', filePath, `~${dominant}px used ${maxCount}/${rounded.length} times (${Math.round(pct * 100)}%)`)];
232
292
  },
233
- // Em-dash overuse: 5+ em-dashes or "--" in body text content
234
- // (occasional em-dash use in prose is fine; the pattern fires only
235
- // when count crosses into AI-cadence territory).
293
+ // Em-dash overuse (ADVISORY): the AI cadence tell is em-dash *saturation*,
294
+ // not the occasional dash. Humans use em-dashes legitimately, so this rule is
295
+ // advisory (surfaced separately, never a failure, hook-skipped by default) and
296
+ // its threshold is deliberately conservative. Two gates must both hold:
297
+ // 1. Absolute floor of EM_DASH_FLOOR (8) dashes — a page with a handful
298
+ // never fires, no matter how short.
299
+ // 2. Density: at least one dash per EM_DASH_CHARS_PER_DASH (500) characters
300
+ // of body text, so a long article that uses eight across several thousand
301
+ // words is left alone while a short, dash-per-clause landing page is not.
302
+ // Raised from the old flat 5-dash floor, which fired on ordinary long prose.
303
+ //
304
+ // stripHtmlToText drops tags but leaves character-entity escapes intact, so
305
+ // a model that writes `&mdash;`, `&#8212;`, or `&#x2014;` renders an em-dash
306
+ // the counter never saw. Decode the em-dash entities (named, zero-padded
307
+ // decimal, upper/lower hex) to the literal glyph first. En-dash entities are
308
+ // deliberately left alone: the rule counts em-dashes, and the literal `–`
309
+ // was never counted either.
236
310
  (content, filePath) => {
237
- const text = stripHtmlToText(content);
311
+ const text = stripHtmlToText(content)
312
+ .replace(/&mdash;|&#0*8212;|&#x0*2014;/gi, '—');
238
313
  let count = 0;
239
314
  const re = /[—]|--(?=\S)/g;
240
315
  while (re.exec(text) !== null) count++;
241
- if (count < 5) return [];
316
+ if (count < EM_DASH_FLOOR) return [];
317
+ // Saturation gate: dashes must be dense in the prose, not sprinkled through
318
+ // a long document. textLength <= count * chars-per-dash means the density is
319
+ // at or above the threshold.
320
+ if (text.length > count * EM_DASH_CHARS_PER_DASH) return [];
242
321
  return [finding('em-dash-overuse', filePath, `${count} em-dashes in body text`)];
243
322
  },
244
323
  // Marketing buzzwords: SaaS phrase list
@@ -274,22 +353,6 @@ const REGEX_ANALYZERS = [
274
353
  if (count === 0) return [];
275
354
  return [finding('marketing-buzzword', filePath, `${count} buzzword phrase${count === 1 ? '' : 's'}: "${firstSample}"`)];
276
355
  },
277
- // Numbered section markers (01 / 02 / 03 ...)
278
- (content, filePath) => {
279
- const text = stripHtmlToText(content);
280
- const re = /\b(0[1-9]|1[0-2])\b/g;
281
- const seen = new Set();
282
- let m;
283
- while ((m = re.exec(text)) !== null) seen.add(m[1]);
284
- if (seen.size < 3) return [];
285
- const sorted = [...seen].sort();
286
- let sequential = 0;
287
- for (let i = 1; i < sorted.length; i++) {
288
- if (parseInt(sorted[i], 10) === parseInt(sorted[i - 1], 10) + 1) sequential++;
289
- }
290
- if (sequential < 2) return [];
291
- return [finding('numbered-section-markers', filePath, `Sequence: ${sorted.slice(0, 6).join(', ')}`)];
292
- },
293
356
  // Aphoristic cadence: manufactured-contrast + short-rebuttal
294
357
  (content, filePath) => {
295
358
  const text = stripHtmlToText(content);
@@ -311,41 +374,143 @@ const REGEX_ANALYZERS = [
311
374
  if (count < 3) return [];
312
375
  return [finding('aphoristic-cadence', filePath, `${count} aphoristic constructions: "${firstSample}"`)];
313
376
  },
314
- // Dark glow (page-level: dark bg + colored box-shadow with blur)
377
+ // Dark glow / chromatic halo shadows (page-level). Shared scanner handles
378
+ // any color format, single-level var() resolution, zero-offset halos on
379
+ // any background, and text-shadow glows.
315
380
  (content, filePath) => {
316
- // Check if page has a dark background
317
- const darkBgRe = /background(?:-color)?\s*:\s*(?:#(?:0[0-9a-f]|1[0-9a-f]|2[0-3])[0-9a-f]{4}\b|#(?:0|1)[0-9a-f]{2}\b|rgb\(\s*(\d{1,2})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\))/gi;
318
- const twDarkBg = /\bbg-(?:gray|slate|zinc|neutral|stone)-(?:9\d{2}|800)\b/;
319
- const hasDarkBg = darkBgRe.test(content) || twDarkBg.test(content);
320
- if (!hasDarkBg) return [];
321
-
322
- // Check for colored box-shadow with blur > 4px
323
- const shadowRe = /box-shadow\s*:\s*([^;{}]+)/gi;
324
- let m;
325
- while ((m = shadowRe.exec(content)) !== null) {
326
- const val = m[1];
327
- const colorMatch = val.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
328
- if (!colorMatch) continue;
329
- const [r, g, b] = [+colorMatch[1], +colorMatch[2], +colorMatch[3]];
330
- if ((Math.max(r, g, b) - Math.min(r, g, b)) < 30) continue; // skip gray
331
- // Check blur: look for pattern like "0 0 20px" (third number > 4)
332
- const pxVals = [...val.matchAll(/(\d+)px|(?<![.\d])\b(0)\b(?![.\d])/g)].map(p => +(p[1] || p[2]));
333
- if (pxVals.length >= 3 && pxVals[2] > 4) {
334
- const lines = content.substring(0, m.index).split('\n');
335
- return [finding('dark-glow', filePath, `Colored glow (rgb(${r},${g},${b})) on dark page`, lines.length)];
336
- }
337
- }
338
- return [];
381
+ const hits = scanCssTextForGlow(content);
382
+ if (hits.length === 0) return [];
383
+ const lines = content.substring(0, hits[0].index).split('\n');
384
+ return [finding('dark-glow', filePath, hits[0].snippet, lines.length)];
385
+ },
386
+ // Radial-gradient background halo on a dark page (the gradient sibling
387
+ // of the dark-glow shadow tell).
388
+ (content, filePath) => {
389
+ const hits = scanCssTextForRadialHalo(content);
390
+ if (hits.length === 0) return [];
391
+ const lines = content.substring(0, hits[0].index).split('\n');
392
+ return [finding('radial-halo', filePath, hits[0].snippet, lines.length)];
339
393
  },
394
+ // Auto-scrolling marquees (<marquee> or infinite horizontal loop
395
+ // animations).
396
+ (content, filePath) => scanCssTextForMarquee(content).map(hit => finding('marquee', filePath, hit.snippet)),
340
397
  ];
341
398
 
342
399
  // ---------------------------------------------------------------------------
343
- // Style block extraction (Vue/Svelte <style> blocks)
400
+ // Structural CSS checks used by source files whose styles are not parsed by
401
+ // the static HTML engine.
402
+ // ---------------------------------------------------------------------------
403
+
404
+ const CHROMATIC_SHADOW_TOKEN_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
405
+
406
+ function insetStripeColorIsChromatic(rawColor) {
407
+ const color = String(rawColor || '').trim().replace(/\s*!important\s*$/i, '');
408
+ if (/^(?:currentcolor|transparent|inherit|unset)$/i.test(color)) return false;
409
+ const variable = color.match(/^var\(\s*(--[\w-]+)/i);
410
+ if (variable) return CHROMATIC_SHADOW_TOKEN_RE.test(variable[1]);
411
+ if (!/^(?:#|rgba?\(|hsla?\(|hwb\(|oklch\(|oklab\(|lch\(|lab\(|color\(|[a-z]+$)/i.test(color)) return false;
412
+ return !isNeutralAuthoredColor(color);
413
+ }
414
+
415
+ /**
416
+ * Blank out comment bodies while preserving every byte offset (and therefore
417
+ * every line number) so commented-out CSS is not scanned as live rules.
418
+ */
419
+ function blankCssComments(css) {
420
+ return css.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '));
421
+ }
422
+
423
+ function scanInsetStripeCss(rawContent, filePath, lineOffset = 0) {
424
+ const content = blankCssComments(rawContent);
425
+ const findings = [];
426
+ const ruleRe = /([^{};]+)\{([^{}]*)\}/g;
427
+ let match;
428
+ // Deriving each line with content.slice(0, offset).split('\n') re-scans the
429
+ // whole prefix per rule, which is O(n^2) on a large stylesheet. Rule matches
430
+ // arrive in source order, so carry a monotonic cursor instead: one pass total.
431
+ let scanOffset = 0;
432
+ let scanLine = 1;
433
+ const lineAtOffset = (offset) => {
434
+ while (scanOffset < offset) {
435
+ if (content[scanOffset] === '\n') scanLine++;
436
+ scanOffset++;
437
+ }
438
+ return scanLine;
439
+ };
440
+ while ((match = ruleRe.exec(content)) !== null) {
441
+ // The selector group is `[^{};]+`, which greedily absorbs the whitespace and
442
+ // newlines trailing the previous rule. Advance past that run before deriving
443
+ // the line, or every rule after the first reports the preceding line.
444
+ const selectorStart = match.index + (match[1].length - match[1].trimStart().length);
445
+ const selector = match[1].trim().replace(/\s+/g, ' ');
446
+ if (!selector) continue;
447
+ if (/:(?:hover|focus|focus-visible|focus-within|active|checked|target)\b/i.test(selector)) continue;
448
+ if (/\[aria-selected\s*[*^$|~]?=\s*["']?true/i.test(selector)) continue;
449
+ if (/\[aria-current(?!\s*[*^$|~]?=\s*["']?false)/i.test(selector)) continue;
450
+ if (/(?:^|[\s._[-])(?:active|current|selected)(?![\w])/i.test(selector)) continue;
451
+ if (/(?:^|[\s>+~,(])(?:button|hr|tr|td|th|table|blockquote|pre|code)(?![\w-])/i.test(selector)) continue;
452
+
453
+ // Read the last of a repeated declaration, not the first: that is what the
454
+ // cascade paints. Taking the first both flagged stripes that a later
455
+ // `box-shadow: none` had cancelled and missed stripes that overrode an
456
+ // earlier value, and mis-skipped rules whose narrow width was overridden.
457
+ const width = lastMatch(match[2], /(?:^|;)\s*(?:width|inline-size)\s*:\s*(\d+(?:\.\d+)?)px/gi);
458
+ if (width && Number(width[1]) <= 40) continue;
459
+ const declaration = lastMatch(match[2], /(?:^|;)\s*box-shadow\s*:\s*([^;]+)/gi);
460
+ if (!declaration || !/\binset\b/i.test(declaration[1])) continue;
461
+ // `!important` qualifies the declaration, not the shadow value, so strip it
462
+ // before the layers are read. Tokenizing split it into its own token, which
463
+ // made the color count wrong and silently stopped flagging stripes declared
464
+ // with it — a shape the previous regex handled.
465
+ const shadowValue = declaration[1].replace(/\s*!\s*important\s*$/i, '').trim();
466
+
467
+ for (const rawLayer of shadowValue.split(/,(?![^(]*\))/)) {
468
+ const layer = rawLayer.trim();
469
+ // Parse the layer by its grammar rather than by one spelling of it.
470
+ // A box-shadow layer is `inset? && <length>{2,4} && <color>?` in any
471
+ // order, so `inset 4px 0 red`, `4px 0 0 red inset`, and `red 4px 0 inset`
472
+ // all paint the same stripe. Matching a fixed token order missed three
473
+ // valid spellings in a row; enumerate the tokens instead. Tokenizing must
474
+ // respect parens: `rgb(0 0 0)` is one color token, and splitting it on
475
+ // whitespace would read its channels as lengths.
476
+ const tokens = tokenizeShadowLayer(layer);
477
+ if (!tokens.some((token) => /^inset$/i.test(token))) continue;
478
+ const rest = tokens.filter((token) => !/^inset$/i.test(token));
479
+ const lengths = rest.filter(isShadowLength);
480
+ const colors = rest.filter((token) => !isShadowLength(token));
481
+ // Only the two offsets are required; omitted blur/spread default to 0,
482
+ // which is exactly the stripe shape. More than one non-length token is a
483
+ // layer shape we do not claim to understand, so leave it alone.
484
+ if (lengths.length < 2 || lengths.length > 4 || colors.length !== 1) continue;
485
+ const values = lengths.map((token) => ({
486
+ n: Number(token.replace(/px$/i, '')),
487
+ hasPx: /px$/i.test(token),
488
+ }));
489
+ const x = values[0];
490
+ const y = values[1];
491
+ const blur = values[2] ? values[2].n : 0;
492
+ const spread = values[3] ? values[3].n : 0;
493
+ if ((x.n !== 0 && !x.hasPx) || (y.n !== 0 && !y.hasPx) || blur !== 0 || spread !== 0) continue;
494
+ const ax = Math.abs(x.n);
495
+ const ay = Math.abs(y.n);
496
+ if (!((ax >= 3 && ax <= 12 && ay === 0) || (ay >= 3 && ay <= 12 && ax === 0))) continue;
497
+ if (!insetStripeColorIsChromatic(colors[0])) continue;
498
+ const edge = ay === 0 ? (x.n > 0 ? 'left' : 'right') : (y.n > 0 ? 'top' : 'bottom');
499
+ const line = lineOffset + lineAtOffset(selectorStart);
500
+ findings.push(finding('side-tab', filePath, `${selector} — inset box-shadow ${ay === 0 ? ax : ay}px stripe (${edge})`, line));
501
+ break;
502
+ }
503
+ }
504
+ return findings;
505
+ }
506
+
507
+ // ---------------------------------------------------------------------------
508
+ // Style block extraction (Astro/Vue/Svelte <style> blocks)
344
509
  // ---------------------------------------------------------------------------
345
510
 
346
511
  function extractStyleBlocks(content, ext) {
347
512
  ext = ext.toLowerCase();
348
- if (ext !== '.vue' && ext !== '.svelte') return [];
513
+ if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
349
514
  const blocks = [];
350
515
  const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
351
516
  let m;
@@ -430,24 +595,24 @@ function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null,
430
595
  }
431
596
 
432
597
  /** Page-level analyzers that scan rendered text content (em-dash use,
433
- * buzzword phrases, numbered section markers, aphoristic cadence).
598
+ * buzzword phrases, aphoristic cadence).
434
599
  * These are detector-agnostic — they work on any HTML/text source
435
600
  * and don't need a parsed DOM. Exported so detectHtml can call them
436
601
  * for `.html` files (which otherwise skip the regex engine). */
437
602
  const TEXT_CONTENT_ANALYZER_IDS = [
438
603
  'em-dash-overuse',
439
604
  'marketing-buzzword',
440
- 'numbered-section-markers',
441
605
  'aphoristic-cadence',
442
606
  ];
443
607
 
444
608
  function runTextContentAnalyzers(content, filePath, options = {}) {
445
609
  const profile = options?.profile;
446
610
  if (!shouldRunPageAnalyzers(content, filePath)) return [];
447
- // The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
611
+ // The 3 text-content analyzers are at indices 2-4 in REGEX_ANALYZERS
612
+ // (single-font's removal on 2026-07-29 shifted every index down one).
448
613
  const findings = [];
449
614
  for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
450
- const analyzer = REGEX_ANALYZERS[3 + i];
615
+ const analyzer = REGEX_ANALYZERS[2 + i];
451
616
  const ruleId = TEXT_CONTENT_ANALYZER_IDS[i];
452
617
  findings.push(...profileFindings(profile, {
453
618
  engine: 'regex',
@@ -472,8 +637,36 @@ function detectText(content, filePath, options = {}) {
472
637
  profile,
473
638
  phase: 'source',
474
639
  }));
640
+ // Pseudo-element stripes (::before/::after absolute bars) carry the same
641
+ // side-tab silhouette without any border token, so the line matchers can't
642
+ // see them (issue #394). The shared scanner already runs on full HTML pages
643
+ // via checkHtmlPatterns; give standalone stylesheets, component style
644
+ // blocks, and CSS-in-JS templates the same coverage. Each hit carries the
645
+ // rule's source offset, so the finding gets a real line and line-scoped
646
+ // inline ignores keep working.
647
+ const pseudoStripeFindings = (text, lineOffset) =>
648
+ scanCssTextForPseudoStripe(text).map(hit =>
649
+ finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
650
+
651
+ if (cssLike.has(ext)) {
652
+ findings.push(...scanInsetStripeCss(content, filePath));
653
+ findings.push(...pseudoStripeFindings(content, 0));
654
+ }
655
+
656
+ // Block-level CSS checks that need multiple declarations must run over the
657
+ // complete source, not line-by-line. This covers standalone stylesheets,
658
+ // component style blocks, inline styles, and CSS-in-JS templates.
659
+ findings.push(...profileFindings(profile, {
660
+ engine: 'regex',
661
+ phase: 'source',
662
+ ruleId: 'codex-grid-background',
663
+ target: filePath,
664
+ }, () => scanCssTextForGridBackground(content).map(hit => {
665
+ const line = content.substring(0, hit.index).split('\n').length;
666
+ return finding('codex-grid-background', filePath, hit.snippet, line);
667
+ })));
475
668
 
476
- // Extract and scan <style> blocks from Vue/Svelte SFCs
669
+ // Extract and scan <style> blocks from Astro/Vue/Svelte components.
477
670
  const styleBlocks = profile
478
671
  ? profileStep(profile, {
479
672
  engine: 'regex',
@@ -488,6 +681,14 @@ function detectText(content, filePath, options = {}) {
488
681
  profile,
489
682
  phase: 'style-block',
490
683
  }));
684
+ // block.startLine is the first line *after* the <style> tag, but block.content
685
+ // begins at the character right after that tag — so its own line 1 sits on the
686
+ // tag's line, whether or not a newline follows immediately. lineAtOffset is
687
+ // 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
688
+ // reported every selector one line low. runRegexMatchers keeps startLine - 1
689
+ // because it indexes its split lines from zero.
690
+ findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 2));
691
+ findings.push(...pseudoStripeFindings(block.content, block.startLine - 2));
491
692
  }
492
693
 
493
694
  // Extract and scan CSS-in-JS template literals
@@ -505,6 +706,8 @@ function detectText(content, filePath, options = {}) {
505
706
  profile,
506
707
  phase: 'css-in-js',
507
708
  }));
709
+ findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 1));
710
+ findings.push(...pseudoStripeFindings(block.content, block.startLine - 1));
508
711
  }
509
712
 
510
713
  if (options?.designSystem) {
@@ -530,12 +733,10 @@ function detectText(content, filePath, options = {}) {
530
733
  // Page-level analyzers only run on full pages
531
734
  if (shouldRunPageAnalyzers(content, filePath)) {
532
735
  const analyzerIds = [
533
- 'single-font',
534
736
  'flat-type-hierarchy',
535
737
  'monotonous-spacing',
536
738
  'em-dash-overuse',
537
739
  'marketing-buzzword',
538
- 'numbered-section-markers',
539
740
  'aphoristic-cadence',
540
741
  'dark-glow',
541
742
  ];
@@ -550,10 +751,9 @@ function detectText(content, filePath, options = {}) {
550
751
  }
551
752
  }
552
753
 
553
- const byProvider = filterByProviders(deduped, options?.providers);
554
754
  // Inline `impeccable-disable*` waivers travel with the file; honor them unless
555
755
  // explicitly bypassed (`--no-config` / `--no-inline-ignores`).
556
- return options?.inlineIgnores === false ? byProvider : applyInlineIgnores(byProvider, content);
756
+ return options?.inlineIgnores === false ? deduped : applyInlineIgnores(deduped, content);
557
757
  }
558
758
 
559
759
  export {