diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js
index e3e5dc21c..8d70e1987 100644
--- a/cli/engine/detect-antipatterns-browser.js
+++ b/cli/engine/detect-antipatterns-browser.js
@@ -1963,20 +1963,19 @@ function scanCssTextForGlow(content) {
return results;
}
-// Decorative grid or line-field backgrounds drawn with hairline
+// Decorative two-axis grid backgrounds drawn with hairline
// linear-gradient layers tiled by a fixed pixel cell. Shared by the HTML
// pattern pass and the regex source engine so standalone CSS, component
// styles, and inline styles receive the same coverage. Both signals must
// co-occur in one declaration block; unrelated rules must not add up across
-// the file. Returns [{ index, snippet }], capped at one finding per source to
-// match the page-level HTML check's existing behavior.
+// the file. A single hairline is a line, divider, or rail, not a grid, even
+// when tiled by a 2D px cell. Returns [{ index, snippet }], capped at one
+// finding per source to match the page-level HTML check's existing behavior.
function scanCssTextForGridBackground(content) {
const hairlineRe = /\b\d{1,3}px\s*,\s*transparent\s+\d{1,3}px/gi;
const invertedHairlineRe = /transparent\s+calc\(100%\s*-\s*\d{1,3}px\)/gi;
const sizeDeclPxRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\b/i;
- const sizeDeclPxPairRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\s+\d{1,3}px/i;
const shorthandPxAnyRe = /\/\s*\d{1,3}px\b/;
- const shorthandPxPairRe = /\/\s*\d{1,3}px\s+\d{1,3}px/;
const bgDeclRe = /\bbackground(?:-image)?\s*:\s*([^;{}"']*)/gi;
const blockRe = /\{([^{}]*)\}|style\s*=\s*"([^"]*)"|style\s*=\s*'([^']*)'/gi;
let blk;
@@ -1993,13 +1992,10 @@ function scanCssTextForGridBackground(content) {
}
if (hairlineCount === 0) continue;
const hasPxCell = sizeDeclPxRe.test(block) || shorthandPxAnyRe.test(bgJoined);
- const hasPxPairCell = sizeDeclPxPairRe.test(block) || shorthandPxPairRe.test(bgJoined);
- if ((hairlineCount >= 2 && hasPxCell) || hasPxPairCell) {
+ if (hairlineCount >= 2 && hasPxCell) {
return [{
index: blk.index,
- snippet: hairlineCount >= 2
- ? 'two-axis grid-line gradient background'
- : 'px-tiled hairline line-field background',
+ snippet: 'two-axis grid-line gradient background',
}];
}
}
diff --git a/cli/engine/rules/checks.mjs b/cli/engine/rules/checks.mjs
index 8a5654625..031501513 100644
--- a/cli/engine/rules/checks.mjs
+++ b/cli/engine/rules/checks.mjs
@@ -721,20 +721,19 @@ function scanCssTextForGlow(content) {
return results;
}
-// Decorative grid or line-field backgrounds drawn with hairline
+// Decorative two-axis grid backgrounds drawn with hairline
// linear-gradient layers tiled by a fixed pixel cell. Shared by the HTML
// pattern pass and the regex source engine so standalone CSS, component
// styles, and inline styles receive the same coverage. Both signals must
// co-occur in one declaration block; unrelated rules must not add up across
-// the file. Returns [{ index, snippet }], capped at one finding per source to
-// match the page-level HTML check's existing behavior.
+// the file. A single hairline is a line, divider, or rail, not a grid, even
+// when tiled by a 2D px cell. Returns [{ index, snippet }], capped at one
+// finding per source to match the page-level HTML check's existing behavior.
function scanCssTextForGridBackground(content) {
const hairlineRe = /\b\d{1,3}px\s*,\s*transparent\s+\d{1,3}px/gi;
const invertedHairlineRe = /transparent\s+calc\(100%\s*-\s*\d{1,3}px\)/gi;
const sizeDeclPxRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\b/i;
- const sizeDeclPxPairRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\s+\d{1,3}px/i;
const shorthandPxAnyRe = /\/\s*\d{1,3}px\b/;
- const shorthandPxPairRe = /\/\s*\d{1,3}px\s+\d{1,3}px/;
const bgDeclRe = /\bbackground(?:-image)?\s*:\s*([^;{}"']*)/gi;
const blockRe = /\{([^{}]*)\}|style\s*=\s*"([^"]*)"|style\s*=\s*'([^']*)'/gi;
let blk;
@@ -751,13 +750,10 @@ function scanCssTextForGridBackground(content) {
}
if (hairlineCount === 0) continue;
const hasPxCell = sizeDeclPxRe.test(block) || shorthandPxAnyRe.test(bgJoined);
- const hasPxPairCell = sizeDeclPxPairRe.test(block) || shorthandPxPairRe.test(bgJoined);
- if ((hairlineCount >= 2 && hasPxCell) || hasPxPairCell) {
+ if (hairlineCount >= 2 && hasPxCell) {
return [{
index: blk.index,
- snippet: hairlineCount >= 2
- ? 'two-axis grid-line gradient background'
- : 'px-tiled hairline line-field background',
+ snippet: 'two-axis grid-line gradient background',
}];
}
}
diff --git a/tests/detect-antipatterns-fixtures.test.mjs b/tests/detect-antipatterns-fixtures.test.mjs
index b94e64efb..a5b17fbea 100644
--- a/tests/detect-antipatterns-fixtures.test.mjs
+++ b/tests/detect-antipatterns-fixtures.test.mjs
@@ -1303,6 +1303,14 @@ describe('detectHtml — generated-UI tells', () => {
}
});
+ it('codex-grid-background: 1D dashed rules and px-pair line-fields stay legal', async () => {
+ const f = await detectHtml(path.join(FIXTURES, 'codex-grid-1d-pass.html'));
+ assert.equal(
+ f.filter(r => r.antipattern === 'codex-grid-background').length, 0,
+ `1D tiled hairlines must not flag, got: ${f.filter(r => r.antipattern === 'codex-grid-background').map(r => r.snippet).join('; ')}`,
+ );
+ });
+
it('gemini-tells: both flag cases surface by default and pass cases stay legal', async () => {
const findings = await detectHtml(path.join(FIXTURES, 'gemini-tells.html'));
// Two flag cases: a CSS img:hover{transform} rule and a Tailwind hover:scale on .
diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js
index 8eda903f4..f26429421 100644
--- a/tests/detect-antipatterns.test.js
+++ b/tests/detect-antipatterns.test.js
@@ -1832,11 +1832,9 @@ describe('codex-grid-background variants', () => {
expect(grids(css)).toHaveLength(1);
});
- test('flags single-axis hairline tiled by a px pair cell', () => {
+ test('keeps single-axis hairline tiled by a px pair cell legal', () => {
const css = `body { background: linear-gradient(90deg, rgba(23,25,24,.035) 1px, transparent 1px) 0 0 / 40px 40px, #f4f1ea; }`;
- const f = grids(css);
- expect(f).toHaveLength(1);
- expect(f[0].snippet).toContain('line-field');
+ expect(grids(css)).toHaveLength(0);
});
test('keeps percent-tiled single hairlines (data-viz track rules) legal', () => {
@@ -1844,6 +1842,36 @@ describe('codex-grid-background variants', () => {
expect(grids(css)).toHaveLength(0);
});
+ test('keeps 1D dashed dot rules legal', () => {
+ const css = `.dot-rule {
+ height: 5px;
+ background-image: linear-gradient(90deg, rgba(255,255,255,.75) 5px, transparent 5px);
+ background-size: 10px 5px;
+ background-repeat: repeat-x;
+ }`;
+ expect(grids(css)).toHaveLength(0);
+ });
+
+ test('keeps 1D progress rails with dash-period px pair tiles legal', () => {
+ const css = `.progress-rail {
+ background-image: linear-gradient(90deg, #eee 1px, transparent 1px);
+ background-size: 8px 4px;
+ background-repeat: repeat-x;
+ }`;
+ expect(grids(css)).toHaveLength(0);
+ });
+
+ test('regex source engine keeps 1D dot rules legal', () => {
+ const css = `.dot-rule {
+ height: 5px;
+ background-image: linear-gradient(90deg, rgba(255,255,255,.75) 5px, transparent 5px);
+ background-size: 10px 5px;
+ background-repeat: repeat-x;
+ }`;
+ const findings = detectText(css, 'dot-rule.css');
+ expect(findings.filter(f => f.antipattern === 'codex-grid-background')).toHaveLength(0);
+ });
+
test('classic two-axis background-size form still flags', () => {
const css = `.hero { background-image:
linear-gradient(#eee 1px, transparent 1px),
diff --git a/tests/fixtures/antipatterns/codex-grid-1d-pass.html b/tests/fixtures/antipatterns/codex-grid-1d-pass.html
new file mode 100644
index 000000000..f87942f2b
--- /dev/null
+++ b/tests/fixtures/antipatterns/codex-grid-1d-pass.html
@@ -0,0 +1,21 @@
+
+
+