Add radial-spotlight-glow detector rule

Flags the decorative low-opacity chromatic radial-gradient "spotlight"
washed behind a hero or section and fading to transparent, an AI-slop
reflex the saturated radial-halo gate lets slip (e.g. rgba(80,111,255,
0.26) -> transparent on a mobile hero).

Gates: a non-repeating radial-gradient whose last stop is transparent,
whose visible stops are all low-opacity (alpha < 0.45) with at most two
of them, at least one chromatic (channel spread >= 24 exempts neutral
vignettes), on a decorative-scale surface (width >= 240, height >= 160,
exempting badges/avatars/small lights). The alpha band is disjoint from
radial-halo (>= 0.7), so the two never double-report.

Wired into both element loops (static-html + injected browser) with the
pure checkRadialSpotlight shared by both adapters. TDD fixture with 5
flag / 9 pass shapes. Browser-path sweep over the eval corpus: 29 hits
on 11 pages, 0 false positives. Count 59 -> 60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-24 17:19:04 -07:00
co-authored by Claude Fable 5
parent e3f732e99c
commit 6ff9f957ac
9 changed files with 433 additions and 5 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
# Impeccable
Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 58 deterministic detector rules for AI-generated frontend design.
Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 60 deterministic detector rules for AI-generated frontend design.
> **Quick start:** From your project root, run `npx impeccable install`, then run `/impeccable init` inside your AI coding tool. Full docs: [impeccable.style](https://impeccable.style).
@@ -13,7 +13,7 @@ Every model trained on the same SaaS templates. Skip the guidance and you get th
Impeccable adds:
- **One setup flow.** `/impeccable init` writes `PRODUCT.md` and offers `DESIGN.md`, so later commands know the audience, brand/product lane, voice, anti-references, colors, type, and components.
- **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more.
- **58 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key.
- **60 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key.
## What's Included
@@ -377,7 +377,7 @@ npx impeccable ignores add-file "src/legacy/**"
npx impeccable ignores add-value overused-font Inter --reason "Brand font"
```
The detector catches 49 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more).
The detector catches 60 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more).
By default, `detect` respects the same `.impeccable/config.json` and `.impeccable/config.local.json` detector config as the design hook: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. Hook lifecycle settings such as `hook.enabled` only affect automatic hook execution.
+2 -2
View File
@@ -1,6 +1,6 @@
# Impeccable CLI
Detect UI anti-patterns and design quality issues from the command line. Scans HTML, CSS, JSX, TSX, Vue, and Svelte files for 58 deterministic rules, including AI-generated UI tells, accessibility violations, and general design quality problems.
Detect UI anti-patterns and design quality issues from the command line. Scans HTML, CSS, JSX, TSX, Vue, and Svelte files for 60 deterministic rules, including AI-generated UI tells, accessibility violations, and general design quality problems.
## Quick Start
@@ -56,7 +56,7 @@ npx impeccable detect --fast src/
**Quality**: tiny body text, cramped padding, long line lengths, small touch targets
58 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop).
60 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop).
## Exit Codes
+1
View File
@@ -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 })),
+135
View File
@@ -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 <pos>`) 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 })),
@@ -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 = {}) {
+9
View File
@@ -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',
+128
View File
@@ -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 <pos>`) 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,
@@ -592,6 +592,47 @@ describe('detectHtml — icon-tile-stack', () => {
});
});
describe('detectHtml — radial-spotlight-glow', () => {
// Two-column fixture convention: left col = should-flag, right col = should-pass.
// The rule's snippet embeds the element's data-name in quotes, e.g.
// radial-gradient spotlight glow "Hero Spotlight Blue" (#506fff a0.26 → transparent).
const SHOULD_FLAG = [
'Hero Spotlight Blue',
'Section Glow Violet',
'Overlay Glow Cyan',
'Two Stop Soft Glow',
'Hex Alpha Glow',
];
const SHOULD_PASS = [
'Opaque Radial Background',
'Small Accent Badge',
'Avatar Glow Light',
'Neutral Vignette',
'White Vignette',
'Rich Radial Composition',
'Rich Transparent Composition',
'Opaque Center Glow',
'Linear Gradient Wash',
];
it('radial-spotlight-glow: flags only the should-flag column', async () => {
const f = await detectHtml(path.join(FIXTURES, 'radial-spotlight-glow.html'));
const flagged = new Set();
for (const r of f) {
if (r.antipattern !== 'radial-spotlight-glow') continue;
const m = (r.snippet || '').match(/"([^"]+)"/);
if (m) flagged.add(m[1]);
}
for (const text of SHOULD_FLAG) {
assert.ok(flagged.has(text), `expected "${text}" to be flagged as radial-spotlight-glow`);
}
for (const text of SHOULD_PASS) {
assert.ok(!flagged.has(text), `"${text}" should NOT be flagged as radial-spotlight-glow`);
}
});
});
describe('detectHtml — undersized-ui-text', () => {
// Two-column fixture: left col = should-flag, right col = should-pass.
// The rule's snippet embeds the element's direct text in quotes, e.g.
+112
View File
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>radial-spotlight-glow fixture</title>
<style>
/* jsdom / static-html engine does no layout: every case declares explicit
pixel width + height so the size gate can read them. */
body { margin: 0; background: #0b0d13; }
.grid { display: flex; flex-wrap: wrap; gap: 24px; padding: 24px; }
.case { color: #e6e6e6; }
/* ───────── SHOULD FLAG: decorative low-opacity chromatic spotlights ───────── */
/* The exact declaration that evaded the detector: soft blue spotlight fading
to transparent, painted on a large mobile hero. */
.flag-hero-blue {
width: 360px; height: 560px;
background: radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), transparent 44%);
}
/* Violet accent wash on a full section. */
.flag-section-violet {
width: 900px; height: 600px;
background-image: radial-gradient(circle, rgba(139,92,246,0.30), transparent 60%);
}
/* Cyan overlay-style glow (the pseudo-element overlay div reflex). */
.flag-overlay-cyan {
width: 760px; height: 480px;
background: radial-gradient(ellipse at top, rgba(34,211,238,0.22), transparent 70%);
}
/* Single glow drawn with two low-alpha stops of one hue fading out. */
.flag-two-stop-rose {
width: 640px; height: 400px;
background: radial-gradient(circle, rgba(255,90,120,0.28) 0%, rgba(255,90,120,0.12) 45%, transparent 75%);
}
/* Same tell written as an 8-digit hex (#506fff at alpha 0x3d ≈ 0.24). */
.flag-hex-alpha {
width: 700px; height: 460px;
background: radial-gradient(circle, #506fff3d, transparent 50%);
}
/* ───────── SHOULD PASS: real backgrounds, small elements, neutrals, rich art ───────── */
/* Opaque radial background — a real surface, not a glow (no transparent end). */
.pass-opaque-bg {
width: 800px; height: 520px;
background: radial-gradient(circle, #1e3a8a, #0f172a);
}
/* Small badge: chromatic low-alpha radial glow, but tiny. */
.pass-badge {
width: 40px; height: 40px;
background: radial-gradient(circle, rgba(80,111,255,0.30), transparent);
}
/* Avatar-scale light: the subject IS a small light, exempt by size. */
.pass-avatar-light {
width: 72px; height: 72px;
background: radial-gradient(circle, rgba(255,196,64,0.35), transparent 70%);
}
/* Neutral near-black vignette — grayscale, not the accent-glow tell. */
.pass-neutral-vignette {
width: 800px; height: 520px;
background: radial-gradient(circle, rgba(0,0,0,0.40), transparent 80%);
}
/* Neutral near-white vignette — grayscale, exempt. */
.pass-white-vignette {
width: 820px; height: 540px;
background: radial-gradient(ellipse, rgba(255,255,255,0.35), transparent 70%);
}
/* Rich three-color radial composition — real color structure, opaque, no fade. */
.pass-rich-composition {
width: 800px; height: 520px;
background: radial-gradient(circle, #ff0080 0%, #7928ca 45%, #0070f3 100%);
}
/* Rich composition that does fade out, but its color stops are opaque. */
.pass-rich-transparent {
width: 780px; height: 500px;
background: radial-gradient(circle, rgba(255,0,128,0.90) 0%, rgba(121,40,202,0.85) 50%, transparent);
}
/* Opaque center glow (the radial-halo case, alpha ≥ 0.45) — not this rule. */
.pass-opaque-center {
width: 800px; height: 520px;
background: radial-gradient(circle, rgba(80,111,255,0.85), transparent);
}
/* Low-alpha chromatic wash, but LINEAR, not radial. */
.pass-linear-wash {
width: 800px; height: 520px;
background: linear-gradient(120deg, rgba(80,111,255,0.28), transparent 60%);
}
</style>
</head>
<body>
<main class="grid">
<!-- should-flag column -->
<div class="case flag-hero-blue" data-name="Hero Spotlight Blue"></div>
<div class="case flag-section-violet" data-name="Section Glow Violet"></div>
<div class="case flag-overlay-cyan" data-name="Overlay Glow Cyan"></div>
<div class="case flag-two-stop-rose" data-name="Two Stop Soft Glow"></div>
<div class="case flag-hex-alpha" data-name="Hex Alpha Glow"></div>
<!-- should-pass column -->
<div class="case pass-opaque-bg" data-name="Opaque Radial Background"></div>
<div class="case pass-badge" data-name="Small Accent Badge"></div>
<div class="case pass-avatar-light" data-name="Avatar Glow Light"></div>
<div class="case pass-neutral-vignette" data-name="Neutral Vignette"></div>
<div class="case pass-white-vignette" data-name="White Vignette"></div>
<div class="case pass-rich-composition" data-name="Rich Radial Composition"></div>
<div class="case pass-rich-transparent" data-name="Rich Transparent Composition"></div>
<div class="case pass-opaque-center" data-name="Opaque Center Glow"></div>
<div class="case pass-linear-wash" data-name="Linear Gradient Wash"></div>
</main>
</body>
</html>