# AI Slop Pattern Catalog

## What AI Slop Is

AI slop is the visual fingerprint of interfaces generated by language models in 2024-2025. It's what happens when an AI defaults to "looks techy and polished" instead of making actual design decisions. The result is a sea of dark-mode dashboards with gradient text, glassmorphic cards, and cyan accents that all look like they graduated from the same prompt. If you can't tell which product an interface belongs to, it's slop.

This matters because users have developed pattern recognition for it. An AI-generated look now signals "nobody designed this" the same way clip art signaled "nobody hired a designer" in 2005.

---

## Visual Patterns

Each pattern includes detection methods for use with the collector's JS extraction snippets, screenshot analysis, and accessibility snapshots.

Severity scale: **1** = mild indicator (common in legit designs too), **2** = notable signal, **3** = strong slop fingerprint.


### 1. Gradient Text
**What it looks like:** Headings or metric values with gradient color fills, usually purple-to-blue or cyan-to-pink. Text used as a canvas for decoration instead of communication.
**How to detect:**
```js
// in-page evaluation
() => {
  const hits = [];
  document.querySelectorAll('h1, h2, h3, h4, h5, h6, [class*="metric"], [class*="stat"]').forEach(el => {
    const s = getComputedStyle(el);
    if (s.backgroundClip === 'text' || s.webkitBackgroundClip === 'text') {
      hits.push({ tag: el.tagName, text: el.textContent.trim().slice(0, 40) });
    }
  });
  return { pattern: 'gradient-text', count: hits.length, hits };
}
```
**Screenshot cues:** Text that shifts color across its width. Often on hero headings or large numbers.
**Severity:** 3


### 2. Glassmorphism Everywhere
**What it looks like:** Containers with blurred, semi-transparent backgrounds. Glass-effect cards stacked on colorful backgrounds, glow borders, frosted panels.
**How to detect:**
```js
// in-page evaluation
() => {
  let count = 0;
  const hits = [];
  document.querySelectorAll('*').forEach(el => {
    const bf = getComputedStyle(el).backdropFilter || getComputedStyle(el).webkitBackdropFilter;
    if (bf && bf !== 'none') {
      count++;
      hits.push({ tag: el.tagName, class: el.className.toString().slice(0, 60) });
    }
  });
  return { pattern: 'glassmorphism', count, hits: hits.slice(0, 10) };
}
```
**Screenshot cues:** Elements that look "frosted" or translucent. Multiple stacked layers of blur.
**Severity:** 2 (1-2 uses can be fine; 3+ is a pattern)


