From af2a14c12c7d014b6a9528fdaea53e5311b61f00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:19:34 +0000 Subject: [PATCH] Sync generated provider output --- .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ .../detector/browser/injected/index.mjs | 1 + .../detector/detect-antipatterns-browser.js | 135 ++++++++++++++++++ .../engines/static-html/detect-html.mjs | 2 + .../detector/registry/antipatterns.mjs | 9 ++ .../scripts/detector/rules/checks.mjs | 128 +++++++++++++++++ 75 files changed, 4125 insertions(+) diff --git a/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.agents/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.agents/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.agents/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.agents/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.claude/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.claude/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.claude/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.claude/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.cursor/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.cursor/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.cursor/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.cursor/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.gemini/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.gemini/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.gemini/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.gemini/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.github/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.github/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.github/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.github/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.github/skills/impeccable/scripts/detector/rules/checks.mjs b/.github/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.github/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.github/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.grok/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.grok/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.grok/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.grok/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.grok/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.grok/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.grok/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.grok/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.grok/skills/impeccable/scripts/detector/rules/checks.mjs b/.grok/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.grok/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.grok/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.kiro/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.kiro/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.kiro/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.kiro/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.opencode/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.opencode/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.opencode/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.opencode/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.pi/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.pi/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.pi/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.pi/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.qoder/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.qoder/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.qoder/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.qoder/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.rovodev/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.rovodev/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.rovodev/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.trae-cn/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.trae-cn/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.trae/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.trae/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.trae/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.trae/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/.vibe/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.vibe/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/.vibe/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.vibe/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/.vibe/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/.vibe/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/.vibe/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/.vibe/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs b/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality, diff --git a/plugin/skills/impeccable/scripts/detector/browser/injected/index.mjs b/plugin/skills/impeccable/scripts/detector/browser/injected/index.mjs index 8d18d8a3a..e61c536ee 100644 --- a/plugin/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/plugin/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -1490,6 +1490,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 0fdc460ff..d07caa7ab 100644 --- a/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -260,6 +260,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', @@ -3559,6 +3568,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -7529,6 +7663,7 @@ if (IS_BROWSER) { ...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), diff --git a/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index bcba026be..1ebb3e427 100644 --- a/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -24,6 +24,7 @@ import { checkElementMotion, checkElementOversizedH1, checkElementQuality, + checkElementRadialSpotlight, checkCreamPalette, checkHtmlPatterns, checkNumberedSectionLabelsFromDoc, @@ -105,6 +106,7 @@ const STATIC_ELEMENT_RULES = [ { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) }, { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) }, { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) }, + { id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) }, ]; async function detectHtml(filePath, options = {}) { diff --git a/plugin/skills/impeccable/scripts/detector/registry/antipatterns.mjs b/plugin/skills/impeccable/scripts/detector/registry/antipatterns.mjs index 2fb79744f..c47d39524 100644 --- a/plugin/skills/impeccable/scripts/detector/registry/antipatterns.mjs +++ b/plugin/skills/impeccable/scripts/detector/registry/antipatterns.mjs @@ -149,6 +149,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'dark mode with glowing accents', }, + { + id: 'radial-spotlight-glow', + category: 'slop', + name: 'Decorative radial spotlight glow', + description: + 'A soft, low-opacity accent-colored radial gradient fading to transparent, dropped behind a hero or section as a "spotlight." It is a reflex AI decoration — the translucent cousin of the saturated radial halo. Let the surface stand on its own, or light the composition with a deliberate material accent rather than a floating colored haze.', + skillSection: 'Color & Contrast', + skillGuideline: 'dark mode with glowing accents', + }, { id: 'marquee', category: 'slop', diff --git a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs index e2aacdc2e..418aa8f45 100644 --- a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs @@ -2766,6 +2766,131 @@ function checkElementAIPaletteDOM(el) { return findings; } +// ─── Decorative radial spotlight glow ─────────────────────────────────────── +// A soft, low-opacity chromatic radial-gradient fading to transparent, painted +// as a decorative wash behind a hero or section. The translucent sibling of the +// `radial-halo` tell: `radial-halo` requires a saturated, near-opaque center on +// a dark page; this catches the low-alpha "spotlight" the halo gate lets slip +// (e.g. `radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), +// transparent 44%)`). The two alpha bands are disjoint, so they never +// double-report the same declaration. +const SPOTLIGHT_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b|\btransparent\b/i; + +// Parse the FIRST non-repeating radial-gradient in a background value into its +// ordered color stops. Each stop is { color: {r,g,b,a} | null, transparent }. +// Returns null when there is no plain radial-gradient to read. +function parseRadialGradientStops(value) { + if (!value || !/radial-gradient/i.test(value)) return null; + const gradRe = /(repeating-)?radial-gradient\(/gi; + let g; + while ((g = gradRe.exec(value)) !== null) { + if (g[1]) continue; // repeating-* is a pattern, not a spotlight + let depth = 0, end = -1; + const open = value.indexOf('(', g.index); + for (let i = open; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { depth--; if (depth === 0) { end = i; break; } } + } + if (end < 0) return null; + const args = splitTopLevelCommas(value.slice(open + 1, end)); + // The optional prelude (shape / size / `at `) carries no color token. + const stopArgs = args.filter(a => SPOTLIGHT_COLOR_TOKEN_RE.test(a)); + if (stopArgs.length < 2) return null; + return stopArgs.map(a => { + const tok = a.match(SPOTLIGHT_COLOR_TOKEN_RE); + if (!tok) return { color: null, transparent: false }; + if (/^transparent$/i.test(tok[0])) return { color: null, transparent: true }; + const color = parseAnyColor(tok[0]); + return { color, transparent: !!color && (color.a ?? 1) <= 0.05 }; + }); + } + return null; +} + +// Pure gate. `label` is a stable identifier the fixture test keys on. +function checkRadialSpotlight({ gradientValue, width, height, label }) { + const stops = parseRadialGradientStops(gradientValue); + if (!stops || stops.length < 2) return []; + + // Must fade OUT: the last stop is transparent / near-zero alpha. A gradient + // between two visible surfaces is a real background, not a floating glow. + const last = stops[stops.length - 1]; + const lastAlpha = last.transparent ? 0 : (last.color ? (last.color.a ?? 1) : 1); + if (lastAlpha > 0.05) return []; + + // The visible (non-transparent, parseable) color stops. + const colored = stops.filter(s => !s.transparent && s.color && (s.color.a ?? 1) > 0.05); + if (colored.length === 0) return []; + // One soft glow, not a multi-color composition: at most two visible stops. + if (colored.length > 2) return []; + // Every visible stop must be LOW opacity. Any opaque stop means a real fill + // or a saturated halo (`radial-halo`'s job), not this translucent spotlight. + if (colored.some(s => (s.color.a ?? 1) >= 0.45)) return []; + // At least one visible stop must be chromatic. A neutral (grayscale) + // near-black / near-white vignette is a legitimate lighting move, exempt. + const chromatic = colored.find(s => hasChroma(s.color, 24)); + if (!chromatic) return []; + + // Decorative-scale gate. Badges, avatars, and actual small "lights" are + // exempt; a spotlight glow only reads as slop when it washes a large surface. + if (!(width >= 240 && height >= 160)) return []; + + const alpha = (chromatic.color.a ?? 1).toFixed(2); + const name = label || 'section'; + return [{ + id: 'radial-spotlight-glow', + snippet: `radial-gradient spotlight glow "${name}" (${colorToHex(chromatic.color)} a${alpha} → transparent) on ${Math.round(width)}x${Math.round(height)} surface`, + }]; +} + +// Read the raw radial-gradient source off an element's computed style, with a +// fallback to the `background` shorthand and the inline style attribute for +// engines that don't decompose the shorthand into backgroundImage. +function elementGradientValue(style, el) { + const bgImage = style.backgroundImage && style.backgroundImage !== 'none' ? style.backgroundImage : ''; + if (/radial-gradient/i.test(bgImage)) return bgImage; + const bg = style.background || ''; + if (/radial-gradient/i.test(bg)) return bg; + const rawStyle = el?.getAttribute?.('style') || ''; + const m = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); + if (m && /radial-gradient/i.test(m[1])) return m[1]; + return ''; +} + +function spotlightLabel(el) { + const dataName = el.getAttribute?.('data-name'); + if (dataName) return dataName; + if (typeof el.id === 'string' && el.id) return el.id; + const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : ''; + if (cls) return cls; + return el.tagName ? el.tagName.toLowerCase() : 'section'; +} + +function checkElementRadialSpotlightDOM(el) { + const style = getComputedStyle(el); + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + const rect = el.getBoundingClientRect(); + return checkRadialSpotlight({ + gradientValue, + width: rect.width, + height: rect.height, + label: spotlightLabel(el), + }); +} + +function checkElementRadialSpotlight(el, style, tag, window) { + const gradientValue = elementGradientValue(style, el); + if (!gradientValue) return []; + // Static engine does no layout — read explicit pixel dimensions from CSS. + return checkRadialSpotlight({ + gradientValue, + width: parseFloat(style.width) || 0, + height: parseFloat(style.height) || 0, + label: spotlightLabel(el), + }); +} + const QUALITY_TEXT_TAGS = new Set(['p', 'li', 'td', 'th', 'dd', 'blockquote', 'figcaption']); // Resolve a CSS font-size value to pixels by walking up the parent chain. @@ -5307,6 +5432,9 @@ export { checkElementMotionDOM, checkElementGlowDOM, checkElementAIPaletteDOM, + checkElementRadialSpotlightDOM, + checkElementRadialSpotlight, + checkRadialSpotlight, resolveFontSizePx, resolveLengthPx, checkQuality,