### 3. Dark Mode with Glowing Accents
**What it looks like:** Dark backgrounds (#0a0a0a to #1a1a1a) paired with neon or bright-colored glows, shadows, and highlights. The "AI dashboard" aesthetic.
**How to detect:**
```js
// in-page evaluation
() => {
  const body = getComputedStyle(document.body);
  const bg = body.backgroundColor;
  const match = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!match) return { pattern: 'dark-glow', detected: false };
  const [r, g, b] = [match[1], match[2], match[3]].map(Number);
  // Threshold: <= 45 catches dark grays like rgb(30,30,33) that the old < 30 missed
  const isDark = r <= 45 && g <= 45 && b <= 45;
  if (!isDark) return { pattern: 'dark-glow', detected: false };
  let glowCount = 0;
  document.querySelectorAll('*').forEach(el => {
    const shadow = getComputedStyle(el).boxShadow;
    if (shadow && shadow !== 'none' && /rgba?\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3}/.test(shadow)) {
      const m = shadow.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);
      if (m) {
        const [cr, cg, cb] = [Number(m[1]), Number(m[2]), Number(m[3])];
        const maxC = Math.max(cr, cg, cb);
        const minC = Math.min(cr, cg, cb);
        // Glow = saturated color (high channel spread), not just any bright shadow
        if (maxC > 100 && (maxC - minC) > 60) glowCount++;
      }
    }
  });
  return { pattern: 'dark-glow', detected: glowCount > 2, glowCount };
}
```
**Screenshot cues:** Very dark background with bright colored halos around cards or buttons.
**Severity:** 3


### 4. Cyan-on-Dark Palette
**What it looks like:** Cyan (#00d4ff range) as the primary accent against near-black backgrounds. The "hacker terminal meets SaaS dashboard" color scheme.
**How to detect:**
```js
// in-page evaluation
() => {
  const isDark = () => {
    const bg = getComputedStyle(document.body).backgroundColor;
    const m = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
    if (!m) return false;
    return Number(m[1]) <= 45 && Number(m[2]) <= 45 && Number(m[3]) <= 45;
  };
  if (!isDark) return { pattern: 'cyan-on-dark', detected: false };
  let cyanCount = 0;
  document.querySelectorAll('a, button, h1, h2, h3, span, [class*="accent"]').forEach(el => {
    const color = getComputedStyle(el).color;
    const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
    if (m && Number(m[1]) < 50 && Number(m[2]) > 180 && Number(m[3]) > 200) cyanCount++;
  });
  return { pattern: 'cyan-on-dark', detected: cyanCount > 2, cyanCount };
}
```
**Severity:** 3


### 5. Purple-to-Blue Gradients
**What it looks like:** Background gradients or accents shifting from purple (#7c3aed range) to blue (#3b82f6 range). The default "AI product" palette.
**How to detect:**
```js
// in-page evaluation
() => {
  let count = 0;
  document.querySelectorAll('*').forEach(el => {
    const bg = getComputedStyle(el).backgroundImage;
    if (bg && bg.includes('gradient') && /purple|#[7-9a-f][0-5].*blue|#[3-5][a-f]/i.test(bg)) count++;
    if (bg && bg.includes('gradient') && /rgb\(\s*1[2-9]\d|2\d{2}.*rgb\(\s*[3-9]\d,\s*[1-9]\d{2}/i.test(bg)) count++;
  });
  return { pattern: 'purple-blue-gradient', count };
}
```
**Screenshot cues:** Sections or buttons that fade from violet/purple on one side to blue on the other.
**Severity:** 2


### 6. Hero Metric Layout
**What it looks like:** Big number (48-72px), small label beneath it, supporting stats in a row, optional gradient accent line. The "analytics dashboard hero" that every AI produces.
**How to detect:**
```js
// in-page evaluation
() => {
  const bigNumbers = [];
  document.querySelectorAll('*').forEach(el => {
    const s = getComputedStyle(el);
    const fontSize = parseFloat(s.fontSize);
    const text = el.textContent.trim();
    // Threshold: 24px catches metric cards, 36px was too strict (missed 24-30px stat displays)
    if (fontSize >= 24 && /^[\d,.%$+\-KMB]+$/.test(text) && text.length < 15) {
      bigNumbers.push({ text, fontSize, tag: el.tagName });
    }
  });
  return { pattern: 'hero-metric', detected: bigNumbers.length >= 2, hits: bigNumbers.slice(0, 5) };
}
```
**Snapshot cues:** Accessibility tree shows large standalone numeric text nodes with small adjacent label nodes.
**Severity:** 3


### 7. Identical Card Grids
**What it looks like:** Three or four cards in a row, each with the same dimensions, same internal layout (icon, heading, description), same spacing. Cookie-cutter feature sections.
**How to detect:**
```js
// in-page evaluation
() => {
  const grids = document.querySelectorAll('[class*="grid"], [style*="grid"], [class*="flex"]');
  const results = [];
  grids.forEach(grid => {
    const children = Array.from(grid.children);
    if (children.length < 3) return;
    // Filter to visible, card-sized elements (min 100x80px) — skip icons, bullets, avatars
    const cardChildren = children.filter(c => c.offsetWidth >= 100 && c.offsetHeight >= 80);
    if (cardChildren.length < 3) return;
    const sizes = cardChildren.map(c => `${c.offsetWidth}x${c.offsetHeight}`);
    const unique = new Set(sizes);
    if (unique.size === 1 && cardChildren.length >= 3) {
      // Check if children have similar internal structure
      const structures = cardChildren.map(c => c.querySelectorAll('*').length);
      const structVar = Math.max(...structures) - Math.min(...structures);
      if (structVar <= 2) results.push({ count: cardChildren.length, size: sizes[0] });
    }
  });
  return { pattern: 'identical-cards', detected: results.length > 0, grids: results };
}
```
**Severity:** 2


### 8. Generic Font Stack
**What it looks like:** The usual suspects: Inter, Roboto, Arial, Open Sans, system-ui with no custom or distinctive typography choices. Zero personality in the type.
**How to detect:**
```js
// in-page evaluation
() => {
  const generic = ['inter', 'roboto', 'arial', 'open sans', 'helvetica', 'system-ui', 'segoe ui', '-apple-system'];
  // Extract only the PRIMARY font (first in the stack), not fallbacks
  // font-family: "Circular", "Inter", sans-serif → checks "circular" only
  function primaryFont(fontFamily) {
    return fontFamily.split(',')[0].trim().replace(/['"]/g, '').toLowerCase();
  }
  const bodyPrimary = primaryFont(getComputedStyle(document.body).fontFamily);
  const h1 = document.querySelector('h1');
  const headingPrimary = h1 ? primaryFont(getComputedStyle(h1).fontFamily) : bodyPrimary;
  const bodyGeneric = generic.some(f => bodyPrimary.includes(f));
  const headingGeneric = generic.some(f => headingPrimary.includes(f));
  const sameFont = bodyPrimary === headingPrimary;
  return {
    pattern: 'generic-fonts',
    bodyFont: bodyPrimary,
    headingFont: headingPrimary,
    bodyGeneric,
    headingGeneric,
    sameFont,
    detected: bodyGeneric && headingGeneric
  };
}
```
**Severity:** 1 (generic fonts are common in legitimate projects too)


### 9. Nested Cards
**What it looks like:** Cards inside cards. A container with rounded corners, padding, and a shadow, sitting inside another container with rounded corners, padding, and a shadow. Visual matryoshka.
**How to detect:**
```js
// in-page evaluation
() => {
  let nested = 0;
  document.querySelectorAll('*').forEach(el => {
    const s = getComputedStyle(el);
    const isCard = parseFloat(s.borderRadius) > 4 && (s.boxShadow !== 'none' || s.border !== '0px none rgb(0, 0, 0)');
    if (!isCard) return;
    let parent = el.parentElement;
    while (parent) {
      const ps = getComputedStyle(parent);
      if (parseFloat(ps.borderRadius) > 4 && (ps.boxShadow !== 'none' || ps.border !== '0px none rgb(0, 0, 0)')) {
        nested++;
        break;
      }
      parent = parent.parentElement;
    }
  });
  return { pattern: 'nested-cards', count: nested, detected: nested > 1 };
}
```
**Severity:** 2


### 10. Rounded Rectangles with Generic Shadows
**What it looks like:** Every container has border-radius between 8-16px and a soft box-shadow. Safe, forgettable, could be any AI output from any prompt.
**How to detect:**
```js
// in-page evaluation
() => {
  let count = 0;
  document.querySelectorAll('div, section, article, aside').forEach(el => {
    const s = getComputedStyle(el);
    const br = parseFloat(s.borderRadius);
    const hasShadow = s.boxShadow !== 'none';
    if (br >= 8 && br <= 20 && hasShadow) count++;
  });
  return { pattern: 'rounded-shadow-boxes', count, detected: count > 4 };
}
```
**Severity:** 1


### 11. Decorative Sparklines
**What it looks like:** Tiny inline charts (SVG paths or canvas elements) that look data-driven but convey no actual information. Vibes-only data visualization.
**How to detect:**
```js
// in-page evaluation
() => {
  const tinySvgs = [];
  document.querySelectorAll('svg').forEach(svg => {
    const rect = svg.getBoundingClientRect();
    if (rect.width < 120 && rect.height < 60 && rect.width > 20) {
      const paths = svg.querySelectorAll('path, polyline, line');
      if (paths.length > 0 && paths.length < 5) {
        tinySvgs.push({ width: rect.width, height: rect.height, paths: paths.length });
      }
    }
  });
  return { pattern: 'decorative-sparklines', count: tinySvgs.length, detected: tinySvgs.length > 2, hits: tinySvgs.slice(0, 5) };
}
```
**Severity:** 2


### 12. Monospace for "Tech Vibes"
**What it looks like:** Monospace fonts used on headings, labels, or body text that isn't code. Deployed to make things feel "technical" or "developer-y" without any functional reason.
**How to detect:**
```js
// in-page evaluation
() => {
  const mono = ['monospace', 'courier', 'consolas', 'fira code', 'jetbrains mono', 'source code pro', 'sf mono'];
  const hits = [];
  document.querySelectorAll('h1, h2, h3, h4, p, span, label, a').forEach(el => {
    if (el.closest('pre, code, kbd, samp')) return; // skip actual code blocks
    const font = getComputedStyle(el).fontFamily.toLowerCase();
    if (mono.some(m => font.includes(m))) {
      hits.push({ tag: el.tagName, text: el.textContent.trim().slice(0, 30) });
    }
  });
  return { pattern: 'monospace-vibes', count: hits.length, detected: hits.length > 2, hits: hits.slice(0, 5) };
}
```
**Severity:** 2


### 13. Bounce/Elastic Animations
**What it looks like:** Elements that overshoot their target position and bounce back. Feels like a 2016 Dribbble shot, not a production interface.
**How to detect:**
```js
// in-page evaluation — check stylesheets for cubic-bezier curves that indicate bounce/elastic
() => {
  const bouncePatterns = /cubic-bezier\(\s*[\d.]+,\s*[\d.]+,\s*[\d.]+,\s*(1\.[3-9]|[2-9])/;
  const elasticKeywords = /bounce|elastic|spring/i;
  let count = 0;
  for (const sheet of document.styleSheets) {
    try {
      for (const rule of sheet.cssRules) {
        const text = rule.cssText;
        if (bouncePatterns.test(text) || elasticKeywords.test(text)) count++;
      }
    } catch (e) { /* cross-origin stylesheet, skip */ }
  }
  return { pattern: 'bounce-animations', count, detected: count > 0 };
}
```
**Severity:** 2


### 14. Modal Overuse
**What it looks like:** Dialogs, overlays, and modals used for tasks that could happen inline. Confirmation modals, settings modals, info modals. Everything gets a modal.
**How to detect:**
```js
// in-page evaluation
() => {
  const modals = document.querySelectorAll(
    '[role="dialog"], [aria-modal="true"], dialog, [class*="modal"], [class*="overlay"], [class*="popup"], [class*="dialog"]'
  );
  return { pattern: 'modal-overuse', count: modals.length, detected: modals.length > 2 };
}
```
**Snapshot cues:** Multiple `dialog` or `[role="dialog"]` nodes in the accessibility tree.
**Severity:** 1


### 15. One-Sided Accent Borders
**What it looks like:** A thick colored border on just the left or top side of a card/container, used as a lazy visual accent. The "I need some color here" solution.
**How to detect:**
```js
// in-page evaluation
() => {
  let count = 0;
  document.querySelectorAll('*').forEach(el => {
    const s = getComputedStyle(el);
    const sides = [
      parseFloat(s.borderLeftWidth), parseFloat(s.borderRightWidth),
      parseFloat(s.borderTopWidth), parseFloat(s.borderBottomWidth)
    ];
    const thick = sides.filter(w => w >= 3);
    const thin = sides.filter(w => w < 2);
    if (thick.length === 1 && thin.length >= 2) count++;
  });
  return { pattern: 'accent-border', count, detected: count > 2 };
}
```
**Severity:** 2


### 16. Pure Black/White Backgrounds
**What it looks like:** Backgrounds using literal #000000 or #ffffff. Real design uses off-blacks and off-whites with subtle tints. Pure values are the "I didn't think about this" choice.
**How to detect:**
```js
// in-page evaluation
() => {
  const body = getComputedStyle(document.body).backgroundColor;
  const m = body.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!m) return { pattern: 'pure-bw', detected: false };
  const [r, g, b] = [m[1], m[2], m[3]].map(Number);
  const pureBlack = r === 0 && g === 0 && b === 0;
  const pureWhite = r === 255 && g === 255 && b === 255;
  return { pattern: 'pure-bw', detected: pureBlack || pureWhite, pureBlack, pureWhite };
}
```
**Severity:** 1


### 17. Everything Centered
**What it looks like:** Every section, every heading, every paragraph -- all center-aligned. No left alignment, no asymmetry, no intentional layout decisions. Just the safe default.
**How to detect:**
```js
// in-page evaluation
() => {
  let centered = 0, total = 0;
  document.querySelectorAll('h1, h2, h3, p, section > div').forEach(el => {
    total++;
    const ta = getComputedStyle(el).textAlign;
    if (ta === 'center' || ta === '-webkit-center') centered++;
  });
  const ratio = total > 0 ? centered / total : 0;
  return { pattern: 'everything-centered', centered, total, ratio: ratio.toFixed(2), detected: ratio > 0.7 && total > 4 };
}
```
**Severity:** 2


### 18. Icon-Above-Heading Pattern
**What it looks like:** Large rounded icons (often in colored circles) sitting above every section heading or feature card. The universal "feature list" template.
**How to detect:**
```js
// in-page evaluation
() => {
  let count = 0;
  document.querySelectorAll('h2, h3, h4').forEach(heading => {
    const prev = heading.previousElementSibling;
    if (!prev) return;
    const isIcon = prev.tagName === 'SVG' || prev.querySelector('svg') ||
      prev.tagName === 'IMG' || prev.tagName === 'I' ||
      (prev.tagName === 'DIV' && prev.children.length <= 1 && prev.getBoundingClientRect().width < 80);
    if (isIcon) count++;
  });
  return { pattern: 'icon-above-heading', count, detected: count >= 3 };
}
```
**Snapshot cues:** Repeating pattern of image/graphic nodes immediately before heading nodes.
**Severity:** 2


### 19. Redundant Information
**What it looks like:** A heading that says "Our Features" followed by a subheading that says "Explore the features we offer." The same information, twice, adding nothing.
**How to detect:**
```js
// in-page evaluation
() => {
  const hits = [];
  document.querySelectorAll('h1, h2, h3').forEach(h => {
    const next = h.nextElementSibling;
    if (!next || !['P', 'SPAN', 'DIV'].includes(next.tagName)) return;
    const hWords = new Set(h.textContent.toLowerCase().replace(/[^a-z\s]/g, '').split(/\s+/).filter(w => w.length > 3));
    const pWords = next.textContent.toLowerCase().replace(/[^a-z\s]/g, '').split(/\s+/).filter(w => w.length > 3);
    if (pWords.length === 0) return;
    const overlap = pWords.filter(w => hWords.has(w)).length / pWords.length;
    if (overlap > 0.4) hits.push({ heading: h.textContent.trim().slice(0, 40), sub: next.textContent.trim().slice(0, 60) });
  });
  return { pattern: 'redundant-info', count: hits.length, detected: hits.length > 1, hits: hits.slice(0, 3) };
}
```
**Severity:** 2


### 20. Every Button is Primary
**What it looks like:** All buttons share the same visual weight. No hierarchy between primary actions and secondary options. When everything is loud, nothing is.
**How to detect:**
```js
// in-page evaluation
() => {
  const buttons = Array.from(document.querySelectorAll('button, a[class*="btn"], [role="button"], a[class*="button"]'));
  if (buttons.length < 3) return { pattern: 'all-primary-buttons', detected: false };
  const styles = buttons.map(b => {
    const s = getComputedStyle(b);
    return `${s.backgroundColor}|${s.fontWeight}|${s.padding}`;
  });
  const unique = new Set(styles);
  const uniformity = 1 - (unique.size / styles.length);
  return { pattern: 'all-primary-buttons', total: buttons.length, uniqueStyles: unique.size, uniformity: uniformity.toFixed(2), detected: uniformity > 0.6 && buttons.length >= 3 };
}
```
**Severity:** 2


### 21. Same Spacing Everywhere
**What it looks like:** Every section, every gap, every padding value is the same (usually 24px or 32px). No visual rhythm, no breathing room, no hierarchy of space.
**How to detect:**
```js
// in-page evaluation
() => {
  const gaps = [];
  document.querySelectorAll('section, [class*="container"], main > div').forEach(el => {
    const s = getComputedStyle(el);
    gaps.push(parseFloat(s.paddingTop), parseFloat(s.paddingBottom));
    gaps.push(parseFloat(s.marginTop), parseFloat(s.marginBottom));
  });
  const nonZero = gaps.filter(g => g > 0);
  if (nonZero.length < 4) return { pattern: 'uniform-spacing', detected: false };
  const unique = new Set(nonZero.map(g => Math.round(g / 4) * 4)); // round to nearest 4px
  const ratio = unique.size / nonZero.length;
  return { pattern: 'uniform-spacing', uniqueValues: unique.size, totalValues: nonZero.length, ratio: ratio.toFixed(2), detected: unique.size <= 2 && nonZero.length > 6 };
}
```
**Severity:** 1


### 22. Neon Accents on Dark
**What it looks like:** Bright, saturated accent colors (often green, pink, or yellow) against near-black backgrounds. Like the UI was designed inside a nightclub.
**How to detect:**
```js
// in-page evaluation
() => {
  const bg = getComputedStyle(document.body).backgroundColor;
  const bgm = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!bgm) return { pattern: 'neon-accents', detected: false };
  const isDark = Number(bgm[1]) <= 45 && Number(bgm[2]) <= 45 && Number(bgm[3]) <= 45;
  if (!isDark) return { pattern: 'neon-accents', detected: false };
  let neonCount = 0;
  document.querySelectorAll('button, a, span, h1, h2, h3, [class*="badge"], [class*="tag"]').forEach(el => {
    const color = getComputedStyle(el).color;
    const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
    if (!m) return;
    const [r, g, b] = [m[1], m[2], m[3]].map(Number);
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    const saturation = max > 0 ? (max - min) / max : 0;
    if (max > 200 && saturation > 0.6) neonCount++;
  });
  return { pattern: 'neon-accents', neonCount, detected: neonCount > 3 };
}
```
**Severity:** 2 (overlaps with pattern 3 but targets accent elements specifically)


### 23. Cards Wrapping Everything
**What it looks like:** Every piece of content sits inside its own rounded, shadowed container. Lists of cards. Cards of cards. The interface is more container than content.
**How to detect:**
```js
// in-page evaluation
() => {
  let cardCount = 0;
  const mainContent = document.querySelector('main') || document.body;
  mainContent.querySelectorAll('div, section, article').forEach(el => {
    const s = getComputedStyle(el);
    const hasRadius = parseFloat(s.borderRadius) > 4;
    const hasShadowOrBorder = s.boxShadow !== 'none' || (s.borderWidth && parseFloat(s.borderWidth) > 0);
    const hasPadding = parseFloat(s.padding) > 8 || parseFloat(s.paddingTop) > 8;
    if (hasRadius && hasShadowOrBorder && hasPadding) cardCount++;
  });
  const totalElements = mainContent.querySelectorAll('div, section, article').length;
  const ratio = totalElements > 0 ? cardCount / totalElements : 0;
  return { pattern: 'card-everything', cardCount, totalElements, ratio: ratio.toFixed(2), detected: cardCount > 6 };
}
```
**Severity:** 2


### 24. Gray Text on Colored Backgrounds
**What it looks like:** Using gray (#666, #999, etc.) for secondary text regardless of the background color. On white it's fine. On colored or dark backgrounds it looks washed out and unreadable.
**How to detect:**
```js
// in-page evaluation
() => {
  const hits = [];
  document.querySelectorAll('p, span, label, small').forEach(el => {
    const s = getComputedStyle(el);
    const color = s.color;
    const cm = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
    if (!cm) return;
    const [r, g, b] = [cm[1], cm[2], cm[3]].map(Number);
    const isGray = Math.abs(r - g) < 15 && Math.abs(g - b) < 15 && r > 80 && r < 180;
    if (!isGray) return;
    // Check parent background
    let parent = el.parentElement;
    while (parent) {
      const pbg = getComputedStyle(parent).backgroundColor;
      const pm = pbg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
      if (pm) {
        const [pr, pg, pb] = [pm[1], pm[2], pm[3]].map(Number);
        const isColored = Math.abs(pr - pg) > 20 || Math.abs(pg - pb) > 20;
        const isDark = pr < 60 && pg < 60 && pb < 60;
        if (isColored || isDark) {
          hits.push({ text: el.textContent.trim().slice(0, 30), color: `rgb(${r},${g},${b})` });
          break;
        }
      }
      parent = parent.parentElement;
    }
  });
  return { pattern: 'gray-on-colored', count: hits.length, detected: hits.length > 2, hits: hits.slice(0, 5) };
}
```
**Severity:** 2


### 25. Generic Hero Sections
**What it looks like:** Full-width hero with a large heading, a subtitle paragraph, and one or two CTA buttons, optionally with a stock illustration or gradient blob. Indistinguishable from a template.
**How to detect:**
```js
// in-page evaluation
() => {
  const hero = document.querySelector('section:first-of-type, header + section, [class*="hero"], main > div:first-child');
  if (!hero) return { pattern: 'generic-hero', detected: false };
  const rect = hero.getBoundingClientRect();
  const isFullWidth = rect.width >= window.innerWidth * 0.9;
  const isTall = rect.height > window.innerHeight * 0.4;
  const hasLargeHeading = !!hero.querySelector('h1');
  const hasParagraph = !!hero.querySelector('p');
  const buttons = hero.querySelectorAll('a, button');
  const hasCTAs = buttons.length >= 1 && buttons.length <= 3;
  const hasImage = !!hero.querySelector('img, svg, [class*="illustration"], [class*="blob"]');
  const signals = [isFullWidth, isTall, hasLargeHeading, hasParagraph, hasCTAs].filter(Boolean).length;
  return {
    pattern: 'generic-hero',
    signals,
    isFullWidth, isTall, hasLargeHeading, hasParagraph, hasCTAs, hasImage,
    detected: signals >= 4
  };
}
```
**Screenshot cues:** Big text, small text, button(s), optional image. The layout every AI defaults to.
**Severity:** 1 (heroes are legitimate; it's the generic execution that's slop)


### 26. Uniform Image Hover-Zoom
**What it looks like:** Every image in a grid lifts or zooms by the exact same amount on hover — the same `scale(1.05)` on card after card, because one component was copy-pasted across the whole gallery. A single product shot that zooms on hover is fine and often good; the tell is *uniformity at scale*, the fingerprint of a templated card.
**How to detect:** This one can't be read from a static snapshot — a `:hover` transform only exists once you actually hover. The collector simulates it and hands you the result in `imageHoverTransforms`:
```js
// collector-provided evidence (bin/pixelslop-browser.cjs → collectImageHoverPass)
// imageHoverTransforms: {
//   tested,            // images hovered
//   transformed,       // how many changed transform on hover
//   uniform,           // TRUE only when 3+ images did the identical transform AND that's ≥60% of movers
//   uniformTransform,  // the shared computed transform value, e.g. "matrix(1.05, 0, 0, 1.05, 0, 0)"
//   uniformCount,      // how many images share it
//   samples            // per-image before/after transform + parsed scale
// }
// Detected ONLY when imageHoverTransforms.uniform === true. A non-uniform result is not slop.
```
**Screenshot cues:** Not visible in a static screenshot — this is a motion tell. Evidence lives in the hover-simulation result, not the still.
**Severity:** 1 (a lone hover-zoom is legitimate; only the uniform-across-many execution is a slop signal)


---

## Source Patterns

These patterns are for code-level analysis when a `--root` flag gives access to the project source. Grep the codebase for these signals.

### S1. Tailwind Gradient Classes
**Grep:** `bg-gradient-to-|from-purple|from-blue|to-cyan|via-blue`
**Why:** Heavy use of Tailwind gradient utilities, especially in the purple-blue-cyan range, is a strong AI generation signal.

### S2. backdrop-filter in CSS
**Grep:** `backdrop-filter:\s*blur`
**Why:** Widespread backdrop-filter usage indicates glassmorphism. One or two instances are fine; five or more is a pattern.

### S3. Hardcoded Color Values
**Grep:** `#000000|#ffffff|#000"|#fff"|bg-black|bg-white`
**Why:** Pure black and white values suggest no intentional color palette was designed.

### S4. Inter/Roboto Font Imports
**Grep:** `fonts.googleapis.com.*(Inter|Roboto|Open\+Sans)|font-family:.*Inter|font-family:.*Roboto`
**Why:** These are the default fonts AI reaches for. Not wrong by themselves, but they contribute to the generic look.

### S5. Identical Component Repetition
**Grep:** Pattern of 3+ sibling elements with identical class structures in JSX/HTML.
**Why:** AI loves generating three identical feature cards. Look for repeated `className` patterns in adjacent elements.

### S6. Box Shadow Everywhere
**Grep:** `shadow-md|shadow-lg|shadow-xl|box-shadow:.*0.*\d+px.*\d+px`
**Why:** Uniform shadow application across the UI suggests AI defaults rather than intentional depth design.

### S7. Modal/Dialog Proliferation
**Grep:** `Dialog|Modal|isOpen|onClose|showModal|setShowModal`
**Why:** Multiple modal state variables indicate modal-first interaction design, a common AI pattern.

### S8. Animation Library Overuse
**Grep:** `framer-motion|animate-bounce|animate-pulse|transition-all`
**Why:** AI tends to add animation to everything. `transition-all` is particularly lazy -- it animates properties you never intended to animate.

### S9. Metric Display Components
**Grep:** `stat-card|metric-card|StatCard|MetricCard|dashboard-stat`
**Why:** Named metric/stat card components are the building blocks of the AI dashboard aesthetic.

### S10. Template Comments
**Grep:** `{/\*.*placeholder.*\*/}|{/\*.*TODO.*replace.*\*/}|Lorem ipsum`
**Why:** Leftover template markers indicate AI-generated scaffolding that wasn't fully customized.

### S11. Repeated Identical Section Structures
**Grep:** `<section` or framework-equivalent section wrappers
**Why:** Three or more consecutive sections sharing the same heading→paragraph→button internal pattern is a strong AI generation signal. Check by comparing DOM structure of adjacent `<section>` elements.

### S12. Placeholder Content Markers
**Grep:** `Lorem ipsum|Coming soon|\\[Your .* here\\]|\\[Insert|placeholder`
**Why:** Leftover placeholder content that was never replaced with real copy. Different from S10 template comments — these appear in rendered content, not code comments.

### S13. Excessive Utility Class Stacking
**Grep:** `class="[^"]{200,}"|className="[^"]{200,}"`
**Why:** Elements with extremely long class strings (200+ chars) suggest copy-paste from AI output without consolidation. Common in Tailwind-heavy AI output where every state, responsive variant, and pseudo-class is stacked inline.

### S14. Identical Button Labels Across Sections
**Grep:** `>Learn More<|>Get Started<|>Read More<|>Try Now<|>Sign Up<`
**Why:** The same generic CTA text appearing 3+ times across different sections. AI tends to reach for the same labels repeatedly. Grep alone flags them; count determines severity.

### S15. Stock Photo Alt Text Patterns
**Grep:** `alt="[^"]*(?:diverse team|professional|working on laptop|business meeting|happy customer|smiling person|team collaboration)[^"]*"`
**Why:** AI-generated alt text uses the same stock-photo descriptors. Real alt text describes what's actually in the specific image, not generic scene descriptions.

### S16. Inline Style Proliferation
**Grep:** `style="[^"]{100,}"`
**Why:** Long inline style strings (100+ chars) suggest AI-generated component styling that wasn't extracted to stylesheets or utility classes. Occasional inline styles are fine; widespread use is a code quality signal.


---

## Severity Bands

Pattern counts map to overall slop ratings. Count each detected visual pattern once (source patterns are supplementary signals, not added to the count).

| Rating | Pattern Count | What It Means |
|---|---|---|
| **CLEAN** | 0-1 | Interface shows intentional design decisions. Minor signals are likely coincidental or legitimate choices. |
| **MILD** | 2-3 | Some AI-generated tendencies showing through. Could be a designed interface with a few lazy defaults, or a lightly customized AI output. |
| **SLOPPY** | 4-6 | Multiple AI generation fingerprints present. The interface would benefit from a design pass by someone with opinions. |
| **TERMINAL** | 7+ | This interface is a showcase of AI defaults. It looks like it was prompted into existence and shipped without design review. Start over, or at minimum do a serious design audit. |

Source pattern matches should be reported separately. They provide context ("the codebase confirms heavy Tailwind gradient usage") but don't inflate the visual score. A page can grep dirty but render clean if the problematic styles are in unused code.


---

## False Positive Guidance

Not every match is slop. Context matters. Here's when to dial back your confidence.

**Generic fonts are sometimes the right call.** A developer tools product using Inter is a reasonable choice, not slop. It becomes a signal only when combined with other patterns -- generic font + identical cards + gradient text is a different story than generic font alone.

**Dark mode is legitimate.** Plenty of real products use dark themes. The slop signal isn't dark mode itself -- it's dark mode + glowing accents + cyan palette + zero visual personality. Dark mode with a thoughtful, distinctive color palette is just a design choice.

**Card layouts are a valid pattern.** Dashboard UIs, e-commerce grids, content feeds -- cards make sense. The slop signal is when every card is identical and cards are used for content that doesn't need containment (a single paragraph does not need a card).

**Centered layouts work for specific contexts.** Landing pages, marketing sites, and single-purpose pages often center-align content. It's a slop signal when the entire multi-page application is centered with no variation.

**Hero sections are fine when they're distinctive.** The pattern to flag is the generic execution (big heading + subtitle + two buttons + blob illustration), not the layout concept itself. A hero with unique typography, unexpected composition, or strong brand identity is not slop.

**Modals have legitimate uses.** Destructive action confirmations, complex multi-step forms, and authentication flows are reasonable modal use cases. The signal is when modals are the default interaction pattern for everything including tasks that could happen inline.

**Small component counts reduce confidence.** A page with only 3 elements showing 2 patterns is less concerning than a page with 30 elements showing 6 patterns. Scale your assessment with the complexity of the page.

When in doubt, ask: "Did a human make this choice, or did the AI fall back to a default?" That's the core question. Defaults are slop. Choices are design.
